如何用 Python 和深度遷移學習做文本分類?

本文爲你展示,如何用10幾行 Python 語句,把 Yelp 評論數據情感分類效果做到一流水平。

疑問

在《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文中,我爲你詳細介紹了遷移學習給圖像分類帶來的優勢,包括:

  • 用時少
  • 成本低
  • 需要的數據量小
  • 不容易過擬合

有的同學,立刻就把遷移學習的這種優勢,聯繫到了自己正在做的研究中,問我:

老師,遷移學習能不能用在文本分類中呢?正在爲數據量太小發愁呢!

好問題!

答案是可以

回顧《如何用機器學習處理二元分類任務?》一文,我們介紹過文本分類的一些常見方法。

首先,要把握語義信息。方法是使用詞嵌入預訓練模型。代表詞語的向量,不再只是一個獨特序號,而能夠在一定程度上,刻畫詞語的意義(具體內容,請參見《如何用Python處理自然語言?(Spacy與Word Embedding)》和《如何用 Python 和 gensim 調用中文詞嵌入預訓練模型?》)。

其次,上述方法只能表徵單個詞語含義,因此需要通過神經網絡來刻畫詞語的順序信息。

例如可以使用一維卷積神經網絡(One Dimensional Convolutional Neural Network, 1DCNN):

或者使用循環神經網絡(Recurrent Neural Network, RNN):

還有的研究者,覺得爲了表徵句子裏詞語順序,用上 CNN 或者 LSTM 這樣的複雜結構,有些浪費。

於是 Google 乾脆提出了 Universal Sentence Encoder ,直接接受你輸入的整句,然後把它統一轉換成向量形式。這樣可以大幅度降低用戶建模和訓練的工作量。

困難

這些方法有用嗎?當然有。但是 Jeremy Howard 指出,這種基於詞(句)嵌入預訓練的模型,都會有顯著缺陷,即領域上下文問題。

這裏爲了簡化,咱們只討論英文這一種語言內的問題。

假設別人是在英文 Wikipedia 上面訓練的詞嵌入向量,你想拿過來對 IMDB 或 Yelp 上的文本做分類。這就有問題了。因爲許多詞語,在不同的上下文裏面,含義是有區別的。直接拿來用的時候,你實際上,是在無視這種區別

那怎麼辦?直覺的想法,自然是退回去,我不再用別人的預訓練結果了。使用目前任務領域的文本,從頭來訓練詞嵌入向量。

可是這樣一來,你訓練工作量陡增。目前主流的 Word2vec , Glove 和 fasttext 這幾個詞嵌入預訓練模型,都出自名門。其中 word2vec 來自於 Google,Glove 來自於斯坦福,fasttext 是 facebook 做的。因爲這種海量文本的訓練,不僅需要掌握技術,還要有大量的計算資源。

同時,你還很可能遭遇數據不足的問題。這會導致你自行訓練的詞嵌入模型,表現上比之前拿來別人的,結果更差。維基百科之所以經常被使用來做訓練,就是因爲文本豐富。而一些評論數據裏面,往往不具備如此豐富的詞彙。

怎麼辦呢?

遷移

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).

在這篇文章裏,他提出了一個構想。

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

文中用了下圖,表達了上述步驟。

注意在這個語言模型中,實際上也是使用了 AWD-LSTM 作爲組塊的(否則無法處理詞語的順序信息)。但是你根本就不必瞭解 AWD-LSTM 的構造,因爲它已經完全模塊化包裹起來了,對用戶透明。

再把我們那幾個比方拿出來說說,給你打打氣:

你不需要了解顯像管的構造和無線信號傳輸,就可以看電視和用遙控器換臺;

你不需要了解機械構造和內燃機原理,就可以開汽車。

用 Python 和 fast.ai 來做遷移學習,你需要的,只是看懂說明書而已。

下面,我們就來實際做一個文本分類任務,體會一下“通用語言模型微調”和深度遷移學習的威力。

數據

我們使用的文本數據,是 Yelp reviews Polarity ,它是一個標準化的數據集。許多文本分類的論文,都會採用它進行效果對比。

我們使用的版本,來自於 fast.ai 開放數據集,存儲在 AWS 上。它和 Yelp reviews Polarity 的原始版本在數據內容上沒有任何區別,只不過是提供的 csv ,從結構上符合 fast.ai 讀取的標準化需求(也就是每一行,都把標記放在文本前面)。

