來玩點NLP — LSTM vs. BERT on IMDb dataset

在深度學習的領域裡,Computer Vision(CV)和Natural Language Process(NLP)為最熱絡的兩個領域。CV 玩了一段時間也想來碰一下NLP,就以基本的文本分類來踏入NLP 的世界吧~~

圖片來源

在深度學習還沒發展起來前,大概是透過詞頻或TF-IDF來萃取文章特徵,然後套用像SVM 等的ML演算法。隨著 RNN 的發展使得NLP獲得許多突破,直到前幾年Transformer, BERT 的出現又往前跨出一大步。網路上有很多資源在講 RNN/LSTM 及BERT 再加上李宏毅老師已經講的非常完美了,因此此篇文章只會稍微帶過方法,focus 在記錄以 pytorch 實現 LSTM and BERT。

初學NLP,網路上東看西看學來的,若有理解錯誤還請指教。

Outline:
1. 資料前處理
2.方法概論 : RNN/LSTM、BERT
3. IMDb 影評分類 data overview
4. 實做

資料前處理

要在文本資料使用ML演算法或類神經網路,首先必須要將文字給轉換成數字向量,電腦才能看得懂。最簡單的做法就是蒐集一大堆的文字,然後給每個不同的字詞一個 index,

  1. 建立語料庫(字典),以及其對應關係(word_ind):
    比如說我們有語料庫(Corpus)只有兩個句子:[["i am happy"],[“she is so cute”]],那麼我們的辭彙(token)共有[“i”,”am”,”happy”,”she”,”is”,”so”,”cute”] 7個字,並且建立對應關係{i:1, am:2, happy:3, she:4, is:5, so:6, cute,7}。有了這樣的關係,一個句子就可以轉換成index的向量,例如 “she is happy” -->[4,5,3]。 而不在語料庫的詞,都視為unknown。
  2. 截長補短將句子補成一樣長度,將資料的維度統一方便後面丟入模型:
    使用的方法是zero padding,很簡單,就是過長的句子截掉,過短的補零。假設我們的資料就是上面的例子[[“i am happy”],[“she is so cute”]],則可以轉成[[1,2,3,0],[4,5,6,7]],變成長度一致的向量。

現在我們能將每個字轉換成一個index,一個句子轉換成一個向量了。但是這個詞彙的 index沒有任何辭彙的意義在裡頭,因此有了所謂 word embedding的技術,他將每個字轉換到一個連續的高維歐氏空間中,而在這個空間裡,意思較相關的字詞在空間中也會比較接近,如下圖:

https://www.youtube.com/watch?v=X7PH3NuYW0Q

而要如何得到 word embedding的轉換函數呢? 目前我知道的主要是word2vec 以及GloVe兩種,分別為prediction base 及count base 的方法,都是蒐集大量文本資料集並以無監督的方式訓練而成。 word2vec 在 簡易語音助理實做 裡有稍微的介紹到。embedding 這一步如果後面套用神經網路的話也可以讓他從頭 train 直接從你的 task 資料中學習。

好,假設我們已經能將詞透過 word embedding轉換成一組連續的向量了,到目前為止,我們的前處理大概是做了以下的流程:

https://developers.google.com/machine-learning/guides/text-classification/step-3

到這裡其實就可以套用一些ML的演算法做想做的事了,但如果只用字詞的組成來表達句子其實是有缺陷的, 我們沒有考慮到詞在句子中的位置資訊,而這對一段句子的意思有很大的影響。例如:

https://www.youtube.com/watch?v=X7PH3NuYW0Q

所以呢~大家會使用像 RNN, LSTM, attention 等的技術,將每個單詞上下文的文字也一起考慮進來,也就是一種具有記憶力的網路架構,能夠記得前面看過的資料。

Recurrent Neural Network(RNN) / Long Short-Term Memory (LSTM)

人在理解一個句子時,通常不是一個一個字逐字理解,而是從以往的知識以及上下文一起來理解文意,RNN 引入這種想法,讓機器模擬人類閱讀。而 LSTM 改善了一般RNN訓練時遇到梯度爆炸或消失的問題,同時也讓模型可以記住較長的資訊(記憶力較好)。

RNN 我是把他想成一個有自我遞迴的 layer ,他將讀過的東西會傳遞給下一個 input,如下圖:

