拼音輸入法作業

拼音輸入法實驗報告

實驗基本思路

       經課上了解以及翻閱資料,該問題可由隱馬爾可夫求解,即隱馬爾可夫的三個基本問題中的第三個,預測問題。其實也包含了第二個問題,學習問題,由於此問題已給定觀測序列(因爲不考慮多音字,相當於觀測狀態數量爲1且概率爲1),語料中的漢字即隱藏狀態序列,因此先用極大似然估計也就是統計字頻,二元的轉移概率,即可得到轉移概率矩陣,初始狀態概率也通過統計字頻求得。接下來就是預測問題,則使用維特比算法求解。

  • 第一步:統計字頻,得到轉移概率矩陣以及初始狀態概率,觀測概率(不考慮多音字時認爲是1即可)

  • 第二步:平滑處理

  • 第三步:維特比算法

1、預處理、統計字頻以及二元字頻

       由於給定的是原始語料,含有不少符號以及數字,而且還有一二級漢字表6763個漢字中沒出現的不常見漢字,因此先對這些其他漢字、字符處理,並且將語料處理成一行一行的句子。
       大致看了新聞語料後發現,其中的有不少數字有不少日期,例如“在10月10日發射XXXXX”,若是隻是簡單的丟棄非6763個漢字中的其他內容,並且在該處斷句,會造成“日”字的在初始狀態出現的概率增大,覺得不合理。而且考慮到將初始概率的求解也統一到轉移概率當中。
       因此自定義了5種隱藏狀態,分別是START,END,NUMBER,OTHER_CHINESE,OTHER_SYMBOL。那麼初始狀態概率則成爲了P(w1|START)也記錄在轉移矩陣中,便於統計。

#此函數定義在src/newtrain.py中
def splitSentence(context, hz2py_dict):
	...
    subdata.append(START)
    for c in context:
        if isChinese(c):
            if hz2py_dict.setdefault(c,None) != None:
                subdata.append(c)
                before = c
            else:
                subdata.append(OTHER_CHINESE)
                before = OTHER_CHINESE
        elif isNumber(c):
            if before == NUMBER:
                continue
            subdata.append(NUMBER)
            before = NUMBER
		...
		...
		...
    return data

以上是劃分句子的部分代碼(自定義的5個狀態:START,在漢字表中爲’s’,對應拼音爲’start’,同理,END,‘e’,‘end’,NUMBER,‘n’,‘number’,OTHER_CHINESE,‘c’,‘other_chinese’,OTHER_SYMBOL,‘y’,‘other_symbol’)。
效果如下:

input:我是中國人,我25歲,在看《我和我的祖國》
output:
s我是中國人e  
s我nce  
sccy我和我cc國ye

統計字頻以及二元字頻
圖1、字頻詞頻統計
因爲在初始字之前插入一個"s"表示開頭,因此初始概率也統一到二元詞頻中。

2、平滑處理

       按我個人的理解,應該是爲了解決訓練集稀疏,低概率的狀態被認爲不可能發生的情況。在本次實驗中,使用了加一平滑以及固定lambda插值平滑。
       在統計字頻的時候發現,有250個字在給定的語料中從未出現過,使用了加一平滑,生成P(wi)的概率(因爲bigram插值平滑要用上unigram的概率),公式如下:
