基于 transformer 的編碼器-解碼器模型是 表征學習 和 模型架構 這兩個領域多年研究成果的結晶。本文簡要介紹了神經編碼器-解碼器模型的歷史,更多背景知識,建議讀者閱讀由 Sebastion Ruder 撰寫的這篇精彩 博文。此外,建議讀者對 自注意力 (self-attention) 架構 有一個基本了解,可以閱讀 Jay Alammar 的 這篇博文 復習一下原始 transformer 模型。
本文分 4 個部分:
背景 - 簡要回顧了神經編碼器-解碼器模型的歷史,重點關注基于 RNN 的模型。
編碼器-解碼器 - 闡述基于 transformer 的編碼器-解碼器模型,并闡述如何使用該模型進行推理。
編碼器 - 闡述模型的編碼器部分。
解碼器 - 闡述模型的解碼器部分。
每個部分都建立在前一部分的基礎上,但也可以單獨閱讀。這篇分享是最后一部分 解碼器。
解碼器
如 編碼器-解碼器 部分所述, 基于 transformer 的解碼器定義了給定上下文編碼序列條件下目標序列的條件概率分布:
根據貝葉斯法則,在給定上下文編碼序列和每個目標變量的所有前驅目標向量的條件下,可將上述分布分解為每個目標向量的條件分布的乘積:
我們首先了解一下基于 transformer 的解碼器如何定義概率分布。基于 transformer 的解碼器由很多 解碼器模塊 堆疊而成,最后再加一個線性層 (即 “LM 頭”)。這些解碼器模塊的堆疊將上下文相關的編碼序列 和每個目標向量的前驅輸入 (這里 為 BOS) 映射為目標向量的編碼序列 。然后,“LM 頭”將目標向量的編碼序列 映射到 logit 向量序列 , 而每個 logit 向量 的維度即為詞表的詞匯量。這樣,對于每個 ,其在整個詞匯表上的概率分布可以通過對 取 softmax 獲得。公式如下:
“LM 頭” 即為詞嵌入矩陣的轉置, 即 。直觀上來講,這意味著對于所有 “LM 頭” 層會將 與詞匯表 中的所有詞嵌入一一比較,輸出的 logit 向量 即表示 與每個詞嵌入之間的相似度。Softmax 操作只是將相似度轉換為概率分布。對于每個 ,以下等式成立:
總結一下,為了對目標向量序列 的條件分布建模,先在目標向量 前面加上特殊的 向量 ( 即 ),并將其與上下文相關的編碼序列 一起映射到 logit 向量序列 。然后,使用 softmax 操作將每個 logit 目標向量 轉換為目標向量 的條件概率分布。最后,將所有目標向量的條件概率 相乘得到完整目標向量序列的條件概率:
與基于 transformer 的編碼器不同,在基于 transformer 的解碼器中,其輸出向量 應該能很好地表征 下一個 目標向量 (即 ),而不是輸入向量本身 (即 )。此外,輸出向量 應基于編碼器的整個輸出序列 。為了滿足這些要求,每個解碼器塊都包含一個 單向自注意層,緊接著是一個 交叉注意層,最后是兩個前饋層。單向自注意層將其每個輸入向量 僅與其前驅輸入向量 (其中 ,且 ) 相關聯,來模擬下一個目標向量的概率分布。交叉注意層將其每個輸入向量 與編碼器輸出的所有向量 相關聯,來根據編碼器輸入預測下一個目標向量的概率分布。
好,我們仍以英語到德語翻譯為例可視化一下 基于 transformer 的解碼器。
我們可以看到解碼器將 : “BOS”、“Ich”、“will”、“ein”、“Auto”、“kaufen” (圖中以淺紅色顯示) 和 “I”、“want”、“to”、“buy”、“a”、“car”、“EOS” ( 即 (圖中以深綠色顯示)) 映射到 logit 向量 (圖中以深紅色顯示)。
因此,對每個 使用 softmax 操作可以定義下列條件概率分布:
總條件概率如下:
其可表示為以下乘積形式:
圖右側的紅框顯示了前三個目標向量 、、 在一個解碼器模塊中的行為。下半部分說明了單向自注意機制,中間說明了交叉注意機制。我們首先關注單向自注意力。
與雙向自注意一樣,在單向自注意中, query 向量 (如下圖紫色所示), key 向量 (如下圖橙色所示),和 value 向量 (如下圖藍色所示) 均由輸入向量 (如下圖淺紅色所示) 映射而來。然而,在單向自注意力中,每個 query 向量 僅 與當前及之前的 key 向量進行比較 (即 ) 并生成各自的 注意力權重 。這可以防止輸出向量 (如下圖深紅色所示) 包含未來向量 (,其中 且 ) 的任何信息 。與雙向自注意力的情況一樣,得到的注意力權重會乘以它們各自的 value 向量并加權求和。
我們將單向自注意力總結如下:
請注意, key 和 value 向量的索引范圍都是 而不是 , 是雙向自注意力中 key 向量的索引范圍。
下圖顯示了上例中輸入向量 的單向自注意力。
可以看出 只依賴于 和 。因此,單詞 “Ich” 的向量表征 ( 即 ) 僅與其自身及 “BOS” 目標向量 ( 即 ) 相關聯,而 不 與 “will” 的向量表征 ( 即 ) 相關聯。
那么,為什么解碼器使用單向自注意力而不是雙向自注意力這件事很重要呢?如前所述,基于 transformer 的解碼器定義了從輸入向量序列 到其 下一個 解碼器輸入的 logit 向量的映射,即 。舉個例子,輸入向量 = “Ich” 會映射到 logit 向量 ,并用于預測下一個輸入向量 。因此,如果 可以獲取后續輸入向量 的信息,解碼器將會簡單地復制向量 “will” 的向量表征 ( 即 ) 作為其輸出 ,并就這樣一直傳播到最后一層,所以最終的輸出向量 基本上就只對應于 的向量表征,并沒有起到預測的作用。
這顯然是不對的,因為這樣的話,基于 transformer 的解碼器永遠不會學到在給定所有前驅詞的情況下預測下一個詞,而只是對所有 ,通過網絡將目標向量 復制到 。以下一個目標變量本身為條件去定義下一個目標向量,即從 中預測 , 顯然是不對的。因此,單向自注意力架構允許我們定義一個 因果的 概率分布,這對有效建模下一個目標向量的條件分布而言是必要的。
太棒了!現在我們可以轉到連接編碼器和解碼器的層 - 交叉注意力 機制!
交叉注意層將兩個向量序列作為輸入: 單向自注意層的輸出 和編碼器的輸出 。與自注意力層一樣, query 向量 是上一層輸出向量 的投影。而 key 和 value 向量 、 是編碼器輸出向量 的投影。定義完 key 、value 和 query 向量后,將 query 向量 與 所有 key 向量進行比較,并用各自的得分對相應的 value 向量進行加權求和。這個過程與 雙向 自注意力對所有 求 是一樣的。交叉注意力可以概括如下:
注意,key 和 value 向量的索引范圍是 ,對應于編碼器輸入向量的數目。
我們用上例中輸入向量 來圖解一下交叉注意力機制。
我們可以看到 query 向量 (紫色)源自 (紅色),因此其依賴于單詞 "Ich" 的向量表征。然后將 query 向量 與對應的 key 向量 (黃色)進行比較,這里的 key 向量對應于編碼器對其輸入 = "I want to buy a car EOS" 的上下文相關向量表征。這將 "Ich" 的向量表征與所有編碼器輸入向量直接關聯起來。最后,將注意力權重乘以 value 向量 (青綠色)并加上輸入向量 最終得到輸出向量 (深紅色)。
所以,直觀而言,到底發生了什么?每個輸出向量 是由所有從編碼器來的 value 向量( )的加權和與輸入向量本身 相加而得(參見上圖所示的公式)。其關鍵思想是: 來自解碼器的 的 query 投影與 來自編碼器的 越相關,其對應的 對輸出的影響越大。
酷!現在我們可以看到這種架構的每個輸出向量 取決于其來自編碼器的輸入向量 及其自身的輸入向量 。這里有一個重要的點,在該架構中,雖然輸出向量 依賴來自編碼器的輸入向量 ,但其完全獨立于該向量的數量 。所有生成 key 向量 和 value 向量 的投影矩陣 和 都是與 無關的,所有 共享同一個投影矩陣。且對每個 ,所有 value 向量 被加權求和至一個向量。至此,關于為什么基于 transformer 的解碼器沒有遠程依賴問題而基于 RNN 的解碼器有這一問題的答案已經很顯然了。因為每個解碼器 logit 向量 直接 依賴于每個編碼后的輸出向量,因此比較第一個編碼輸出向量和最后一個解碼器 logit 向量只需一次操作,而不像 RNN 需要很多次。
總而言之,單向自注意力層負責基于當前及之前的所有解碼器輸入向量建模每個輸出向量,而交叉注意力層則負責進一步基于編碼器的所有輸入向量建模每個輸出向量。
為了驗證我們對該理論的理解,我們繼續上面編碼器部分的代碼,完成解碼器部分。
詞嵌入矩陣 為每個輸入詞提供唯一的 上下文無關 向量表示。這個矩陣通常也被用作 “LM 頭”,此時 “LM 頭”可以很好地完成“編碼向量到 logit” 的映射。
與編碼器部分一樣,本文不會詳細解釋前饋層在基于 transformer 的模型中的作用。Yun 等 (2017) 的工作認為前饋層對于將每個上下文相關向量 映射到所需的輸出空間至關重要,僅靠自注意力層無法完成。這里應該注意,每個輸出詞元 對應的前饋層是相同的。有關更多詳細信息,建議讀者閱讀論文。
fromtransformersimportMarianMTModel,MarianTokenizer importtorch tokenizer=MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de") model=MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de") embeddings=model.get_input_embeddings() #createtokenidsforencoderinput input_ids=tokenizer("Iwanttobuyacar",return_tensors="pt").input_ids #passinputtokenidstoencoder encoder_output_vectors=model.base_model.encoder(input_ids,return_dict=True).last_hidden_state #createtokenidsfordecoderinput decoder_input_ids=tokenizer("Ichwillein",return_tensors="pt",add_special_tokens=False).input_ids #passdecoderinputidsandencodedinputvectorstodecoder decoder_output_vectors=model.base_model.decoder(decoder_input_ids,encoder_hidden_states=encoder_output_vectors).last_hidden_state #deriveembeddingsbymultiplyingdecoderoutputswithembeddingweights lm_logits=torch.nn.functional.linear(decoder_output_vectors,embeddings.weight,bias=model.final_logits_bias) #changethedecoderinputslightly decoder_input_ids_perturbed=tokenizer(" Ichwilldas",return_tensors="pt",add_special_tokens=False).input_ids decoder_output_vectors_perturbed=model.base_model.decoder(decoder_input_ids_perturbed,encoder_hidden_states=encoder_output_vectors).last_hidden_state lm_logits_perturbed=torch.nn.functional.linear(decoder_output_vectors_perturbed,embeddings.weight,bias=model.final_logits_bias) #compareshapeandencodingoffirstvector print(f"Shapeofdecoderinputvectors{embeddings(decoder_input_ids).shape}.Shapeofdecoderlogits{lm_logits.shape}") #comparevaluesofwordembeddingof"I"forinput_idsandperturbedinput_ids print("Isencodingfor`Ich`equaltoitsperturbedversion?:",torch.allclose(lm_logits[0,0],lm_logits_perturbed[0,0],atol=1e-3))
輸出:
Shapeofdecoderinputvectorstorch.Size([1,5,512]).Shapeofdecoderlogitstorch.Size([1,5,58101]) Isencodingfor`Ich`equaltoitsperturbedversion?:True
我們首先比較解碼器詞嵌入層的輸出維度 embeddings(decoder_input_ids) (對應于 ,這里
正如預期的那樣,解碼器輸入詞嵌入和 lm_logits 的輸出, 即 和 的最后一個維度不同。雖然序列長度相同 (=5),但解碼器輸入詞嵌入的維度對應于 model.config.hidden_size,而 lm_logit 的維數對應于詞匯表大小 model.config.vocab_size。其次,可以注意到,當將最后一個單詞從 “ein” 變為 “das”, 的輸出向量的值不變。鑒于我們已經理解了單向自注意力,這就不足為奇了。
最后一點, 自回歸 模型,如 GPT2,與刪除了交叉注意力層的 基于 transformer 的解碼器模型架構是相同的,因為純自回歸模型不依賴任何編碼器的輸出。因此,自回歸模型本質上與 自編碼 模型相同,只是用單向注意力代替了雙向注意力。這些模型還可以在大量開放域文本數據上進行預訓練,以在自然語言生成 (NLG) 任務中表現出令人印象深刻的性能。在 Radford 等 (2019) 的工作中,作者表明預訓練的 GPT2 模型無需太多微調即可在多種 NLG 任務上取得達到 SOTA 或接近 SOTA 的結果。你可以在 此處 獲取所有 transformers 支持的 自回歸 模型的信息。
好了!至此,你應該已經很好地理解了 基于 transforemr 的編碼器-解碼器模型以及如何在 transformers 庫中使用它們。
非常感謝 Victor Sanh、Sasha Rush、Sam Shleifer、Oliver ?strand、Ted Moskovitz 和 Kristian Kyvik 提供的寶貴反饋。
附錄
如上所述,以下代碼片段展示了如何為 基于 transformer 的編碼器-解碼器模型編寫一個簡單的生成方法。在這里,我們使用 torch.argmax 實現了一個簡單的 貪心 解碼法來對目標向量進行采樣。
fromtransformersimportMarianMTModel,MarianTokenizer importtorch tokenizer=MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de") model=MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de") #createidsofencodedinputvectors input_ids=tokenizer("Iwanttobuyacar",return_tensors="pt").input_ids #createBOStoken decoder_input_ids=tokenizer("",add_special_tokens=False,return_tensors="pt").input_ids assertdecoder_input_ids[0,0].item()==model.config.decoder_start_token_id,"`decoder_input_ids`shouldcorrespondto`model.config.decoder_start_token_id`" #STEP1 #passinput_idstoencoderandtodecoderandpassBOStokentodecodertoretrievefirstlogit outputs=model(input_ids,decoder_input_ids=decoder_input_ids,return_dict=True) #getencodedsequence encoded_sequence=(outputs.encoder_last_hidden_state,) #getlogits lm_logits=outputs.logits #samplelasttokenwithhighestprob next_decoder_input_ids=torch.argmax(lm_logits[:,-1:],axis=-1) #concat decoder_input_ids=torch.cat([decoder_input_ids,next_decoder_input_ids],axis=-1) #STEP2 #reuseencoded_inputsandpassBOS+"Ich"todecodertosecondlogit lm_logits=model(None,encoder_outputs=encoded_sequence,decoder_input_ids=decoder_input_ids,return_dict=True).logits #samplelasttokenwithhighestprobagain next_decoder_input_ids=torch.argmax(lm_logits[:,-1:],axis=-1) #concatagain decoder_input_ids=torch.cat([decoder_input_ids,next_decoder_input_ids],axis=-1) #STEP3 lm_logits=model(None,encoder_outputs=encoded_sequence,decoder_input_ids=decoder_input_ids,return_dict=True).logits next_decoder_input_ids=torch.argmax(lm_logits[:,-1:],axis=-1) decoder_input_ids=torch.cat([decoder_input_ids,next_decoder_input_ids],axis=-1) #let'sseewhatwehavegeneratedsofar! print(f"Generatedsofar:{tokenizer.decode(decoder_input_ids[0],skip_special_tokens=True)}") #Thiscanbewritteninaloopaswell.
輸出:
Generatedsofar:Ichwillein
在這個示例代碼中,我們準確地展示了正文中描述的內容。我們在輸入 “I want to buy a car” 前面加上 ,然后一起傳給編碼器-解碼器模型,并對第一個 logit (對應代碼中第一次出現 lm_logits 的部分) 進行采樣。這里,我們的采樣策略很簡單: 貪心地選擇概率最高的詞作為下一個解碼器輸入向量。然后,我們以自回歸方式將采樣得的解碼器輸入向量與先前的輸入一起傳遞給編碼器-解碼器模型并再次采樣。重復 3 次后,該模型生成了 “Ich will ein”。結果沒問題,開了個好頭。
責任編輯:彭菁
-
編碼器
+關注
關注
45文章
3638瀏覽量
134426 -
模型
+關注
關注
1文章
3226瀏覽量
48807 -
rnn
+關注
關注
0文章
89瀏覽量
6886
原文標題:解碼器 | 基于 Transformers 的編碼器-解碼器模型
文章出處:【微信號:zenRRan,微信公眾號:深度學習自然語言處理】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論