https://colah.github.io/posts/2015-08-Understanding-LSTMs/

所以用在剛剛那個例子,”white blood cells destroying an inflection”,當模型讀到 “destroying”這個字時,他會包含前面 ”white”,”blood”,”cells” 的資訊

圖片來源

而RNN 也有bidirectional 的版本,他在判斷一個字的輸入時,同時input (看過) 他的前文以及後文,如下圖:

vhttps://www.youtube.com/watch?v=xCGidAeyS4M

RNN在訓練的時候由於其目標函數非常的陡峭崎嶇(因 recurrent 的 weight每次input 都不斷連乘,輸入的序列長度指數成長的影響了output),訓練時容易遇到gradient explode 以及gradient vanish 的問題,導致很難訓練,因此後來出現了LSTM。

根據 pytorch 的LSTM 公式搭配下面的圖即可以知道LSTM是怎樣運算的:

不要看到這麼多複雜的運算就嚇到,直接把他想成一個neural的運算,在keras 或 pytorch 都是一個指令就能建立 LSTM layer!

LSTM 不僅改善了RNN的訓練問題,在許多應用上也大幅改善了RNN的成果,也因此現在一般人在講RNN 通常就是指LSTM,若要表達原始的RNN 要稱 "SimpleRNN"。

BERT (Bidirectional Encoder Representation from Transformer)

https://www.seroundtable.com/google-curriculum-optimizing-bert-28964.html

近年來,自從2017年底 Google 發表了Transformer的經典論文 ”Attention Is All You Need” 後,各種NLP task 都被transformer, self-attention 給洗榜過一遍,開啟了"大注意時代"。

BERT 是 2019年 Google 以無監督的方式利用大量無標註文本訓練出來的語言代表模型,其架構為 Transformer 中的 Encoder。BERT 提供了一個 pre-trained 的 fine-tuning based language representation。

如同computer vision 領域裡有 ImageNet 的各種知名pre-trained 好的網路給大家可以站在巨人的肩膀上 fine-tune 自己的 task。
而BERT 是 Google研究團隊使用大量資料集 BooksCorpus(800M words)以及 English Wikipedia(2500M words) 總共33億個字來做pre-train,BERT-base使用4個TPU花費了4天才跑完(google暴力美學阿!)

https://www.youtube.com/watch?v=UYPa347-DdE

BERT提供了一個對人類語言有一定認知的一個 pre-train model,並提供了四種 fine-tune BERT 的 NLP應用範例,因此使用上我們只要把BERT當做一個~magic~ 的 layer,套上自己對應的 task 來fine tune就好 !

那什麼是self-attention? 什麼是transformer? 這實在說來話長,大家可以去看李宏毅老師的教學影片,講的無敵清楚,看過後再去看paper或網路上其他人的教學就容易許多。

好了! 說了那麼多,要是coding不出來就只是在空談阿!

不要急~先找個資料集來瞧瞧~

IMDb 影評分類 data overview

資料載點:http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz

此資料蒐集50000筆電影影評的文字資料,切分成25000筆 train, 25000筆 test。而在train & test 裡各自有neg 及pos 各12500筆負雷和好雷的影評資料,可以說是非常親人的入們款資料集!

來看一篇資料:

train>neg>1_1.txt: 影評url
Robert DeNiro plays the most unbelievably intelligent illiterate of all time. This movie is so wasteful of talent, it is truly disgusting. The script is unbelievable. The dialog is unbelievable. Jane Fonda’s character is a caricature of herself, and not a funny one. The movie moves at a snail’s pace, is photographed in an ill-advised manner, and is insufferably preachy. It also plugs in every cliche in the book. Swoozie Kurtz is excellent in a supporting role, but so what?<br /><br />Equally annoying is this new IMDB rule of requiring ten lines for every review. When a movie is this worthless, it doesn’t require ten lines of text to let other readers know that it is a waste of time and tape. Avoid this movie.

ok~憑我拙劣的英文能力也能看出這的確是一篇負評

接著我們就以 LSTM 以及 BERT 分別來實作電影影評分類,看看各自的表現如何~

實作 — LSTM

這部分我參考 PyTorch实现LSTM情感分析 這篇做一些修改來實做。 訓練的框架是用 pytorch,另外有使用到 gensim來load embedding 的pretrain weight(optional)