P(wi)=C(wi)+1j(C(wj)+1)=C(wi)+1jC(wj)+VP(w_i)=\frac{C(w_i) + 1}{\sum_j (C(w_j)+1)}=\frac{C(w_i)+1}{{\sum_j C(w_j)}+V}
隨後運用公式生成二元模型條件概率:
P(wiwi1)=C(wi1wi)C(wi1)P(w_i\mid w_{i-1})=\frac{C(w_{i-1}w_i)}{C(w_{i-1})}
λP(wiwi1)+(1λ)P(wi)P(wiwi1) \lambda P(w_i\mid w_{i-1})+(1-\lambda)P(w_i)\Longrightarrow P(w_i\mid w_{i-1})
圖二、未進行插值平滑時的轉移概率
圖三、進行插值平滑後的轉移概率

       其實還有這樣的一種情況出現,上述公式中的C(wi1)C(w_{i-1})爲0的時候,會導致分母爲0的情況,則可以取P(wiwi1,C(wi1)=0)=P(wi)P(w_i\mid w_{i-1},C(w_{i-1}) = 0) = P(w_i)

       不過在本次實驗中,爲了減少轉移矩陣的大小,取P(wiwi1,C(wi1)=0)=1VP(w_i\mid w_{i-1},C(w_{i-1}) = 0) = \frac {1}{V},而不是爲一整行每個都單獨存一個不同的數,而且對於例如圖中,“是”的那一整行的情況,代碼是這樣處理的,只對“是中”進行插值平滑處理,其他平分剩餘的概率值,寫成公式如下:
對於每一行:
1、若C(wi1)==0C(w_{i-1})==0,則:
Pwiwi1,C(wi1)=0=1VP{w_i\mid w_{i-1},C(w_{i-1} )= 0} = \frac {1}{V}
2、若C(wi1)!=0C(w_{i-1})!=0C(wi1wi)!=0C(w_{i-1}w_i)!=0,則:
P(wiwi1)=λC(wi1wi)C(wi1)+(1λ)P(wi)P(w_i\mid w_{i-1}) = \lambda \frac{C(w_{i-1}w_i)}{C(w_{i-1})}+(1-\lambda)P(w_i)
3、若C(wi1)!=0C(w_{i-1})!=0C(wi1wi)==0C(w_{i-1}w_i)==0,則
P(wiwi1)=1V0P(w_i\mid w_{i-1}) = \frac{1- \sum 上一公式計算出的概率}{V - 不爲0的個數}
在代碼中的實現如下:

#此函數定義在src/newtrain.py中
def createTransition(bigram_prob,unigram_prob,hz2py_dict, bigram_smooth):
    transition_prob = dict()
    transition_prob['default'] = {'default': math.log(1.0 / HANZI_COUNT)}#第一種情況
	...
	...
	...
                transition_prob[wi_1][wi] = math.log( bigram_smooth*math.exp(bigram_prob[wi_1][wi])\
                + (1 - bigram_smooth)*math.exp(unigram_prob[wi]))
                prob -= math.exp(transition_prob[wi_1][wi]) #第二種情況
	...
	...
	...
        transition_prob[wi_1]['default'] = math.log(prob) - math.log(HANZI_COUNT - exesist)#第三種情況

    return transition_prob

       若是完全按照插值平滑,太多不同的值,沒法壓縮,生成的轉移矩陣寫入到文件後將會達到1.3G的大小,讀文件很耗時,而且維特比算法運算時也耗時,所以對平滑處理進行了改進,完全是處於效率方面的考慮,在我的測試中似乎不僅是效率提高了,而且精準也有些提高,但這樣的平滑處理有沒啥影響我也不太清楚。

3、維特比算法

       最後就是維特比算法,按照《統計學習方法》書中所給的僞代碼自己實現了一遍,這裏並沒啥技術點存在,不在此贅述。有一點說一下,因爲我對於每個句子都加了’s’'e’標示開頭和結尾,因此初始狀態概率向量就是一個除’s’狀態爲1其他狀態都爲0的向量。可以更方便一些。

實驗效果和分析

       使用微信羣中共享的一份測試數據,該數據有8722行,共有84238個漢字,分別在不同的lambda情況共進行了15次預測,句子錯誤率平均在50.99%,字錯誤率平均值爲12.85%。

./data/input_test.txt	#拼音文件
./data/YUANSHI_test.txt	#漢子文件

效果差與好的例子

