from: https://zhuanlan.zhihu.com/p/51679783
2018年3月份,ELMo[1][2]出世,該paper是NAACL18 Best Paper。在之前2013年的word2vec及2014年的GloVe的工作中,每個詞對應一個vector,對於多義詞無能為力。ELMo的工作對於此,提出了一個較好的解決方案。不同於以往的一個詞對應一個向量,是固定的。在ELMo世界里,預訓練好的模型不再只是向量對應關系,而是一個訓練好的模型。使用時,將一句話或一段話輸入模型,模型會根據上線文來推斷每個詞對應的詞向量。這樣做之后明顯的好處之一就是對於多義詞,可以結合前后語境對多義詞進行理解。比如appele,可以根據前后文語境理解為公司或水果。
筆者也看了網上的很多資料,感覺很多資料要么含糊其辭,要么蜻蜓點水,並不能讓筆者真正明白ELMo到底是怎么一回事,又是怎么工作的。ELMo的原理也不復雜,只是單純的看此篇paper又缺乏很多nlp的語言模型(LM)知識的話,就不容易理解了。同時,paper不同於教科書,一點一點的來,paper是假設很多背景點到你都明白的情況下寫的。本博客中,筆者試圖對EMLo論文所做工作做一個較為清晰的解釋,同時基於tensorflow hub的ELMo模型進行簡單的上手使用。
目前,深度學習有各種網絡結構及各種名稱,在筆者眼中,不過是一些數學公式起了名字罷了。ok,開始吧。
一、 ELMo原理
1.1 RNN及LSTM基礎
這里先簡單介紹下RNN和LSTM。這些對於后面理解ELMo是很有用的。對於此內容的讀者可以跳過此內容。
RNN的網絡結構如下圖圖1,這是一層RNN的結構,這個結構是展開圖。RNN是有時序的,每個時序里,都是單元的重復。在第t時刻時,假定輸入為xtxt,隱狀態向量為ht−1ht−1,則下一隱狀態htht則由下圖圖2的公式產生。
結合圖1和圖2,我們知道,htht表示的是隱狀態,在圖1里也是每個時序的輸出。對於文本,就是前文的文本留下來的對后面的推斷有用的信息。 其中xtxt是n*1的列向量,htht是m*1的列向量。矩陣WW的維數是m*n,矩陣UU的維數是m*m,bb是m*1的列向量。Wxt+Uht−1+bWxt+Uht−1+b的結果是m*1的列向量。f是針對每個列元素進行分別使用的函數,在圖1中取的是tanh函數,實際上取其他函數也沒問題。 從圖2的公式可以明白,RNN就是結合前文傳來的信息WxtWxt及輸入的信息Uht−1Uht−1及偏置b的信息,得到輸出及下一個因狀態htht的神經網絡。
總結而言,RNN的輸入是n*1的列向量,輸出及隱狀態是m*1的列向量。 參數個數:mn+mm+m=m(n+m+1)mn+mm+m=m(n+m+1),其中WW,UU,bb是參數,hh是生成的,不是參數。
RNN有各種問題,比如梯度消失問題,所以下面介紹LSTM。LSTM的結構如下圖圖3,對於lstm的輸入和輸出下圖圖4。關於lstm里面的參數及公式,如下圖圖5。可以結合圖4和圖5來理解lstm。lstm是復雜、高級版的rnn。
其中,輸入依然為xtxt,維度為n*1的列向量。輸出及隱狀態htht是m*1的列向量。 ctct是攜帶信息的列向量,向量維度是m*1。WW系列的維度與RNN的WW一致,均為m*n。UU與RNN的UU一致,均為m*m。bb系列的維度是m*1的列向量。可以看出,lstm的輸入與輸出/隱狀態與rnn是一致的。
圖5的公式中it,ft,otit,ft,ot分別表示輸入門、遺忘門、輸出門三者的結果。這三個門是決定信息是否繼續走下去。而tanh是將信息整理到區間(-1,1)的,也就是生成候選信息。同樣的,這里σ,tanhσ,tanh這兩個函數都是針對列向量的每個元素分別作用的。
到這里,我們可以整理下lstm的神經單元個數及參數個數。我們可以這么理解,比如itit的這個公式,這是一個輸入門。我們都很熟悉前饋(全連接)網絡,可以將此門視為一個全連接網絡層,該網絡層的輸入是xt,ht−1xt,ht−1,輸出為itit(m*1的列向量),該網絡的神經網絡單元數是m+n個,參數個數是mn+mm+mmn+mm+m。遺忘門和輸出門與此一致。同理gtgt的生成,也可以視為一個全連接網絡。
所以,在上面的lstm中,單元的總個數是4m,參數的總個數是4m(n+m+1)。
1.2 前向lstm語言模型基礎 & elmo的雙向lstm語言模型
這里分為兩部分,1.2.1講述一些lstm語言模型的基礎,1.2.2講述elmo中的lstm模型。
1.2.1 前向lstm語言模型基礎
給定一串長度為N的詞條(t1,t2,…,tN)(t1,t2,…,tN),前向語言模型通過對給定歷史(t1,…tk−1)(t1,…tk−1)預測tktk進行建模,圖如下圖6(值得注意的是,圖6不是ELMo的最終圖,只是解釋lstm語言模型的圖),對應的公式如下圖圖7。
到了此處,大家可能會迷惑這個網絡的輸入輸出是什么?具體的流程是什么?這個問題很基礎又關鍵。
以“the cat sat on the mat”這句話為例。在某一個時刻k(對應於1.1節中的t時刻)時,輸入為the,輸出cat的概率。過程是這里面包含了幾步,第一步:將the轉換成word embedding。所謂word embedding就是一個n*1維的列向量,這個很好理解。那單詞怎么轉成word embedding的呢?如果大家用過word2vec,glove就知道,就是簡單的查表。在本篇paper中,用的不是word2vec,glove,畢竟2018年了。作者用的是cnn-big-lstm[5]生成的word embedding,其實跟word2vec等也差不多,就是提前單獨訓練好的模型,模型喂入單詞就能得到單詞的word embedding。總之,在這里一步里,就是簡單將單詞轉換成了n*1的列向量,而這個列向量,對應於1.1節中的輸入xtxt。第二步:將上一時刻的輸出/隱狀態hk−1hk−1(對應於1.1節中的ht−1ht−1)及第一步中的word embedding一並送入lstm,並得到輸出及隱狀態hkhk對應於1.1中的htht)。其中,隱狀態hk−1hk−1是一個m*1的列向量。在1.1中,我們有講lstm的原理。在這一步里,lstm的輸出及隱狀態都是hkhk,是一個m*1維的列向量。請大家務必注意hkhk,這個hkhk與我們后文提到elmo向量有着直接的關系。第三步:將lstm的輸出hkhk,與上下文矩陣W′W′相乘,即W′hkW′hk得到一個列向量,再將該列向量經過softmax歸一化。其中,假定數據集有VV個單詞,W′W′是|V|*m的矩陣,hkhk是m*1的列向量,於是最終結果是|V|*1的歸一化后向量,即從輸入單詞得到的針對每個單詞的概率。
從上面三步,就可以明白這個前向lstm語言模型的工作流程了。其實很多神經網絡語言模型都很類似,除了lstm,還可以用rnn及前饋神經網絡,差不多的。
1.2.2 elmo的雙向lstm語言模型
有了前面1.1節及1.2.1節的基礎,elmo的雙向lstm語言模型就很好解釋了。ELMo的整體圖如下圖圖8。相對於上面的圖6,有兩個改進,第一個是使用了多層LSTM,第二個是增加了后向語言模型(backward LM)。
對於多層lstm,每層的輸出都是1.1節中提到的隱向量htht,在ELMo里,為了區分,前向lstm語言模型的第j層第k時刻的輸出向量命名為hLMk,j−→−hk,jLM→。
對於后向語言模型,跟前向語言模型類似,除了它是給定后文來預測前文。后向lstm語言模型的公式如下圖圖9所示,可以對照着前向語言lstm語言模型的公式(圖7所示)來看。還是非常好理解的。類似的,我們設定后向lstm的第j層的第k時刻的輸出向量命名為hLMk,j←−−hk,jLM←。
圖7和圖9分別是前向、后向lstm語言模型所要學習的目標函數(注意此處是追求概率最大化的,跟通常的目標函數追求最小化有所不同,要是追求最小化,前面加負號即可)。elmo使用的雙向lstm語言模型,論文中簡稱biLM。作者將圖7和圖9的公式結合起來,得到所要優化的目標:最大化對數前向和后向的似然概率,如下圖圖10所示。
圖10中的參數說明,ΘLSTM−→−−−ΘLSTM→表示前向lstm的網絡參數,反向的lstm的網絡參數同理。兩個網絡里都出現了ΘxΘx和ΘsΘs,表示兩個網絡共享的參數。其中ΘxΘx表示映射層的共享,即1.2.1節中提到的第一步中,將單詞映射為word embedding的共享,就是說同一個單詞,映射為同一個word embedding。ΘsΘs表示1.2.1節中提到的第三步中的上下文矩陣的參數,這個參數在前向和后向lstm中是相同的。
1.3 ELMo
所謂ELMo不過是一些網絡層的組合。都有哪些網絡層呢?對於每個單詞(token)tktk,對於L層的雙向lstm語言模型,一共有2L+1個表征(representations),如下圖11所示:
其中,hLMk,0hk,0LM是前文提到的word embedding,也就是lstm的輸入。對於每一層的雙向lstm語言模型,hLMk,j=[hLMk,j−→−;hLMk,j←−−]hk,jLM=[hk,jLM→;hk,jLM←]。值得注意的是,每一層有一個前向lstm的輸出,一個后向lstm的輸出,兩者就是簡單的拼接起來的。也就是如果分別都是m*1維的列向量,拼完之后就是2m*1的列向量,就這么簡單。
既然ELMo有這么多向量了,那怎么使用呢?最簡單的方法就是使用最頂層的lstm輸出,即hLMk,Lhk,LLM,但是我們有更好的方法使用這些向量。即如下圖圖12的方法,我們對於每層向量,我們加一個權重staskjsjtask(一個實數),將每層的向量與權重相乘,然后再乘以一個權重γγ。每層lstm輸出,或者每層lstm學到的東西是不一樣的,針對每個任務每層的向量重要性也不一樣,所以有L層lstm,L+1個權重,加上前面的γγ,一共有L+2個權重。注意下此處的權重個數,后面會用到。為何要乘以γγ,因為下一節1.4節我們會看到,我們會將此向量與另一向量再次拼接,所以此處有一個縮放系數。
筆者思考一個問題,為何不把L+1個向量一起拼接起來?這樣子網絡可以學的更充分。筆者猜想,可能是考慮維數太高,其實也沒那么高了。考慮這些信息有疊加?總之,筆者不確定。
總之,看到上圖圖12,就是我們所說的ELMo向量了。它是多個輸出層及輸入層,按照一定權重相乘得到的。這個權重怎么來的?針對具體的nlp任務,我們用的時候,需要再次訓練去得到的這個權重。最簡單的方法,就是權重都設為一樣。
1.4 ELMo用於有監督的nlp任務
在1.3節生成了ELMo向量(圖12所示)之后,使用方法依然是拼接。將單詞/詞條的表征xkxk與ELMotaskkELMoktask拼接起來就可以了,即一個單詞的最終向量是這樣的 [xk;ELMotaskk][xk;ELMoktask]。 這就是最終的使用方法。
原文中有下面一段話,筆者沒讀太明白。先放在這里。
For some tasks (e.g., SNLI, SQuAD), we observe further improvements by also including ELMo at the output of the task RNN by introducing another set of output specific linear weights and replacing hkhk with [hk;ELMotaskk][hk;ELMoktask].
作者同時提到,通過加入一定的l2正則, λ||w||2λ||w||2,有助於提高模型泛化性能。
1.5 預訓練的雙向語言模型架構
論文的作者有預訓練好的ELMo模型,映射層(單詞到word embedding)使用的Jozefowicz的CNN-BIG-LSTM[5],即輸入為512維的列向量。同時LSTM的層數L,最終使用的是2,即L=2。每層的LSTM的單元數是4096。每個LSTM的輸出也是512維列向量。每層LSTM(含前、向后向兩個)的單元個數是4096個(從1.1節可以知公式4m*2 = 4*512*2 = 4096)。也就是每層的單個lstm的輸入是512維,輸出也是512維。
一旦模型預訓練完成,便可以用於nlp其他任務。在一些領域,可以對biLM(雙向lstm語言模型)進行微調,對任務的表現會有所提高,這種可以認為是一種遷移學習(transfer learning)。
1.6 ELMo使用方法總結 及 效果展示
對於預訓練好的雙向lstm語言模型,我們可以送入一段話,然后模型會得到圖11的向量,然后我們加上一定的權重(可訓練)即可得到圖12的ELMo向量。最終將ELMo向量與xkxk拼接作為單詞的特征,用於后續的處理。
對於部分任務,可以對雙向lstm語言模型微調,可能有所提升。
至於ELMo的效果,下面可以看圖13,總之是很多任務都提升就對了。
1.7 ELMo學到了什么
ELMo到底學到了什么呢?我們前文提到的多義詞問題解決了嗎?
可以觀察下圖圖14,可以看到,加入elmo之后,可以明顯將play的兩種含義區分出來,而GLoVe並不能。所以答案很明顯。
Word sense disambiguation(詞義消歧)
作者是通過實驗證明的,如下圖圖15。biLM表示我們的模型。第一層,第二層分別使用的結果顯示,越高層,對語義理解越好,表示對詞義消歧做的越好。這表明,越高層,越能捕獲詞意信息。
POS tagging(詞性標注)
這是另一個任務的實驗了,如下圖15,第一層效果好於第二層。表明,低層的更能學到詞的句法信息和詞性信息。
總體而言,biLM每層學到的東西是不一樣的,所以將他們疊加起來,對任務有較好的的提升。
1.8 ELMo的缺點
前文提了這么多elmo的優點,現在說一說缺點。這些缺點筆者是搬運[6]的觀點。[6]的觀點是站在現在的時間點上(BERT已發布)看的,他的觀點如下:
那么站在現在這個時間節點看,ELMO 有什么值得改進的缺點呢?首先,一個非常明顯的缺點在特征抽取器選擇方面,ELMO 使用了 LSTM 而不是新貴 Transformer,Transformer 是谷歌在 17 年做機器翻譯任務的“Attention is all you need”的論文中提出的,引起了相當大的反響,很多研究已經證明了 Transformer 提取特征的能力是要遠強於 LSTM 的。如果 ELMO 采取 Transformer 作為特征提取器,那么估計 Bert 的反響遠不如現在的這種火爆場面。另外一點,ELMO 采取雙向拼接這種融合特征的能力可能比 Bert 一體化的融合特征方式弱,但是,這只是一種從道理推斷產生的懷疑,目前並沒有具體實驗說明這一點。
二、 ELMo簡單上手
既然elmo有這么有用,該怎么使用呢?這里介紹一下簡單的使用方法。
有三種方法可以使用預訓練好的elmo模型。一、elmo官方allenNLP發布的基於pytorch實現的版本[7];二、elmo官方發布的基於tensorflow實現的版本[8];三、tensorflow-hub中google基於tensorflow實現的elmo的版本[9]。 本節內容介紹第三個版本。
先簡單介紹下tensorflow-hub,hub類似於github的hub,tensorflow-hub的目標就是講機器學習的算法,包含數據、訓練結果、參數等都保存下來,類似於github一樣,拿來就可以直接用。所有人都可以在這里提交自己的模型及數據、參數等。這里實現的elmo是google官方實現並預訓練好的模型。有人基於此模型+keras寫的博客及代碼教程大家可以參考下[10][11],此代碼使用的google的elmo的第一個版本,目前最新的是第二個版本。
下面看代碼的簡單上手使用,大家可能需要先安裝tensorflow_hub。
import tensorflow_hub as hub
# 加載模型
elmo = hub.Module("https://tfhub.dev/google/elmo/2", trainable=True)
# 輸入的數據集
texts = ["the cat is on the mat", "dogs are in the fog"]
embeddings = elmo(
texts,
signature="default",
as_dict=True)["default"]
上述代碼中,hub.Module加載模型,第一次會非常慢,因為要下載模型。該模型是訓練好的模型,也就是lstm中的參數都是固定的。這里的trainable=True是指1.3節中提到的4個權重參數可以訓練。texts是輸入數據集的格式,也有另一種輸入格式,代碼如下。signature為default時,輸入就是上面的代碼,signature為tokens時,就是下面的方式輸入。注意最后一行的中括號里的default,表示輸出的內容。這個default位置有五個參數可以選,分別為:1. word_emb,表示word embedding,這個就純粹相當於我們之前提到的lstm輸入的位置的word embedding,維數是[batch_size, max_length, 512],batch_size表示樣本的個數,max_length是樣本中tokens的個數的最大值,最后是每個word embedding是512維。2. lstm_outputs1,第一層雙向lstm的輸出,維數是[batch_size, max_length, 1024]。3. lstm_outputs2,第二層雙向lstm的輸出,維數是[batch_size, max_length, 1024]。4. elmo,輸入層及兩個輸出層,三層乘以權重。其中權重是可以訓練的,如1.3節所講。維數是[batch_size, max_length, 1024]。5.default,a fixed mean-pooling of all contextualized word representations with shape [batch_size, 1024]。 所以可以看到,要想訓練權重,要使用elmo這個參數。
elmo = hub.Module("https://tfhub.dev/google/elmo/2", trainable=True) # 另一種方式輸入數據 tokens_input = [["the", "cat", "is", "on", "the", "mat"], ["dogs", "are", "in", "the", "fog", ""]] # 長度,表示tokens_input第一行6一個有效,第二行5個有效 tokens_length = [6, 5] # 生成elmo embedding embeddings = elmo( inputs={ "tokens": tokens_input, "sequence_len": tokens_length }, signature="tokens", as_dict=True)["default"]
上面生成的embedding,想變成numpy向量,可以使用下面的代碼。
from tensorflow.python.keras import backend as K
sess = K.get_session()
array = sess.run(embeddings)
至此,關於elmo的所有內容已經完畢了。更多的使用,還需要再探索。謝謝大家。
參考資料
- Peters, Matthew E., et al. “Deep contextualized word representations.” arXiv preprint arXiv:1802.05365 (2018).
- https://allennlp.org/elmo
- Kim Y, Jernite Y, Sontag D, et al. Character-Aware Neural Language Models[C]//AAAI. 2016: 2741-2749.
- http://colah.github.io/posts/2015-08-Understanding-LSTMs/
- Jozefowicz R, Vinyals O, Schuster M, et al. Exploring the limits of language modeling[J]. arXiv preprint arXiv:1602.02410, 2016.
- https://zhuanlan.zhihu.com/p/51132034
- https://github.com/allenai/allennlp/blob/master/tutorials/how_to/elmo.md
- https://github.com/allenai/bilm-tf
- https://tfhub.dev/google/elmo/2
- https://towardsdatascience.com/elmo-embeddings-in-keras-with-tensorflow-hub-7eb6f0145440
- https://github.com/strongio/ker