首先 load 資料並做前處理:

到這邊我們已經把資料都給 load 進來並且去除一些html tag, 標點符號, 換行符號的前處理。

train_data 是一個list 存放每一篇影評而train_tokenize是一個list 將上述的train_data依照空個切開:

train_data & train_tokenize

接著將單辭轉換成 index →zero-padding →切出validation set →轉成 pytorch tensor 最後創建dataloader:

到這裡我們已經把文章轉換成 index 的tensor 如下:

train_features

可以看到長度較短的文章後面都被zero-padding 成 0,而長度較長的也被截成相同長度的tensor。

來看個preprocess 的結果,比如一個句子: "an apple a day, keeps the doctor away":

### let us see some preprocess result ###
text = "an apple a day, keeps the doctor away"
token = tokenizer(text)
print('text: '+text)
print('token: '+ str(token))
print('encode to index: '+str(encode_samples([token])))
text: an apple a day, keeps the doctor away
token: ['an', 'apple', 'a', 'day,', 'keeps', 'the', 'doctor', 'away']
encode to index: [[23314, 28267, 26943, 0, 49709, 27809, 29103, 70204]]

可以看到每個字都被截出來轉成一個index,其中因為我在建置辭庫時已經前處理過將標點符號都濾除了,所以逗號 ","並沒有在我的辭庫裡,而在上面的”an apple a day, keeps the doctor away” 裡的逗號就被視為unknown 給予index: 0

接著我導入了pre-train好的 word-embedding weight,透過 genism 讀取GloVe官方提供的pre-train word vector,這步也可以不做,直接讓模型從頭學習embedding layer。

然後繼承 nn.Module 來建立網路架構:

架構很簡單就是 embedding(from pre-train weight) →兩層LSTM → Linear
(pytorch LSTM layer 講解)

最後終於要來訓練模型啦~

在1080ti 上花不到10分鐘就train完了,最後 testing set 的準確率達到88% !

實作 — BERT

這邊參考 進擊的 BERT:NLP 界的巨人之力與遷移學習 ,這篇裡頭的任務是成對句子的分類任務(判斷兩個句子是否是相同來源),而我把它改成單一句子的分類任務。

所需套建除了pytorch 外套用了HuggingFace 團隊的 GitHub 專案來實現BERT。直接安裝:

pip install transformers

首先import 相關模組:

import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F
import time
import os
import re
from itertools import chain
from transformers import BertTokenizer
PRETRAINED_MODEL_NAME = "bert-base-uncased" #英文pretrain(不區分大小寫)
print(torch.__version__)
#1.3.1

載入BERT token:

可以看到bert-base-uncase 約有3萬多個token。
接著要讀取IMDb的影評文字資料並且透過 BERT 的tokenizer 轉換成對應的index,並且創建進入BERT 需要的 tensor 如下圖,paper 上的範例,但我們的task只有一個句子,所以segments_tensor的部分會全部給1

來看一下到目前為止做的事情:

怕版面太長,我把字的數量減少一些來呈現

接著建立pytorch dataloader 來一次取一個batch的資料放到GPU訓練:

這邊記得在train 的dataset shuffle要寄得給true,因為我們的資料很整齊,前面都是pos 後面都是neg,這樣train 的時候一個batch會完全都是相同label,模型會train到奇怪的地方去。

接下來 load BERT pre-train model,單一句子分類的FINETUNE_TASK是歸類在 bertForSequenceClassification

這邊可以看到load進來的架構:

沒錯! 搞那麼久,這邊一行BertForSequenceClassification.from_pretraine就建好模型架構了!!

我們可以透過 model.config 來查看或調整模型參數。

最後來fine-tune BERT:

可以看到train的越多好像也沒什麼改進,且val loss甚至還一直上升,可能有些sample 錯的比較誇張導致 loss拉高,anyway validation acc 達到92%,讓我們來看看testing set的預測結果:

準確率也達到了92%耶~開心^^

其實在paper上fine tune的例子幾乎都只用了3~5個epoch,在加上剛剛上面的training 過程,我便直接使用train 一個epoch的模型來跑test,結果第一個epoch 在testing set也有92%的準確率......所以BERT本身就對語言已經有一定的認知,把training data看過一遍就大概能做出不錯的分類了。