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)