
本文為你展示,如何用10幾行 Python 語句,把 Yelp 評論數(shù)據(jù)情感分類效果做到一流水平。
疑問
在《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文中,我為你詳細介紹了遷移學習給圖像分類帶來的優(yōu)勢,包括:
用時少
成本低
需要的數(shù)據(jù)量小
不容易過擬合
有的同學,立刻就把遷移學習的這種優(yōu)勢,聯(lián)系到了自己正在做的研究中,問我:
老師,遷移學習能不能用在文本分類中呢?正在為數(shù)據(jù)量太小發(fā)愁呢!
好問題!
答案是可以。
回顧《如何用機器學習處理二元分類任務?》一文,我們介紹過文本分類的一些常見方法。
首先,要把握語義信息。方法是使用詞嵌入預訓練模型。代表詞語的向量,不再只是一個獨特序號,而能夠在一定程度上,刻畫詞語的意義(具體內容,請參見《如何用Python處理自然語言?(Spacy與Word Embedding)》和《如何用 Python 和 gensim 調用中文詞嵌入預訓練模型?》)。

其次,上述方法只能表征單個詞語含義,因此需要通過神經(jīng)網(wǎng)絡來刻畫詞語的順序信息。
例如可以使用一維卷積神經(jīng)網(wǎng)絡(One Dimensional Convolutional Neural Network, 1DCNN):

或者使用循環(huán)神經(jīng)網(wǎng)絡(Recurrent Neural Network, RNN):

還有的研究者,覺得為了表征句子里詞語順序,用上 CNN 或者 LSTM 這樣的復雜結構,有些浪費。
于是 Google 干脆提出了Universal Sentence Encoder,直接接受你輸入的整句,然后把它統(tǒng)一轉換成向量形式。這樣可以大幅度降低用戶建模和訓練的工作量。

困難
這些方法有用嗎?當然有。但是 Jeremy Howard 指出,這種基于詞(句)嵌入預訓練的模型,都會有顯著缺陷,即領域上下文問題。
這里為了簡化,咱們只討論英文這一種語言內的問題。
假設別人是在英文 Wikipedia 上面訓練的詞嵌入向量,你想拿過來對 IMDB 或 Yelp 上的文本做分類。這就有問題了。因為許多詞語,在不同的上下文里面,含義是有區(qū)別的。直接拿來用的時候,你實際上,是在無視這種區(qū)別。
那怎么辦?直覺的想法,自然是退回去,我不再用別人的預訓練結果了。使用目前任務領域的文本,從頭來訓練詞嵌入向量。
可是這樣一來,你訓練工作量陡增。目前主流的Word2vec,Glove和fasttext這幾個詞嵌入預訓練模型,都出自名門。其中 word2vec 來自于 Google,Glove 來自于斯坦福,fasttext 是 facebook 做的。因為這種海量文本的訓練,不僅需要掌握技術,還要有大量的計算資源。
同時,你還很可能遭遇數(shù)據(jù)不足的問題。這會導致你自行訓練的詞嵌入模型,表現(xiàn)上比之前拿來別人的,結果更差。維基百科之所以經(jīng)常被使用來做訓練,就是因為文本豐富。而一些評論數(shù)據(jù)里面,往往不具備如此豐富的詞匯。
怎么辦呢?
遷移
Jeremy Howard 提出了一種方法,叫做“用于文本分類的通用語言模型微調(ULMFiT)”。論文在這里:Howard, J., & Ruder, S. (2018). Universal language model fine-tuning for text classification. In Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (Vol. 1, pp. 328-339).

在這篇文章里,他提出了一個構想。
有人(例如早期研究者,或者大機構)在海量數(shù)據(jù)集(例如Wikipedia)上訓練語言模型。之后發(fā)布這個模型,而不只是詞嵌入向量的表達結果;
普通用戶拿到這個模型后,把它在自己的訓練文本(例如 Yelp 或者 IMDB 評論)上微調,這樣一來,就有了符合自己任務問題領域上下文的語言模型;
之后,把這個語言模型的頭部,加上一個分類器,在訓練數(shù)據(jù)上學習,這就有了一個針對當前任務的完整分類模型;
如果效果還不夠好,可以把整個兒分類模型再進行微調。
文中用了下圖,表達了上述步驟。

