在構建了基于n-gram的糾錯檢錯模型之后,我們自然不能放過如今大紅大紫的神經網絡,鑒于神經網絡的靈活性和訓練的耗時性,我們在方法嘗試和模型訓練上花了很多時間,期間走過不少彎路,也因工業界大佬進行交流走了捷徑,總得來說,神經網絡的表現雖然沒有我預想的那么神奇,但也還是可以的。
本文介紹使用LSTM進行檢錯糾錯的幾個方案,需要注意的點以及和n-gram模型進行最終融合的嘗試。
整體分為四個部分:
- 備選詞生成(詞級語言模型)
- 句子檢錯模型:
- 序列標注模型
- 字級語言模型
- tricks和坑
- 模型融合
需要提醒的是,鑒于神經網絡的不穩定性以及數據分布的多樣性,因此在本文提出的方法不一定適用于讀者的數據,僅供參考。此外,為對項目進行保密,文中不會給出具體的準確率、F1數值,只會以好、差等程度詞表明模型結果。
備選詞生成(詞級語言模型):
Part 0 : 介紹
我們首先使用LSTM構造的是詞級語言模型,目的是為了取代從一對詞語搭配中獲取備選詞的方法,而檢錯方法還是使用n-gram。 LSTM構造的語言模型和n-gram語言模型本源是一樣的,都是通過前面的文本獲取下一字或詞的概率,只不過n-gram是基于統計的,只能人為設定有限的n,而LSTM能夠綜合一個詞前面所有詞的信息給出概率。
經過在項目通過各種角度使用語言模型,我總結出語言模型使用的三種思路:
- 檢測句子流暢度
- 檢測錯誤詞語
- 生成備選詞(句子)
這三個思路使用的語言模型的原理是一樣的,只是使用的角度不同。檢測句子流暢度是掃描整個句子,計算每個字/詞的概率而后乘起來(鑒于字詞概率都會遠小于1,導致乘起來后值過小,一般語言模型都會采用log概率,即計算出概率后再取log),將乘出來的數成為句子流暢度。檢測錯誤詞語就是通過處理前面的文本,計算下一個已知的詞的概率,若是概率低于閾值(閾值一般是人工設定),那么就可以標記成錯誤詞。而只要將下一個詞擴展成詞表,對詞表里每個詞都求一個概率,然后取出概率最大的詞,那么我們就可以做到備選詞生成了,若是不斷把概率最大的備選詞填入原本句子中再輸入語言模型,就可以做到句子生成了。
例如,對于一句話 "STA 我 今天 吃 蘋果 了 END":
- 檢測句子流暢度的話,就是計算 "STA"后是"我"的概率,【"STA", "我"】后面是"今天"的概率。。到 【"STA", "我", "今天", "吃", "蘋果", "了"】后面是"END"的概率,而后將所有概率取log乘起來,得到句子流暢度分數。
- 檢測錯誤詞語,同樣也是計算一遍所有詞語的概率,而后將他們放到一起,根據認為設定的閾值篩出概率較低的詞。
- 生成備選詞,則是首先掃描到例如 "STA 我 今天 吃",而后計算詞表中每個詞的概率,即 若是將一個詞接到前面掃描的部分的后面,那個詞的概率,若詞表有【"STA", "我", "今天", "吃", "蘋果", "了", "END"】這么些詞,那就是計算【"STA 我 今天 吃" + "STA"】的概率,【"STA 我 今天 吃" + "我"】的概率。。。,【"STA 我 今天 吃" + "END"】概率,每個加進去的詞都會得到一個概率,若是取概率最大的詞出來即可作為備選詞了。
Part 1 : 模型模塊詳述
在這個部分,我梳理一下常用的用神經網絡處理文本問題的流程,下圖是在lstm后接入詞表大小的全連接層的整體流程圖:
整個過程分為以下五個部分(省略預處理過程):
- tokenizing
- indexing
- embedding
- lstm
- 下游任務
接下來我將對每個部分進行介紹。
1. tokenizing
tokenizing,從詞義和上圖中就可以知道,就是分詞,而為什么單獨將它拿出來,則是因為它具有獨特的意義:
一:統計詞數、詞頻。通過分詞之后,我們能夠清楚地了解語料中總共有多少詞以及每個詞的頻率,那么我們就可以根據詞頻進行刪減,例如刪掉大量只出現幾次的自造詞、語氣詞或罕見詞,以此節省計算內存并減少需要計算的參數;或者將頻率過高的停用詞替換成統一的標簽,防止停用詞對模型造成影響。
二:構造 "詞"-> "索引"表。在分詞的過程中工具還會構造 "詞"-> "索引"表,這個表是做什么用的呢?要知道模型內部是不認字符的,只認數據,為了將詞轉換成模型可以處理的向量,就需要先將它們轉成索引,而后通過 "索引"->"向量"矩陣直接查到對應的embedding向量,就可以成功地把詞轉換成向量了。而我們也可以構造反轉表: "索引"->"詞",就可以在最后得到概率最大的詞的索引時查表得到詞的字符表示。
三:選擇粒度。在英文中,詞之間以空格分離,所以英文處理就沒有分詞方面的困惑(應該很少有人會把句子按字母粒度分開吧?)。而中文因為沒有空格區分,因此可能需要進行分詞的操作,然而因為中文的多義性,分詞的結果不一定能讓人滿意,有可能引入額外的錯誤使得模型跑偏,而字級粒度就可以很好地防止分詞錯誤的問題了。但詞級也同樣有其優點,鑒于錯誤很可能產生散串,詞級在檢測散串上更有優勢。選擇哪種粒度就見仁見智了,不過選擇的階段就是在tokenizing部分進行的。
2. indexing
indexing要做的事情很簡單,就是利用tokenizing階段產生的 詞-索引 表,把句子中的詞替換成索引,而這個階段也同樣有其額外的功能,那就是padding。
padding這個技巧在文本處理中十分常見,含義就是填充,將句子通過填充達到一定長度。原因很簡單:句子一般是不等長的,而矩陣要求每行元素等長,這個矛盾的解決那就是通過引入padding,通過在不等長的句子后面補數據(一般補0),從而把indexing后的句子補到統一的長度(稱為max length),這就可以以batch進行訓練了。當然,如果你每次只用一個句子訓練,那就不用padding了,不過速度就可想而知了。那么有人可能會說:padding最起碼會加長計算時間吧,如果是雙向LSTM,還應該會影響訓練效果吧,可以盡可能消除padding的影響嗎?答案是可以的,起碼在tensorflow中,就可以用dynamic rnn,通過sequence length參數將batch里每個句子的長度傳進去,就可以實現每個句子不等長訓練了(不過句子在傳進去之前還是要padding成同一長度的)。
3. embedding
前面提到,模型不認字符,只認數字,那么我們就需要將字詞轉換成向量(至于為什么不是數字,那是因為單個數字所能保存的信息量太少,遠不如多維向量能表示的信息多)。而這個向量,可以通過隨機初始化而后自己訓練,也可以借用其他人訓練好的embedding再進行磨合(不過慎用,tricks和坑部分會談到)。常用的embedding自然是word2vec了,不過近來不斷有新的out-performing的embedding出現,如ELMO、Google的大殺器BERT。
4. lstm
這個階段就是進行語言模型的建模了,LSTM每階段有兩個向量:hidden state和output,因hidden state中包含從開始到最后語言模型的記憶,所以我們選擇使用final hidden state作為該階段的輸出物,使用hidden state實際上就是將前面所有詞的信息(但有些信息會被模型遺忘)用一個向量表示(就是seq2seq的Encode階段),而后就可以用這個濃縮的向量進行各種下游任務了。
5. 下游任務
下游任務的定義其實頗為靈活,若是對于訓練embedding為主的模型,那么下游任務就是在embedding后的模型層,而對于我們的這個語言模型,下游模型層自然是lstm后的網絡層,具體就是詞表大小的全連接層。
雖然我們在前文講到語言模型的三種用法的時候是先提到計算單個詞語概率以此檢測錯誤詞語,再擴展到計算整個詞表概率以此生成備選詞,但三種用法的模型層都是一樣的,都是詞表大小的全連接層。對于檢測錯誤詞語的用法,只需要在詞表所有的概率中取出要檢測的詞的概率即可。
Part 2 : 文本獲取與處理
在這個模型中,我們使用的語料和上一篇文章(基于n-gram的糾錯檢錯模型)一樣:
- 網上獲取的搜狗通用語料
- 金融新聞語料
- 人工糾正語句
語料處理過程的一部分也如前一篇文章那樣,分詞-數字轉星號-長句切短句-加入STA、END標簽。
而后我們統計了語料中的詞頻并將過低的詞以UNK替換,并刪去過高的詞。
Part 3 : 模型構建及評價
模型訓練工具包上,鑒于筆者當時是首次用神經網絡處理實際項目問題,先前都是用tensorflow跟著教程或寫寫小demo,因此為了效率和準確使用了封裝較好的keras,而keras當時的版本沒有dynamic rnn,因此padding后的整個句子都會被lstm處理。
將模型在語料上滿懷期待地進行訓練過后,因為備選詞生成沒有標準答案,只能人工判斷是否正確,于是我開始輸入不完整的句子讓模型給出備選詞來檢測模型結果,然而遺憾的是模型生成的基本都是牛頭不對馬嘴的詞,或者是為了通用性用了很常見的詞。嘗試了數套參數和配置之后結果都差不多,我開始感覺到實際工業界和學術界似乎有些不同,但又不確定是否是我本身實現的問題,因此暫定備選詞仍然采用依存句法提取的詞語搭配,開始轉為使用神經網絡做句子檢錯。
Part 4:模型轉移,從備選詞生成到詞級檢錯
上文提到,備選詞生成和錯誤檢測使用的模型是一樣的,只是使用模型的方法不同。將詞級模型從備選詞生成轉換成錯誤檢測并沒有花太大的功夫,在LSTM中從只取final hidden state變成每個time step的hidden step都要取,在原本給出的整個詞表中根據概率取概率最大的詞變成根據索引取特定詞的概率。然而模型在檢錯方面的準確率,F1等指標表現很差,遠不如n-gram的分數。鑒于經驗不足,筆者在嘗試了幾套參數和配置之后開始嘗試下一個模型,至于可能的表現不足的原因,筆者在后面會給出說明。
序列標注模型
Part 0 : 介紹
先前的使用詞級語言模型的思路是師兄給出的,在嘗試詞級語言模型效果不理想之后,筆者提出可以將檢錯問題視作序列標注問題,正確的標0,錯誤的標1,將句子詞或字序列輸入模型后根據預測值得到錯誤字詞。在閱讀論文之后,筆者得知這也是文本檢錯的一個常規思路,也是很符合直覺的。
序列標注模型實際上就是個多分類模型,當問題是檢錯的時候就是個二分類模型,它和其它數據型分類模型的區別在于構建特征的時候需要考慮時序特性,就是利用LSTM將文本序列信息嵌入到特征向量中。
Part 1 : 模型模塊簡述
將序列模型的模塊和語言模型的對比我們可以發現,兩個模型還是有些區別的,區別在embedding后面的兩個模塊。
在lstm上序列標注模型使用了雙向LSTM,在取hidden state的時候并不是僅取最后的hidden state,而是將每個階段的hidden state均取出,至于forward和backward的hidden state是sum還是concat就見仁見智了。
最后的全連接層使用的是兩個神經元,將每個time Step的hidden state作為特征預測這個詞是否有錯。
Part 2 : 訓練集處理與構造
使用序列標注模型,訓練集就不能像使用語言模型那樣直接把整個句子丟進去就行,需要獲取標注好的語料或者人為構造標注好的訓練語料。至于該模型的訓練集獲取和構造,詳見拼音型簡單錯誤語料獲取與處理一文。
Part 3 : 模型效果評價
模型訓練既然不必多言,在實際跑出來之后進行測試,效果仍是慘不忍睹。而后筆者再搜羅各種檢錯糾錯的論文閱讀,從其中挖掘了使用序列標注模型時可以改進的方向和常用的方法,在正欲嘗試改進時發生了一些事情(見"字級語言模型"Part 0),使得筆者留下序列標注改進這么個坑,構建新的模型去了。
Part 4:可擴展方向
bilstm-crf模型
經過閱讀論文筆者發現,序列標注模型,尤其檢錯糾錯的序列標注,最常用的是bi lstm-crf模型,通過lstm對每個字的每個類給出分數,將所有分數輸入crf模型中,求解概率最大的標注序列。
加入詞性特征和拼音特征
拼音特征的引入是為了更好地識別同音字錯誤,而詞性特征的引入和語法正確性相關,筆者認為還跟錯誤產生的散串有關。散串理論認為當句子中有錯誤字、詞時,分詞后的句子會產生不協調的散串,這些散串以單字分開,它們和前后無法構成詞因此獨自成詞,而它們本身單獨成詞很少見或者無法單獨成詞。根據這個理論,當句子中產生散串時,它們的詞性序列就會很異常,從而被模型捕捉并識別。
字級語言模型
<span id="jump">Part 0 : 介紹</span>
在評估完一號版本的序列標注模型后,我有幸參加了MSRA-SCUT機器學習夏令營,并在夏令營最后一日前往CVTE參觀。在參觀途中,我有幸和CVTE的自然語言部門的技術負責人就語音識別后文本糾錯領域進行一對一交流,在交流過程中他告訴我,經過他們在工業界的實驗,效果最好的是使用雙向LSTM的字級語言模型,而生成式模型也僅限于在學術論文中好看了,在實際工業界的效果并不好。
經過大佬的指點,我們又重回語言模型檢錯方案,只是這次使用字級粒度,在實驗過程中,盡管在大方向上有了大佬的指導,但還是踩了不少坑,在后面的"tricks和坑"部分會詳述。
Part 1:模型模塊簡述
字級語言模型的模塊有些像前面兩個模型模塊的組合,在LSTM方面使用雙向LSTM,取每個階段的hidden state,然后把hidden state作為特征進行詞表大小的概率預測,根據索引查找對應詞的概率,并和閾值比較判定是否認為出錯。
Part 2:訓練集構造
訓練集和詞級類似,但為了嘗試不同語料對模型的作用,筆者又獲取了金融領域的保險推銷對話、電商平臺客服對話,并分別構造訓練集用于構造不同的模型。
語料的處理方式也和前文詞級語言模型提到的一樣,區別在于不需要進行分詞處理,直接一字一斷,不過要注意不要把STA、END、UNK標簽截斷了。
Part 3:模型構建及評價
經過前兩個模型實現的訓練,筆者對神經網絡實際應用開發有了進一步了解,這次字級語言模型就使用tensorflow1.2開發了,經過艱苦的debug以及嘗試各種配置的過程,最終模型跑出來結果還不錯,算是有了份可以見人的成果了,起碼在檢錯效果上將網上大廠公布的語言模型和檢錯模型能比下去。
Tricks 和 坑
在這個部分,我將總結前面幾個模型構建和訓練過程中使用的一些tricks和踩過并意識到的坑(可能還有沒意識到的)。
tricks
tensorflow使用方面的tricks 見文章 tensorflow 文本序列檢錯的tricks (0)
數據盡可能保存成文件
神經網絡的訓練本身就已經很耗時間了,我們不希望前期的數據處理再占用大量的時間,如果不保存成文件,那么模型每次在訓練前都需要對語料進行處理,想想就耗時。
鑒于筆者采用的是tensorflow1.2,還不支持各種高級的DatasetAPI,因此各種數據以文本形式保存下來,其中涉及到的數據以及形式有:
- 詞-索引表。保存了這個表,那么模型就不需要再將整個語料過一遍以獲取完整詞表,此外我們還可以根據詞表大小進行詞刪減等操作。
- embedding。這個不必說,必定要保存的。
- padding后的句子索引序列。 在前期處理階段就將語料轉換成序列(此時最好保存成一份文件),而后對不等長序列進行padding,補到最大長度,保存補后的索引序列,那么模型之后就能夠直接讀入并構造矩陣了。而前面小括號內推薦額外保存不等長序列是為了之后想要修改最大長度或padding使用的數據會更方便。
大佬指點
在與CVTE的大佬交流后,筆者還獲取了一些tricks,不論用沒用到,在此分享出來:
- 神經網絡和n-gram同樣也僅能捕獲句子的簡單詞語錯誤,因為短句子稍有些錯就很難還原原本的意思。
- 句子是長是短沒什么影響,按逗號句號把一個長句子劃分成短句子的方向沒問題。
- 用雙向lstm,候選集詞語挑選用搜索引擎的ranking技術。
- 加速訓練方面:
- 富采樣
- batch normalization
- 多顯卡下用顯卡并行加速訓練
- 提高模型準確率的方法(比賽中常用的技巧):
- 先設置embedding不動而后訓練模型參數,然后設置參數不變訓練embedding,只進行一輪
坑
訓練數據和測試數據同分布
訓練數據的測試數據盡可能同分布,在文本中就是指詞語的組合方式、用詞、錯誤類型都差不多,例如都是電話中的對話,都是拼音類型為主的錯誤等等。
這在學術界看來再正常不過,畢竟數據集都是純粹的,直接隨機劃分訓練集驗證集測試集即可。在工業界也簡單,直接雇傭大量人手從要檢測的大量語料中先糾一部分作為訓練語料就行。然而苦的是夾在學術界和工業界之間的我們,既沒有學術界那種那么純粹而大量的數據,又沒有工業界的財力可以先"人工"再"智能",于是只能人工糾正一小部分,再靠網絡搜集盡可能相似分布的數據。然而經過實驗表明,哪怕最好的相似數據,效果也不如人工糾正的那小部分數據。
embedding不要亂用
筆者之前很大一段時間都是直接使用其他人發布的已經預訓練過的embedding,認為只需要在他們訓練好的基礎上針對我們的語料進行更新即可,然而現實給了我個下馬威,在將預訓練embedding換成隨機初始化embedding之后,模型的準確率和F1瞬間上了一個臺階。在擁有足夠數據的情況下,或者哪怕數據量不是那么夠,也盡可能自己訓練embedding吧,領域不匹配的話,其他人預訓練好的embedding對你的模型來說就是毒藥。
檢錯模型評價
在檢錯模型評價環節,筆者將對前面提到的三個模型進行對比,分析優劣并給出評價。
比較分為兩個部分:
- n-gram VS lstm
- 序列標注模型 VS 語言模型
- 詞級粒度 VS 字級粒度
n-gram VS lstm
在實際進行項目之前,筆者一直受到神經網絡鼓吹者的影響,感覺神經網絡仿佛已經萬能了,傳統機器學習方法都很過時,效果都遠不如神經網絡。然而,現實告訴我,方法在準確率上是和冷門熱門無關的。現在轉頭一看,n-gram和lstm實際上各有優劣。
我們來看看n-gram和lstm相比的優勢:
- n-gram在訓練測試同分布的要求較低。使用搜狗通用大規模語料訓練出來的n-gram語言模型表現已經不錯了,而同樣的大規模語料在lstm上卻是慘不忍睹,因其和測試語料的分布不同,導致模型無法學習到需要學習的特征。
- n-gram在散串上的識別很強。同樣是詞級粒度,當文本中出現錯誤導致產生若干散串之后,n-gram能夠很敏捷地察覺出來,然而LSTM較難做到。
- n-gram解釋性強,原理清晰。這個自然不必多言,從理論到實際使用上經歷許多年許多人的磨礪,這個語言模型早已身經百戰。
然而LSTM也有其優勢的,不然也無法紅火到今日:
- 人工參與較n-gram少,畢竟神經網絡最大的優勢就在于特征自動學習,再加上如今各種高度抽象的深度學習工具包,只需要寥寥數行代碼就可以構建一個神經網絡。
- 語料數目要求不高。寫到這里筆者內心其實也有些猶豫,畢竟神經網絡本身要求數據量要足夠大才能玩得轉,然而經過實驗,層數較少的神經網絡在語料較少而分布接近的情況下也同樣表現很好,而基于統計的n-gram若想要得到同樣表現,就需要數倍數十倍于LSTM的語料用于訓練了。
序列標注模型 VS 語言模型
首先用訓練集進行對比:
- 序列標注模型語料需要使用標注好的數據
- 語言模型需要使用正確的數據
高下立判。序列標注模型需要標注好的數據才可以訓練,對學術界而言自然不是問題,但對于工業界和夾在工業學術之間的我們顯然不希望在構建數據上花費大量時間和精力。而語言模型只需要使用正確的句子就可以,而正確的句子在網絡上到處都是,Wikipedia、百度百科、知乎什么的,一爬一大堆,雖然還是要注意分布相似性問題,但序列標注模型也同樣需要考慮。因此語言模型在訓練數據準備上無疑優于序列標注模型很多。
而后根據模型復雜度進行比較:
- 序列標注模型最后的全連接層只需要和類別數目相同的神經元,在本文中即兩個神經元,分別代表正確和錯誤。
- 語言模型最后的全連接層需要詞表大小的神經元。
在模型復雜度方面序列標注模型要占優勢,畢竟詞表少說也有上千上萬個,需要更新的參數數目瞬間就多了幾個量級。
最后將模型用途進行比較:
- 序列標注模型僅能用于標注。
- 語言模型可以用于檢測句子流暢度、檢測錯誤詞、生成備選詞。
用途一比,語言模型又大勝,序列標注模型僅專注于標注,而語言模型可以一模型多用,最后全連接層使用詞表大小的神經元大大增加了語言模型的能力。
如此對比一看,序列標注模型或許更適合在學術界使用,畢竟他們擁有大量純粹的數據,而語言模型更適合工業界使用超大量語料訓練,得到一個核彈級多功能通用模型。
字級 VS 詞級
在大佬的指點下我們從詞級語言模型到字級語言模型,使用發現效果很不錯之后,我們回頭開始比較詞級和字級的區別,分析為什么字級模型優于詞級。
- 字級詞表較詞級小。顯然,中文常用字就那么幾千個,因此字級若再壓縮的話可以將詞表控制在兩三千的規模。而詞級因組合方式多樣,再加上專有名詞、成語等因素存在,導致常用詞的詞表都可以輕松達到上萬規模。詞表越大,需要更新和計算的參數越多,消耗的內存空間也越大,而詞級的詞表因規模大,更可能稀疏,致使效果不好。
- 詞級需要分詞,分詞不總是完美的。在使用詞級模型時,我們需要對語料進行分詞處理,在分詞工具上使用較為廣泛使用的jieba分詞,但分詞工具總會有不完美的地方,尤其是作用在對話的文本中,其中包含大量重復和倒裝的結構,導致模型在訓練階段就受到不少干擾。
然而,詞級模型還是有它的優勢的,在備選詞生成上,字級模型生成備選詞的效果就不如詞級。
檢錯模型融合
在獲取了n-gram詞級語言模型和LSTM字級語言模型后,我們思考能否將它們融合起來以獲取更好的效果。經過實驗,我們將句子分別輸入兩個模型,將詞語的分數標記給詞中每個字,而后取兩個模型給出分數的最高值,再和閾值比較高低判定是否可能出錯。
句子糾錯
在檢錯方面得到一定成果之后,我們開始結合檢錯和備選詞生成進行句子糾錯,其核心思想用一句話來描述就是:
用語言模型檢錯 -> 備選詞替換 -> 語言模型檢測句子流暢度,挑選流暢度最高的句子。
雖說一句話就能將核心思想描述了,但內部還是有不少細節的。
在語言模型方面采用的是融合之后的n-gram + 字級LSTM,備選詞生成采用依存句法提取的詞語搭配對,流暢度檢驗使用了百度訓練的語言模型。
在使用語言模型檢錯之后,我們還需要將句子分詞,獲取檢錯模型認為錯誤的詞語的前置詞,而后用前置詞根據詞語搭配得到若干備選詞,將備選詞替換錯誤詞后得到若干句子,包括原始句子和幾個不同備選詞替換后的句子,將這幾個句子輸入語言模型中獲取句子流暢度,選擇流暢度最高的句子作為最后輸出。
最后使用語言模型檢測句子流暢度這一操作,能夠非常有效地把在檢錯階段對全對的句子的誤判消除,挑選備選詞替換后的句子流暢度都不如原始的句子。這種召回機制能夠有效防止正確句子經過模型之后反而變成錯誤的了。
感悟
這是我進入大學后歷時最久也是最大的項目,在這個項目進行的幾個月中,我從一個對機器學習只了解皮毛的大二半新生,變成對機器學習稍有了解的大三老油條。這個項目使我離實際工業界進了一步,使我明白了不能只看最新最火技術,傳統技術同樣有可取之處的道理,也使我對自然語言處理這個領域有進一步了解和興趣,或許還要沿著這條路繼續向下走。
更多實戰文章歡迎關注微信公眾號【AI實戰派】
我的個人博客:Zedom1.top