簡介
我致力於研究自然語言處理(NLP)領域相關問題。每個NLP問題都是一次獨特的挑戰,同時又反映出人類語言是多麼複雜、美麗又絕妙。
但是一個讓NLP從業者頭疼的問題是機器無法理解語句的真正含義。是的,我指的是自然語言處理中的上下文問題。傳統的NLP技術和架構能很好地處理基礎任務,但當我們嘗試將上下文納入變數時其效果就會下降。
近18個月以來NLP領域的格局發生了重大變化,諸如Google的BERT和Zalando的Flair等NLP模型已經能夠分析語句並掌握上下文中的資訊。
ELMo模型
能夠理解上下文語境是NLP領域的一項重大突破,這歸功於ELMo(Embeddings from Language Models),它是AllenNLP研發的一種最先進的NLP架構。當你讀完這篇文章,你會和我一樣成為ELMo的忠實粉絲。
在這篇文章中,我們會探索ELMo(嵌入語言模型),並通過python使用它在一個真實的資料集上構建一個令人興奮的NLP模型。
注:這篇文章假設你熟悉多種word embeddings和LSTM(Long short-term memory)結構,你可以參閱以下文章來了解有關這些專題的更多資訊:
An Intuitive Understanding of Word Embeddings
(https://www.analyticsvidhya.com/blog/2017/06/word-embeddings-count-word2veec/?utm_medium=ELMoNLParticle&utm_source=blog )
Essentials of Deep Learning : Introduction to Long Short Term Memory
(https://www.analyticsvidhya.com/blog/2017/12/fundamentals-of-deep-learning-introduction-to-lstm/?utm_medium=ELMoNLParticle&utm_source=blog )
目錄
1. 什麼是ELMo?
2. 理解ELMo工作原理
3. ELMo與其他詞嵌入的區別是什麼?
4. 在python中應用ELMo模型進行文字分類:
理解問題陳述
資料集介紹
匯入庫
匯入和檢查資料
文字清洗和預處理
簡要介紹TensorFlow Hub
準備ELMo模型向量
構建模型並評估
5. 我們還能用ELMo做什麼?
6. 結語
1. 什麼是ELMo?
我們提到的ELMo並不是《芝麻街》(Sesame Street)中的角色,這也是一個體現了上下文語境的重要性的典型例子。
ELMo是一種在詞向量(vector)或詞嵌入(embedding)中表示詞彙的新方法。這些詞嵌入方法在下列幾種NLP問題中能有效生成最先進(SOAT)的結果:
全球的自然語言處理學家都開始在學術或應用領域的NLP問題中使用ELMo。建議你檢視ELMo的初始論文(https://arxiv.org/pdf/1802.05365.pdf)。通常我不會建議大家去讀學術論文因為它們往往又長又複雜,但這篇論文不同,它很好地解釋了ELMo原理和設計過程。
2. 理解ELMo工作原理
在實踐之前讓我們需要先直觀瞭解一下ELMo是如何運作的。為什麼說這一步很重要?
試想如下場景:你已經成功地從GitHub上下載了ELMo的python程式碼並在自己的文字資料集上構建了模型,但只得到了一般的結果,所以你需要改進。如果你不理解ELMo的架構你將如何改進呢?如果沒有研究過又怎麼知道需要調整哪些引數呢?
這種思路適用於其他所有機器學習演算法,你不需要了解它們的推導過程但必須對它們有足夠的認識來玩轉和改進你的模型。
現在,讓我們回到ELMo的工作原理。
正如我之前提到的,ELMo的詞向量是在雙層雙向語言模型(two-layer bidirectional language model , biLM)上計算的。這種模型由兩層疊在一起,每層都有前向(forward pass)和後向(backward pass)兩種迭代。
上圖中的結構使用字元級卷積神經網路(convolutional neural network, CNN)來將文字中的詞轉換成原始詞向量(raw word vector)
將這些原始詞向量輸入雙向語言模型中第一層
前向迭代中包含了該詞以及該詞之前的一些詞彙或語境的資訊
後向迭代中包含了該詞之後的資訊
這兩種迭代的資訊組成了中間詞向量(intermediate word vector)
這些中間詞向量被輸入到模型的下一層
最終表示(ELMo)就是原始詞向量和兩個中間詞向量的加權和
因為雙向語言模型的輸入度量是字元而不是詞彙,該模型能捕捉詞的內部結構資訊。比如beauty和beautiful,即使不瞭解這兩個詞的上下文,雙向語言模型也能夠識別出它們的一定程度上的相關性。
3. ELMo與其他詞嵌入的區別是什麼?
與word2vec或GLoVe等傳統詞嵌入不同,ELMo中每個詞對應的向量實際上是一個包含該詞的整個句子的函式。因此,同一個詞在不同的上下文中會有不同的詞向量。
你可能會問:這種區別會對我處理NLP問題有什麼幫助嗎?讓我通過一個例子來解釋清楚:
我們有以下兩個句子:
I read the book yesterday.
Can you read the letter now?
花些時間考慮下這兩個句子的區別,第一個句子中的動詞“read”是過去式,而第二個句子中的“read”卻是現在式,這是一種一詞多義現象。
語言是多麼精妙而複雜
傳統的詞嵌入會對兩個句子中的詞“read”生成同樣的向量,所以這些架構無法區別多義詞,它們無法識別詞的上下文。
與之相反,ELMo的詞向量能夠很好地解決這種問題。ELMo模型將整個句子輸入方程式中來計算詞嵌入。因此,上例中兩個句子的“read”會有不同的ELMo向量。
4. 實現:在python中應用ELMo模型進行文字分類
現在是你們最期待的部分——在python中實現ELMo!讓我們逐步進行:
理解問題陳述
處理資料科學問題的第一步是明確問題陳述,這將是你接下來行動的基礎
對於這篇文章,我們已經有如下問題陳述:
情感分析一直是NLP領域中的一個關鍵問題。這次我們從Twitter上收集了消費者對於生產並銷售手機、電腦等高科技產品的多個公司的推文,我們的任務是判斷這些推文是否包含負面評價。
這顯然是一個文字的二分類任務,要求我們從提取的推文預測情感。
資料集介紹
我們已經分割了資料集:
訓練集中有7920條推文
測試集中有1953條推文
你可以從這裡下載資料集
https://datahack.analyticsvidhya.com/contest/linguipedia-codefest-natural-language-processing-1/#data_dictionary
注:資料集中的多數髒話已經被替換成了“$&@*#”,但可能部分推文中還有一些髒話存在。
好了,讓我們開啟最喜歡的Python IDE開始程式設計吧!
匯入庫
匯入我們將要用到的庫:
import pandas as pd
import numpy as np
import spacy
from tqdm import tqdm
import re
import time
import pickle
pd.set_option('display.max_colwidth', 200)
匯入和檢查資料
# read data
train = pd.read_csv("train_2kmZucJ.csv")
test = pd.read_csv("test_oJQbWVk.csv")
train.shape, test.shape
Output:
((7920, 3), (1953, 2))
訓練集中有7920條推文,訓練集中有1953條推文。接下來我們檢查下訓練集中的類別分佈:
train['label'].value_counts(normalize = True)
Output:
0 0.744192
1 0.255808
Name: label, dtype: float64
這裡1代表負面推文,0代表非負面的推文。
現在讓我們看一下訓練集的前五行:
train.head()
我們有三列資料,“tweet”列是獨立變數,“label”列是目標變數
文字清洗和預處理
理想狀況下我們會有一個整潔且結構化的資料集,但目前NLP領域還很難做到。
我們需要花費一定時間來清洗資料,為模型構建做準備。從清洗後的文字中提取特徵會變得簡單,甚至特徵中也會包含更多資訊。你會發現你的資料質量越高,模型的表現也就會越好。
所以讓我們先清理一下已有的資料集吧。
可以發現有些推文中有URL連結,它們對情感分析沒有幫助,所以我們需要移除它們。
# remove URL's from train and test
train['clean_tweet'] = train['tweet'].apply(lambda x: re.sub(r'http\S+', '', x))
test['clean_tweet'] = test['tweet'].apply(lambda x: re.sub(r'http\S+', '', x))
我們使用正規表示式(Regular Expression)來移除URL。
注:你可以從這裡瞭解正規表示式
https://www.analyticsvidhya.com/blog/2015/06/regular-expression-python/regular-expressions-python?utm_medium=ELMoNLParticle&utm_source=blog
我們現在來做一些常規的資料清理工作:
# remove punctuation marks
punctuation = '!"#$%&()*+-/:;<=>?@[\\]^_`{|}~'
train['clean_tweet'] = train['clean_tweet'].apply(lambda x: ''.join(ch for ch in x if ch not in set(punctuation)))
test['clean_tweet'] = test['clean_tweet'].apply(lambda x: ''.join(ch for ch in x if ch not in set(punctuation)))
# convert text to lowercase
train['clean_tweet'] = train['clean_tweet'].str.lower()
test['clean_tweet'] = test['clean_tweet'].str.lower()
# remove numbers
train['clean_tweet'] = train['clean_tweet'].str.replace("[0-9]", " ")
test['clean_tweet'] = test['clean_tweet'].str.replace("[0-9]", " ")
# remove whitespaces
train['clean_tweet'] = train['clean_tweet'].apply(lambda x:' '.join(x.split()))
test['clean_tweet'] = test['clean_tweet'].apply(lambda x: ' '.join(x.split()))
接下來我們將文字標準化,這一步會將詞簡化成它的基本形式,比如“produces”、“production”和“producing”會變成“product”。通常來講,同一個詞的多種形式並不重要,我們只需要它們的基本形式就可以了。
我們使用流行的spaCy庫來進行標準化:
# import spaCy's language model
nlp = spacy.load('en', disable=['parser', 'ner'])
# function to lemmatize text
def lemmatization(texts):
output = []
for i in texts:
s = [token.lemma_ for token in nlp(i)]
output.append(' '.join(s))
return output
在測試集和訓練集中進行歸類(Lemmatize):
train['clean_tweet'] = lemmatization(train['clean_tweet'])
test['clean_tweet'] = lemmatization(test['clean_tweet'])
現在讓我們看一下原始推文和清洗後的推文的對比:
train.sample(10)
仔細檢視上圖中的兩列推文的對比,清洗後的推文變得更加清晰易理解。
然而,在清洗文字這一步中其實還有很多可以做的,我鼓勵大家進一步探索資料,去發現文字中可以提升的地方。
簡要介紹TensorFlow Hub
等等,TensorFlow跟我們這篇教程有什麼關係呢?
TensorFlow Hub是一個允許遷移學習(Transfer learning)的庫,它支援用多種機器學習模型處理不同任務。ELMo是其中一例,這也是為什麼我們的實現中需要通過TensorFlow Hub來使用ELMo。
我們首先需要安裝TensorFlow Hub,你必須安裝或升級到1.7版本以上來使用:
$ pip install "tensorflow>=1.7.0"
$ pip install tensorflow-hub
準備ELMo模型向量
我們現在需要引入事先訓練過的ELMo模型。請注意這個模型的大小超過350 mb所以可能需要一些時間來下載。
import tensorflow_hub as hub
import tensorflow as tf
elmo = hub.Module("https://tfhub.dev/google/elmo/2", trainable=True)
我會先展示如何給一個句子生成ELMo向量,你只需要將一列字串輸入elmo中:
# just a random sentence
x = ["Roasted ants are a popular snack in Columbia"]
# Extract ELMo features
embeddings = elmo(x, signature="default", as_dict=True)["elmo"]
embeddings.shape
Output:
TensorShape([Dimension(1), Dimension(8), Dimension(1024)])
這個輸出是一個三維張量(1, 8, 1024):
第一個維度表示訓練樣本的數量,在這個案例中是1;
第二個維度表示輸入列表中的最大長度,因為我們現在只輸入了一個字串,所以第二個維度就是該字串的長度8;
第三個維度等於ELMo向量的長度。
輸入中的每個詞都有個長度為1024的ELMo向量。
讓我們開始提取測試集和訓練集中清洗過推文的ELMo向量。如果想得到整個的推文的ElMo向量,我們需要取推文中每個詞的向量的平均值。
我們可以通過定義一個函式來實現:
def elmo_vectors(x):
embeddings = elmo(x.tolist(), signature="default", as_dict=True)["elmo"]
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(tf.tables_initializer())
# return average of ELMo features
return sess.run(tf.reduce_mean(embeddings,1))
如果使用上述程式碼來一次性處理所有推文,你可能會耗盡所有記憶體。我們可以通過將訓練集和測試集分割成一系列容量為100條的樣本來避免這個問題,然後將他們相繼傳遞給elmo_vectors()函式。
我選擇用列表儲存這些樣本:
list_train = [train[i:i+100] for i in range(0,train.shape[0],100)]
list_test = [test[i:i+100] for i in range(0,test.shape[0],100)]
現在讓我們在這些樣本上迭代並提取ELMo向量,這會花很長時間:
# Extract ELMo embeddings
elmo_train = [elmo_vectors(x['clean_tweet']) for x in list_train]
elmo_test = [elmo_vectors(x['clean_tweet']) for x in list_test]
一旦我們得到了所有向量,我們可以將它們整合成一個陣列:
elmo_train_new = np.concatenate(elmo_train, axis = 0)
elmo_test_new = np.concatenate(elmo_test, axis = 0)
我建議你將這些陣列儲存好,因為我們需要很長時間來得到它們的ELMo向量。我們可以將它們存為pickle檔案:
# save elmo_train_new
pickle_out = open("elmo_train_03032019.pickle","wb")
pickle.dump(elmo_train_new, pickle_out)
pickle_out.close()
# save elmo_test_new
pickle_out = open("elmo_test_03032019.pickle","wb")
pickle.dump(elmo_test_new, pickle_out)
pickle_out.close()
然後用以下程式碼來將它們重新載入:
# load elmo_train_new
pickle_in = open("elmo_train_03032019.pickle", "rb")
elmo_train_new = pickle.load(pickle_in)
# load elmo_train_new
pickle_in = open("elmo_test_03032019.pickle", "rb")
elmo_test_new = pickle.load(pickle_in)
構建模型並評估
讓我們用ELMo來構建NLP模型吧!
我們可以用訓練集的ELMo向量來構建一個分類模型。然後,我們會用該模型在測試集上進行預測。但在做這些之前,我們需要將elmo_train_new分成訓練集和驗證集來檢驗我們的模型。
from sklearn.model_selection import train_test_split
xtrain, xvalid, ytrain, yvalid = train_test_split(elmo_train_new, train['label'], random_state=42, test_size=0.2)
由於我們的目標是設定基線分數,我們將用ELMo向量作為特徵來構建一個簡單的邏輯迴歸模型:
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
lreg = LogisticRegression()
lreg.fit(xtrain, ytrain)
到了預測的時間了!首先,在驗證集上:
preds_valid = lreg.predict(xvalid)
我們用F1矩陣來評估我們的模型,因為這是競賽的官方評估指標:
f1_score(yvalid, preds_valid)
Output: 0.789976
驗證集上的F1分數很不錯,接下來我們在測試集上進行預測:
# make predictions on test set
preds_test = lreg.predict(elmo_test_new)
準備將要上傳到競賽頁面的提交檔案:
# prepare submission dataframe
sub = pd.DataFrame({'id':test['id'], 'label':preds_test})
# write predictions to a CSV file
sub.to_csv("sub_lreg.csv", index=False)
公開排行榜顯示我們的預測結果得到了0.875672分,可以說這個結果非常的好,因為我們只進行了相對基礎的預處理過程,而且用了一個很簡單的模型。可以預見如果我們用了更先進的技術將會得到更好的分數,大家可以自行嘗試並將結果告訴我!
5. 我們還能用ELMo做什麼?
我們剛剛見證了在文字識別中ELMo是多麼高效,如果能搭配一個更復雜的模型它一定會有更出色的表現。ELMo的應用並不侷限於文字分類,只要你需要將文字資料向量化都可以用它。
以下是幾種可以使用ELMo進行處理的NLP問題:
機器翻譯(Machine Translation)
語言模型(Language Modeling)
文字摘要(Text Summarization)
命名實體識別(Named Entity Recognition)
問答系統(Question-Answering Systems)
6. 結語
ELMo無疑是NLP的重大進步,並且將保持趨勢。鑑於NLP研究的進展速度非常快,最近幾個月還出現了其他新的最先進的詞嵌入,如Google BERT和Falando's Flair。可以說令NLP從業者激動的時代到來了!
我強烈建議你在其他資料集上使用ELMo,並親自體驗效能提升的過程。如果你有任何問題或希望與我和社群分享你的經驗,請在下面的評論板塊中進行。如果你剛在NLP領域起步,你還應該檢視以下NLP相關資源:
Natural Language Processing (NLP) course
(https://courses.analyticsvidhya.com/courses/natural-language-processing-nlp?utm_medium=ELMoNLParticle&utm_source=blog )
Certified Program: Natural Language Processing (NLP) for Beginners
(https://courses.analyticsvidhya.com/bundles/nlp-combo?utm_medium=ELMoNLParticle&utm_source=blog )
你也可以通過Analytics Vidhya的安卓軟體來閱讀本文。
原文標題:A Step-by-Step NLP Guide to Learn ELMo for Extracting Features from Text
原文連結:https://www.analyticsvidhya.com/blog/2019/03/learn-to-use-elmo-to-extract-features-from-text/
編輯:王菁
校對:林亦霖