注意在這個語言模型中,實際上也是使用了AWD-LSTM作為組塊的(否則無法處理詞語的順序信息)。但是你根本就不必了解? AWD-LSTM 的構造,因為它已經(jīng)完全模塊化包裹起來了,對用戶透明。
再把我們那幾個比方拿出來說說,給你打打氣:
你不需要了解顯像管的構造和無線信號傳輸,就可以看電視和用遙控器換臺;
你不需要了解機械構造和內燃機原理,就可以開汽車。
用 Python 和 fast.ai 來做遷移學習,你需要的,只是看懂說明書而已。

下面,我們就來實際做一個文本分類任務,體會一下“通用語言模型微調”和深度遷移學習的威力。
數(shù)據(jù)
我們使用的文本數(shù)據(jù),是Yelp reviews Polarity,它是一個標準化的數(shù)據(jù)集。許多文本分類的論文,都會采用它進行效果對比。
我們使用的版本,來自于 fast.ai 開放數(shù)據(jù)集,存儲在 AWS 上。它和Yelp reviews Polarity的原始版本在數(shù)據(jù)內容上沒有任何區(qū)別,只不過是提供的 csv ,從結構上符合 fast.ai 讀取的標準化需求(也就是每一行,都把標記放在文本前面)。
點擊這個鏈接,你就能看到 fast.ai 全部開放數(shù)據(jù)內容。

其中很多其他數(shù)據(jù)類別,對于你的研究可能會有幫助。
我們進入“自然語言處理”(NLP)板塊,查找到Yelp reviews - Polarity。

這個數(shù)據(jù)集有幾百兆。不算小,但是也算不上大數(shù)據(jù)。你可以把它下載到電腦中,解壓后查看。

注意在壓縮包里面,有2個csv文件,分別叫做train.csv(訓練集)和test.csv(測試集)。
我們打開readme.txt看看,其中數(shù)據(jù)集的作者提到:
The Yelp reviews polarity dataset is constructed by considering stars 1 and 2 negative, and 3 and 4 positive. For each polarity 280,000 training samples and 19,000 testing samples are take randomly. In total there are 560,000 trainig samples and 38,000 testing samples. Negative polarity is class 1, and positive class 2.
之所以叫做極性(Polarity)數(shù)據(jù),是因為作者根據(jù)評論對應的打分,分成了正向和負向情感兩類。因此我們的分類任務,是二元的。訓練集里面,正負情感數(shù)據(jù)各 280,000 條,而測試集里面,正負情感數(shù)據(jù)各有 19,000 條。
網(wǎng)頁上面,有數(shù)據(jù)集作者的論文鏈接。該論文發(fā)表于 2015 年。這里有論文的提要,包括了不同方法在相同數(shù)據(jù)集上的性能對比。

如圖所示,性能是用錯誤率來展示的。Yelp reviews - Polarity這一列里面,最低的錯誤率已經(jīng)用藍色標出,為 4.36, 那么準確率(accuracy)便是 95.64%。
注意,寫學術論文的時候,一定要注意引用要求。如果你在自己的研究中,使用該數(shù)據(jù)集,那么需要在參考文獻中,添加引用:
Xiang Zhang, Junbo Zhao, Yann LeCun. Character-level Convolutional Networks for Text Classification. Advances in Neural Information Processing Systems 28 (NIPS 2015).
環(huán)境
為了運行深度學習代碼,你需要一個 GPU 。但是你不需要去買一個,租就好了。最方便的租用方法,就是云平臺。
在《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文中,我們提到了,建議使用 Google Compute Platform 。每小時只需要 0.38 美元,而且如果你是新用戶, Google 會先送給你300美金,1年內有效。
我為你寫了個步驟詳細的設置教程,請使用這個鏈接訪問。

當你的終端里面出現(xiàn)這樣的提示的時候,就證明一切準備工作都就緒了。

我把教程的代碼,已經(jīng)放到了 github 上面,請使用以下語句,下載下來。
gitclonehttps://github.com/wshuyi/demo-nlp-classification-fastai.git

之后,就可以呼叫 jupyter 出場了。
jupyter lab

