用TensorFlow Estimator實現文本分類
選自ruder.io
作者:Sebastian Ruder
機器之心編譯
參與:Geek AI、張倩
本文探討了如何使用自定義的 TensorFlow Estimator、嵌入技術及 tf.layers 模塊來處理文本分類任務,使用的數據集為 IMDB 評論數據集。通過本文你將學到如何使用 word2vec 詞嵌入和遷移學習技術,在有標籤數據稀缺時獲得更好的模型性能。
本文主要內容如下:
使用 Datasets 裝載數據
使用預封裝好的評估器(estimator)構建基線
使用詞嵌入技術
通過卷積層和 LSTM 層構建定製化的評估器
裝載預訓練好的詞向量
使用 TensorBoard 評估並對比模型
本文選自介紹 TensorFlow 的 Datasets 和 Estimators 模塊系列博文的第四部分。讀者無需閱讀所有之前的內容,如果想重溫某些概念,可以查看以下鏈接:
第一部分重點討論了預建評估器(https://developers.googleblog.com/2017/09/introducing-tensorflow-datasets.html)
第二部分討論了特徵列(https://developers.googleblog.com/2017/11/introducing-tensorflow-feature-columns.html)
第三部分講解了如何創建一個自定義的評估器(https://developers.googleblog.com/2017/12/creating-custom-estimators-in-tensorflow.html)。
第四部分的內容將會建立在上述所有章節的基礎上,我們將處理一系列不同的自然語言處理(NLP)問題。本文演示了如何使用自定義的 TensorFlow 評估器、嵌入技術及 tf.layers 模塊(https://www.tensorflow.org/api_docs/python/tf/layers)來處理文本分類任務。在這篇文章中,我們會學習 word2vec 詞嵌入和遷移學習技術,在有標籤數據稀缺時獲得更好的模型性能。
我們將展示相關的代碼片段。這裡是完整的 Jupyter Notebook 代碼,你可以在本地或者 Google Colaboratory 上運行它。清晰的「.py」源文件可以通過以下鏈接獲得:(https://github.com/eisenjulian/nlp_estimator_tutorial/blob/master/nlp_estimators.py)。
有一點需要注意的是,此代碼只是為了演示評估器的功能是如何運行的,並沒有為了獲得最佳的性能進行進一步優化。
本文的任務
我們將使用的數據集是 IMDB 大規模電影評論數據集(http://ai.stanford.edu/~amaas/data/sentiment/),它包含 25,000 篇高度分化的電影評論作為訓練數據,另有 25,000 篇作為測試數據。我們將使用這個數據集去訓練一個能夠預測一條評論是正面還是負面的二分類模型。
舉例來說,這裡有一條數據集中的負面評論(得到了 222 個贊):
現在我喜歡義大利恐怖電影。越俗越好!然而,這並不是俗氣的義大利電影。這是放了一周的有腐爛肉丸的義大利面醬汁。這電影從任何層面看都很業餘!沒有懸念,沒有恐懼感,只有幾滴血落在周圍提醒你:你實際上在看恐怖電影。
Keras 為導入數據集提供了一個方便的處理程序,這個數據集也可以以一個序列化的 numpy 數組「.npz」文件的形式從這裡(https://s3.amazonaws.com/text-datasets/imdb.npz)下載獲得。文本分類中的標準做法是限制辭彙表的規模以防止數據集變得過於稀疏且維度過高,從而防止過擬合。因此,每條評論由一系列單詞索引組成,從「4」(在數據集中出現最頻繁的單詞「the」)一直到「4999」(代表單詞「orange」)。索引「1」代表句子的開頭,索引「2」被分配給所有未知的(也被稱為「辭彙表之外的」,即 OOV)單詞。這些索引是通過在一個數據管道中進行預處理之後得到的。這個預處理的步驟包括數據清洗、正則化,並且首先對每個句子進行分詞,接著根據頻率構建一個字典對每個單詞進行索引。
在內存中載入數據後,我們用「0」將每個句子填充到固定的長度進行對齊(這裡長度為 200)。這樣一來,我們就擁有了兩個二維的 25,000*200 的數組分別作為訓練和測試數組。
輸入函數
評估器框架使用輸入函數將數據管道和模型本身分離。可以使用一些輔助方法來創建他們,無論你的數據是存儲在一個「.csv」文件還是「pandas.DataFrame」中,也無論它是否存儲在內存中。在我們的例子中,訓練集合和測試集合都適用「Dataset.from_tensor_slices」讀取數據。
我們通過隨機化處理將訓練數據打亂,並且沒有預定義我們想要訓練模型的迭代次數,這裡我們僅僅需要對測試數據迭代一次就能進行模型的評估。我們也需要一個額外的「len」關鍵字去獲取原始、未填充的序列的長度,我們將會在後面用到它們。
構建基線
通過嘗試一些基礎的基線來開始機器學習項目是一種很好的做法。這個基線越簡單越好,因為有一個簡單、魯棒的基線至關重要,它可以幫助我們理解通過對模型增添額外的複雜性可以獲得多大的性能提升。很有可能,一個簡單的解決方案就足以滿足我們的要求。
考慮到這一點,讓我們首先嘗試一個最簡單的文本分類模型。這將會是一個稀疏的線性模型,它給每個單詞賦予一個權重,並且將所有的結果相加,無論單詞順序如何。由於這個模型並不關心句子中單詞的順序,所以我們通常把它稱為詞袋方法(BOW)。讓我們看看如何通過評估器(Estimator)實現這個模型。
我們從定義用做我們分類器輸入的特徵列開始。正如我們在第二部分中看到的,「categorical_column_with_identity」是對這個文本輸入進行預處理的正確選擇。如果我們拿到的是原始文本單詞,其它的特徵列「feature_columns」可以為我們做很多的預處理工作。我們現在可以使用預製好的「LinearClassifier」評估器了。
最終,我們創建了一個簡單的函數來訓練分類器並且另外創建了一個精確率-召回率曲線。由於我們不打算在這篇博文中取得最優的模型性能,所以我們僅僅對我們的模型訓練 25,000 步。
選擇一個簡單模型的好處之一是,它的可解釋性要強的多。一個模型越複雜,他就越難被檢驗,並且更容易像一個黑箱子一樣工作。在這個例子中,我們可以從我們模型的上一個檢查點裝載權重,並且看看哪些單詞相應的權重的絕對值最大。結果看起來就像我們所期望的那樣。
正如我們看到的,像「refreshing」這樣的擁有最大的正權值的單詞顯然與正面的語義相關,而擁有很大的負權重的單詞不容置疑地會激發負面的情緒。我們可以對模型做一個簡單但強而有力的修改去提升模型的能力,那就是根據單詞的 tf-idf 值賦予它們權重。
嵌入
增加模型複雜性的下一個步驟是詞嵌入。嵌入是稀疏高維數據的密集低維表示。它使得我們的模型能學習到每個單詞的更有意義的表示,而不僅僅是一個索引。儘管單一的維度可能沒有太大的意義,低維空間(當從一個足夠大的語料庫中學習時)已經被證實可以捕獲諸如時態、複數、性別、關聯主題等關係。我們可以通過將我們現有的特徵列轉換為「embedding_column」來增加詞嵌入。模型可見的特徵表示是每個單詞的詞嵌入的平均值(具體對「combiner」的討論參見本文檔:https://www.tensorflow.org/api_docs/python/tf/feature_column/embedding_column)可以將嵌入的特徵插入預封裝的 DNNClassifier 中。
這裡我們要提醒一下那些觀察力敏銳的人:一個「embedding_column」僅僅是一個將全連接層應用到稀疏的單詞的二值特徵向量的一種有效方法,它根據選擇的組合器(combiner)乘以一個相應的常數。這樣做的一個直接後果是,直接在「LinearClassifier」中使用一個「embedding_column」是沒有任何意義的,因為之間沒有非線性映射的兩個連續的線性層不會給模型增添預測能力,當然,除非詞嵌入是預訓練好的。
我們可以使用 TensorBoard 中的 t-SNE(https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding)將我們 50 維的詞向量方案可視化為 R^3。我們估計相似的詞彼此距離會比較接近。這可以稱為一種檢驗我們的模型權重並且發現意想不到的表現的有效方法。
卷積
在這裡,讓模型變得更」深」,進一步增添更多的全連接層、並圍繞層的規模和訓練的函數進行操作,是一種可能的改進方法。然而,通過這樣做,我們會增加額外的複雜性,並且忽略句子中的重要結構。實際上,單詞並不是存在於真空中的(獨立存在),它的意義是由它本身和與其相鄰的單片語合而成的。
卷積是一種利用這種結構的方法,這類似於我們如何為圖像分類建立顯著的像素集合。從直覺上來說,特定的單詞序列,或 n-gram,無論在句子中的整體位置如何,通常具有相同的含義。通過卷積操作引入一個結構先驗,使我們能夠對相鄰單詞之間的交互建模,從而給我們提供了一個更好的表示這種意義的方法。
下圖展示了一個 d×m 維的過濾器矩陣F在每個 3-gram 單詞窗口的滑動,去構建一個新的特徵映射。此後,池化層通常被用於組合相鄰的結果。
來源: Learning to Rank Short Text Pairs with Convolutional Deep Neural Networks,Severyn 等著,[2015]。
讓我們看看整個模型的框架。Dropout 層的使用是一種正則化技術,它使模型更不容易發生過擬合。
創建一個自定義評估器
正如我們在之前的博文中看到的,「tf.estimator」框架提供了一個訓練機器學習模型的高級 API,定義了「train()」,「evaluate()」以及「predict()」操作,能夠很方便地處理檢查點、載入數據、初始化、服務、構建計算圖(graph)和會話(session)。我們有一小部分預製評估器,就像我們之前用到的那些,但是很有可能你需要構建你自己的評估器。
要編寫一個自定義的評估器意味著,你需要編寫一個「model_fn(features, labels, mode, params)」函數,其返回值為一個 EstimatorSpec。你要做的第一步是將特徵映射到我們的嵌入層中:
接著我們使用「tf.layers」按順序處理每一個輸出。
最後我們會使用一個模型頭「Head」對象去簡化「model_fn」最後一個部分的編寫。模型頭「head」已經知道如何計算預測值、損失、訓練操作(train_op)、度量並且導出這些輸出,並且可以跨模型重用。這種方法也被用於預製評估器中,並為我們提供一個能夠在所有模型上使用的統一的評估函數。我們將使用「binary_classification_head」,這是一個針對單標籤二分類模型的頭,它使用「sigmoid_cross_entropy_with_logits」作為底層的損失函數。
運行這個模型和之前一樣簡單:
LSTM 網路
使用「Estimator」API 和相同的模型頭,我們可以創建一個使用長短期記憶(LSTM)神經元而不是卷積神經元的分類器。像這樣的遞歸模型是自然語言處理應用最成功的構建模塊。一個 LSTM 按順序處理整個文檔,在其內存中存儲當前狀態的同時也通過它的神經元對序列進行遞歸操作。
與 CNN 相比,遞歸模型的缺點之一是:由於遞歸的性質,模型會會變得越來越深、越來越複雜,通常會導致訓練時間加長,收斂性變差。LSTM(和一般的 RNN)可能會遇到像梯度彌散或梯度爆炸這樣的收斂問題,也就是說,只要有足夠的調優,他們就能夠在許多問題取得目前最好的結果。一般說來,CNN 擅長於特徵提取,而 RNN 則擅長依賴整個句子語義的任務,比如問答或機器翻譯。
每個神經元一次處理一個詞嵌入,並且根據依賴於嵌入向量 x_t 和之前的狀態 h_t-1 的可微的計算更新它的內部狀態。為了更好地理解 LSTM 的工作原理,可以參考 Chris Olah 的博文(https://colah.github.io/posts/2015-08-Understanding-LSTMs/)。
來源:Understanding LSTM Networks by Chris Olah
完整的 LSTM 模型可以表示成下面的簡單流程圖:
在本文的開頭,我們將所有的文檔都向上填充到了 200 個單詞,這對於構建一個合適的張量是十分必要的。然而,當一個文檔包含的單詞少於 200 個時,我們不希望 LSTM 繼續填充單詞的處理,因為這樣不會增加信息,還會降低性能。因此,我們還希望在填充之前,為我們的網路提供原始序列長度的信息。接下來,在模型的內部,它會將最後一個狀態複製到序列的末尾。我們可以通過在我們的輸入函數中添加「len」特徵做到這一點。我們現在可以遵循上面的邏輯,用我們的 LSTM 神經元替代卷積、池化、平整化層。
預訓練的向量
我們之前展示過的絕大多數模型都依賴於將詞嵌入作為第一層。到目前為止,我們已經隨機地初始化了這個嵌入層。然而,許多之前的研究表明,在大量未標記的語料庫上使用預訓練的嵌入作為初始化是很有幫助的,特別是當只對少量標記示例進行訓練時。最流行的預訓練詞嵌入技術是 word2vec。通過預訓練的嵌入來利用未標註數據的知識是遷移學習的一個實例。為此,我們將展示如何在評估器「Estimator」中使用他們。我們將使用來自於另一個流行的模型「GloVe」的預訓練向量。
在將向量從文件載入到內存之後,我們將他們使用和我們的辭彙表相同的索引存儲為一個 numpy 數組。創建的數組的大小為(5000,50)。在每個行索引中,它包含代表和我們的辭彙表中索引相同的單詞的 50 維向量,
最後,我們可以使用一個自定義的初始化函數,並且將結果傳給「params」對象,再將這個對象不作任何修改直接用於我們的「cnn_model_fn」。
運行 TensorBorad
現在,我們可以啟動 TensorBoard,將我們訓練出來的模型進行比較,觀察它們在訓練時間和性能方面都有何差異。在終端上運行:
我們可以在訓練和測試中可視化許多收集到的度量結果,包括每個模型在每一個訓練步驟上的損失函數值,以及精確度-召回率曲線。這當然是為我們的用例選擇最佳模型的最實用的方法,也是選擇分類閾值的最佳方法。
得到預測結果
為了得到在新的句子上的預測結果,我們可以使用「Estimator」實例中的「predict」方法,它能為每個模型載入最新的檢查點並且對不可見的示例進行評估。但是在將數據傳給模型之前,我們必須進行清理、分詞並且將每個單詞映射到相應的索引上。具體代碼如下:
值得注意的是,檢查點本身並不足以作出預測,為了將存儲的權重映射到相應的張量(tensor)上,用於構建評估器的實際代碼也是必需的。將保存的檢查點和創建他們的代碼分支關聯起來是一種很好的做法。如果有興趣將模型以一種完全可恢復的方式導出,可以查看「SaveModel」類,這對於通過使用 TensorFlow Serving 提供的 API 構建模型十分有用。
總結
在這篇博文中,我們探索了如何使用評估器(estimator)進行文本分類,特別是針對 IMDB 評論數據集。我們訓練並且可視化了我們的詞嵌入模型,也載入了預訓練的嵌入模型。我們從一個簡單的基線開始,成功構建了我們的卷積神經網路和長短期記憶神經網路。
更多細節請查看:
能在本地或 Colaboratory 上運行的 Jupyter notebook(https://github.com/eisenjulian/nlp_estimator_tutorial/blob/master/nlp_estimators.ipynb)
本文使用的完整源代碼(https://github.com/eisenjulian/nlp_estimator_tutorial/blob/master/nlp_estimators.py)
TensorFlow 嵌入技術指南(https://www.tensorflow.org/programmers_guide/embedding)
TensorFlow 詞向量表示教程(https://www.tensorflow.org/tutorials/word2vec)
NLTK 的原始文本處理(http://www.nltk.org/book/ch03.html)章節,講述如何設計語言數據管道
本文為機器之心編譯,轉載請聯繫本公眾號獲得授權。
------------------------------------------------
※用數據做酷的事!手把手教你搭建問答系統
※FAIR新一代無監督機器翻譯:模型更簡潔,性能更優
TAG:機器之心 |