點擊這個鏈接,你就能看到 fast.ai 全部開放數據內容。

其中很多其他數據類別,對於你的研究可能會有幫助。

我們進入“自然語言處理”(NLP)板塊,查找到 Yelp reviews - Polarity

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

注意在壓縮包裏面,有2個 csv 文件,分別叫做 train.csv(訓練集)和 test.csv(測試集)。

我們打開 readme.txt 看看,其中數據集的作者提到:

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)數據,是因爲作者根據評論對應的打分,分成了正向和負向情感兩類。因此我們的分類任務,是二元的。訓練集裏面,正負情感數據各 280,000 條,而測試集裏面,正負情感數據各有 19,000 條。

網頁上面,有數據集作者的論文鏈接。該論文發表於 2015 年。這裏有論文的提要,包括了不同方法在相同數據集上的性能對比。

如圖所示,性能是用錯誤率來展示的。 Yelp reviews - Polarity 這一列裏面,最低的錯誤率已經用藍色標出,爲 4.36, 那麼準確率(accuracy)便是 95.64%。

注意,寫學術論文的時候,一定要注意引用要求。如果你在自己的研究中,使用該數據集,那麼需要在參考文獻中,添加引用:

Xiang Zhang, Junbo Zhao, Yann LeCun. Character-level Convolutional Networks for Text Classification. Advances in Neural Information Processing Systems 28 (NIPS 2015).

環境

爲了運行深度學習代碼,你需要一個 GPU 。但是你不需要去買一個,租就好了。最方便的租用方法,就是雲平臺。

在《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文中,我們提到了,建議使用 Google Compute Platform 。每小時只需要 0.38 美元,而且如果你是新用戶, Google 會先送給你300美金,1年內有效。

我爲你寫了個步驟詳細的設置教程,請使用這個鏈接訪問。

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

我把教程的代碼,已經放到了 github 上面,請使用以下語句,下載下來。

git clone https://github.com/wshuyi/demo-nlp-classification-fastai.git

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

jupyter lab

注意因爲你是在 Google Compute Platform 雲端執行 jupyter ,因此瀏覽器不會自動彈出。

你需要打開 Firefox 或者 Chrome,在其中輸入這個鏈接(http://localhost:8080/lab?)。

打開左側邊欄裏面的 demo.ipynb

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

但是,跟之前一樣,我還是建議你跟着教程的說明,一步步執行它們。以便更加深刻體會每一條語句的含義。

載入

在 Jupyter Lab 中,我們可以使用 !+命令名稱 的方式,來執行終端命令(bash command)。我們下面就使用 wget 來從 AWS 下載 Yelp 評論數據集。

!wget https://s3.amazonaws.com/fast-ai-nlp/yelp_review_polarity_csv.tgz

在左邊欄裏,你會看到 yelp_review_polarity_csv.tgz 這個文件,被下載了下來。

對於 tgz 格式的壓縮包,我們採用 tar 命令來解壓縮。

!tar -xvzf yelp_review_polarity_csv.tgz

左側邊欄裏,你會看到 yelp_review_polarity_csv 目錄解壓完畢。

我們雙擊它,看看內容。

文件下載和解壓成功。下面我們從 fast.ai 調用一些模塊,來獲得一些常見的功能。

from fastai import *
from fastai.text import *
from fastai.core import *

我們設置 path 指向數據文件夾。

path = Path('yelp_review_polarity_csv')

然後我們檢查一下訓練數據。

train_csv = path/'train.csv'
train = pd.read_csv(train_csv, header=None)
train.head()

每一行,都包括一個標籤,以及對應的評論內容。這裏因爲顯示寬度的限制,評論被摺疊了。我們看看第一行的評論內容全文:

train.iloc[0][1]

對於驗證集,我們也仿照上述辦法查看。注意這裏數據集只提供了訓練集和“測試集”,因此我們把這個“測試集”當做驗證集來使用。

valid_csv = path/'test.csv'
valid = pd.read_csv(valid_csv, header=None)
valid.head()

下面我們把數據讀入。

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)數據。

第二行,是構建分類模型(Classifier)數據。

