本篇整理了五個通用的小訣竅能讓自然語言模型學得更好。由於最佳化模型之前我們需要先有一個模型,所以接下來我會建立一個經典的小模型作為範例,並且將訣竅一個一個套用到該模型上。在有限的篇幅內,我會盡量介紹這些訣竅特別擅長與不擅長處理哪些任務。
建立模型
我選擇使用由左至右生成句子的自然語言生成模型。相比其他更為複雜的生成句子的做法,由左至右生成是很經典且最容易實現的,效果也沒有明顯比較差。同時我使用 Transformer 作為基底打造模型,因為 Transformer 可平行化的程度更高,運算效率也明顯更傑出。我將模型、資料前處理與資料放在Github,讀者可以自行下載並練習。
自然語言生成模型內部可以分成兩個結構:編碼器 Encoder 與解碼器 Decoder。編碼器的功能是提供解碼器所需要的「碼(code)」,協助解碼器生成輸出。code 是一種媒介,意思是包含資訊的一組數;解碼器的功能是根據 code 生成句子,模型所講出來的句子正是解碼後的結果。只要是自然語言模型就一定會有一個解碼器,舉個例子:在中翻英的任務中,我們可以設計一個編碼器讀取中文的句子,並輸出一段 code ,接著將這段 code 輸入解碼器,令解碼器根據這段code 輸出相對應的英文句子。如下圖:
自然語言生成模型不一定會需要編碼器。如果我只是單純想要模型能夠學到輸入資料分佈,那麼只有解碼器就夠了。在沒有編碼器的情況下,解碼器就會根據本身的參數抽樣得到解碼器覺得像句子的東西。讓解碼器講句子的同時更新參數,直到講出來的句子的機率分佈跟輸入資料的機率分佈相似。我的最佳化模型參數的方法是Maximize Log Likelihood (MLE),也是經典且最有效率的方法之一。
訣竅一:跟著老師走
我們繼續剛才的中翻英例子,由於模型一開始就懂說中文句子的機率幾乎為零,這造成了一個困擾:模型總是在錯誤的前提下更新參數。實際一點的說法是,模型的第一個字可能就說錯了。但輸出第二個字時需要第一個字。所以學好第一個字之前,第二個字很難學好,以此類推,句子越長問題越大。這個問題會延長訓練時間,常見的解決方法是把正解輸入到解碼器內,但是不能讓解碼器看到正解。該作法叫做Teacher forcing,流程如下圖:
這樣一來,該神經元就不會受到錯誤的前提所干擾,可以直接學在特定前文下,應該要輸出什麼文字。使用這個訣竅時一定要確保每一個時間點 (time step) 的輸出神經元,只看得到該時間點之前的輸入。如果可以看到下一個時間點的輸入的話,那麼模型就會偏好直接把正解複製到輸出,而不是學習如何輸出句子。
訣竅二:詞嵌入 Word Embedding
文字 (word) 對於自然語言生成模型而言只是一個代號,而且自然語言模型無法像人類一樣,從字面理解文字之間的意思。為了讓自然語言模型了解文字與文字之間的關係,我們把文字定義成一個在多維空間中的向量,並且用向量差來描述文字之間的關係。舉個例子,假設我們想讓自然語言模型知道「國家」與「首都」兩類文字之間的關係,我們可以把這兩類文字放在一個多維空間中如下圖:
代表文字的向量我們稱之為詞嵌入 (word embedding) 。詞嵌入可以從很多不同的方法獲得,例如人為給定、讓自然語言模型自己學。讓自然語言模型自己學可能不是一個好選擇,因為根據訓練方法的不同,模型的詞嵌入不一定能夠得到完善的訓練。例如翻譯模型可能學不好詞嵌入,因為相較於單一語言,雙語句子的數量少很多,或是需要投入更多的成本才能擁有一樣的句數。而句子的數量對於詞嵌入的品質有很大的影響。
詞嵌入預訓練 Embedding pre-training
目前最有名也最易用的詞嵌入預訓練是Word2Vec,由Mikolov發表於2013年。他強調訓練詞嵌入最重要的是句子的數量而不是模型的複雜度。他使用的模型是只有一層的線性DNN,因此訓練的速度很快。同時他盡可能地使用越多的句子來訓練模型,所以單位時間的效果比同期的其他模型好。Word2Vec模型使用的任務是猜字謎,這也是經驗上公認學習詞嵌入最有效的方法之一。猜字謎有兩種命題,一是從前後文猜中間文字(continuous bag-of-word, a.k.a. CBOW),二是從中間文字猜前後文(Skip-gram),兩者的概念如下圖:
Word2Vec 已經被包裝成一個 python module ,並且附上完整的使用方法,使用方便、效果卓越,我這裡就不贅述了。
訣竅三:填充 Padding
人講出來的句子有長有短,我們也希望機器可以做一樣的事情,但是這有點困難。為了要讓模型內的計算可以被平行化的,我們會固定輸入與輸出的句子長度,否則我們就要加很多判斷式來掌握目前句子的長度。通常我們會選擇一個比大部分句子字數稍長的長度作為定義長度,並且在所有句子後面填滿無意義的 Token ,使每個句子達到所定義長度,如下圖。利用這個小技巧,我們就可以強制每一個句子的長度都一樣長,使得模型內的計算可以被平行化。
Padding 本身毫無意義
使用Padding時必須要注意模型是否過度重視Padding。假設平均每個句子只有5個字,卻Padding 到100個字這麼長,那麼模型就很有可能只學會輸出Padding,而不會學習輸出句子。為了避免這樣的慘事,我們要降低Padding在更新參數上的權重 (weight) 。由於Padding 本身毫無意義,所以權重給零即可。在示範模型中,我沒有調整權重,原因是我Padding的程度不高且可以忽略。當然,有調整權重的自然語言模型將會做得更好。
Padding 也可以很靈活
Padding 可以安排在句子的最前面,也可以安排在句子的最後面,讀者可以依照任務的需求做調整。我舉個例子,如果想要製作中翻英的自然語言模型,那麼我會選擇把中文句子的文字順序反轉,然後 Padding 在最前面;而英文句子的文字順序保持不變,且 Padding 在最後面。這樣一來,意義相近的兩個字在神經元上的距離可能會比較近,而且中文句子與英文句子之間沒有無意義的Token。Padding 後的中翻英任務如下圖:
而本次的示範模型中我並沒有做這樣的調整,理由是Transformer的架構可以允許每一個時間點獲取其他任一時間點的資訊。所以字與字的距離變得沒有意義,也就沒有調整字序的必要。如果讀者想要用LSTM或是GRU取代 Transformer,那麼就應該好好考慮要怎麼安排字序與Padding。
訣竅四:層歸一化 Layer Normalization
這個做法可以加速模型學習的速度。Layer Normalization 字面上的意思是選取某一層神經網路,使得該層所有神經元的輸出的平均值為零、標準差為一。這個做法可以加速消除掉因為隨機初始化模型參數所導致的極端輸出值,經常被使用在自回歸神經網路 (Recurrent Neural Network)。目前的研究還無法斷定Layer Normalization好用的原因。另一個有名的方法是Batch Normalization,但是這個方法並不適合用在自然語言生成模型,我這裡就不展開了。
訣竅五:隨機關閉神經元 Dropout
當參數數量比資料數量還要多的時候,訓練過程中就容易發生過擬合 (overfitting) 。換句話說就是模型過度的模仿訓練資料的分佈情形,卻偏離了真實的資料分佈。過擬合很好判斷,觀察模型是否也能夠在沒訓練過的資料集上工作就可以了。可是這對於避免過擬合毫無幫助,頂多是讓我們知道它已經發生了。要避免過擬合就必須減少參數的數量,但是參數的數量又會直接的影響到模型的能力,所以這是個兩難。Dropout 巧妙的化解了這個兩難,在訓練模型的時候隨機關閉某些神經元,使得模型在訓練過程中不容易過擬合。在測試的時候則是神經元全開,讓模型發揮全部性能。
小結
以上提到的訣竅都是通用的,只要是個自然語言生成模型,用上面的小訣竅都可以得到更好的結果。作為練習,我非常建議讀者可以用Transformer以外的自回歸模型 (如 LSTM, GRU) 重新實作出一個類似的翻譯模型。改寫的過程中,不一定每個訣竅都要用上。先做出可以用的小模型再漸漸的擴充是比較好的選擇。一方面可以確保即使後期模型失敗,也有前期的模型可以用;在早期做出能工作的模型也會比較有成就感。
參考
Efficient Estimation of Word Representations in Vector Space, Tomas Mikolov, Kai Chen, Greg Corrado, Jeffrey Dean, 2013
Dropout: A Simple Way to Prevent Neural Networks from Overfitting, Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, Ruslan Salakhutdinov; 15(56):1929−1958, 2014.
Attention Is All You Need, Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, Illia Polosukhin, 2017
所有文章分類