原文 預測結果
特朗普提名布魯耶特出任美國能源部長 特朗普提名補錄也特出任美國能源部長
雖然衝突的結果難免一拍兩散 雖然衝突的結果難免疫排兩三
其背後掩蓋的種種荒誕與黑暗 其背後掩蓋的重中黃石與黑暗
一名自殺式襲擊者在阿富汗東部楠格哈爾省地區的抗議活動期間引爆炸彈 一名自殺式襲擊者在阿富汗東部楠格哈爾省地區的抗議活動期間引爆炸彈

       第一個例子,應該說是人名未能正確預測,這類專有名詞,效果不好,或許需要基於詞的模型,並且配有知識庫,將達到更好的效果,但“特朗普”卻正確預測了,可能是訓練的語料中出現的比較多吧。
       第二個例子,將“難免一拍兩散”預測成了“難免疫排兩三”,可能是訓練用的語料中,免疫出現的比較多,而我們又是用的二元模型,只考慮前後兩個字,因此對於這種情況,可能基於字的三元模型即可解決
       第三個例子,我主要想說的是“石”,因爲這個是多音字,而我的模型並未考慮多音字的情況,查看訓練結果的時候我發現幾乎所有“dan”的音的字都預測成了“石”,這個問題需要改一下觀測概率,應該把多音字考慮上。
       第四個例子,至於爲什麼可以完美預測如此長的句子,不是很清楚。

不同平滑參數λ\lambda對預測效果的影響

在0.8到1的區間均分取了15個值

句子預測錯誤率

圖4、句子預測錯誤率

       15個λ\lambda下的預測錯誤率分佈在[50.66%,51.60%]之間,平均值爲50.99%,當λ\lambda取0.843時得到了最低的句預測錯誤率爲50.66%。

字預測錯誤率

圖5、字預測錯誤率

       15個λ\lambda下的預測錯誤率分佈在[12.79%,12.97%]之間,平均值爲12.85%,當λ\lambda取0.929時得到了最低的字預測錯誤率爲12.79%。

       從結果可以看出,似乎λ\lambda只要在[0.8,1]之間,似乎對預測準確度影響不是很大,也可能用於測試的拼音數據比較單一,而且和訓練用的語料類似,所以看不出什麼影響,但網上看別人資料說在0.95的時候效果比較好。

運行效率分析

模型加載大約需要6秒,根據不同\(\lambda\)生成整個轉移概率矩陣需要6秒,在這15次測試中預測用時平均爲1400秒,最差一次爲1500秒,也就是單個句子平均用時161毫秒,單個字平均用時16.7毫秒。維特比算法時間複雜度大概是O(tn2)O(t*n^2),不過由於轉移矩陣比較稀疏,實際應該更快一些。

收穫

對隱馬爾可夫更加熟悉了,其次則是使用動態規劃的維特比算法,在此次試驗中曾想過要如何來使用A算法,但看到樹上基本都是用動態規劃(算是A的一種特例),而且不知道怎麼定義評價函數。或許可以用更高效些的近似算法,減少搜尋的分支。然後是在真實輸入法中,應該會列出前K個候選,這樣的功能也可以考慮實現。

改進方案

  1. 基於字的三元四元模型,但據說增加耗時的同時,效果提升得並不多,可能不太建議;
  2. 基於詞的二元三元模型,應該可以有不錯的提升,後期打算嘗試;
  3. 另一種無腦的方法,利用比較火熱的深度學習,可以比較省事又效果好。

附:

目錄結構:
./src/
  --yuliao/
    ----sina_news/
    ----sina_news_gbk/
  --pinyinhanzibiao/
    ----一二級漢字表.txt
	...
  --model_data/
    ----transition_prob.json
    ...
  --newtrain.py	#預處理以及生成模型參數
  --predict.py	#維特比算法
  --compare.py	#比較不同lambda值的影響
  --main.py		#支持傳參 main.py ./data/input.txt ./data/output.txt
  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章