它們要做以下幾個事兒:

  • 語言模型中,對於訓練集的文本,進行標記化(Tokenizing)和數字化(Numericalizing)。這個過程,請參考我在《如何用Python和機器學習訓練中文文本情感分類模型?》一文中的介紹;
  • 語言模型中,對於驗證集文本,同樣進行標記化(Tokenizing)和數字化(Numericalizing);
  • 分類模型中,直接使用語言模型中標記化(Tokenizing)和數字化(Numericalizing)之後的詞彙(vocabs)。並且讀入標籤(labels)。

因爲我們的數據量有數十萬,因此執行起來,會花上幾分鐘。

結束之後,我們來看看數據載入是否正常。

data_lm.train_ds.vocab_size

訓練數據裏面,詞彙一共有60002條。

我們看看,詞彙的索引是怎麼樣的:

data_lm.train_ds.vocab.itos

分類器裏面,訓練集標籤正確載入了嗎?

data_lm.train_ds.labels

驗證集的呢?

data_lm.valid_ds.labels

數據載入後,我們就要開始借來預訓練語言模型,並且進行微調了。

語言模型

本文使用 fast.ai 自帶的預訓練語言模型 wt103_v1,它是在 Wikitext-103 數據集上訓練的結果。

我們把它下載下來:

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')

左側邊欄裏,在數據目錄下,我們會看到一個新的文件夾,叫做 models

其中包括兩個文件:

好了,現在數據、語言模型預訓練參數都有了,我們要構建一個 RNNLearner ,來生成我們自己的語言模型。

learn = RNNLearner.language_model(data_lm, pretrained_fnames=['lstm_wt103', 'itos_wt103'], drop_mult=0.5)

這裏,我們指定了語言模型要讀入的文本數據爲 data_lm,預訓練的參數爲剛剛下載的兩個文件,第三個參數 drop_mult 是爲了避免過擬合,而設置的 Dropout 比例。

下面,我們還是讓模型用 one cycle policy 進行訓練。如果你對細節感興趣,可以點擊這個鏈接瞭解具體內容。

learn.fit_one_cycle(1, 1e-2)

因爲我們的數據集包含數十萬條目,因此訓練時間,大概需要1個小時左右。請保持耐心。

50多分鐘後,還在跑,不過已經可以窺見曙光了。

當命令成功執行後,我們可以看看目前的語言模型和我們的訓練數據擬合程度如何。

你可能會覺得,這個準確率也太低了!

沒錯,不過要注意,這可是語言模型的準確率,並非是分類模型的準確率。所以,它和我們之前在這張表格裏看到的結果,不具備可比性。

我們對於這個結果,不夠滿意,怎麼辦呢?

方法很簡單,我們微調它。

回顧下圖,剛纔我們實際上是凍結了預訓練模型底層參數,只用頭部層次擬合我們自己的訓練數據。

微調的辦法,是不再對預訓練的模型參數進行凍結。“解凍”之後,我們依然使用“歧視性學習速率”(discriminative learning rate)進行微調。

如果你忘了“歧視性學習速率”(discriminative learning rate)是怎麼回事兒,請參考《如何用 Python 和 fast.ai 做圖像深度遷移學習?》一文的“微調”一節。

注意這種方法,既保證靠近輸入層的預訓練模型結構不被破壞,又儘量讓靠近輸出層的預訓練模型參數儘可能向着我們自己的訓練數據擬合。

learn.unfreeze()
learn.fit_one_cycle(1, 1e-3)

好吧,又是一個多小時。出去健健身,活動一下吧。

當你準時回來的時候,會發現模型的效能已經提升了一大截。

前前後後,你已經投入了若干小時的訓練時間,就爲了打造這個符合任務需求的語言模型。

現在模型訓練好了,我們一定不能忘記做的工作,是把參數好好保存下來。

learn.save_encoder('ft_enc')

這樣,下次如果你需要使用這個任務的語言模型,就不必拿 wt103_v1 從頭微調了。而只需要讀入目前存儲的參數即可。

分類

語言模型微調好了,下面我們來構造分類器。

learn = RNNLearner.classifier(data_clas, drop_mult=0.5)
learn.load_encoder('ft_enc')
learn.fit_one_cycle(1, 1e-2)

雖然名稱依然叫做 learn ,但注意這時候我們的模型,已經是分類模型,而不再是語言模型了。我們讀入的數據,也因應變化成了 data_clas ,而非 data_lm

這裏,load_encoder 就是把我們的語言模型參數,套用到分類模型裏。

我們還是執行 "one cycle policy" 。

