NLP 入門 (1) — Text Classification (Sentiment Analysis) — 極簡易情感分類器 Bag of words + Naive Bayes
使用到的函式庫(Libraries): pandas, scikit-learn.
碎念:
因為最近從 ULMFiT, BERT, XLNet 論文地獄中解救出來,就不禁想起當初是怎麼接觸到Natural Language Processing (NLP)的,適逢 NLP 的 model 們有逐漸統一的趨向,從以前的百家爭鳴到現在 Language Model + Fine-tuning 的大架構逐漸完整 (現在不知道Language Model 這些是什麼沒關係),我認為現在是入門NLP最好的時機,因為相對來說,要讀的論文從2017年的Attention Is All You Need 開始就好, 所以幾篇論文的進度就可以讓你成為現在很棒的NLP專家。因此我的文章將會從最簡單的 bag-of-words 談論到最近2019.06才提出的XLNet,藉由把自己的理解寫下來,如果有讀者有任何疑問的地方也都歡迎隨時提出交流。
我當初入門NLP是在就讀研究所時修了Machine Learning的課,當時老師介紹了Naive Bayes 這個 model 糊裡糊塗地實作了一個 spam classifier (垃圾郵件分類器),當初覺得自己的分類器可以達到 95.2% 的準確率就很厲害,後來學了現在的model基本上都是直接碾壓…,廢話不多說直接切入正題吧。
什麼是詞袋(Bag-of-words)?
如果我們要讓機器學習如何對影評,或者餐廳評論做分類(正面、負面),第一個出現在我們眼前的問題是:
機器要怎麼看懂文字? ”這部電影真好看“,“這部電影甘納賽(像大便)”,這類文字要怎麼轉成機器可讀的模式呢?「快樂」、「難過」在機器眼中不過是0101的ASCII組合。
第一種方法就是詞袋(Bag of words)。
詞袋的特性:
所有下面的程式碼都在我的colab
一、每篇文章/評論,會用一個一維的vector來表示,每個vector的長度就是全部詞彙的總數(有點抽象沒關係,等等會有程式碼來幫你釐清觀念)
二、文字之間的順序在詞袋之中無法被保存。
三、文字之間的「意義」沒有被保存,「貓」和「狗」以及「貓」和「飛機」,貓和狗應該是較為相似的,但在詞袋之中也沒辦法體現這種「距離」的概念(到下一篇word-embedding就會提到解決方法)。
Features: (總共單字的數量,也會是文字向量的長度)
[‘and’, ‘document’, ‘first’, ‘is’, ‘one’, ‘second’, ‘the’, ‘third’, ‘this’]
Values:
#原句:'This is the first document.'
[[0 1 1 1 0 0 1 0 1]# document, first, is, the, this#原句:'This document is the second document.'
[0 2 0 1 0 1 1 0 1] # document*2, is, second, the ,this#原句:'And this is the third one.'
[1 0 0 1 1 0 1 1 1] # and, is, one, the, third, this#原句:'Is this the first document?'
[0 1 1 1 0 0 1 0 1]]# document, first, is, the, this
在Colab之中我也寫了很簡短的code(炫技),來轉成pandas中的Dataframe。
值得注意的是第一個句子和第二個句子開頭的This 都是大寫,但這邊Countvectorizer 已經自動把它們小寫化了,經過查文件以後發現Countvectorizer有一個參數是lowercase,默認值是True,如果有認為大小寫會影響的效能的話,只要在CountVectorizer初始化時多放一個參數lowercase=False就搞定啦,還希望大家可以自己多做實驗來比較效能,因為很多情況都是根據個案的效能而做調整的。
在進入Naive Bayes之前,再複習一下詞袋的特性:
一、每篇文章/評論/句子 (你可以任意切分)都被用一維的vector來表示,每個vector的index就代表特定的單字。
二、文字之間的順序經由詞袋之後就無法保存了,也許這也是為什麼循環神經網路(RNN),「曾經」在 Natural Language Processing (NLP)叱吒風雲。(這邊我埋了兩個伏筆一個是RNN,最快會在下一篇blog講到,其次是「曾經」,最快會在第四篇(BERT)的時候講到)
三、文字之間的「距離」無法被體現,詞袋出來的結果只會是該單字出現幾次,僅此而已。
「錢,不是問題」、「不,錢是問題」會被轉換成完全相同的 vector。
詞袋到這邊算是簡單的告一個段落了,我們已經將純文字轉換成這些0101的詞向量(word vector)features,有了features之後下一步是什麼呢?丟到model,丟到model,丟到model。
什麼是Naive Bayes?
將文字向量化(Vectorize)之後,下一步就是開心地丟到gradient boosting???(在做kaggle的練習的時候,常常不知道該選擇什麼model,就會胡亂選boosting類的model),但這邊要介紹的是除了詞袋(bag of words)之外,這篇文章第二個主角 Naive Bayes classifier(單純貝氏分類器),在介紹這個model怎麼使用之前,我覺得還是有必要介紹一下什麼是貝氏定理:
用白話文分解這個公式:
P : 機率
A|B : 已知 B發生的情況下,發生 A
P(A|B): 已知 B發生的情況下,發生 A 的條件機率
P(B|A): 已知 A發生的情況下,發生 B 的條件機率
從條件機率的定義中我們可以知道:
(1)
P (A ∩ B): A 和 B 同時發生的機率
(2) 相反也成立:
首先把 (1) 同乘以 P(B):
P(A | B) * P(B) = P (A ∩ B) …(3)
接著把 (2) 同乘以 P(A)
P(B | A) * P(A) = P (A ∩ B)…(4)
把 (3) (4) 合併
P(A | B) * P(B) = P (A ∩ B) = P(B | A) * P(A)
保留最左邊和最右邊:
P(A | B) * P(B) = P(B | A) * P(A)
同除以 P(B) 就得到貝氏定理的公式:
如果對機率有更多興趣,都請參考wikipedia, 還有這篇很棒的文章。
Naive Bayes Classifier真實應用:
假設今天我們要分析影評的評價,讓機器告訴我們這則影評究竟是正面(positive)或者是負面(negative),這個貝氏定理要怎麼幫助我們呢?以下例子的說明我都盡量用中文來方便說明,但是最後的colab程式實作我仍會以英文IMDB影評這個資料集來做分析。
假設我們今天有3筆被標籤過的影評資料,我們想預測第四筆未知的影評,一起來在腦中建立一個 Naive Bayes Classifer吧!
根據貝氏定理:
從上面三個有標記的訓練資料我們可以得到以下的:
P(正面 | 這): 在出現”這”以後,影評是”正面”的條件機率,
P(正面 | 一部): 在出現”一部”以後,影評是”正面”的條件機率,
這些有什麼用呢?
Naive Bayes 就是建立在一個”Naive”的假設之下,它假設所有的詞出現的機率,彼此之間是獨立的(很重要,如果沒有這個假設 Naive Bayes 就不成立),所以今天我們想根據已有標籤的資料,來預測「我覺得這部電影真的很好看」是屬於正面還是反面的評論:
- P(正面 |我覺得這部電影真的很好看) = P(正面 | 我) * P(正面 | 覺得) * P(正面 | 這部) * P(正面 | 電影) * P(正面 | 真的) * P(正面 | 很) * P(正面 | 好看)
- P(負面 |我覺得這部電影真的很好看) = P(負面 | 我) * P(負面 | 覺得) * P(負面 | 這部) * P(負面 | 電影) * P(負面 | 真的) * P(負面 | 很) * P(負面 | 好看)
因為詞之間出現的機率彼此之間是獨立的,才能用相乘。藉由比較 (1), (2) 我們就可以判斷究竟「我覺得這部電影真的很好看」是正面的評價,還是負面的評價。那麼問題就來了,等號後面那一長串要怎麼求呢? 根據貝氏定理我們知道:
Naive Bayes在實作的時候有兩個細節需要注意,原本只是想介紹比較粗略的概念,沒想到這裡也碰到了這個問題。
一、在預測的時候發現到在training data沒有出現過的單字/詞:
複習一下我們的資料集:
仔細觀察可以發現,「我」這個字/詞 並沒有出現在任何正面和負面的例子中。(只有在要預測的例子之中出現)
P(我 | 正面) 會讓整串條件機率都為0 ( 0 * P(正面 | 覺得) * P(正面 | 這部) * P(正面 | 電影) * P(正面 | 真的) * P(正面 | 很) * P(正面 | 好看) = 0)所以整串文字屬於正面和負面的機率會一樣。
很顯然的這不是我們想要的結果,因為除了這個我們沒有遇過的單字之外,這串影評還有很多有用的信息,可是卻被一顆老鼠屎毀了。要怎麼解決這問題呢?其實也很簡單,就是使用smoothing,假設這個字/詞至少出現了1次,(更多smoothing細節,下文也會有更完整的說明),當然這些處理細節在sklearn實作的時候,都已經幫我們處理掉了。偉哉sklearn!
二、在長文件的時候會出現underflow的現象:
機率的定義就是 0 ≤ P ≤ 1,因此在文章很長的時候,會出現很多小於1的數值相乘,造成的結果會是一個很小很小的數字,甚至有可能小於電腦浮點數運算的最小時,也就會等於0,也可以在python 視窗試試看這個運算0.1 ** 500 看看答案是多少 (0)。 很直觀的解決方法就是取log,
在我看來可以取log來解決這個問題有兩個原因,其一:log 0 的地方也被我們 smooth 巧妙的避免了,所以不會有無法定義的地方 (log 0 = undefined)。再者:在 0 < X ≤ 1,X 和 log X 有著相同的趨勢(突然忘了數學特別的名詞), X 越大 log X 也越大, X 越小 log X也越小。
記得在取log之後,運算時機率之間的相乘要記得改成相加,才能真正有效避免underflow。
會遇到的問題都解決了那就開始來建一個人腦Naive Bayes Classifier吧!
所有屬於正面的詞:(這)、(真的)、(是)、(一部)、(好看)、(的)、(電影)、(好看)、(的)、(電影)、(你)、(絕對)、(不會)、(想)、(錯過)
總共有15個詞
所有屬於負面的詞:(爛透)、(的)、(電影)、(不要)、(浪費)、(你)、(的)、(時間)
總共有8個詞
要比較的是:
(1) P(正面 | 我覺得這部電影真的很好看) =
(2) P(負面|我覺得這部電影真的很好看) =
畫紅線是因為,我們最終目的只是要比較 (1) (2),他們有同樣的分母,所以可以同乘以 P(我) * P(覺得) * …. * P(好看) 來把它們消掉。
先算正面的機率: P(正面 | 我覺得這部電影真的很好看) =
P(我 | 正面) * P(覺得 | 正面) * P(這部 | 正面) * P(電影 | 正面) * P(真的 | 正面) * P(很 | 正面) * (好看 | 正面) * P(正面)⁷
= P(我 | 正面) = 0 + 1/ 15 + 1 = 原本出現0次,smooth之後分子變成1次,原本正面的單詞總共有15的smooth之後分母也+1 所以就是 1/16P(覺得 | 正面) = 0 + 1 / 15 + 1 = 1/16 *P(這部 | 正面) = 0 + 1 / 15 + 1 = 1/16 *P(電影 | 正面) = 2 + 1 / 15 + 1 = 3/16 *P(真的 | 正面) = 1 + 1 / 15 + 1 = 2/16 *P(很 | 正面) = 0 + 1 / 15 + 1 = 1/16 *P(好看 | 正面) = 2 + 1 / 15 + 1 = 3/16 *P(正面)⁷ = (2/3)⁷ = 0.0585= 0.000016075102881= 1.6075102881e-05
再算正面的機率: P(負面 | 我覺得這部電影真的很好看) =
P(我 | 負面) * P(覺得 | 負面) * P(這部 | 負面) * P(電影 | 負面) * P(真的 | 負面) * P(很 | 負面) * (好看 | 負面) * (1/3)⁷ =
P(我 | 負面) = 0 + 1/ 8 + 1 = 1/9P(覺得 | 負面) = 0 + 1 / 8 + 1 = 1/9 *P(這部 | 負面) = 0 + 1 / 8 + 1 = 1/9 *P(電影 | 負面) = 1 + 1 / 8 + 1 = 2/9 *P(真的 | 負面) = 0 + 1 / 8 + 1 = 1/9 *P(很 | 負面) = 0 + 1 / 8 + 1 = 1/9 *P(好看 | 負面) = 0 + 1 / 8 + 1 = 1/9 *P(負面)⁷ = (1/3)⁷= 0.000457247370828= 0.000000001720783= 1.720783e-09
結果:
可以比較正面與反面的機率,得到1.6075102881e-05 > 1.720783e-09的結論,所以機器會認為「我覺得這部電影真的很好看。」屬於正面!從這個例子之中也可以觀察到,正面和負面的訓練資料只分別有16和9個詞,可是計算出來的機率已經非常小,當今天訓練資料上萬筆的時候,underflow(小到被電腦誤以為是0)是不可避免的,因此再來計算一次log的版本,看看是不是能得到一樣的結果(正面機率 > 負面機率)?
log(P(正面 | 我覺得這部電影真的很好看) ) = log(P(我 | 正面) )+…+log(P(好看 | 正面))
= log(1/16) + log(1/16) + log(1/16) + log(3/16) + log(2/16) + log(1/16) + log(3/16) + 7*log(2/3)
= -8.40620
log(P(負面| 我覺得這部電影真的很好看) ) = log(P(我 | 負面) )+…+log(P(好看 | 負面))
= log(1/9) +log(1/9) + log(1/9) + log(2/9) + log(1/9) + log(1/9) + log(1/9) + 7 * log(1/3)
= -9.71851
可得到相同的結論,Naive Bayes 在實作上來說非常簡單,只要把詞袋的結果再丟給Sklearn 內建的Naive Bayes模型就好:基本上四行code就搞定:
可以發現Naive Bayes的計算我們以人腦都能完成,電腦再處理25,000則 IMDB影評也在很短的時間內就完成。
下圖比較了 Naive Bayes (記作MNB)與其他接下來文章會提到的 Models 在IMDB、Yelp-2、Yelp-5資料集上的效能比較,和其他的Models相比,Naive Bayes的運算速度真的很快,效能也遠比 Support Vector Machine和 Gradient boosting 好得多,但離最先進(State-of-the-art, SOTA) 的Model還是有一定的落差。(接下來的文章就會順著這幾個model的脈絡走下去)
可以注意到的是MNB有 (rmsw -> Remove Stop words), (TF-IDF 另外一種取代 bag-of-word 的方式),但由於篇幅關係,我應該會再寫一篇文章來補充關於 Stop words 和 TF-IDF,第一次寫這種文章希望不會太難消化,有什麼問題都歡迎在底下留言或者Email我討論 (sfhsu29@gmail.com)。