1. 自然語言處理簡介
根據工業界的估計,僅有21% 的資料是以結構化的形式展現的[1]。在日常生活中,大量的資料是以文字、語音的方式產生(例如簡訊、微博、錄音、聊天記錄等等),這種方式是高度無結構化的。如何去對這些文字資料進行系統化分析、理解、以及做資訊提取,就是自然語言處理(Natural Language Processing,NLP)需要做的事情。
在NLP中,常見的任務包括:自動摘要、機器翻譯、命名體識別(NER)、關係提取、情感分析、語音識別、主題分割,等等……
在NLP與深度學習系列文章中,不會逐一解釋各個NLP任務,而是主要介紹深度學習模型在NLP中的應用。整體分為以下幾點:
- 首先介紹NLP基本流程以及在資料預處理方面的技術
- 而後會介紹最初期使用的神經網路:SimpleRNN、LSTM
- 繼而引入使得文字處理效能得到很大提升的Attention機制以及Transformer模型
- 最後介紹近幾年非常熱門的預訓練模型BERT,以及如何使用BERT預訓練模型的例子
下面首先介紹的NLP任務的一個基本工作流程。
2. NLP 任務流程
典型的NLP任務分為以下幾步:
- 資料收集
- 資料標註
- 文字標準化(Normalization)
- 文字向量化/特徵化(Vectorization/Featuring)
- 建模
前期主要是資料收集,並根據任務型別對資料做標註(例如情感分析中,對好、壞評價做標註)。接下來的2個步驟均是對文字進行預處理的步驟,為了提取文字中隱含的資訊,最後通過機器學習建模,達到任務目標。其中 3 – 5 這幾步是迭代的流程,為了模型的精度更準確,需要迭代這個過程,進行不斷嘗試。
資料收集以及標註並非在本文討論範圍內,接下來介紹文字標準化的目標與方法。
3. 文字標準化
由於文字資料在可用的資料中是非常無結構的,它內部會包含很多不同型別的噪點。所以在對文字進行預處理之前,它暫時是不適合被用於做直接分析的。
文字預處理過程主要是對 文字資料進行清洗與標準化。這個過程會讓我們的資料沒有噪聲,並可以對它直接做分析。
而文字標準化是NLP任務裡的一個資料預處理過程。它的主要目標與常規資料預處理的目標一致:提升文字質量,使得文字資料更便於模型訓練。
文字標準化主要包含4個步驟:
- 大小寫標準化(Case Normalization)
- 分詞(Tokenization)與 停止詞移除(stop word removal)
- 詞性(Parts-of-Speech,POS)標註(Tagging)
- 詞幹提取(Stemming)
3.1. 大小寫標準化
大小寫標準化是將大寫字元轉為小寫字元,一般在西語中會用到。但是對於中文,不需要做此操作。而且Case Normalization 也並非是在所有任務場景中都有用,例如在英文垃圾郵件分類中,一般一個明顯的特徵就是充斥著大寫單詞,所以在這種情況下,並不需要將單詞轉為小寫。
3.2. 分詞
文字資料一般序列的形式存在,分詞是為了將文字轉為單詞列表,這個過程稱為分詞(tokenization),轉為的單詞稱為token。根據任務的類別,單詞並非是分詞的最小單位,最小單位為字元。在一個英語單詞序列中,例如 ride a bike,單詞分詞的結果為 [ride, a, bkie]。字元分詞的結果為[r, i, d, e, a, b, k, e]。
在中文中,分詞的最小單元可以不是單個字,而是詞語。
3.3. 停止詞移除
停止詞移除是將文字中的標點、停頓詞(例如 is,in,of等等)、特殊符號(如@、#等)移除。大部分情況下,此步驟能提升模型效果,但也並非在任何時候都有用。例如在騷擾郵件、垃圾郵件識別中,特殊字元相對較多,對於分辨是否是垃圾郵件有一定幫助。
3.4. 詞性標註
語言是有語法結構的,在大部分語言中,單詞可以被大體分為動詞、名詞、形容詞、副詞等等。詞性標註的目的就是就是為了一條語句中的單詞標註它的詞性。
3.5. 詞幹提取
在部分語言中,例如英語,一個單詞會有多種表示形式。例如play,它的不同形式有played,plays,playing等,都是play的變種。雖然他們的意思稍微有些區別,但是大部分情況下它們的意思是相近的。詞幹提取就是提取出詞根(例如play 就是它各種不同形式的單詞的詞根),這樣可以減少詞庫的大小,並且增加單詞匹配的精度。
這些文字標準化的步驟,可以用於對文字進行預處理。在進一步基於這些文字資料進行分析時,我們需要將它轉化為特徵。根據使用用途不同,文字特徵可以根據各種技術建立而成。如:句法分析(Syntactical Parsing),N元語法(N-grams),基於單詞計數的特徵,統計學特徵,以及詞向量(word embeddings)等。
其中詞向量是當前主要的技術,下面主要介紹詞向量。
4.文字向量化/特徵化
向量化是將單詞轉為詞向量的過程,也稱為詞嵌入(word embedding),這裡嵌入的意思是說將單詞所包含的資訊嵌入到了向量中。
在word embedding出現之前,有2種文字向量化的方式,下面簡單地介紹一下。
4.1. 基於單詞計數的特徵
此方法非常簡單,首先將語料庫文字進行分詞,得到單詞數。然後在對句子構建向量時,可以根據句子中包含的單詞數構建向量。
舉個例子,假設語料庫為“我愛我的家,我的家是中國”。在進行分詞後可以得到:
{'愛', '是', ',', '我', '中', '國', '家', '的'}
對於一個新的句子,例如”我愛我的國“,基於單詞計數的表示即為:
[1, 0, 0, 2, 0, 1, 0, 1]
可以看到這種方法僅是對句子中的單詞進行了統計,並不包含單詞具體代表的含義(例如多義詞的意義無法在此體現)。這種稱為不包含上下文(context-free)的向量化。不過它提供了一種用於衡量兩個文件相似度的方法。一般會通過餘弦相似度或是距離來比較兩個文件的相近程度。
4.2. 基於統計學的特徵
在對文字做向量化時,一個常用的技術是詞頻-逆文件頻率(Term Frequency – Inverse Document Frequency),常稱為TF-IDF。TF-IDF 最初源於解決資訊檢索問題。它的目的是在於:基於單詞在文件裡出現的頻率(不考慮嚴格的排序),將文件轉化為向量模型。
這裡Term Frequency很好理解,就是某個單詞在文件中出現的頻率。
在介紹Inverse Document Frequency(IDF)前,我們看一個例子。假設現在要通過單詞檢索文件,這裡文件主要為各類食譜。如果我們使用單詞如蘋果、醋、醬油這類經常在食譜中出現的單詞,則會有大量的文件可以匹配。而若是我們使用一些不常見的詞,例如黑莓,則可以顯著縮小要搜尋的食譜文件。也就是說,若是一個單詞越是不常見,則越有助於檢索需要的文件。所以對於這類不常見的詞,我們希望給它一個更高的分數。反之,對於在各個文件中都頻繁出現的詞,希望給它們更低的分數。這就是IDF的思想。
TF-IDF 的計算,數學上表示可以寫為:
TF-IDF = TF(t, d) x IDF(t)
這裡t表示term,也就是單詞;D表示Document,文件。
IDF的定義為:
IDF(t) = log( N/(1+nt) )
這裡N表示語料庫中的文件總數,nt表示有多少文件中存在單詞t。這個加1是為了防止除以0。
4.3. 詞向量
上面介紹了2種方式,僅僅是解決了用一個向量代表了一個文件,但無法體現詞與詞之間的關係。而從常理來看,詞與詞之間是存在聯絡的。例如,炒鍋與鍋鏟,這2個詞,從直覺上來看,會經常在一起出現。而炒鍋與人行橫道,應該基本不會出現在一起。
詞向量,也稱為詞嵌入,是將單詞對映(或稱嵌入)到一個高維空間中,使得意義相近的詞在空間內距離相近;意義不同的詞在空間內距離相遠。
4.3.1. Word2Vec
在詞嵌入技術中,一個具有時代意義的方法是Word2Vec,於2013年由Google的工程師提出。它本身算是神經網路處理任務的一個副產品。例如,搭建一個神經網路,每次取一個批次的5個單詞,中間的單詞作為target,周圍的4個詞作為輸入,來訓練神經網路。初始的輸入詞向量使用one-hot編碼。這樣再訓練完成後,第一層的輸入層引數,即為所得的詞向量矩陣。
Word2Vec論文提出了2種訓練方式:continuous bag-of-word(CBOW)和continuous skip-gram。在論文提出時,CBOW是當時主流方法;不過最後skip-gram模型與負取樣的整合方法,已經成了Word2Vec的代名詞。
Word2Vec已經有很多優秀的文章講解過,在此不再贅述。下面主要舉例說明skip-gram負取樣的方式。
假設語料庫中有一條句子為:“需要把魚煎到棕黃再翻面”。
我們設定一個單詞數為5的視窗,也就是一次處理5個單詞,例如“要把魚煎到”這5個詞。中間的詞“魚”會被用於輸入到搭建好的神經網路中,用於預測它前面的2個詞(“要把”),以及後面的2個詞(“煎到”)。
假設語料庫中有10000個單詞,神經網路的任務就是要判斷給定一組詞,它們是否相關。例如,對(魚,煎)判斷為true,對(魚,樹)判定為false。這種方法就是Skip-Gram Negative Sampling(SGNS),基於的假設就是:與某個詞相關的詞會更高概率一起出現(或是離的不遠),所以可以從一段短語中拿出一個詞,用於預測它周圍的詞。
SGNS的方法可以顯著降低訓練超大型語料庫的時間,最終第一層輸入層的權重矩陣即為詞向量矩陣。
當然,Word2Vec也有它的侷限性,一個典型的侷限就是沒有全域性的統計資訊,因為它在訓練的時候最長是以一個視窗為單位,能看到的只有視窗內的上下文資訊。
4.3.2. GloVe
GloVe (Global Vectors for Word Representation) 模型於2014年提出,於Word2Vec論文發表1年後。它們生成詞向量的方法非常相似,都是通過一個詞(例如上述例子中的“魚”)周圍的詞,來生成這個詞(例如“魚”)的詞嵌入。不過相對於Word2Vec,GloVe利用了全域性的文字統計資訊,也就是構建語料庫的共現矩陣。 共現矩陣簡單來說,就是2個單詞在視窗中一同出現的次數,以矩陣的形式表示。在有了全域性統計資訊(共現矩陣)後,接下來的問題是如何將全域性資訊應用到詞向量生成中。
在原論文中,作者用了2個單詞ice和steam來描述這個理念。假設有另一單詞solid,用來探查ice與steam之間的關係。在steam上下文中出現solid的概率為 p(solid | steam),從直覺上來看,它的概率應該會很小(因為steam與solid從直覺上同時出現的概率不會很高)。
而對於ice上下文中出現solid的概率 p(solid | ice),直覺上應該會很高(因為ice是固體,直覺上它們同時出現的概率會很高)。
那如果我們計算p(solid | ice)/ p(solid | steam) 的比值,則預期的結果應該會很高。
而若是用gas作為探測詞,則 p(gas | ice)/p(gas | steam) 的比值應該會很低(因為gas是氣體,在直覺上在steam的上下文中出現的概率高,而在ice的上下文中出現的概率低)。
而若是用water這類與ice和steam相關性都很低的詞作為探測詞,則p(water | ice)/p(water | steam) 的概率應該接近於1。論文中也舉了另一個與ice和steam不相干關的詞fasion,p(fasion | ice)/p(fasion | steam) 的結果也近似於1。
也就是說,共現矩陣的概率的比值,可以用來區分詞。GloVe的過程就是確保這種關係被用於生成詞嵌入,將全域性資訊引入到了詞向量的生成過程中。
若是對GloVe方法有興趣,可以閱讀這位博主的介紹:
https://blog.csdn.net/XB_please/article/details/103602964
或是GloVe論文:
https://nlp.stanford.edu/pubs/glove.pdf
對於GloVe的效果,論文中提到是遠高於word2vec。
在使用GloVe時,可以直接從stanford的官網下載預訓練的GloVe詞嵌入,分為50、100、200、300維的詞嵌入。地址為:
http://nlp.stanford.edu/data/glove.6B.zip
4.3.3. BERT
Word2vec與GloVe都有一個特點,就是它們是上下文無關(context-free)的詞嵌入。所以它們沒有解決:一個單詞在不同上下文中代表不同的含義的問題。例如,對於單詞bank,它在不同的上下文中,有銀行、河畔這種差別非常大的含義。BERT的出現,解決了此問題,並極大地提升了baseline。
另一方面,BERT還解決了GloVe的一個侷限性問題,就是:詞庫不夠。例如在使用GloVe預訓練的詞嵌入應用到 IMDB資料集上時,大約有15%的詞不在GloVe的詞庫中。當然,這也是由於一個詞會有多種形式,導致所需詞庫巨大。
在BERT中,使用了WordPiece的分詞方法,詞庫大小為30000。其實這個大小是遠小於GloVe的詞庫大小,GloVe詞庫為40000。這是由於BERT使用的subword分詞方法可以顯著減少詞庫的大小,WordPiece基於的是BPE(Byte Pair Encoding),BPE屬於subword分詞法中的一種。
簡單地說,subword分詞法主要做的就是將單詞進行進一步的拆分,讓詞庫更加精簡。更精簡的詞庫可以降低訓練時間,並減少記憶體使用。Subword分詞法,以英語語言為例,舉個簡單的例子,例如在詞庫中引入2個新的詞,分別為-ing與-ion。則任何結尾為-ing或-ion的詞,均可分為2個詞,一個是字首詞,一個是-ing或-ion中的任何一個。這樣就極大減少了詞庫的大小。當然,WordPiece以及BPE中使用的方法並沒有這麼簡單。若是對BPE與WordPiece演算法有興趣,可以閱讀這位博主的介紹:
https://www.cnblogs.com/huangyc/p/10223075.html
在BERT中,對它使用的WordPiece分詞,我們可以看一個例子:
#!pip install transformers==3.0.2 import tensorflow as tf from transformers import BertTokenizer import numpy as np bert_name = 'bert-base-cased' tokenizer = BertTokenizer.from_pretrained(bert_name, add_special_tokens=True, do_lower_case=False, max_length=150, pad_to_max_length=True) # tokenize single sequence tokens = tokenizer.encode_plus("Don't be lured", add_special_tokens=True, max_length=9, pad_to_max_length=True, truncation='longest_first', return_token_type_ids=True) res = [] reverse_dic = [(id, item) for item, id in tokenizer.vocab.items()] for tk in tokens['input_ids']: res.append(reverse_dic[tk][1]) print(res) ['[CLS]', 'Don', "'", 't', 'be', 'lure', '##d', '[SEP]', '[PAD]']
可以看到其中lured被拆分成‘lure’與‘##d’。另外的[CLS] 、[SEP] 與[PAD] 是BERT Tokenizer中的保留詞,分別代表“分類任務”、“Sequences之間的間隔”,以及序列補全(序列補全與截斷是NLP任務中常用的方法,用於將不同長度的文字統一長度)。
更多有關BERT的具體內容會在後續BERT章節進行介紹。
5. 總結
在文字資料進行了標準化與向量化後,即可根據任務型別進行建模,將資料輸入到模型中進行訓練。文字標準化 => 向量化 => 建模,也是一個迭代的過程。下一章會介紹NLP任務早期建模使用的神經網路:SimpleRNN、LSTM以及雙向迴圈神經網路。
References
[1] Natural Language Processing | NLP in Python | NLP Libraries (analyticsvidhya.com)
[2] Essentials of NLP | Advanced Natural Language Processing with TensorFlow 2 (oreilly.com)