注意因為你是在 Google Compute Platform 云端執(zhí)行 jupyter ,因此瀏覽器不會自動彈出。
你需要打開 Firefox 或者 Chrome,在其中輸入這個鏈接(http://localhost:8080/lab?)。

打開左側邊欄里面的demo.ipynb。

本教程全部的代碼都在這里了。當然,你如果比較心急,可以選擇執(zhí)行Run->Run All Cells,查看全部運行結果。

但是,跟之前一樣,我還是建議你跟著教程的說明,一步步執(zhí)行它們。以便更加深刻體會每一條語句的含義。
載入
在 Jupyter Lab 中,我們可以使用!+命令名稱的方式,來執(zhí)行終端命令(bash command)。我們下面就使用wget來從 AWS 下載 Yelp 評論數(shù)據(jù)集。
!wget https://s3.amazonaws.com/fast-ai-nlp/yelp_review_polarity_csv.tgz

在左邊欄里,你會看到y(tǒng)elp_review_polarity_csv.tgz這個文件,被下載了下來。

對于tgz格式的壓縮包,我們采用tar命令來解壓縮。
!tar -xvzf yelp_review_polarity_csv.tgz

左側邊欄里,你會看到y(tǒng)elp_review_polarity_csv目錄解壓完畢。

我們雙擊它,看看內容。

文件下載和解壓成功。下面我們從 fast.ai 調用一些模塊,來獲得一些常見的功能。
fromfastaiimport*fromfastai.textimport*fromfastai.coreimport*
我們設置path指向數(shù)據(jù)文件夾。
path = Path('yelp_review_polarity_csv')
然后我們檢查一下訓練數(shù)據(jù)。
train_csv = path/'train.csv'train = pd.read_csv(train_csv, header=None)train.head()

每一行,都包括一個標簽,以及對應的評論內容。這里因為顯示寬度的限制,評論被折疊了。我們看看第一行的評論內容全文:
train.iloc[0][1]

對于驗證集,我們也仿照上述辦法查看。注意這里數(shù)據(jù)集只提供了訓練集和“測試集”,因此我們把這個“測試集”當做驗證集來使用。
valid_csv = path/'test.csv'valid = pd.read_csv(valid_csv, header=None)valid.head()

下面我們把數(shù)據(jù)讀入。
data_lm = TextLMDataBunch.from_csv(path, valid='test')data_clas = TextClasDataBunch.from_csv(path, valid='test', vocab=data_lm.train_ds.vocab)
注意,短短兩行命令,實際上完成了若干功能。
第一行,是構建語言模型(Language Model, LM)數(shù)據(jù)。
第二行,是構建分類模型(Classifier)數(shù)據(jù)。
它們要做以下幾個事兒:
語言模型中,對于訓練集的文本,進行標記化(Tokenizing)和數(shù)字化(Numericalizing)。這個過程,請參考我在《如何用Python和機器學習訓練中文文本情感分類模型?》一文中的介紹;
語言模型中,對于驗證集文本,同樣進行標記化(Tokenizing)和數(shù)字化(Numericalizing);
分類模型中,直接使用語言模型中標記化(Tokenizing)和數(shù)字化(Numericalizing)之后的詞匯(vocabs)。并且讀入標簽(labels)。
因為我們的數(shù)據(jù)量有數(shù)十萬,因此執(zhí)行起來,會花上幾分鐘。

結束之后,我們來看看數(shù)據(jù)載入是否正常。
data_lm.train_ds.vocab_size

訓練數(shù)據(jù)里面,詞匯一共有60002條。
我們看看,詞匯的索引是怎么樣的:
data_lm.train_ds.vocab.itos

分類器里面,訓練集標簽正確載入了嗎?
data_lm.train_ds.labels

驗證集的呢?
data_lm.valid_ds.labels

數(shù)據(jù)載入后,我們就要開始借來預訓練語言模型,并且進行微調了。
語言模型
本文使用 fast.ai 自帶的預訓練語言模型wt103_v1,它是在Wikitext-103數(shù)據(jù)集上訓練的結果。
我們把它下載下來:
model_path = path/'models'model_path.mkdir(exist_ok=True)url ='http://files.fast.ai/models/wt103_v1/'download_url(f'{url}lstm_wt103.pth', model_path/'lstm_wt103.pth')download_url(f'{url}itos_wt103.pkl', model_path/'itos_wt103.pkl')

左側邊欄里,在數(shù)據(jù)目錄下,我們會看到一個新的文件夾,叫做models。

其中包括兩個文件:

好了,現(xiàn)在數(shù)據(jù)、語言模型預訓練參數(shù)都有了,我們要構建一個 RNNLearner ,來生成我們自己的語言模型。
learn = RNNLearner.language_model(data_lm, pretrained_fnames=['lstm_wt103','itos_wt103'], drop_mult=0.5)
這里,我們指定了語言模型要讀入的文本數(shù)據(jù)為data_lm,預訓練的參數(shù)為剛剛下載的兩個文件,第三個參數(shù)drop_mult是為了避免過擬合,而設置的 Dropout 比例。
下面,我們還是讓模型用one cycle policy進行訓練。如果你對細節(jié)感興趣,可以點擊這個鏈接了解具體內容。
learn.fit_one_cycle(1,1e-2)
因為我們的數(shù)據(jù)集包含數(shù)十萬條目,因此訓練時間,大概需要1個小時左右。請保持耐心。

50多分鐘后,還在跑,不過已經(jīng)可以窺見曙光了。

當命令成功執(zhí)行后,我們可以看看目前的語言模型和我們的訓練數(shù)據(jù)擬合程度如何。

你可能會覺得,這個準確率也太低了!
沒錯,不過要注意,這可是語言模型的準確率,并非是分類模型的準確率。所以,它和我們之前在這張表格里看到的結果,不具備可比性。

我們對于這個結果,不夠滿意,怎么辦呢?
方法很簡單,我們微調它。
回顧下圖,剛才我們實際上是凍結了預訓練模型底層參數(shù),只用頭部層次擬合我們自己的訓練數(shù)據(jù)。

微調的辦法,是不再對預訓練的模型參數(shù)進行凍結?!敖鈨觥敝?,我們依然使用“歧視性學習速率”(discriminative learning rate)進行微調。
如果你忘了“歧視性學習速率”(discriminative learning rate)是怎么回事兒,請參考《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文的“微調”一節(jié)。
注意這種方法,既保證靠近輸入層的預訓練模型結構不被破壞,又盡量讓靠近輸出層的預訓練模型參數(shù)盡可能向著我們自己的訓練數(shù)據(jù)擬合。
learn.unfreeze()learn.fit_one_cycle(1,1e-3)

好吧,又是一個多小時。出去健健身,活動一下吧。
當你準時回來的時候,會發(fā)現(xiàn)模型的效能已經(jīng)提升了一大截。

前前后后,你已經(jīng)投入了若干小時的訓練時間,就為了打造這個符合任務需求的語言模型。
現(xiàn)在模型訓練好了,我們一定不能忘記做的工作,是把參數(shù)好好保存下來。
learn.save_encoder('ft_enc')
這樣,下次如果你需要使用這個任務的語言模型,就不必拿wt103_v1從頭微調了。而只需要讀入目前存儲的參數(shù)即可。
分類
語言模型微調好了,下面我們來構造分類器。
learn = RNNLearner.classifier(data_clas, drop_mult=0.5)learn.load_encoder('ft_enc')learn.fit_one_cycle(1,1e-2)
雖然名稱依然叫做learn,但注意這時候我們的模型,已經(jīng)是分類模型,而不再是語言模型了。我們讀入的數(shù)據(jù),也因應變化成了data_clas,而非data_lm。
這里,load_encoder就是把我們的語言模型參數(shù),套用到分類模型里。
我們還是執(zhí)行 "one cycle policy" 。

這次,在20多分鐘的訓練之后,我們語言模型在分類任務上得出了第一次成績。
接近95%的準確率,好像很不錯嘛!
但是,正如我在《文科生用機器學習做論文,該寫些什么?》一文中給你指出的那樣,對于別人已經(jīng)做了模型的分類任務,你的目標就得是和別人的結果去對比了。
回顧別人的結果:

對,最高準確率是 95.64% ,我們的模型,還是有差距的。
怎么辦?
很簡單,我們剛剛只是微調了語言模型而已。這回,我們要微調分類模型。
先做一個省事兒的。就是對于大部分層次,我們都保持凍結。只把分類模型的最后兩層解凍,進行微調。
learn.freeze_to(-2)learn.fit_one_cycle(1, slice(5e-3/2.,5e-3))
半小時以后,我們獲得了這樣的結果:

這次,我們的準確率,已經(jīng)接近了97% ,比別人的 95.64% 要高了。
而且,請注意,此時訓練損失(train loss)比起驗證損失(valid loss)要高。沒有跡象表明過擬合發(fā)生,這意味著模型還有改進的余地。
你如果還不滿意,那么咱們就干脆把整個兒模型解凍,然后再來一次微調。
learn.unfreeze()learn.fit_one_cycle(1, slice(2e-3/100,2e-3))
因為微調的層次多了,參數(shù)自然也多了許多。因此訓練花費時間也會更長。大概一個小時以后,你會看到結果:

準確率已經(jīng)躍升到了 97.28%。
再次提醒,此時訓練損失(train loss)依然比驗證損失(valid loss)高。模型還有改進的余地……
對比
雖然我們的深度學習模型,實現(xiàn)起來非常簡單。但是把咱們2018年做出來的結果,跟2015年的文章對比,似乎有些不大公平。
于是,我在 Google Scholar 中,檢索yelp polarity,并且把檢索結果的年份限定在了2017年以后。

對第一屏上出現(xiàn)的全部文獻,我一一打開,查找是否包含準確率對比的列表。所有符合的結果,我都列在了下面,作為對比。
下表來自于:Sun, J., Ma, X., & Chung, T. S. (2018). Exploration of Recurrent Unit in Hierarchical Attention Neural Network for Sentence Classification. ??????? ???????, 964-966.

注意這里最高的數(shù)值,是 93.75 。
下表來自于:Murdoch, W. J., & Szlam, A. (2017). Automatic rule extraction from long short term memory networks. arXiv preprint arXiv:1702.02540.

這里最高的數(shù)值,是 95.4 。
下表來自于:Chen, M., & Gimpel, K. (2018). Smaller Text Classifiers with Discriminative Cluster Embeddings. In Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 2 (Short Papers) (Vol. 2, pp. 739-745).

這里最高的數(shù)值,是 95.8 。
下表來自于:Shen, D., Wang, G., Wang, W., Min, M. R., Su, Q., Zhang, Y., ... & Carin, L. (2018). Baseline needs more love: On simple word-embedding-based models and associated pooling mechanisms. arXiv preprint arXiv:1805.09843.

這里最高的數(shù)值,是 95.81 。
這是一篇教程,并非學術論文。所以我沒有窮盡查找目前出現(xiàn)的最高 Yelp Reviews Polarity 分類結果。
另外,給你留個思考題——咱們這種對比,是否科學?歡迎你在留言區(qū),把自己的見解反饋給我。
不過,通過跟這些近期文獻里面的最優(yōu)分類結果進行比較,相信你對咱們目前達到的準確率,能有較為客觀的參照。
小結
本文我們嘗試把遷移學習,從圖像分類領域搬到到了文本分類(自然語言處理)領域。
在 fast.ai 框架下,我們的深度學習分類模型代碼很簡單。刨去那些預處理和展示數(shù)據(jù)的部分,實際的訓練語句,只有10幾行而已。
回顧一下,主要的步驟包括:
獲得標注數(shù)據(jù),分好訓練集和驗證集;
載入語言模型數(shù)據(jù),和分類模型數(shù)據(jù),進行標記化和數(shù)字化預處理;
讀入預訓練參數(shù),訓練并且微調語言模型;
用語言模型調整后的參數(shù),訓練分類模型;
微調分類模型
值得深思的是,在這種流程下,你根本不需要獲得大量的標注數(shù)據(jù),就可以達到非常高的準確率。
在 Jeremy Howard 的論文里,就有這樣一張對比圖,令人印象非常深刻。

同樣要達到 20% 左右的驗證集錯誤率,從頭訓練的話,你需要超過1000個數(shù)據(jù),而如果使用半監(jiān)督通用語言模型微調(ULMFiT, semi-supervised),你只需要100個數(shù)據(jù)。如果你用的是監(jiān)督通用語言模型微調(ULMFiT, supervised),100個數(shù)據(jù)已經(jīng)能夠直接讓你達到10%的驗證集錯誤率了。
這給那些小樣本任務,尤其是小語種上的自然語言處理任務,帶來了顯著的機遇。
Czapla 等人,就利用這種方法,輕松贏得了 PolEval'18 比賽的第一名,領先第二名 35% 左右。

作者:王樹義
鏈接:http://www.itdecent.cn/p/1b89704db572
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯(lián)系作者獲得授權并注明出處。