允中 發自 凹非寺
量子位 報導 | 公眾號 QbitAI
編者按:
語言模型的身影遍布在NLP研究中的各個角落,想要了解NLP領域,就不能不知道語言模型。
想要讓模型能落地奔跑,就需藉助深度學習框架之力,Tensorflow、PyTorch自然是主流,但在Dropout都成獨家專利之後,不儲備「B計劃」,多少讓人有些擔驚受怕
這裡有一份飛槳(PaddlePaddle)語言模型應用實例,從基礎概念到代碼實現,娓娓道來,一一說明。現在,量子位分享轉載如下,宜學習,宜收藏。
剛入門深度學習與自然語言處理(NLP)時,在學習了 Goldberg 特別棒的入門書 NN4NLP,斯坦福 cs224n 等等後,也無限次起念頭,寫個系列吧,但都不了了之了。
近來,NLP 領域因為超大預訓練模型,很多研究需要耗費大量計算資源(比如百度新發布持續學習語義理解框架 ERNIE 2.0,該模型在共計 16 個中英文任務上超越了 BERT 和 XLNet,取得了 SOTA 效果),這樣的項目基本上就是在燒錢,小家小戶玩不起,於是就傻傻地等著大佬們發出論文,放出代碼,刷新榜單。不過這也意味著一個總結的好機會,加上額外的推動,便重新起了念頭。
這個系列會介紹我認為現代 NLP 最重要的幾個主題,同時包括它們的實現與講解。
這裡會使用的百度的開源深度學習平臺飛槳(PaddlePaddle),關於這點,有如下幾個原因。
首先,不久前和一個科技媒體朋友聊天,因為當時封鎖華為事件的原因,聊到了美國企業是否可能對我們封鎖深度學習框架,比如說主流的 Tensorflow 和 Pytorch,我當時答是說不定可能呢,畢竟谷歌連 Dropout 都能去申請專利。只要之後改一下許可,不讓使用這些框架的更新,估計我們也沒辦法,於是就想著可以了解一下國內百度的框架飛槳。
去飛槳的 PaddleNLP 看了一下,內容很豐富,感覺飛槳對 NLP 這塊支持非常好,值得關注。
項目地址:
https://github.com/PaddlePaddle/models/tree/develop/PaddleNLP
語言模型
現代 NLP 領域的一個核心便是語言模型 (Language Model),可以說它無處不在,一方面它給 NLP 發展帶來巨大推動,是多個領域的關鍵部分,但另一方面,成也蕭何敗也蕭何,語言模型其實也限制了 NLP 發展,比如說在創新性生成式任務上,還有如何用語言模型獲得雙向信息。
那到底什麼是語言模型?
什麼是語言模型
就是語言的模型(認真臉),開個玩笑,語言模型通俗點講其實就是判斷一句話是不是人話,正式點講就是計算一句話的概率,這個概率值表示這個本文有多大概率是一段正常的文本。
對於一句話,比如說用臉滾出來的一句話:「哦他發看和了犯點就看見發」,很明顯就不像人話,所以語言模型判斷它是人話的概率就小。而一句很常用的話:「好的,謝謝」,語言模型就會給它比較高的概率評分。
用數學的方式來表示,語言模型需要獲得這樣的概率:
其中 X 表示句子,x1,x2… 代表句子中的詞。怎麼計算這樣一個概率呢,一個比較粗暴的方法就是有個非常非常大的語料庫,裡面有各種各樣的句子,然後我們一個個數,來計算不同句子的概率,但稍微想想就知道這個方法不太可能,因為句子組合無窮無盡。
為更好計算,利用條件概率公式和鏈式法則,按照從左到右的句序,可以將公式轉換成:
題變成了如何求解:
怎麼根據前面所有的詞預測下一個詞,當然這個問題對於現在還有點複雜,之後可以用 RNN 模型來計算,但現在讓我們先假設對於一個詞離它近的詞重要性更大,於是基於馬爾可夫性假設,一個詞只依賴它前面 n-1 個詞,這種情況下的語言模型就被稱為 N-gram 語言模型。
比如說基於前面2個詞來預測下一個詞就是 3-gram (tri-gram) 語言模型:
細心些的話,會發現,當 n-gram 中的 n 增大,就會越接近原始語言模型概率方程。
當然n並不是越大越好,因為一旦n過大,計算序列就會變長,在計算時 n-gram 時詞表就會太大,也就會引發所謂的 The Curse of Dimension (維度災難) 。因此一般大家都將n的大小取在3,4,5附近。
早期實現:數一數就知道了
最早了解類似語言模型計算概率,是在研究生階段當時號稱全校最難的資訊理論課上,老師強烈安利香農的經典論文 A Mathematical Theory of Communication,論文中有一小節中,他就給利用類似計算上述語言模型概率的方法,生成了一些文本。
其中一個就是用 2-gram (bi-gram) 的頻率表來生成的,這已經相當於一個 bi-gram 語言模型了。
同樣,要構建這樣一個 n-gram 語言模型,最主要工作就是,基於大量文本來統計 n-gram 頻率。
當時有個課程作業,就是先準備一些英文文本,然後一個一個數 n-gram,之後除以總數算出語言模型中需要的概率估計值,這種方法叫 Count-based Language Model。
傳統 NLP 中搭建語言模型便是這樣,當然還有更多技巧,比如平滑算法,具體可以參考 Jurafsky 教授的書和課。
但這種方法會有一個很大的問題,那就是前面提到的維度災難,而這裡要實現的神經網絡語言模型(Neural Network Language Model),便是用神經網絡構建語言模型,通過學習分布式詞表示(即詞向量)的方式解決了這個問題。
語言模型能幹什麼
不過在談神經網絡語言模型前,我們先來看看語言模型的用途。
那它有什麼用呢,如之前提到,語言模型可以說是現代 NLP 核心之一,無處不在。比如說詞向量,最早算是語言模型的副產品;同時經典的序列到序列(seq2seq) 模型,其中解碼器還可以被稱為,Conditional Language Model(條件語言模型);而現在大火的預訓練模型,主要任務也都是語言模型。
在實際 NLP 應用中,我認為能總結成以下三條:
第一,給句子打分,排序。先在大量文本上訓練,之後就能用獲得的語言模型來評估某句話的好壞。這在對一些生成結果進行重排序時非常有用,能很大程度地提高指標,機器翻譯中有一個技巧便是結合語言模型 Loss 來重排序生成的候選結果。
第二,用於文本生成。首先其訓練方式是根據前面詞,生成之後詞。於是只要不斷重複此過程(自回歸)就能生成長文本了。比較有名的例子就包括最近的 GPT2,其標題就叫 「 Better Language Models and Their Implications.」 它生成的句子效果真的非常棒,可以自己體驗一番https://talktotransformer.com/.
第三,作為預訓練模型的預訓練任務。最近很火的預訓練模型,幾乎都和語言模型脫不開關係。
比如說 ELMo 就是先訓練雙向 LSTM 語言模型,之後雙向不同層向量拼接獲得最後的 ELMo詞向量,還有 BERT 裡最主要的方法就是 Masked Language Model (遮掩語言模型)。
而最近的 XLNet 中最主要訓練任務也叫做 Permutation language Model (排列語言模型),可見語言模型在其中的重要性重要性。
神經網絡語言模型架構
接下來簡單介紹一下這裡要實現的網絡結構,借鑑自 Bengio 的經典論文 A Neural Probabilistic Language Model 中的模型。
這裡我們訓練Tri-gram語言模型,即用前面兩個詞預測當前詞。
於是輸入就是兩個單詞,然後查表取出對應詞向量,之後將兩個詞向量拼接起來,過一個線性層,加入 tanh 激活函數,最後再過線性層輸出分數,通過 softmax 將分數轉換成對各個詞預測的概率,一般取最大概率位置為預測詞。
用公式表達整個過程就是:
整個結構非常簡單,接下來就來看看如何用 飛槳來實現這個結構吧,同時介紹以下 飛槳的基本思想,和一般訓練流程。
項目地址:
https://github.com/PaddlePaddle/models/tree/develop/PaddleNLP/language_model
PaddlePaddle代碼基本實現
這裡拿一個小例子來解說,假設我們在一個叫做 PaddlePaddle 的世界,這個世界的人們只會說三句話,每句話三個詞,我們需要建立一個 Tri-gram 語言模型,來通過一句話前兩個詞預測下一個詞。
關於整個流程,主要分成準備,數據預處理,模型構建,訓練,保存,預測幾個階段,這也是一般一個 NLP 任務的基礎流程。
準備
首先,先導入需要的庫。
之後準備訓練數據與詞表,統計所有不同詞,建立詞表,然後按照順序建立一個單詞到 id 的映射表和配套的 id 到單詞映射表。因為模型無法直接讀這些詞,所以需要單詞與 id 之間的轉換。
準備好數據後,設置模型參數和訓練相關參數,因為任務很簡單,所以參數都設很小。
數據預處理
根據 PaddlePaddle 數據輸入要求,需要準備數據讀取器 (reader),之後通過它來讀取數據,對輸入數據進行一些前處理,最後作為 batch 輸出。
構建模型
這裡從飛槳中較底層 API 來進行構建,理解更透徹。先創建所需參數矩陣,之後按照前面的公式來一步步運算。
先根據輸入的獨熱(one-hot)向量,取出對應的詞向量,因為每個例子輸入前兩個詞,因此每個例子可獲得兩個詞向量,之後按照步驟,將它們拼接起來,然後與 W1 和 b1 進行運算,過 tanh 非線性,最後再拿結果與 W2 和 b2 進行運算,softmax 輸出結果。
接下來構建損失函數,我們用常用的交叉熵(cross-entropy)損失函數,直接調 API。
開始訓練
終於進入了訓練環節,不過為了更好理解,先稍稍介紹一點 飛槳的設計思想。
飛槳同時為用戶提供動態圖和靜態圖兩種計算圖。動態圖組網更加靈活、調試網絡便捷,實現AI 想法更快速;靜態圖部署方便、運行速度快,應用落地更高效。
如果想了解飛槳動態圖更多內容,可以參考GitHub項目地址:https://github.com/PaddlePaddle/models/tree/v1.5.1/dygraph
實際應用中,靜態圖更為常見,下面我們以靜態圖為例介紹一個完整的實現:
首先,需要先定義 Program,整個 Program 中包括了各種網絡定義,操作等等,定義完之後,再創建一個 Executor 來運行 Program,用過類似框架的同學應該並不陌生。
因此先來看看這兩行代碼,fluid 中最重要的兩個 Program,將它們取出來。
default_startup_program 主要定義了輸入輸出,創建模型參數,還有可學習參數的初始化;而 default_main_program 則是定義了神經網絡模型,前向反向,還有優化算法的更新。
之後將之前定義好的一些模塊放入訓練代碼中。
簡單解釋一下代碼,訓練時需要exe.run來執行每一步的訓練,對於run需要傳入主程序,還有輸入 Feeder,和需要拿出來(fetch)的輸出。
之後運行就能看到訓練 log 了。
能明顯看到 loss 在不斷下降,等訓練完成,我們就獲得一個訓練好的模型。
保存模型
在預測前可以嘗試先保存一個模型,可以便於之後使用,比如 load 出來做預測。
很簡單,只需要傳入保存的路徑』./model』,預測需要 feed 的數據』input』,之後需要 fetch 出的預測結果 prediction,最後加上執行器 exe,就 OK 了。
非常快。
預測階段
預測階段其實和訓練階段類似,但因為主程序都保存下來了,所以只用先建立執行器 Executor,同時建立一個用於預測的作用域。
然後在預測作用域中 load 出模型,進行預測運算,大部分操作都和訓練很類似了。唯一不同就是 load 模型這塊,其實就是把之前保存下來的參數給 load 出來了,然後用於預測。
結果如何?
模型完美地學習到了 PaddlePaddle 世界中僅有的幾個 trigram 規則,當然因為該任務非常簡單,所以模型一下就能學會。
更多嘗試
在了解完以上這個小例子之後,就能在它基礎上做很多修改了,感興趣的同學不妨拿下面的幾個思路作為練習。
比如說用一個大數據集,加上更大模型,來進行訓練,可以嘗試復現 Bengio 論文中的模型規模,大致結構差不多,只是修改一下參數大小。
其實還可以在這裡嘗試些小技巧,比如共享詞向量表為 softmax 前全連接層的權重 W2,以及加入 Bengio 論文中提到的類似殘差連接直接將 embedding 連到輸出的部分。
這次在這裡介紹神經網絡語言模型,並通過 飛槳來實現了一個簡單的小例子,主要想做的是:
第一,語言模型任務在 NLP 領域很重要,想首先介紹一下;
第二,Bengio 這篇神經網絡語言模型的論文非常經典,比如說提出了用神經網絡實現語言模型,同時還最早提出詞表示來解決「維數災難」問題,通過復現,也好引出之後詞向量,還有seq2seq 等話題;
第三,通過用 飛槳來實現這樣一個簡單例子,可以拋開各種模型與數據複雜度,更直觀了解一個飛槳程序是如何構建的,也為之後講解飛槳更複雜程序打下基礎。