作者:李理,環(huán)信人工智能研發(fā)中心vp,十多年自然語(yǔ)言處理和人工智能研發(fā)經(jīng)驗(yàn)。主持研發(fā)過(guò)多款智能硬件的問(wèn)答和對(duì)話系統(tǒng),負(fù)責(zé)環(huán)信中文語(yǔ)義分析開(kāi)放平臺(tái)和環(huán)信智能機(jī)器人的設(shè)計(jì)與研發(fā)。
本文是作者正在編寫(xiě)的《深度學(xué)習(xí)理論與實(shí)戰(zhàn)》的部分內(nèi)容。
導(dǎo)語(yǔ)
Google BERT 模型最近橫掃了各大評(píng)測(cè)任務(wù),在多項(xiàng)任務(wù)中取得了最好的結(jié)果,而且很多任務(wù)比之前最好的系統(tǒng)都提高了非常多,可以說(shuō)是深度學(xué)習(xí)最近幾年在 NLP的一大突破。但它并不是憑空出現(xiàn)的,最近一年大家都非常關(guān)注的 UnsupervisedSentence Embedding 取得了很大的進(jìn)展,包括 ELMo 和 OpenAI GPT 等模型都取得了很好的結(jié)果。而 BERT 在它們的基礎(chǔ)上改進(jìn)了語(yǔ)言模型單向信息流的問(wèn)題,并且借助 Google 強(qiáng)大的工程能力和計(jì)算資源的優(yōu)勢(shì),從而取得了巨大的突破。
本文從理論和編程實(shí)戰(zhàn)角度詳細(xì)的介紹 BERT 和它之前的相關(guān)的模型,包括
Transformer 模型。希望讀者閱讀本文之后既能理解模型的原理,同時(shí)又能很快的把模型用于解決實(shí)際問(wèn)題。本文假設(shè)讀者了解基本的深度學(xué)習(xí)知識(shí)包括 RNN/LSTM、Encoder-Decoder 和 Attention 等。
Sentence Embedding 簡(jiǎn)介
前面我們介紹了 Word Embedding,怎么把一個(gè)詞表示成一個(gè)稠密的向量。Embedding幾乎是在 NLP 任務(wù)使用深度學(xué)習(xí)的標(biāo)準(zhǔn)步驟。我們可以通過(guò) Word2Vec、GloVe 等從未標(biāo)注數(shù)據(jù)無(wú)監(jiān)督的學(xué)習(xí)到詞的 Embedding,然后把它用到不同的特定任務(wù)中。這種方法得到的 Embedding 叫作預(yù)訓(xùn)練的 (pretrained)Embedding。如果特定任務(wù)訓(xùn)練數(shù)據(jù)較多,那么我們可以用預(yù)訓(xùn)練的 Embedding 來(lái)初始化模型的 Embedding,然后用特定任務(wù)的監(jiān)督數(shù)據(jù)來(lái) fine-tuning。如果監(jiān)督數(shù)據(jù)較少,我們可以固定 (fix)Embedding,只讓模型學(xué)習(xí)其它的參數(shù)。這也可以看成一種 Transfer Learning。
但是 NLP 任務(wù)的輸入通常是句子,比如情感分類,輸入是一個(gè)句子,輸出是正向或者負(fù)向的情感。我們需要一種機(jī)制表示一個(gè)句子,最常見(jiàn)的方法是使用 CNN 或者 RNN 對(duì)句子進(jìn)行編碼。用來(lái)編碼的模塊叫作編碼器 (Encoder),編碼的輸出是一個(gè)向量。和詞向量一樣,我們期望這個(gè)向量能夠很好的把一個(gè)句子映射到一個(gè)語(yǔ)義空間,相似的句子映射到相近的地方。編碼句子比編碼詞更加復(fù)雜,因?yàn)樵~組成句子是有結(jié)構(gòu)的 (我們之前的 Paring 其實(shí)就是尋找這種結(jié)構(gòu)),兩個(gè)句子即使詞完全相同但是詞的順序不同,語(yǔ)義也可能相差很大。
傳統(tǒng)的編碼器都是用特定任務(wù)的監(jiān)督數(shù)據(jù)訓(xùn)練出來(lái)的,它編碼的目的是為了優(yōu)化具體這個(gè)任務(wù)。因此它編碼出的向量是適合這個(gè)任務(wù)的——如果這個(gè)任務(wù)很關(guān)注詞序,那么它在編碼的使用也會(huì)關(guān)注詞序;如果這個(gè)任務(wù)關(guān)注構(gòu)詞法,那么學(xué)到的編碼器也需要關(guān)注構(gòu)詞法。
但是監(jiān)督數(shù)據(jù)總是很少的,獲取的成本也極高。因此最近 (2018 年上半年),無(wú)監(jiān)督的通用 (universal) 的句子編碼器成為熱點(diǎn)并且有了一些進(jìn)展。無(wú)監(jiān)督的意思是可以使用未標(biāo)注的原始數(shù)據(jù)來(lái)學(xué)習(xí)編碼器 (的參數(shù)),而通用的意思是學(xué)習(xí)到的編碼器不需要 (太多的)fine-tuning 就可以直接用到所有 (只是是很多) 不同的任務(wù)中,并且能得到很好的效果。
評(píng)測(cè)工具
在介紹 Unsupervised Sentence Embedding 的具體算法之前我們先介紹兩個(gè)評(píng)測(cè)工具(平臺(tái))。
SentEval
簡(jiǎn)介
Sentence Embedding(包括 Word Embedding) 通常有兩類評(píng)價(jià)方法:intrinsic 和 ex-trinsic。前者只評(píng)價(jià) Embedding 本身,比如讓人來(lái)主觀評(píng)價(jià)。而后者通過(guò)下游 (Downstream) 的任務(wù)間接的來(lái)評(píng)價(jià) Embedding 的好壞。前一種方法耗費(fèi)人力,而且我們學(xué)習(xí) Embedding 的目的也是為了解決后面的真實(shí)問(wèn)題,因此 extrinsic 的評(píng)價(jià)更加重要。但是下游的任務(wù)通常很復(fù)雜,Embedding 只是其中的一個(gè)環(huán)節(jié),因此很難說(shuō)明最終效果的提高就是由于 Embedding 帶來(lái)的,也許只是某個(gè)預(yù)處理或者超參數(shù)的調(diào)節(jié)帶來(lái)的提高,但是卻可能被作者認(rèn)為是 Embedding 的功勞。另外下游任務(wù)很多,很多文章的結(jié)果也很難比較。
為了解決這些問(wèn)題,F(xiàn)acebook 做了 SentEval 這個(gè)工具。這是一個(gè)用于評(píng)估Universal Sentence Representation 的工具,所謂的 Universal Sentence Representation是指與特定任務(wù)無(wú)關(guān)的通用的句子表示 (Embedding) 方法。為了保證公平公正,這個(gè)工具只評(píng)價(jià)句子的 Embedding,對(duì)于具體的任務(wù),大家都使用相同的預(yù)處理,網(wǎng)絡(luò)結(jié)構(gòu)和后處理,從而能夠保證比較公平的評(píng)測(cè)。
SentEval 任務(wù)分類
SentEval 任務(wù)分為如下類別:
分類問(wèn)題 (包括二分類和多分類)
Natural Language Inference
語(yǔ)義相似度計(jì)算
圖像檢索 (Image Retrieval)
圖 17.1: SentEval 的分類任務(wù)
分類很簡(jiǎn)單,輸入是一個(gè)字符串 (一個(gè)句子或者文章),輸出是一個(gè)分類標(biāo)簽。所以任務(wù)如圖17.1所示。包括情感分類、句子類型分類等等任務(wù)。
Natural Language Inference(NLI) 任務(wù)也叫 recognizing textual entailment(RTE),它的輸入是兩個(gè)句子,需要機(jī)器判斷第一個(gè)句子和第二個(gè)句子的關(guān)系。它們的關(guān)系通常有 3 種:矛盾 (contradiction)、無(wú)關(guān) (neutral) 和蘊(yùn)含 (entailment)。
SNLI(https://nlp.stanford.edu/projects/snli/) 是很常用的 NLI 數(shù)據(jù)集,示例是來(lái)自這個(gè)數(shù)據(jù)集的例子。比如下面的兩個(gè)句子是矛盾的:
A man inspects the uniform of a figure in some East Asian country.
The man is sleeping.
一個(gè)人不能同時(shí)在觀察和睡覺(jué)。而下面兩個(gè)句子的關(guān)系是無(wú)關(guān)的:
A smiling costumed woman is holding an umbrella.
A happy woman in a fairy costume holds an umbrella.
而下面兩個(gè)的第一個(gè)句子蘊(yùn)含了第二個(gè)句子:
A soccer game with multiple males playing.
Some men are playing a sport.
語(yǔ)義相似度計(jì)算的輸入是兩個(gè)句子,輸出是它們的相似度,一般相似度會(huì)分為幾個(gè)程度,所以輸出也是標(biāo)簽。當(dāng)然最簡(jiǎn)單的是分成兩類——相似與不相似,比如MRPC 就是這樣的任務(wù),這個(gè)任務(wù)又叫 Paraphrase Detection,判斷兩個(gè)句子是否同義復(fù)寫(xiě)。
Image Retrieval 的輸入是一幅圖片和一段文字,如果文字能很好的描述圖片的內(nèi)容,那么輸出一個(gè)高的分值,否則輸出低分。
SentEval 包括的 NLI 和圖像檢索任務(wù)如圖17.2所示。
圖 17.2: SentEval 的 NLI 和 Image Retrieval 任務(wù)
SentEval 的用法
SentEval 依賴 NumPy/SciPy、PyTorch(>=0.4.0) 和 scikit-learn(>=0.18.0)。
然后從https://github.com/facebookresearch/SentEval.git clone 代碼。SentEval 提供了一些baseline 系統(tǒng),包括 bow、infersent 和 skipthought 等等。讀者如果實(shí)現(xiàn)了一種新的Sentence Embedding 算法,那么可以參考 baseline 的代碼用 SentEval 來(lái)評(píng)價(jià)算法的好壞。
我們這里只介紹最簡(jiǎn)單的 bow 的用法,它就是把 Pretraining 的 Word Embedding加起來(lái)得到 Sentence Embedding。
我們首先下載 fasttext 的 Embedding:
然后運(yùn)行:
main 函數(shù)代碼為:
首先構(gòu)造 senteval.engine.SE,然后列舉需要跑的 task,最后調(diào)用 se.eval 得到結(jié)果。
構(gòu)造 senteval.engine.SE 需要傳入 3 個(gè)參數(shù),params_senteval, batcher 和 prepare。params_senteval 是控制 SentEval 模型訓(xùn)練的一些超參數(shù)。比如 bow.py 里的:
而后兩個(gè)參數(shù)是函數(shù),我們先看 prepare:
這個(gè)函數(shù)相當(dāng)于初始化的回調(diào)函數(shù),參數(shù)會(huì)傳入 params 和 samples,samples 就是所有的句子,我們需要根據(jù)這些句子來(lái)做一些初始化的工作,結(jié)果存在 params 里,后面會(huì)用到。這里我們用 samples 構(gòu)造 word2id——word 到 id 的映射,另外根據(jù)word2id,從預(yù)訓(xùn)練的詞向量里提取需要的詞向量 (因?yàn)轭A(yù)訓(xùn)練的詞向量有很多詞,但是在某個(gè)具體任務(wù)中用到的詞是有限的,我們只需要提取需要的部分),另外把詞向量的維度保持到 params 里。
batcher 函數(shù)的輸入?yún)?shù)是前面的 params 和 batch,batch 就是句子列表,我們需要對(duì)它做 Sentence Embedding,這里的實(shí)現(xiàn)很簡(jiǎn)單,就是把詞向量加起來(lái)求平均值得到句子向量。
GLUE
Facebook 搞了個(gè)標(biāo)準(zhǔn),Google 也要來(lái)一個(gè),所以就有了 GLUE(https://gluebenchmark.com/)。GLUE 是 General Language Understanding Evaluation 的縮寫(xiě)。它們之間很多的任務(wù)都是一樣的,我們這里就不詳細(xì)介紹了,感興趣的讀者可以參考論文”GLUE: A Multi-Task Benchmark and Analysis Platform for Natural LanguageUnderstanding”。
Transformer
簡(jiǎn)介
Transformer 模型來(lái)自與論文 Attention Is All You Need(https://arxiv.org/abs/1706.03762)。
這個(gè)模型最初是為了提高機(jī)器翻譯的效率,它的 Self-Attention 機(jī)制和 Position Encoding 可以替代 RNN。因?yàn)?RNN 是順序執(zhí)行的,t 時(shí)刻沒(méi)有處理完成就不能處理 t+1 時(shí)刻,因此很難并行。但是后來(lái)發(fā)現(xiàn) Self-Attention 效果很好,在很多其它的地方也可以是 Transformer 模型。
圖解
我們首先通過(guò)圖的方式直觀的解釋 Transformer 模型的基本原理,這部分內(nèi)容主要來(lái)自文章The Illustrated Transformer(http://jalammar.github.io/illustratedtransformer/)。
模型概覽
我們首先把模型看成一個(gè)黑盒子,如圖15.51所示,對(duì)于機(jī)器翻譯來(lái)說(shuō),它的輸入是源語(yǔ)言 (法語(yǔ)) 的句子,輸出是目標(biāo)語(yǔ)言 (英語(yǔ)) 的句子。
把黑盒子稍微打開(kāi)一點(diǎn),Transformer(或者任何的 NMT 系統(tǒng)) 都可以分成
Encoder 和 Decoder 兩個(gè)部分,如圖15.52所示。
再展開(kāi)一點(diǎn),Encoder 由很多 (6 個(gè)) 結(jié)構(gòu)一樣的 Encoder 堆疊 (stack) 而成,Decoder 也是一樣。如圖15.53所示。注意:每一個(gè) Encoder 的輸入是下一層 Encoder輸出,最底層 Encoder 的輸入是原始的輸入 (法語(yǔ)句子);Decoder 也是類似,但是最后一層 Encoder 的輸出會(huì)輸入給每一個(gè) Decoder 層,這是 Attention 機(jī)制的要求。
每一層的 Encoder 都是相同的結(jié)構(gòu),它有一個(gè) Self-Attention 層和一個(gè)前饋網(wǎng)絡(luò)(全連接網(wǎng)絡(luò)) 組成,15.54如圖所示。每一層的 Decoder 也是相同的結(jié)果,它除了 Self-Attention 層和全連接層之外還多了一個(gè)普通的 Attention 層,這個(gè) Attention 層使得 Decoder 在解碼時(shí)會(huì)考慮最后一層 Encoder 所有時(shí)刻的輸出。它的結(jié)構(gòu)如圖17.3所示。
加入 Tensor
前面的圖示只是說(shuō)明了 Transformer 的模塊,接下來(lái)我們加入 Tensor,了解這些模塊是怎么串聯(lián)起來(lái)的。
輸入的句子是一個(gè)詞 (ID) 的序列,我們首先通過(guò) Embedding 把它變成一個(gè)連續(xù)稠密的向量,如圖17.4所示。
Embedding 之后的序列會(huì)輸入 Encoder,首先經(jīng)過(guò) Self-Attention 層然后再經(jīng)過(guò)全連接層,如圖17.5所示。
我們?cè)谟?jì)算 zi 是需要依賴所有時(shí)刻的輸入 x1, ..., xn,不過(guò)我們可以用矩陣運(yùn)算一下子把所有的 zi 計(jì)算出來(lái) (后面介紹)。而全連接網(wǎng)絡(luò)的計(jì)算則完全是獨(dú)立的,計(jì)算 i 時(shí)刻的輸出只需要輸入 zi 就足夠了,因此很容易并行計(jì)算。
圖17.6更加明確的表達(dá)了這一點(diǎn)。圖中 Self-Attention 層是一個(gè)大的方框,表示它的輸入是所有的 x1, ..., xn,輸出是 z1, ..., zn。而全連接層每個(gè)時(shí)刻是一個(gè)方框,表示計(jì)算 ri 只需要 zi。此外,前一層的輸出 r1, ...,rn 直接輸入到下一層。
Self-Attention 簡(jiǎn)介
比如我們要翻譯如下句子”The animal didn’t cross the street because it was tootired”(這個(gè)動(dòng)物無(wú)法穿越馬路,因?yàn)樗惲?。這里的 it 到底指代什么呢,是animal 還是 street?要知道具體的指代,我們需要在理解 it 的時(shí)候同時(shí)關(guān)注所有的單詞,重點(diǎn)是 animal、street 和 tired,然后根據(jù)知識(shí) (常識(shí)) 我們知道只有 animal 才能tired,而 stree 是不能 tired 的。Self-Attention 運(yùn)行 Encoder 在編碼一個(gè)詞的時(shí)候考慮句子中所有其它的詞,從而確定怎么編碼當(dāng)前詞。如果把 tired 換成 narrow,那么it 就指代的是 street 了。
而 LSTM(即使是雙向的) 是無(wú)法實(shí)現(xiàn)上面的邏輯的。為什么呢?比如前向的
LSTM,我們?cè)诰幋a it 的時(shí)候根本沒(méi)有看到后面是 tired 還是 narrow,所有它無(wú)法把it 編碼成哪個(gè)詞。而后向的 LSTM 呢?當(dāng)然它看到了 tired,但是到 it 的時(shí)候它還沒(méi)有看到 animal 和 street 這兩個(gè)單詞,當(dāng)然就更無(wú)法編碼 it 的內(nèi)容了。
當(dāng)然多層的 LSTM 理論上是可以編碼這個(gè)語(yǔ)義的,它需要下層的 LSTM 同時(shí)編碼了 animal 和 street 以及 tired 三個(gè)詞的語(yǔ)義,然后由更高層的 LSTM 來(lái)把 it 編碼成 animal 的語(yǔ)義。但是這樣模型更加復(fù)雜。
下圖17.7是模型的最上一層 (下標(biāo) 0 是第一層,5 是第六層)Encoder 的Attention可視化圖。這是 tensor2tensor 這個(gè)工具輸出的內(nèi)容。我們可以看到,在編碼 it 的時(shí)候有一個(gè) Attention Head(后面會(huì)講到) 注意到了Animal,因此編碼后的 it 有 Animal的語(yǔ)義。
Self-Attention 詳細(xì)介紹
下面我們?cè)敿?xì)的介紹 Self-Attention 是怎么計(jì)算的,首先介紹向量的形式逐個(gè)時(shí)刻計(jì)算,這便于理解,接下來(lái)我們把它寫(xiě)出矩陣的形式一次計(jì)算所有時(shí)刻的結(jié)果。
對(duì)于輸入的每一個(gè)向量 (第一層是詞的 Embedding,其它層是前一層的輸出),我們首先需要生成 3 個(gè)新的向量 Q、K 和 V,分別代表查詢 (Query) 向量、Key 向量和 Value 向量。Q 表示為了編碼當(dāng)前詞,需要去注意 (attend to) 其它 (其實(shí)也包括它自己) 的詞,我們需要有一個(gè)查詢向量。而 Key 向量可以任務(wù)是這個(gè)詞的關(guān)鍵的用于被檢索的信息,而 Value 向量是真正的內(nèi)容。
我們對(duì)比一些普通的 Attention(Luong 2015),使用內(nèi)積計(jì)算 energy 的情況。如圖17.8所示,在這里,每個(gè)向量的 Key 和 Value 向量都是它本身,而 Q 是當(dāng)前隱狀態(tài) ht,計(jì)算 energy etj 的時(shí)候我們計(jì)算 Q(ht) 和Key(barhj)。然后用 softmax 變成概率,最后把所有的 barhj 加權(quán)平均得到 context 向量。
而 Self-Attention 里的 Query 不是隱狀態(tài),并且來(lái)自當(dāng)前輸入向量本身,因此叫作 Self-Attention。另外 Key 和 Value 都不是輸入向量,而是輸入向量做了一下線性變換。
當(dāng)然理論上這個(gè)線性變換矩陣可以是 Identity 矩陣,也就是使得Key=Value=輸入向量。因此可以認(rèn)為普通的 Attention 是這里的特例。這樣做的好處是系統(tǒng)可以學(xué)習(xí)的,這樣它可以根據(jù)數(shù)據(jù)從輸入向量中提取最適合作為 Key(可以看成一種索引)和 Value 的部分。類似的,Query 也是對(duì)輸入向量做一下線性變換,它讓系統(tǒng)可以根據(jù)任務(wù)學(xué)習(xí)出最適合的 Query,從而可以注意到 (attend to) 特定的內(nèi)容。
具體的計(jì)算過(guò)程如圖17.9所示。比如圖中的輸入是兩個(gè)詞”thinking” 和”machines”,我們對(duì)它們進(jìn)行Embedding(這是第一層,如果是后面的層,直接輸入就是向量了),得到向量 x1, x2。接著我們用 3 個(gè)矩陣分別對(duì)它們進(jìn)行變換,得到向量 q1, k1, v1 和q2, k2, v2。比如 q1 = x1WQ,圖中 x1 的 shape 是 1x4,WQ 是 4x3,得到的 q1 是 1x3。其它的計(jì)算也是類似的,為了能夠使得 Key 和 Query 可以內(nèi)積,我們要求 WK 和WQ 的 shape 是一樣的,但是并不要求 WV 和它們一定一樣 (雖然實(shí)際論文實(shí)現(xiàn)是一樣的)。每個(gè)時(shí)刻 t 都計(jì)算出 Qt, Kt, Vt 之后,我們就可以來(lái)計(jì)算 Self-Attention 了。以第一個(gè)時(shí)刻為了,我們首先計(jì)算 q1 和 k1, k2 的內(nèi)積,得到 score,過(guò)程如圖17.10所示。
接下來(lái)使用 softmax 把得分變成概率,注意這里把得分除以之后再計(jì)算的 softmax,根據(jù)論文的說(shuō)法,這樣計(jì)算梯度時(shí)會(huì)更加文檔 (stable)。計(jì)算過(guò)程如圖17.11所示。
接下來(lái)用 softmax 得到的概率對(duì)所有時(shí)刻的 V 求加權(quán)平均,這樣就可以認(rèn)為得到的向量根據(jù) Self-Attention 的概率綜合考慮了所有時(shí)刻的輸入信息,計(jì)算過(guò)程如圖17.12所示。
這里只是演示了計(jì)算第一個(gè)時(shí)刻的過(guò)程,計(jì)算其它時(shí)刻的過(guò)程是完全一樣的。
矩陣計(jì)算
前面介紹的方法需要一個(gè)循環(huán)遍歷所有的時(shí)刻 t 計(jì)算得到 zt,我們可以把上面的向量計(jì)算變成矩陣的形式,從而一次計(jì)算出所有時(shí)刻的輸出,這樣的矩陣運(yùn)算可以充分利用硬件資源 (包括一些軟件的優(yōu)化),從而效率更高。
第一步還是計(jì)算 Q、K 和 V,不過(guò)不是計(jì)算某個(gè)時(shí)刻的 qt, kt, vt 了,而是一次計(jì)算所有時(shí)刻的 Q、K 和 V。
計(jì)算過(guò)程如圖17.13所示。這里的輸入是一個(gè)矩陣,矩陣的第 i 行表示第 i 個(gè)時(shí)刻的輸入 xi。
接下來(lái)就是計(jì)算 Q 和 K 得到 score,然后除以,然后再 softmax,最后加權(quán)平均得到輸出。全過(guò)程如圖17.14所示。
Multi-Head Attention
這篇論文還提出了 Multi-Head Attention 的概念。其實(shí)很簡(jiǎn)單,前面定義的一組 Q、K 和 V 可以讓一個(gè)詞 attend to 相關(guān)的詞,我們可以定義多組 Q、K 和 V,它們分別可以關(guān)注不同的上下文。
計(jì)算 Q、K 和 V 的過(guò)程還是一樣,這不過(guò)現(xiàn)在變換矩陣從一組
變成了多組,,...。如圖所示。
對(duì)于輸入矩陣 (time_step, num_input),每一組 Q、K 和 V 都可以得到一個(gè)輸
出矩陣 Z(time_step, num_features)。如圖17.16所示。
但是后面的全連接網(wǎng)絡(luò)需要的輸入是一個(gè)矩陣而不是多個(gè)矩陣,因此我們可以
把多個(gè) head 輸出的 Z 按照第二個(gè)維度拼接起來(lái),但是這樣的特征有一些多,因此Transformer 又用了一個(gè)線性變換 (矩陣 WO) 對(duì)它進(jìn)行了壓縮。這個(gè)過(guò)程如圖17.17所示。
上面的步驟涉及很多步驟和矩陣運(yùn)算,我們用一張大圖把整個(gè)過(guò)程表示出來(lái),如圖17.18所示。我們已經(jīng)學(xué)習(xí)過(guò)來(lái) Transformer 的 Self-Attention 機(jī)制,下面我們通過(guò)一個(gè)具體的例子來(lái)看看不同的 Attention Head 到底學(xué)習(xí)到了什么樣的語(yǔ)義。
圖17.19是一個(gè) Attention Head 學(xué)習(xí)到的語(yǔ)義,我們可以看到對(duì)于 it 一個(gè) Head會(huì)注意到”the animal” 而另外一個(gè) Head 會(huì)注意到”tired”。
如果把所有的 Head 混在一起,如圖17.20所示,那么就很難理解它到底注意的是什么內(nèi)容。從上面兩圖的對(duì)比也能看出使用多個(gè) Head 的好處——每個(gè) Head(在數(shù)據(jù)的驅(qū)動(dòng)下) 學(xué)習(xí)到不同的語(yǔ)義。
位置編碼 (Positional Encoding)
注意:這是原始論文使用的位置編碼方法,而在 BERT 模型里,使用的是簡(jiǎn)單的可以學(xué)習(xí)的 Embedding,和 Word Embedding 一樣,只不過(guò)輸入是位置而不是詞而已。
我們的目的是用 Self-Attention 替代 RNN,RNN 能夠記住過(guò)去的信息,這可以通過(guò) Self-Attention“實(shí)時(shí)”的注意相關(guān)的任何詞來(lái)實(shí)現(xiàn)等價(jià) (甚至更好) 的效果。RNN還有一個(gè)特定就是能考慮詞的順序 (位置) 關(guān)系,一個(gè)句子即使詞完全是相同的但是語(yǔ)義可能完全不同,比如” 北京到上海的機(jī)票” 與” 上海到北京的機(jī)票”,它們的語(yǔ)義就有很大的差別。我們上面的介紹的 Self-Attention 是不考慮詞的順序的,如果模型參數(shù)固定了,上面兩個(gè)句子的北京都會(huì)被編碼成相同的向量。但是實(shí)際上我們可以期望這兩個(gè)北京編碼的結(jié)果不同,前者可能需要編碼出發(fā)城市的語(yǔ)義,而后者需要包含目的城市的語(yǔ)義。而 RNN 是可以 (至少是可能) 學(xué)到這一點(diǎn)的。當(dāng)然 RNN 為了實(shí)現(xiàn)這一點(diǎn)的代價(jià)就是順序處理,很難并行。
為了解決這個(gè)問(wèn)題,我們需要引入位置編碼,也就是 t 時(shí)刻的輸入,除了Embedding 之外 (這是與位置無(wú)關(guān)的),我們還引入一個(gè)向量,這個(gè)向量是與 t 有關(guān)的,我們把 Embedding 和位置編碼向量加起來(lái)作為模型的輸入。這樣的話如果兩個(gè)詞在不同的位置出現(xiàn)了,雖然它們的 Embedding 是相同的,但是由于位置編碼不同,最終得到的向量也是不同的。
位置編碼有很多方法,其中需要考慮的一個(gè)重要因素就是需要它編碼的是相對(duì)位置的關(guān)系。比如兩個(gè)句子:” 北京到上海的機(jī)票” 和” 你好,我們要一張北京到上海的機(jī)票”。顯然加入位置編碼之后,兩個(gè)北京的向量是不同的了,兩個(gè)上海的向量也是不同的了,但是我們期望 Query(北京 1)*Key(上海 1) 卻是等于 Query(北京 2)*Key(上海 2) 的。具體的編碼算法我們?cè)诖a部分再介紹。
位置編碼加入模型如圖17.21所示。
一個(gè)具體的位置編碼的例子如圖17.22所示。
殘差連接
每個(gè) Self-Attention 層都會(huì)加一個(gè)殘差連接,然后是一個(gè) LayerNorm 層,如圖17.23所示。
圖17.24展示了更多細(xì)節(jié):輸入 x1, x2 經(jīng) self-attention 層之后變成 z1, z2,然后和殘差連接的輸入 x1, x2 加起來(lái),然后經(jīng)過(guò) LayerNorm 層輸出給全連接層。全連接層也是有一個(gè)殘差連接和一個(gè) LayerNorm 層,最后再輸出給上一層。
Decoder 和 Encoder 是類似的,如圖17.25所示,區(qū)別在于它多了一個(gè)EncoderDecoder Attention 層,這個(gè)層的輸入除了來(lái)自 Self-Attention 之外還有 Encoder 最后一層的所有時(shí)刻的輸出。
Encoder-Decoder Attention 層的 Query 來(lái)自下一層,而 Key 和 Value 則來(lái)自Encoder 的輸出。
代碼
本節(jié)內(nèi)容來(lái)自
http://nlp.seas.harvard.edu/2018/04/03/attention.html。讀者可以從https://github.com/harvardnlp/annotated-transformer.git 下載代碼。這篇文章原名叫作《The Annotated Transformer》。相當(dāng)于原始論文的讀書(shū)筆記,但是不同之處在于它不但詳細(xì)的解釋論文,而且還用代碼實(shí)現(xiàn)了論文的模型。
注意:本書(shū)并不沒(méi)有完全翻譯這篇文章,而是根據(jù)作者自己的理解來(lái)分析和閱讀其源代碼。而 Transformer 的原來(lái)在前面的圖解部分已經(jīng)分析的很詳細(xì)了,因此這里關(guān)注的重點(diǎn)是代碼。網(wǎng)上有很多 Transformer 的源代碼,也有一些比較大的庫(kù)包含了Transformer 的實(shí)現(xiàn),比如 Tensor2Tensor 和 OpenNMT 等等。作者選擇這個(gè)實(shí)現(xiàn)的原因是它是一個(gè)單獨(dú)的 ipynb 文件,如果我們要實(shí)際使用非常簡(jiǎn)單,復(fù)制粘貼代碼就行了。而 Tensor2Tensor 或者 OpenNMT 包含了太多其它的東西,做了過(guò)多的抽象。
雖然代碼質(zhì)量和重用性更好,但是對(duì)于理解論文來(lái)說(shuō)這是不必要的,并且增加了理解的難度。
運(yùn)行
這里的代碼需要 PyTorch-0.3.0,所以建議讀者使用 virtualenv 安裝。另外為了在Jupyter notebook 里使用這個(gè) virtualenv,需要執(zhí)行如下命令:
背景介紹
前面提到過(guò) RNN 等模型的缺點(diǎn)是需要順序計(jì)算,從而很難并行。因此出現(xiàn)了Extended Neural GPU、ByteNet 和 ConvS2S 等網(wǎng)絡(luò)模型。這些模型都是以 CNN 為基礎(chǔ),這比較容易并行。但是和 RNN 相比,它較難學(xué)習(xí)到長(zhǎng)距離的依賴關(guān)系。
本文的 Transformer 使用了 Self-Attention 機(jī)制,它在編碼每一詞的時(shí)候都能夠注意 (attend to) 整個(gè)句子,從而可以解決長(zhǎng)距離依賴的問(wèn)題,同時(shí)計(jì)算 Self-Attention可以用矩陣乘法一次計(jì)算所有的時(shí)刻,因此可以充分利用計(jì)算資源 (CPU/GPU 上的矩陣運(yùn)算都是充分優(yōu)化和高度并行的)。
模型結(jié)構(gòu)
目前的主流神經(jīng)序列轉(zhuǎn)換 (neural sequence transduction) 模型都是基于 EncoderDecoder 結(jié)構(gòu)的。所謂的序列轉(zhuǎn)換模型就是把一個(gè)輸入序列轉(zhuǎn)換成另外一個(gè)輸出序列,它們的長(zhǎng)度很可能是不同的。比如基于神經(jīng)網(wǎng)絡(luò)的機(jī)器翻譯,輸入是法語(yǔ)句子,輸出是英語(yǔ)句子,這就是一個(gè)序列轉(zhuǎn)換模型。類似的包括文本摘要、對(duì)話等問(wèn)題都可以看成序列轉(zhuǎn)換問(wèn)題。我們這里主要關(guān)注機(jī)器翻譯,但是任何輸入是一個(gè)序列輸出是另外一個(gè)序列的問(wèn)題都可以考慮使用 Encoder-Decoder 模型。
Encoder 講輸入序列 (x1, ..., xn) 映射 (編碼) 成一個(gè)連續(xù)的序列 z = (z1, ..., zn)。而Decoder 根據(jù) z 來(lái)解碼得到輸出序列 y1, ..., ym。Decoder 是自回歸的 (auto-regressive)——它會(huì)把前一個(gè)時(shí)刻的輸出作為當(dāng)前時(shí)刻的輸入。
Encoder-Decoder 結(jié)構(gòu)模型的代碼如下:
EncoderDecoder 定義了一種通用的 Encoder-Decoder 架構(gòu),具體的 Encoder、Decoder、src_embed、target_embed 和 generator 都是構(gòu)造函數(shù)傳入的參數(shù)。這樣我們做實(shí)驗(yàn)更換不同的組件就會(huì)更加方便。
注意:Generator 返回的是 softmax 的 log 值。在 PyTorch 里為了計(jì)算交叉熵?fù)p失,有兩種方法。第一種方法是使用 nn.CrossEntropyLoss(),一種是使用 NLLLoss()。第一種方法更加容易懂,但是在很多開(kāi)源代碼里第二種更常見(jiàn),原因可能是它后來(lái)才有,大家都習(xí)慣了使用 NLLLoss。
我們先看 CrossEntropyLoss,它就是計(jì)算交叉熵?fù)p失函數(shù),比如:
比如上面的代碼,假設(shè)是 5 分類問(wèn)題,x 表示模型的輸出 logits(batch=1),而 y 是真實(shí)分類的下標(biāo) (0-4)。
實(shí)際的計(jì)算過(guò)程為:
比如 logits 是 [0,1,2,3,4],真實(shí)分類是 3,那么上式就是:
因此我們也可以使用 NLLLoss() 配合 F.log_softmax 函數(shù) (或者nn.LogSoftmax,這不是一個(gè)函數(shù)而是一個(gè) Module 了) 來(lái)實(shí)現(xiàn)一樣的效果:
NLLLoss(Negative Log Likelihood Loss) 是計(jì)算負(fù) log 似然損失。它輸入的 x 是log_softmax 之后的結(jié)果 (長(zhǎng)度為 5 的數(shù)組),y 是真實(shí)分類 (0-4),輸出就是 x[y]。因此上面的代碼
Transformer 模型也是遵循上面的架構(gòu),只不過(guò)它的 Encoder 是 N(6) 個(gè) EncoderLayer 組成,每個(gè) EncoderLayer 包含一個(gè) Self-Attention SubLayer 層和一個(gè)全連接SubLayer 層。而它的 Decoder 也是 N(6) 個(gè) DecoderLayer 組成,每個(gè) DecoderLayer包含一個(gè) Self-Attention SubLayer 層、Attention SubLayer 層和全連接 SubLayer 層。如圖17.26所示。
Encoder 和 Decoder Stack
前面說(shuō)了 Encoder 和 Decoder 都是由 N 個(gè)相同結(jié)構(gòu)的 Layer 堆積 (stack) 而成。因此我們首先定義 clones 函數(shù),用于克隆相同的 SubLayer。
這里使用了 nn.ModuleList,ModuleList 就像一個(gè)普通的 Python 的 List,我們可以使用下標(biāo)來(lái)訪問(wèn)它,它的好處是傳入的 ModuleList 的所有 Module 都會(huì)注冊(cè)的PyTorch 里,這樣 Optimizer 就能找到這里面的參數(shù),從而能夠用梯度下降更新這些參數(shù)。但是 nn.ModuleList 并不是 Module(的子類),因此它沒(méi)有 forward 等方法,我們通常把它放到某個(gè) Module 里。
接下來(lái)我們定義 Encoder:
class Encoder(nn.Module):
"Encoder是N個(gè)EncoderLayer的stack"
def __init__(self, layer, N):
super(Encoder, self).__init__()
# layer是一個(gè)SubLayer,我們clone N個(gè)
self.layers = clones(layer, N)
# 再加一個(gè)LayerNorm層
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
"逐層進(jìn)行處理"
for layer in self.layers:
x = layer(x, mask)
# 最后進(jìn)行LayerNorm,后面會(huì)解釋為什么最后還有一個(gè)LayerNorm。
return self.norm(x)
Encoder 就是 N 個(gè) SubLayer 的 stack,最后加上一個(gè) LayerNorm。我們來(lái)看 Layer-Norm:
classLayerNorm(nn.Module):
def__init__(self, features, eps=1e-6):
super(LayerNorm,self).__init__()
self.a_2 = nn.Parameter(torch.ones(features))
self.b_2 = nn.Parameter(torch.zeros(features))
self.eps = eps
defforward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
returnself.a_2 * (x - mean) / (std + self.eps) + self.b_2
LayerNorm 我們以前介紹過(guò),代碼也很簡(jiǎn)單,這里就不詳細(xì)介紹了。注意 LayerNormalization 不是 Batch Normalization。
如圖17.26所示,原始論文的模型為:
x -> attention(x) -> x+self-attention(x) -> layernorm(x+self-attention(x))
=> y
y -> dense(y) -> y+dense(y) -> layernorm(y+dense(y)) => z(輸入下一層)
這里稍微做了一點(diǎn)修改,在 self-attention 和 dense 之后加了一個(gè) dropout 層。另外一個(gè)不同支持就是把 layernorm 層放到前面了。這里的模型為:
x -> layernorm(x) -> attention(layernorm(x)) -> x + attention(layernorm(x))=> y
y -> layernorm(y) -> dense(layernorm(y)) -> y+dense(layernorm(y))
原始論文的 layernorm 放在最后;而這里把它放在最前面并且在 Encoder 的最后一層再加了一個(gè) layernorm,因此這里的實(shí)現(xiàn)和論文的實(shí)現(xiàn)基本是一致的,只是給最底層的輸入 x 多做了一個(gè) layernorm,而原始論文是沒(méi)有的。下面是 Encoder 的forward 方法,這樣對(duì)比讀者可能會(huì)比較清楚為什么 N 個(gè)EncoderLayer 處理完成之后還需要一個(gè) LayerNorm。
def forward(self, x, mask):
"逐層進(jìn)行處理"
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
不管是 Self-Attention 還是全連接層,都首先是 LayerNorm,然后是 Self-Attention/Dense,然后是 Dropout,最好是殘差連接。這里面有很多可以重用的代碼,我們把它封裝成 SublayerConnection。
class SublayerConnection(nn.Module):
"""
LayerNorm + sublayer(Self-Attenion/Dense) + dropout + 殘差連接
為了簡(jiǎn)單,把LayerNorm放到了前面,這和原始論文稍有不同,原始論文LayerNorm在最后。
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"sublayer是傳入的參數(shù),參考DecoderLayer,它可以當(dāng)成函數(shù)調(diào)用,這個(gè)函數(shù)的有一個(gè)輸入?yún)?shù)"
return x + self.dropout(sublayer(self.norm(x)))
這個(gè)類會(huì)構(gòu)造 LayerNorm 和 Dropout,但是 Self-Attention 或者 Dense 并不在這里構(gòu)造,還是放在了 EncoderLayer 里,在 forward 的時(shí)候由 EncoderLayer 傳入。這樣的好處是更加通用,比如 Decoder 也是類似的需要在 Self-Attention、Attention 或者 Dense 前面后加上 LayerNorm 和 Dropout 以及殘差連接,我們就可以復(fù)用代碼。但是這里要求傳入的 sublayer 可以使用一個(gè)參數(shù)來(lái)調(diào)用的函數(shù) (或者有 __call__)。
有了這些代碼之后,EncoderLayer 就很簡(jiǎn)單了:
class EncoderLayer(nn.Module):
"EncoderLayer由self-attn和feed forward組成"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
"Follow Figure 1 (left) for connections."
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
return self.sublayer[1](x, self.feed_forward)
為了復(fù)用,這里的 self_attn 層和 feed_forward 層也是傳入的參數(shù),這里只構(gòu)造兩個(gè) SublayerConnection。forward 調(diào)用 sublayer[0](SublayerConnection) 的 __call__方法,最終會(huì)調(diào)到它的 forward 方法,而這個(gè)方法需要兩個(gè)參數(shù),一個(gè)是輸入 Tensor,一個(gè)是一個(gè) callable,并且這個(gè) callable 可以用一個(gè)參數(shù)來(lái)調(diào)用。而 self_attn 函數(shù)需要 4 個(gè)參數(shù) (Query 的輸入,Key 的輸入,Value 的輸入和 Mask),因此這里我們使用lambda 的技巧把它變成一個(gè)參數(shù) x 的函數(shù) (mask 可以看成已知的數(shù))。因?yàn)?lambda的形參也叫 x,讀者可能難以理解,我們改寫(xiě)一下:
def forward(self, x, mask):
z = lambda y: self.self_attn(y, y, y, mask)
x = self.sublayer[0](x, z)
return self.sublayer[1](x, self.feed_forward)
self_attn 有 4 個(gè)參數(shù),但是我們知道在 Encoder 里,前三個(gè)參數(shù)都是輸入 y,第四個(gè)參數(shù)是 mask。這里 mask 是已知的,因此我們可以用 lambda 的技巧它變成一個(gè)參數(shù)的函數(shù) z = lambda y: self.self_attn(y, y, y, mask),這個(gè)函數(shù)的輸入是 y。
self.sublayer[0] 是個(gè) callable,self.sublayer[0](x, z) 會(huì)調(diào)用 self.sublayer[0].__call__(x,z),然后會(huì)調(diào)用 SublayerConnection.forward(x, z),然后會(huì)調(diào)用 sublayer(self.norm(x)),sublayer 就是傳入的參數(shù) z,因此就是 z(self.norm(x))。z 是一個(gè) lambda,我們可以先簡(jiǎn)單的看成一個(gè)函數(shù),顯然這里要求函數(shù) z 的輸入是一個(gè)參數(shù)。
理解了 Encoder 之后,Decoder 就很簡(jiǎn)單了。
class Decoder(nn.Module):
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
Decoder 也是 N 個(gè) DecoderLayer 的 stack,參數(shù) layer 是 DecoderLayer,它也是一個(gè)callable,最終 __call__ 會(huì)調(diào)用 DecoderLayer.forward 方法,這個(gè)方法 (后面會(huì)介紹)需要 4 個(gè)參數(shù),輸入 x,Encoder 層的輸出 memory,輸入 Encoder 的 Mask(src_mask)和輸入 Decoder 的 Mask(tgt_mask)。所有這里的 Decoder 的 forward 也需要這 4 個(gè)參數(shù)。
class DecoderLayer(nn.Module):
"Decoder包括self-attn, src-attn, 和feed forward "
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
m = memory
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
return self.sublayer[2](x, self.feed_forward)
DecoderLayer 比 EncoderLayer 多了一個(gè) src-attn 層,這是 Decoder 時(shí) attend toEncoder 的輸出 (memory)。src-attn 和 self-attn 的實(shí)現(xiàn)是一樣的,只不過(guò)使用的Query,Key 和 Value 的輸入不同。普通的 Attention(src-attn) 的 Query 是下層輸入進(jìn)來(lái)的 (來(lái)自 self-attn 的輸出),Key 和 Value 是 Encoder 最后一層的輸出 memory;而 Self-Attention 的 Query,Key 和 Value 都是來(lái)自下層輸入進(jìn)來(lái)的。
Decoder 和 Encoder 有一個(gè)關(guān)鍵的不同:Decoder 在解碼第 t 個(gè)時(shí)刻的時(shí)候只能使用 1...t 時(shí)刻的輸入,而不能使用 t+1 時(shí)刻及其之后的輸入。因此我們需要一個(gè)函數(shù)來(lái)產(chǎn)生一個(gè) Mask 矩陣,代碼如下:
def subsequent_mask(size):
"Mask out subsequent positions."
attn_shape = (1, size, size)
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
return torch.from_numpy(subsequent_mask) == 0
我們閱讀代碼之前先看它的輸出:
print(subsequent_mask(5))
# 輸出
1 0 0 0 0
1 1 0 0 0
1 1 1 0 0
1 1 1 1 0
1 1 1 1 1
我們發(fā)現(xiàn)它輸出的是一個(gè)方陣,對(duì)角線和下面都是 1。第一行只有第一列是 1,它的意思是時(shí)刻 1 只能 attend to 輸入 1,第三行說(shuō)明時(shí)刻 3 可以 attend to 1,2,3 而不能attend to4,5 的輸入,因?yàn)樵谡嬲?Decoder 的時(shí)候這是屬于 Future 的信息。知道了這個(gè)函數(shù)的用途之后,上面的代碼就很容易理解了。代碼首先使用 triu 產(chǎn)生一個(gè)上三角陣:
然后需要把 0 變成 1,把 1 變成 0,這可以使用 matrix == 0 來(lái)實(shí)現(xiàn)。
###MultiHeadedAttention
Attention(包括 Self-Attention 和普通的 Attention) 可以看成一個(gè)函數(shù),它的輸入是Query,Key,Value 和 Mask,輸出是一個(gè) Tensor。其中輸出是 Value 的加權(quán)平均,而權(quán)重來(lái)自 Query 和 Key 的計(jì)算。
具體的計(jì)算如圖17.27所示,計(jì)算公式為:
代碼為:
def attention(query, key, value, mask=None, dropout=None):
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim = -1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn
我們使用一個(gè)實(shí)際的例子跟蹤一些不同 Tensor 的 shape,然后對(duì)照公式就很容易理解。比如 Q 是 (30,8,33,64),其中 30 是 batch,8 是 head 個(gè)數(shù),33 是序列長(zhǎng)度,64 是每個(gè)時(shí)刻的特征數(shù)。K 和 Q 的 shape 必須相同的,而 V 可以不同,但是這里的實(shí)現(xiàn) shape 也是相同的。
上面的代碼實(shí)現(xiàn),和公式里稍微不同的是,這里的 Q 和 K 都是 4d 的 Tensor,包括 batch 和 head 維度。matmul 會(huì)把 query 和 key 的最后兩維進(jìn)行矩陣乘法,這樣效率更高,如果我們要用標(biāo)準(zhǔn)的矩陣 (二維 Tensor) 乘法來(lái)實(shí)現(xiàn),那么需要遍歷 batch維和 head 維:
而上面的寫(xiě)法一次完成所有這些循環(huán),效率更高。輸出的 score 是 (30, 8, 33, 33),前面兩維不看,那么是一個(gè) (33, 33) 的 attention 矩陣 a,aij 表示時(shí)刻 i attend to j 的得分 (還沒(méi)有經(jīng)過(guò) softmax 變成概率)。
接下來(lái)是 scores.masked_fill(mask == 0, -1e9),用于把 mask 是 0 的變成一個(gè)很小的數(shù),這樣后面經(jīng)過(guò) softmax 之后的概率就很接近零 (但是理論上還是用來(lái)很少一點(diǎn)點(diǎn)未來(lái)的信息)。
這里 mask 是 (30, 1, 1, 33) 的 tensor,因?yàn)?8 個(gè) head 的 mask 都是一樣的,所有第二維是 1,masked_fill 時(shí)使用 broadcasting 就可以了。這里是 self-attention 的mask,所以每個(gè)時(shí)刻都可以 attend 到所有其它時(shí)刻,所有第三維也是 1,也使用broadcasting。如果是普通的 mask,那么 mask 的 shape 是 (30, 1, 33, 33)。這樣講有點(diǎn)抽象,我們可以舉一個(gè)例子,為了簡(jiǎn)單,我們假設(shè) batch=2, head=8。
第一個(gè)序列長(zhǎng)度為 3,第二個(gè)為 4,那么 self-attention 的 mask 為 (2, 1, 1, 4),我們可以用兩個(gè)向量表示:
它的意思是在 self-attention 里,第一個(gè)序列的任一時(shí)刻可以 attend to 前 3 個(gè)時(shí)刻(因?yàn)榈?4 個(gè)時(shí)刻是 padding 的);而第二個(gè)序列的可以 attend to 所有時(shí)刻的輸入。而 Decoder 的 src-attention 的 mask 為 (2, 1, 4, 4),我們需要用 2 個(gè)矩陣表示:
第一個(gè)序列的mask矩陣
第二個(gè)序列的mask矩陣
接下來(lái)對(duì) score 求 softmax,把得分變成概率 p_attn,如果有 dropout 還對(duì)p_attn 進(jìn)行 Dropout(這也是原始論文沒(méi)有的)。最后把 p_attn 和 value 相乘。p_attn是 (30, 8, 33, 33),value 是 (30, 8, 33, 64),我們只看后兩維,(33x33) x (33x64) 最終得到 33x64。
接下來(lái)就是輸入怎么變成 Q,K 和 V 了,我們之前介紹過(guò),對(duì)于每一個(gè) Head,都使用三個(gè)矩陣 WQ, WK, WV 把輸入轉(zhuǎn)換成 Q,K 和 V。然后分別用每一個(gè) Head 進(jìn)行 Self-Attention 的計(jì)算,最后把 N 個(gè) Head 的輸出拼接起來(lái),最后用一個(gè)矩陣 WO把輸出壓縮一下。具體計(jì)算過(guò)程為:
其中,
在這里,我們的 Head 個(gè)數(shù) h=8,dk = dv = dmodel/h = 64。
詳細(xì)結(jié)構(gòu)如圖17.28所示,輸入 Q,K 和 V 經(jīng)過(guò)多個(gè)線性變換后得到 N(8) 組Query,Key 和 Value,然后使用 Self-Attention 計(jì)算得到 N 個(gè)向量,然后拼接起來(lái),最后使用一個(gè)線性變換進(jìn)行降維。
代碼如下:
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h
self.h = h
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
# 所有h個(gè)head的mask都是相同的
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1)
首先使用線性變換,然后把d_model分配給h個(gè)Head,每個(gè)head為d_k=d_model/h
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2) for l, x
in zip(self.linears, (query, key, value))]
# 2) 使用attention函數(shù)計(jì)算
x, self.attn = attention(query, key, value, mask=mask,
dropout=self.dropout)
# 3)
把8個(gè)head的64維向量拼接成一個(gè)512的向量。然后再使用一個(gè)線性變換(512,521),shape不變。
x = x.transpose(1, 2).contiguous() \
.view(nbatches, -1, self.h * self.d_k)
return self.linears[-1](x)
我們先看構(gòu)造函數(shù),這里 d_model(512) 是 Multi-Head 的輸出大小,因?yàn)橛?h(8)個(gè) head,因此每個(gè) head 的 d_k=512/8=64。接著我們構(gòu)造 4 個(gè) (d_model x d_model)的矩陣,后面我們會(huì)看到它的用處。最后是構(gòu)造一個(gè) Dropout 層。
然后我們來(lái)看 forward 方法。輸入的 mask 是 (batch, 1, time) 的,因?yàn)槊總€(gè) head的 mask 都是一樣的,所以先用 unsqueeze(1) 變成 (batch, 1, 1, time),mask 我們前面已經(jīng)詳細(xì)分析過(guò)了。
接下來(lái)是根據(jù)輸入 query,key 和 value 計(jì)算變換后的 Multi-Head 的 query,key和 value。這是通過(guò)下面的語(yǔ)句來(lái)實(shí)現(xiàn)的:
zip(self.linears, (query, key, value)) 是把(self.linears[0],self.linears[1],self.linears[2])和 (query, key, value) 放到一起然后遍歷。
我們只看一個(gè) self.linears[0](query)。根據(jù)構(gòu)造函數(shù)的定義self.linears[0] 是一個(gè) (512, 512) 的矩陣,而 query 是 (batch, time,512),相乘之后得到的新 query 還是 512(d_model) 維的向量,然后用 view 把它變成(batch, time, 8, 64)。然后 transponse 成 (batch, 8,time,64),這是 attention 函數(shù)要求的 shape。分別對(duì)應(yīng) 8 個(gè) Head,每個(gè) Head 的 Query 都是 64 維。
Key 和 Value 的運(yùn)算完全相同,因此我們也分別得到 8 個(gè) Head 的 64 維的 Key和 64 維的 Value。
接下來(lái)調(diào)用 attention 函數(shù),得到 x 和 self.attn。其中 x 的 shape 是 (batch, 8,time, 64),而 attn 是 (batch, 8, time, time)。
x.transpose(1, 2) 把 x 變成 (batch, time, 8, 64),然后把它 view 成 (batch, time,512),其實(shí)就是把最后 8 個(gè) 64 維的向量拼接成 512 的向量。
最后使用 self.linears[-1] 對(duì) x 進(jìn)行線性變換,因?yàn)?self.linears[-1] 是 (512, 512) 的,因此最終的輸出還是 (batch, time, 512)。
我們最初構(gòu)造了 4 個(gè) (512, 512) 的矩陣,前 3 個(gè)用于對(duì) query,key 和 value 進(jìn)行變換,而最后一個(gè)對(duì) 8 個(gè) head 拼接后的向量再做一次變換。
##MultiHeadedAttention 的應(yīng)用
在 Transformer 里,有 3 個(gè)地方用到了 MultiHeadedAttention:
Encoder 的 Self-Attention 層里,query,key 和 value 都是相同的值,來(lái)自下層
的輸入。Mask 都是 1(當(dāng)然 padding 的不算)。
Decoder 的 Self-Attention 層,query,key 和 value 都是相同的值,來(lái)自下層的輸入。但是 Mask 使得它不能訪問(wèn)未來(lái)的輸入。
Encoder-Decoder 的普通 Attention,query 來(lái)自下層的輸入,而 key 和 value 相同,是 Encoder 最后一層的輸出,而 Mask 都是 1。
##全連接 SubLayer
除了 Attention 這個(gè) SubLayer 之外,我們還有全連接的 SubLayer,每個(gè)時(shí)刻的全連接層是可以獨(dú)立并行計(jì)算的 (當(dāng)然參數(shù)是共享的)。全連接層有兩個(gè)線性變換以及它們之間的 ReLU 激活組成:
全連接層的輸入和輸出都是 d_model(512) 維的,中間隱單元的個(gè)數(shù)是 d_ff(2048)。代碼實(shí)現(xiàn)非常簡(jiǎn)單:
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))
在兩個(gè)線性變換之間除了 ReLu 還使用了一個(gè) Dropout。
##Embedding 和 Softmax
輸入的詞序列都是 ID 序列,我們需要 Embedding。源語(yǔ)言和目標(biāo)語(yǔ)言都需要Embedding,此外我們需要一個(gè)線性變換把隱變量變成輸出概率,這可以通過(guò)前面的類 Generator 來(lái)實(shí)現(xiàn)。我們這里實(shí)現(xiàn) Embedding:
代碼非常簡(jiǎn)單,唯一需要注意的就是 forward 處理使用 nn.Embedding 對(duì)輸入 x 進(jìn)行Embedding 之外,還除以了。
##Positional Encoding
位置編碼的公式為:
假設(shè)輸入是 ID 序列長(zhǎng)度為 10,如果輸入 Embedding 之后是 (10, 512),那么位置編碼的輸出也是 (10, 512)。上式中 pos 就是位置 (0-9),512 維的偶數(shù)維使用 sin 函數(shù),而奇數(shù)維使用 cos 函數(shù)。這種位置編碼的好處是:可以表示成 PEpos 的線性函數(shù),這樣網(wǎng)絡(luò)就能容易的學(xué)到相對(duì)位置的關(guān)系。代碼為:
代碼細(xì)節(jié)請(qǐng)讀者對(duì)照公式,這里值得注意的是調(diào)用了 Module.register_buffer 函數(shù)。這個(gè)函數(shù)的作用是創(chuàng)建一個(gè) buffer,比如這里把 pi 保存下來(lái)。
register_buffer通常用于保存一些模型參數(shù)之外的值,比如在 BatchNorm 中,我們需要保存 running_mean(Moving Average),它不是模型的參數(shù) (不用梯度下降),但是模型會(huì)修改它,而且在預(yù)測(cè)的時(shí)候也要使用它。這里也是類似的,pe 是一個(gè)提前計(jì)算好的常量,我們?cè)?forward 要用到它。我們?cè)跇?gòu)造函數(shù)里并沒(méi)有把 pe 保存到 self 里,但是在forward 的時(shí)候我們卻可以直接使用它 (self.pe)。如果我們保存 (序列化) 模型到磁盤(pán)的話,PyTorch 框架也會(huì)幫我們保存 buffer 里的數(shù)據(jù)到磁盤(pán),這樣反序列化的時(shí)候能恢復(fù)它們。
完整模型
構(gòu)造完整模型的函數(shù)代碼如下:
首先把 copy.deepcopy 命名為 c,這樣使下面的代碼簡(jiǎn)潔一點(diǎn)。然后構(gòu)造 MultiHeadedAttention,PositionwiseFeedForward 和 PositionalEncoding 對(duì)象。接著就是構(gòu)造 EncoderDecoder 對(duì)象。它需要 5 個(gè)參數(shù):Encoder、Decoder、src-embed、tgt-embed和 Generator。
我們首先后面三個(gè)簡(jiǎn)單的參數(shù),Generator 直接構(gòu)造就行了,它的作用是把模型的隱單元變成輸出詞的概率。而 src-embed 是一個(gè) Embeddings 層和一個(gè)位置編碼層c(position),tgt-embed 也是類似的。
最后我們來(lái)看 Decoder(Encoder 和 Decoder 類似的)。Decoder 由 N 個(gè) DecoderLayer 組成,而 DecoderLayer 需要傳入 self-attn, src-attn,全連接層和 Dropout。因?yàn)樗械?MultiHeadedAttention 都是一樣的,因此我們直接 deepcopy 就行;同理所有的 PositionwiseFeedForward 也是一樣的網(wǎng)絡(luò)結(jié)果,我們可以 deepcopy 而不要再構(gòu)造一個(gè)。
訓(xùn)練
在介紹訓(xùn)練代碼前我們介紹一些Batch類。
Batch構(gòu)造函數(shù)的輸入是src和trg,后者可以為None,因?yàn)樵兕A(yù)測(cè)的時(shí)候是沒(méi)有tgt的。我們用一個(gè)例子來(lái)說(shuō)明Batch的代碼,這是訓(xùn)練階段的一個(gè)Batch,src是(48,20),48是batch大小,而20是最長(zhǎng)的句子長(zhǎng)度,其它的不夠長(zhǎng)的都padding成20了。而trg是(48,25),表示翻譯后的最長(zhǎng)句子是25個(gè)詞,不足的也 padding過(guò)了。
我們首先看src_mask怎么得到,(src!=pad)把src中大于0的時(shí)刻置為1,這
樣表示它可以 attendto的范圍。然后 unsqueeze(-2)把把src_mask變成(48/batch,1,20/time)。它的用法參考前面的attention函數(shù)。
對(duì)于訓(xùn)練來(lái)說(shuō) (TeachingForcing模式),Decoder 有一個(gè)輸入和一個(gè)輸出。比如句子”
最終得到的 trg_mask 的 shape 是 (48/batch, 24, 24),表示 24 個(gè)時(shí)刻的 Mask矩陣,這是一個(gè)對(duì)角線以及之下都是1的矩陣,前面已經(jīng)介紹過(guò)了。
注意src_mask的shape是(batch,1,time),而trg_mask是(batch,time,time)。
因?yàn)閟rc_mask的每一個(gè)時(shí)刻都能attendto所有時(shí)刻(padding的除外),一次只需要一個(gè)向量就行了,而trg_mask需要一個(gè)矩陣。
訓(xùn)練的代碼就非常簡(jiǎn)單了,下面是訓(xùn)練一個(gè)Epoch的代碼:
它遍歷一個(gè) epoch 的數(shù)據(jù),然后調(diào)用 forward,接著用 loss_compute 函數(shù)計(jì)算梯度,更新參數(shù)并且返回 loss。這里的 loss_compute 是一個(gè)函數(shù),它的輸入是模型的預(yù)測(cè) out,真實(shí)的標(biāo)簽序列 batch.trg_y 和 batch 的詞個(gè)數(shù)。實(shí)際的實(shí)現(xiàn)是MultiGPULossCompute 類,這是一個(gè) callable。本來(lái)計(jì)算損失和更新參數(shù)比較簡(jiǎn)單,但是這里為了實(shí)現(xiàn)多 GPU 的訓(xùn)練,這個(gè)類就比較復(fù)雜了。
MultiGPULossCompute
這里介紹了 Transformer 最核心的算法,這個(gè)代碼還包含了 Label Smoothing,BPE等技巧,有興趣的讀者可以自行閱讀,本書(shū)就不介紹了。
Skip Thought Vector
簡(jiǎn)介
我們之前學(xué)習(xí)過(guò) word2vec,其中一種模型是 Skip-Gram 模型,根據(jù)中心詞預(yù)測(cè)周圍的 (context) 詞,這樣我們可以學(xué)到詞向量。那怎么學(xué)習(xí)到句子向量呢?一種很自然想法就是用一個(gè)句子預(yù)測(cè)它周圍的句子,這就是 Skip Thought Vector 的思路。它需要有連續(xù)語(yǔ)義相關(guān)性的句子,比如論文中使用的書(shū)籍。一本書(shū)由很多句子組成,前后的句子是有關(guān)聯(lián)的。那么我們?cè)趺从靡粋€(gè)句子預(yù)測(cè)另一個(gè)句子呢?這可以使用Encoder-Decoder,類似于機(jī)器翻譯。比如一本書(shū)里有 3 個(gè)句子”I got back home”、”I could see the cat on the steps”和”This was strange”。我們想用中間的句子”I could see the cat on the steps.” 來(lái)預(yù)測(cè)前后兩個(gè)句子。
如圖15.83所示,輸入是句子”I could see the cat on the steps.”,輸出是兩個(gè)句子”Igot back home.” 和”This was strange.”。
我們首先用一個(gè) Encoder(比如 LSTM 或者 GRU) 把輸入句子編碼成一個(gè)向量。而右邊是兩個(gè) Decoder(我們?nèi)蝿?wù)前后是不對(duì)稱的,因此用兩個(gè) Decoder)。因?yàn)槲覀儾恍枰A(yù)測(cè) (像機(jī)器翻譯那樣生成一個(gè)句子),所以我們只考慮 Decoder 的訓(xùn)練。Decoder 的輸入是”
經(jīng)過(guò)訓(xùn)練之后,我們就得到了一個(gè) Encoder(Decoder 不需要了)。給定一個(gè)新的句子,我們可以把它編碼成一個(gè)向量。這個(gè)向量可以用于下游 (down stream) 的任務(wù),比如情感分類,語(yǔ)義相似度計(jì)算等等。
數(shù)據(jù)集
和訓(xùn)練 Word2Vec 不同,Word2Vec 只需要提供句子,而 Skip Thought Vector 需要文章 (至少是段落)。論文使用的數(shù)據(jù)集是 BookCorpus(http://yknzhu.wixsite.com/mbweb),目前網(wǎng)站已經(jīng)不提供下載鏈接了!
BookCorpus 的統(tǒng)計(jì)信息如圖15.84所示,有一萬(wàn)多本書(shū),七千多萬(wàn)個(gè)句子。
模型
接下來(lái)我們介紹一些論文中使用的模型,注意這是 2015 年的論文,過(guò)去好幾年了,其實(shí)我們是可以使用更新的模型。但是基本的思想還是一樣的。Encoder 是一個(gè) GRU。假設(shè)句子,t 時(shí)刻的隱狀態(tài)是認(rèn)為編碼了字符串?的語(yǔ)義,因此可以看成對(duì)整個(gè)句子語(yǔ)義的編碼。t 時(shí)刻 GRU 的計(jì)算公式為:
這就是標(biāo)準(zhǔn)的 GRU,其中是的Embedding 向量,是重置 (reset) 門,是更新 (update) 門,⊙ 是 element-wise 的乘法。
Decoder 是一個(gè)神經(jīng)網(wǎng)絡(luò)語(yǔ)言模型。
和之前我們?cè)跈C(jī)器翻譯里介紹的稍微有一些區(qū)別。標(biāo)準(zhǔn) Encoder-Decoder 里Decoder 每個(gè)時(shí)刻的輸入是和,Decoder 的初始狀態(tài)設(shè)置為 Encoder 的輸出。而這里 Decodert 時(shí)刻的輸入除了和
,還有 Encoder 的輸出。
計(jì)算出 Decoder 每個(gè)時(shí)刻的隱狀態(tài)之后,我們?cè)谟靡粋€(gè)矩陣 V 把它投影到詞的空間,輸出的是預(yù)測(cè)每個(gè)詞的概率分布。注意:預(yù)測(cè)前一個(gè)句子和后一個(gè)句子是兩個(gè) GRU 模型,它們的參數(shù)是不共享的,但是投影矩陣 V 是共享的。當(dāng)然輸入到的 Embedding 矩陣也是共享的。和 Word2Vec 對(duì)比的話,V 是輸出向量 (矩陣) 而這個(gè) Embedding(這里沒(méi)有起名字) 是輸入向量 (矩陣)。
詞匯擴(kuò)展
這篇論文還有一個(gè)比較重要的方法就是詞匯擴(kuò)展。因?yàn)?BookCorpus 相對(duì)于訓(xùn)練Word2Vec 等的語(yǔ)料來(lái)說(shuō)還是太小,很多的詞都根本沒(méi)有在這個(gè)語(yǔ)料中出現(xiàn),因此直接使用的話效果肯定不好。
本文使用了詞匯擴(kuò)展的辦法。具體來(lái)說(shuō)我們可以先用海量的語(yǔ)料訓(xùn)練一個(gè)
Word2Vec,這樣可以把一個(gè)詞映射到一個(gè)語(yǔ)義空間,我們把這個(gè)向量叫作 Vw2v。而我們之前訓(xùn)練的得到的輸入向量也是把一個(gè)詞映射到另外一個(gè)語(yǔ)義空間,我們記作Vrnn。
我們假設(shè)它們之間存在一個(gè)線性變換 f :。這個(gè)線性變換的參數(shù)是矩陣 W,使得。那怎么求這個(gè)變換矩陣 W 呢?因?yàn)閮蓚€(gè)訓(xùn)練語(yǔ)料會(huì)有公共的詞 (通常訓(xùn)練 word2vec 的語(yǔ)料比 skip vector 大得多,從而詞也多得多)。因此我們可以用這些公共的詞來(lái)尋找 W。尋找的依據(jù)是:遍歷所有可能的 W,使得Wvw2v 和 vrnn 盡量接近。用數(shù)學(xué)語(yǔ)言描述就是:
訓(xùn)練細(xì)節(jié)
首先訓(xùn)練了單向的 GRU,向量的維度是 2400,我們把它叫作 uni-skip 向量。此外還訓(xùn)練了 bi-skip 向量,它是這樣得到的:首先訓(xùn)練 1200 維的 uni-skip,然后句子倒過(guò)來(lái),比如原來(lái)是”aa bb”、”cc dd” 和”ee ff”,我們是用”cc dd” 來(lái)預(yù)測(cè)”aa bb” 以及”eeff”,現(xiàn)在反過(guò)來(lái)變成”ff ee”、”dd cc” 和”bb aa”。這樣也可以訓(xùn)練一個(gè)模型,當(dāng)然也
就得到一個(gè) encoder(兩個(gè) decoder 不需要了),給定一個(gè)句子我們把它倒過(guò)來(lái)然后也編碼成 1200 為的向量,最后把這個(gè)兩個(gè) 1200 維的向量拼接成 2400 維的向量。模型訓(xùn)練完成之后還需要進(jìn)行詞匯擴(kuò)展。通過(guò) BookCorpus 學(xué)習(xí)到了 20,000 個(gè)詞,而 word2vec 共選擇了 930,911 詞,通過(guò)它們共同的詞學(xué)習(xí)出變換矩陣 W,從而使得我們的 Skip Thought Vector 可以處理 930,911 個(gè)詞。
實(shí)驗(yàn)
為了驗(yàn)證效果,本文把 Sentence Embedding 作為下游任務(wù)的輸入特征,任務(wù)包括分類 (情感分類),SNI(RTE) 等。前者的輸入是一個(gè)句子,而后者的輸入是兩個(gè)句子。
Semantic relatedness 任務(wù)
這里使用了 SICK(SemEval 2014 Task 1,給定兩個(gè)句子,輸出它們的語(yǔ)義相關(guān)性 1-5五個(gè)分類) 和 Microsoft Paraphrase Corpus(給定兩個(gè)句子,判斷它們是否一個(gè)意思/兩分類)。
它們的輸入是兩個(gè)句子,輸出是分類數(shù)。對(duì)于輸入的兩個(gè)句子,我們用 SkipThought Vector 把它們編碼成兩個(gè)向量 u 和 v,然后計(jì)算 u · v 與 |u ? v|,然后把它們拼接起來(lái),最后接一個(gè) logistic regression 層 (全連接加 softmax)。使用這么簡(jiǎn)單的分類模型的原因是想看看 Sentence Embedding 是否能夠?qū)W習(xí)到復(fù)雜的非線性的語(yǔ)義關(guān)系。使用結(jié)果如圖15.85所示??梢钥吹叫Ч€是非常不錯(cuò)的,和 (當(dāng)時(shí)) 最好的結(jié)果差別不大,而那些結(jié)果都是使用非常復(fù)雜的模型得到結(jié)果,而這里只使用了簡(jiǎn)單的邏輯回歸模型。
COCO 圖像檢索任務(wù)
這個(gè)任務(wù)的輸入是一幅圖片和一個(gè)句子,模型輸出的是它們的相關(guān)性 (句子是否描述了圖片的內(nèi)容)。句子我們可以用 Skip Thought Vector 編碼成一個(gè)向量;而圖片也可以用預(yù)訓(xùn)練的 CNN 編碼成一個(gè)向量。模型細(xì)節(jié)這里不再贅述了,最終的結(jié)果如圖15.86所示。
分類任務(wù)
這里比較了 5 個(gè)分類任務(wù):電影評(píng)論情感分類 (MR), 商品評(píng)論情感分類 (CR) [37],主觀/客觀分類 (SUBJ) [38], 意見(jiàn)分類 (MPQA) 和 TREC 問(wèn)題類型分類。結(jié)果如圖15.87所示。
ELMo
簡(jiǎn)介
ELMo 是 Embeddings from Language Models 的縮寫(xiě),意思就是語(yǔ)言模型得到的 (句子)Embedding。另外 Elmo 是美國(guó)兒童教育電視節(jié)目芝麻街 (Sesame Street) 里的小怪獸15.88的名字。原始論文叫做《Deep contextualized word representations》。
這篇論文的想法其實(shí)非常非常簡(jiǎn)單,但是取得了非常好的效果。它的思路是用深度的雙向 RNN(LSTM) 在大量未標(biāo)注數(shù)據(jù)上訓(xùn)練語(yǔ)言模型,如圖15.89所示。然后在實(shí)際的任務(wù)中,對(duì)于輸入的句子,我們使用這個(gè)語(yǔ)言模型來(lái)對(duì)它處理,得到輸出的向量,因此這可以看成是一種特征提取。但是和普通的 Word2Vec 或者 GloVe 的pretraining 不同,ELMo 得到的 Embedding 是有上下文的。比如我們使用 Word2Vec也可以得到詞”bank” 的 Embedding,我們可以認(rèn)為這個(gè) Embedding 包含了 bank 的語(yǔ)義。但是 bank 有很多意思,可以是銀行也可以是水邊,使用普通的 Word2Vec 作
為 Pretraining 的 Embedding,只能同時(shí)把這兩種語(yǔ)義都編碼進(jìn)向量里,然后靠后面的模型比如 RNN 來(lái)根據(jù)上下文選擇合適的語(yǔ)義——比如上下文有 money,那么它更可能是銀行;而如果上下文是 river,那么更可能是水邊的意思。但是 RNN 要學(xué)到這種上下文的關(guān)系,需要這個(gè)任務(wù)有大量相關(guān)的標(biāo)注數(shù)據(jù),這在很多時(shí)候是沒(méi)有的。而ELMo 的特征提取可以看成是上下文相關(guān)的,如果輸入句子有 money,那么它就 (或者我們期望) 應(yīng)該能知道 bank 更可能的語(yǔ)義,從而幫我們選擇更加合適的編碼。
無(wú)監(jiān)督的預(yù)訓(xùn)練
給定一個(gè)長(zhǎng)度為 N 的句子,假設(shè)為 t1, t2, ..., tN,語(yǔ)言模型會(huì)計(jì)算給定 t1, ..., tk?1 的條件下出現(xiàn) tk 的概率:
傳統(tǒng)的 N-gram 語(yǔ)言模型不能考慮很長(zhǎng)的歷史,因此現(xiàn)在的主流是使用多層雙向的RNN(LSTM/GRU) 來(lái)實(shí)現(xiàn)語(yǔ)言模型。在每個(gè)時(shí)刻 k,RNN 的第 j 層會(huì)輸出一個(gè)隱狀態(tài),其中 j = 1, 2, ..., L,L 是 RNN 的層數(shù)。最上層是,對(duì)它進(jìn)行 softmax之后就可以預(yù)測(cè)輸出詞的概率。類似的,我們可以用一個(gè)反向的 RNN 來(lái)計(jì)算概率:
通過(guò)這個(gè) RNN,我們可以得到。我們把這兩個(gè)方向的 RNN 合并起來(lái)就得到 Bi-LSTM。我們優(yōu)化的損失函數(shù)是兩個(gè) LSTM 的交叉熵加起來(lái)是最小的:
這兩個(gè) LSTM 有各自的參數(shù)和,但是 word embedding 參數(shù)和 softmax 參數(shù)是共享的。
ELMo
ELMo 會(huì)根據(jù)不同的任務(wù),把上面得到的雙向的 LSTM 的不同層的隱狀態(tài)組合起來(lái)。對(duì)于輸入的詞 tk,我們可以得到 2L+1 個(gè)向量,分別是=1, 2, ..., L},我們把它記作。其中是詞的 Embedding,它與上下文無(wú)關(guān),而其它的是把雙向的 LSTM 的輸出拼接起來(lái)的,它們與上下文相關(guān)的。
為了進(jìn)行下游 (downstream) 的特定任務(wù),我們會(huì)把不同層的隱狀態(tài)組合起來(lái),組合的參數(shù)是根據(jù)特定任務(wù)學(xué)習(xí)出來(lái)的,公式如下:
這里的是一個(gè)縮放因子,而用于把不同層的輸出加權(quán)組合出來(lái)。在實(shí)際的任務(wù)中,RNN 的參數(shù)都是固定的,可以調(diào)的參數(shù)只是
和。當(dāng)然這里 ELMo 只是一個(gè)特征提取,實(shí)際任務(wù)會(huì)再加上一些其它的網(wǎng)絡(luò)結(jié)構(gòu),那么那些參數(shù)也是一起調(diào)整的。
實(shí)驗(yàn)結(jié)果
圖15.90是 ELMo 在 SQuAD、SNLI 等常見(jiàn)任務(wù)上的效果,相對(duì)于 Baseline 系統(tǒng)都有不小的提高。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4331瀏覽量
62618 -
深度學(xué)習(xí)
+關(guān)注
關(guān)注
73文章
5503瀏覽量
121162 -
nlp
+關(guān)注
關(guān)注
1文章
488瀏覽量
22037
原文標(biāo)題:詳解谷歌最強(qiáng)NLP模型BERT(理論+實(shí)戰(zhàn))
文章出處:【微信號(hào):rgznai100,微信公眾號(hào):rgznai100】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論