這次,在20多分鐘的訓練之後,我們語言模型在分類任務上得出了第一次成績。

接近95%的準確率,好像很不錯嘛!

但是,正如我在《文科生用機器學習做論文,該寫些什麼?》一文中給你指出的那樣,對於別人已經做了模型的分類任務,你的目標就得是和別人的結果去對比了。

回顧別人的結果:

對,最高準確率是 95.64% ,我們的模型,還是有差距的。

怎麼辦?

很簡單,我們剛剛只是微調了語言模型而已。這回,我們要微調分類模型。

先做一個省事兒的。就是對於大部分層次,我們都保持凍結。只把分類模型的最後兩層解凍,進行微調。

learn.freeze_to(-2)
learn.fit_one_cycle(1, slice(5e-3/2., 5e-3))

半小時以後,我們獲得了這樣的結果:

這次,我們的準確率,已經接近了97% ,比別人的 95.64% 要高了。

而且,請注意,此時訓練損失(train loss)比起驗證損失(valid loss)要高。沒有跡象表明過擬合發生,這意味着模型還有改進的餘地。

你如果還不滿意,那麼咱們就乾脆把整個兒模型解凍,然後再來一次微調。

learn.unfreeze()
learn.fit_one_cycle(1, slice(2e-3/100, 2e-3))

因爲微調的層次多了,參數自然也多了許多。因此訓練花費時間也會更長。大概一個小時以後,你會看到結果:

準確率已經躍升到了 97.28%。

再次提醒,此時訓練損失(train loss)依然比驗證損失(valid loss)高。模型還有改進的餘地……

對比

雖然我們的深度學習模型,實現起來非常簡單。但是把咱們2018年做出來的結果,跟2015年的文章對比,似乎有些不大公平。

於是,我在 Google Scholar 中,檢索 yelp polarity ,並且把檢索結果的年份限定在了2017年以後。

對第一屏上出現的全部文獻,我一一打開,查找是否包含準確率對比的列表。所有符合的結果,我都列在了下面,作爲對比。

下表來自於:Sun, J., Ma, X., & Chung, T. S. (2018). Exploration of Recurrent Unit in Hierarchical Attention Neural Network for Sentence Classification. 한국정보과학회 학술발표논문집, 964-966.

注意這裏最高的數值,是 93.75 。

下表來自於:Murdoch, W. J., & Szlam, A. (2017). Automatic rule extraction from long short term memory networks. arXiv preprint arXiv:1702.02540.

這裏最高的數值,是 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).

這裏最高的數值,是 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.

這裏最高的數值,是 95.81 。

這是一篇教程,並非學術論文。所以我沒有窮盡查找目前出現的最高 Yelp Reviews Polarity 分類結果。

另外,給你留個思考題——咱們這種對比,是否科學?歡迎你在留言區,把自己的見解反饋給我。

不過,通過跟這些近期文獻裏面的最優分類結果進行比較,相信你對咱們目前達到的準確率,能有較爲客觀的參照。

小結

本文我們嘗試把遷移學習,從圖像分類領域搬到到了文本分類(自然語言處理)領域。

在 fast.ai 框架下,我們的深度學習分類模型代碼很簡單。刨去那些預處理和展示數據的部分,實際的訓練語句,只有10幾行而已。

回顧一下,主要的步驟包括:

  • 獲得標註數據,分好訓練集和驗證集;
  • 載入語言模型數據,和分類模型數據,進行標記化和數字化預處理;
  • 讀入預訓練參數,訓練並且微調語言模型;
  • 用語言模型調整後的參數,訓練分類模型;
  • 微調分類模型

值得深思的是,在這種流程下,你根本不需要獲得大量的標註數據,就可以達到非常高的準確率。

在 Jeremy Howard 的論文裏,就有這樣一張對比圖,令人印象非常深刻。

同樣要達到 20% 左右的驗證集錯誤率,從頭訓練的話,你需要超過1000個數據,而如果使用半監督通用語言模型微調(ULMFiT, semi-supervised),你只需要100個數據。如果你用的是監督通用語言模型微調(ULMFiT, supervised),100個數據已經能夠直接讓你達到10%的驗證集錯誤率了。

這給那些小樣本任務,尤其是小語種上的自然語言處理任務,帶來了顯著的機遇。

Czapla 等人,就利用這種方法,輕鬆贏得了 PolEval'18 比賽的第一名,領先第二名 35% 左右。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章