前言
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding(/arxiv.org/abs/1810.04805)。BERT解決方案刷新了各大NLP任務的榜單,在各種NLP任務上都做到state of the art。這里我把BERT說成是解決方案,而不是一個算法,因為這篇文章并沒有提出新的算法模型,還是沿用了之前已有的算法模型。BERT最大的創新點,在于提出了一套完整的方案,利用之前最新的算法模型,去解決各種各樣的NLP任務,因此BERT這篇論文對于算法模型完全不做介紹,以至于在我直接看這篇文章的時候感覺云里霧里。但是本文中,我會從算法模型到解決方案,進行完整的詮釋。本文中我會分3個部分進行介紹,第一部分我會大概介紹一下NLP的發展,第二部分主要講BERT用到的算法,最后一部分講BERT具體是怎么操作的。
NLP發展
要處理NLP問題,首先要解決文本的表示問題。雖然我們人去看文本,能夠清楚明白文本中的符號表達什么含義,但是計算機只能做數學計算,需要將文本表示成計算機可以處理的形式。最開始的方法是采用one hot,比如,我們假設英文中常用的單詞有3萬個,那么我們就用一個3萬維的向量表示這個詞,所有位置都置0,當我們想表示apple這個詞時,就在對應位置設置1,如圖1.1所示。這種表示方式存在的問題就是,高維稀疏,高維是指有多少個詞,就需要多少個維度的向量,稀疏是指,每個向量中大部分值都是0。另外一個不足是這個向量沒有任何含義。
后來出現了詞向量,word embedding,用一個低維稠密的向量去表示一個詞,如圖1.2所示。通常這個向量的維度在幾百到上千之間,相比one hot幾千幾萬的維度就低了很多。詞與詞之間可以通過相似度或者距離來表示關系,相關的詞向量相似度比較高,或者距離比較近,不相關的詞向量相似度低,或者距離比較遠,這樣詞向量本身就有了含義。文本的表示問題就得到了解決。詞向量可以通過一些無監督的方法學習得到,比如CBOW或者Skip-Gram等,可以預先在語料庫上訓練出詞向量,以供后續的使用。順便提一句,在圖像中就不存在表示方法的困擾,因為圖像本身就是數值矩陣,計算機可以直接處理。
NLP中有各種各樣的任務,比如分類(Classification),問答(QA),實體命名識別(NER)等。對于這些不同的任務,最早的做法是根據每類任務定制不同的模型,輸入預訓練好的embedding,然后利用特定任務的數據集對模型進行訓練,如圖1.3所示。這里存在的問題就是,不是每個特定任務都有大量的標簽數據可供訓練,對于那些數據集非常小的任務,恐怕就難以得到一個理想的模型。
看一下圖像領域是如何解決這個問題的。圖像分類是計算機視覺中最基本的任務,當我要解決一個小數據集的圖像分類任務時,該怎么做?CV領域已經有了一套成熟的解決方案。我會用一個通用的網絡模型,比如VGG,ResNet或者GoogleNet,在ImageNet上做預訓練(pre-training)。ImageNet有1400萬張有標注的圖片,包含1000個類別,這樣的數據規模足以訓練出一個規模龐大的模型。在訓練過程中,模型會不斷的學習如何提取特征,底層的CNN網絡結構會提取邊緣,角,點等通用特征,模型越往上走,提取的特征也越抽象,與特定的任務更加相關。當完成預訓練之后,根據我自己的分類任務,調整最上層的網絡結構,然后在小數據集里對模型進行訓練。在訓練時,可以固定住底層的模型參數只訓練頂層的參數,也可以對整個模型進行訓練,這個過程叫做微調(fine-tuning),最終得到一個可用的模型。總結一下,整個過程包括兩步,拿一個通用模型在ImageNet上做預訓練(pre-training),然后針對特定任務進行微調(fine-tuning),完美解決了特定任務數據不足的問題。還有一個好處是,對于各種各樣的任務都不再需要從頭開始訓練網絡,可以直接拿預訓練好的結果進行微調,既減少了訓練計算量的負擔,也減少了人工標注數據的負擔。
NLP領域也引入了這種做法,用一個通用模型,在非常大的語料庫上進行預訓練,然后在特定任務上進行微調,BERT就是這套方案的集大成者。BERT不是第一個,但目前為止,是效果最好的方案。BERT用了一個已有的模型結構,提出了一整套的預訓練方法和微調方法,我們在后文中再進行詳細的描述。
算法
BERT所采用的算法來自于2017年12月份的這篇文章,Attenion Is All You Need(/arxiv.org/abs/1706.03762),同樣來自于谷歌。這篇文章要解決的是翻譯問題,比如從中文翻譯成英文。這篇文章完全放棄了以往經常采用的RNN和CNN,提出了一種新的網絡結構,即Transformer,其中包括encoder和decoder,我們只關注encoder。這篇英文博客(/jalammar.github.io/illus)對Transformer介紹得非常詳細,有興趣的讀者可以看一下,如果不想看英文博客也可以看本文,本文中的部分圖片也截取自這篇博客。
圖2.1是Transformer encoder的結構,后文中我們都簡稱為Transformer。首先是輸入word embedding,這里是直接輸入一整句話的所有embedding。如圖2.1所示,假設我們的輸入是Thinking Machines,每個詞對應一個embedding,就有2個embedding。輸入embedding需要加上位置編碼(Positional Encoding),為什么要加位置編碼,后文會做詳細介紹。然后經過一個Multi-Head Attention結構,這個結構是算法單元中最重要的部分,我們會在后邊詳細介紹。之后是做了一個shortcut的處理,就是把輸入和輸出按照對應位置加起來,如果了解殘差網絡(ResNet)的同學,會對這個結構比較熟悉,這個操作有利于加速訓練。然后經過一個歸一化normalization的操作。接著經過一個兩層的全連接網絡,最后同樣是shortcut和normalization的操作??梢钥吹?,除了Multi-Head Attention,都是常規操作,沒有什么難理解的。這里需要注意的是,每個小模塊的輸入和輸出向量,維度都是相等的,比如,Multi-Head Attention的輸入和輸出向量維度是相等的,否則無法進行shortcut的操作;Feed Forward的輸入和輸出向量維度也是相等的;最終的輸出和輸入向量維度也是相等的。但是Multi-Head Attention和Feed Forward內部,向量維度會發生變化。
來詳細看一下Multi-Head Attention的結構。這個Multi-Head表示多頭的意思,先從最簡單的看起,看看單頭Attention是如何操作的。從圖2.1的橙色方塊可以看到,embedding在進入到Attention之前,有3個分叉,那表示說從1個向量,變成了3個向量。具體是怎么算的呢?我們看圖2.3,定義一個WQ矩陣(這個矩陣隨機初始化,通過訓練得到),將embedding和WQ矩陣做乘法,得到查詢向量q,假設輸入embedding是512維,在圖3中我們用4個小方格表示,輸出的查詢向量是64維,圖3中用3個小方格以示不同。然后類似地,定義WK和WV矩陣,將embedding和WK做矩陣乘法,得到鍵向量k;將embeding和WV做矩陣乘法,得到值向量v。對每一個embedding做同樣的操作,那么每個輸入就得到了3個向量,查詢向量,鍵向量和值向量。需要注意的是,查詢向量和鍵向量要有相同的維度,值向量的維度可以相同,也可以不同,但一般也是相同的。
接下來計算每一個embedding的輸出,以第一個詞Thinking為例,參看圖2.4。用查詢向量q1跟鍵向量k1和k2分別做點積,得到112和96兩個數值。這也是為什么前文提到查詢向量和鍵向量的維度必須要一致,否則無法做點積。然后除以常數8,得到14和12兩個數值。這個常數8是鍵向量的維度的開方,鍵向量和查詢向量的維度都是64,開方后是8。做這個尺度上的調整目的是為了易于訓練。然后把14和12丟到softmax函數中,得到一組加和為1的系數權重,算出來是大約是0.88和0.12。將0.88和0.12對兩個值向量v1和v2做加權求和,就得到了Thinking的輸出向量z1。類似的,可以算出Machines的輸出z2。如果一句話中包含更多的詞,也是相同的計算方法。
通過這樣一系列的計算,可以看到,現在每個詞的輸出向量z都包含了其他詞的信息,每個詞都不再是孤立的了。而且每個位置中,詞與詞的相關程度,可以通過softmax輸出的權重進行分析。如圖2.5所示,這是某一次計算的權重,其中線條顏色的深淺反映了權重的大小,可以看到it中權重最大的兩個詞是The和animal,表示it跟這兩個詞關聯最大。這就是attention的含義,輸出跟哪個詞關聯比較強,就放比較多的注意力在上面。
上面我們把每一步計算都拆開了看,實際計算的時候,可以通過矩陣來計算,如圖2.6所示。
講完了attention,再來講Multi-Head。對于同一組輸入embedding,我們可以并行做若干組上面的操作,例如,我們可以進行8組這樣的運算,每一組都有WQ,WK,WV矩陣,并且不同組的矩陣也不相同。這樣最終會計算出8組輸出,我們把8組的輸出連接起來,并且乘以矩陣WO做一次線性變換得到輸出,WO也是隨機初始化,通過訓練得到,計算過程如圖2.7所示。這樣的好處,一是多個組可以并行計算,二是不同的組可以捕獲不同的子空間的信息。
到這里就把Transformer的結構講完了,同樣都是做NLP任務,我們來和RNN做個對比。圖2.8是個最基本的RNN結構,還有計算公式。當計算隱向量h4時,用到了輸入x4,和上一步算出來的隱向量h3,h3包含了前面所有節點的信息。h4中包含最多的信息是當前的輸入x4,越往前的輸入,隨著距離的增加,信息衰減得越多。對于每一個輸出隱向量h都是如此,包含信息最多得是當前的輸入,隨著距離拉遠,包含前面輸入的信息越來越少。但是Transformer這個結構就不存在這個問題,不管當前詞和其他詞的空間距離有多遠,包含其他詞的信息不取決于距離,而是取決于兩者的相關性,這是Transformer的第一個優勢。第二個優勢在于,對于Transformer來說,在對當前詞進行計算的時候,不僅可以用到前面的詞,也可以用到后面的詞。而RNN只能用到前面的詞,這并不是個嚴重的問題,因為這可以通過雙向RNN來解決。第三點,RNN是一個順序的結構,必須要一步一步地計算,只有計算出h1,才能計算h2,再計算h3,隱向量無法同時并行計算,導致RNN的計算效率不高,這是RNN的固有結構所造成的,之前有一些工作就是在研究如何對RNN的計算并行化。通過前文的介紹,可以看到Transformer不存在這個問題。通過這里的比較,可以看到Transformer相對于RNN有巨大的優勢。
RNN的結構包含了序列的時序信息,而Transformer卻完全把時序信息給丟掉了。為了解決時序的問題,Transformer的作者用了一個絕妙的辦法,這就是我在前文提到的位置編碼(Positional Encoding)。位置編碼是和word embedding同樣維度的向量,將位置embedding和詞embedding加在一起,作為輸入embedding,如圖2.9所示。位置編碼可以通過學習得到,也可以通過設置一個跟位置或者時序相關的函數得到,比如設置一個正弦或者余弦函數,這里不再多說。
把圖2.1的結構作為一個基本單元,把N個這樣的基本單元順序連起來,就是BERT的算法模型,如圖2.10所示。從前面的描述中可以看到,當輸入有多少個embedding,那么輸出也就有相同數量的embedding,可以采用和RNN采用相同的叫法,把輸出叫做隱向量。在做具體NLP任務的時候,只需要從中取對應的隱向量作為輸出即可。
BERT
在介紹BERT之前,我們先看看另外一套方案。在第一部分說過,BERT并不是第一個提出預訓練加微調的方案,此前還有一套方案叫GPT,這也是BERT重點對比的方案,文章在這,Improving Language Understanding by Generative Pre-Training(hs3-us-west-2.amazonaws.com/)。GPT的模型結構和BERT是相同的,都是圖2.10的結構,只是BERT的模型規模更加龐大。GPT是這么預訓練的,在一個8億單詞的語料庫上做訓練,給出前文,不斷地預測下一個單詞。比如這句話,Winter is coming,當給出第一個詞Winter之后,預測下一個詞is,之后再預測下一個詞coming。不需要標注數據,通過這種無監督訓練的方式,得到一個預訓練模型。
我們再來看看BERT有什么不同。BERT來自于Bidirectional Encoder Representations from Transformers首字母縮寫,這里提到了一個雙向(Bidirectional)的概念。BERT在一個33億單詞的語料庫上做預訓練,語料庫就要比GPT大了幾倍。預訓練包括了兩個任務,第一個任務是隨機地扣掉15%的單詞,用一個掩碼MASK代替,讓模型去猜測這個單詞;第二個任務是,每個訓練樣本是一個上下句,有50%的樣本,下句和上句是真實的,另外50%的樣本,下句和上句是無關的,模型需要判斷兩句的關系。這兩個任務各有一個loss,將這兩個loss加起來作為總的loss進行優化。下面兩行是一個小栗子,用括號標注的是扣掉的詞,用[MASK]來代替。
正樣本:我[MASK](是)個算法工程師,我服務于WiFi萬能鑰匙這家[MASK](公司)。
負樣本:我[MASK](是)個算法工程師,今天[MASK](股票)又跌了。
我們來對比下GPT和BERT兩種預訓練方式的優劣。GPT在預測詞的時候,只預測下一個詞,因此只能用到上文的信息,無法利用到下文的信息。而BERT是預測文中扣掉的詞,可以充分利用到上下文的信息,這使得模型有更強的表達能力,這也是BERT中Bidirectional的含義。在一些NLP任務中需要判斷句子關系,比如判斷兩句話是否有相同的含義。BERT有了第二個任務,就能夠很好的捕捉句子之間的關系。圖3.1是BERT原文中對另外兩種方法的預訓練對比,包括GPT和ELMo。ELMo采用的還是LSTM,這里我們不多講ELMo。這里會有讀者困惑,這里的結構圖怎么跟圖2.10不一樣?如果熟悉LSTM的同學,看到最右邊的ELMo,就會知道那些水平相連的LSTM其實只是一個LSTM單元。左邊的BERT和GPT也是一樣,水平方向的Trm表示的是同一個單元,圖中那些復雜的連線表示的是詞與詞之間的依賴關系,BERT中的依賴關系既有前文又有后文,而GPT的依賴關系只有前文。
講完了這兩個任務,我們再來看看,如何表達這么復雜的一個訓練樣本,讓計算機能夠明白。圖3.2表示“my dog is cute, he likes playing.”的輸入形式。每個符號的輸入由3部分構成,一個是詞本身的embedding;第二個是表示上下句的embedding,如果是上句,就用A embedding,如果是下句,就用B embedding;最后,根據Transformer模型的特點,還要加上位置embedding,這里的位置embedding是通過學習的方式得到的,BERT設計一個樣本最多支持512個位置;將3個embedding相加,作為輸入。需要注意的是,在每個句子的開頭,需要加一個Classification(CLS)符號,后文中會進行介紹。
完成預訓練之后,就要針對特定任務就行微調了,這里描述一下論文中的4個例子,看圖3.4。首先說下分類任務,分類任務包括對單句子的分類任務,比如判斷電影評論是喜歡還是討厭;多句子分類,比如判斷兩句話是否表示相同的含義。圖3.4(a)(b)是對這類任務的一個示例,左邊表示兩個句子的分類,右邊是單句子分類。在輸出的隱向量中,取出CLS對應的向量C,加一層網絡W,并丟給softmax進行分類,得到預測結果P,計算過程如圖3.3中的計算公式。在特定任務數據集中對Transformer模型的所有參數和網絡W共同訓練,直到收斂。新增加的網絡W是HxK維,H表示隱向量的維度,K表示分類數量,W的參數數量相比預訓練模型的參數少得可憐。
對于問答任務,如圖3.4(c),以SQuAD v1.1為例,給出一個問題Question,并且給出一個段落Paragraph,然后從段落中標出答案的具體位置。需要學習一個開始向量S,維度和輸出隱向量維度相同,然后和所有的隱向量做點積,取值最大的詞作為開始位置;另外再學一個結束向量E,做同樣的運算,得到結束位置。附加一個條件,結束位置一定要大于開始位置。最后再看NER任務,實體命名識別,比如給出一句話,對每個詞進行標注,判斷屬于人名,地名,機構名,還是其他。如圖3.4(d)所示,加一層分類網絡,對每個輸出隱向量都做一次判斷。可以看到,這些任務,都只需要新增少量的參數,然后在特定數據集上進行訓練即可。從實驗結果來看,即便是很小的數據集,也能取得不錯的效果。