0、踩坑背景
仍然是torch-rnn/LanguageModel.lua文件中的一些問題,仍然是這個狗血的LM:encode_string函數:
function LM:encode_string(s) local encoded = torch.LongTensor(#s) for i = 1, #s do local token = s:sub(i, i) local idx = self.token_to_idx[token] assert(idx ~= nil, 'Got invalid idx') encoded[i] = idx end return encoded end
上篇文章Torch-RNN運行過程中的坑 [1](讀取Lua非空table,size為0)就在這部分測試table是不是空,填了個坑。
在填坑過程中,發現for...in pair table打印出的字典是沒有問題的,但是上述encode_string函數的token竟然是亂碼,每次從字典table中命中不了,每次返回的都是nil。
查了console編碼,是沒有問題的(UTF-8)。這是咋回事哩?
1、Lua中的string sub函數切分中文
我們捋一下整個工程,
第一步使用preprocess.py對文本文件轉為json和hdf5文件,值得一提的是,json中使用Unicode存儲字典文件;
第二步train.lua中對json和hdf5文件進行訓練;
第三步sample.lua中對checkpoint和start_text進行抽樣。
由於是對start_text進行character拆分時亂碼,我們肯定首先對encode_string函數中的s:sub(i, i)函數進行debug。
這個sub函數首先在lua console中看看它到底返回的是啥:
> s1='a1b2c3' > print(s1:sub(1,1)) a --lua中從1開始計數,和java substring和python slice不同的是,start/end index它都包含 > print(s1:sub(1,2)) a1 > print(s1:sub(3,6)) b2c3 > print(s1:sub(3,7)) b2c3 --lua還是比較智能,越界也沒有報錯,比java更腳本化一些 > print(s1:sub(0,7)) a1b2c3 > print(s1:sub(-1,7)) 3 --腳本語言都有的負數下標
大概有個了解,就是java中的substring嘛,有些細節不一樣。真的是這樣嗎?見下:
> s2='1a新華' > print(s2:sub(1,2)) 1a --前倆字母,正常 > print(s2:sub(1,3)) 1a --WTF?這就尷尬了,我的漢字呢 > print(s2:sub(1,4)) 1a --??? > print(s2:sub(1,5)) 1a新 --???一個“新”字占了3個字節 > print(s2:sub(3,5)) 新 > print(s2:sub(3,6)) 新 > print(s2:sub(3,7)) 新 > print(s2:sub(3,8)) 新華 --???“華”字也是占了三個字節 > print(s2:sub(3,9)) 新華 > print(string.len(s2)) 8 --這。。。正常length 4的是8?
這“1a新華”四個字的length Lua給我輸出個8,讓我這個java選手情何以堪。。。。
好奇害死貓,嘗試在python console下,也輸出的是8,看來此處必有蹊蹺。嗯。
2、各種字符編碼的區別與聯系?
一個漢字占了3個字符,學過計組的同學醒醒了,當年老師敲黑板的重點就是這個。。。
這里首先拋出ruanyf十年前寫的一篇的淺顯易懂的文章 《字符編碼筆記:ASCII,Unicode和UTF-8》。太通俗易懂了,我這里簡要搬運下。。。
總結一下考點:
首先,ASCII碼是1byte,8bit,最多可以表示2^8=256種字符,表示英文字符綽綽有余。
但是,以中文為代表的各國優秀博大精深語言(文中說漢字有10W+),漸漸走進了國際化道路,當年定制ASCII碼的同志們發現1byte根本表示不了這么多字符。
於是,我國1980年推出了簡體中文GB2312(1995年推出的GBK的前身),兩個字節byte表示一個漢字。
再后來,1990年,各國都開發了各自的編碼,這時候一個偉大的聯盟組織、非營利機構統一碼聯盟承擔了Unicode(萬國碼、國際碼、統一碼、單一碼)的標准制定,該機構致力於讓Unicode方案替換既有的字符編碼方案。每個國家的字符都有對應的編碼區間,終於實現了同一個世界,同一套編碼。。。
但是Unicode還是有些小瑕疵,沒有大量推廣使用,這時候UTF-8左右Unicode的一種實現方式,閃亮登場。
文中還有一些大小端、BIG-5、UTF-8 BOM方面的考點,感興趣的同學可以點鏈接。
3、有點跑題,咱們的問題怎么解決,切分中文string亂碼?
如果單純的string全都是中文,那還好辦了,直接三個三個切分就成,如果混合字符串呢?例如“1a新華”這樣的。
這時候就要對編碼區間進行判斷了。
function LM:encode_string(s) local encoded = torch.LongTensor(#s) local i = 1 -- cause 中文在UTF8中存儲占位3個字節,而英文仍然是1個字節[ascii] for token in s.gmatch(s, "[%z\1-\127\194-\244][\128-\191]*") do local idx = self.token_to_idx[token] assert(idx ~= nil, 'Got invalid idx') encoded[i] = idx i = i+1 end return encoded end
按照博文 lua 含中文的字符串處理--分離字符、計算字符數、截取指定長度 中對regex的解釋是:
- --[[
- UTF8的編碼規則:
- 1. 字符的第一個字節范圍: 0x00—0x7F(0-127),或者 0xC2—0xF4(194-244); UTF8 是兼容 ascii 的,所以 0~127 就和 ascii 完全一致
- 2. 0xC0, 0xC1,0xF5—0xFF(192, 193 和 245-255)不會出現在UTF8編碼中
- 3. 0x80—0xBF(128-191)只會出現在第二個及隨后的編碼中(針對多字節編碼,如漢字)
- ]]
這樣就不會出現問題了~