原創(chuàng) | Attention is all you need 論文解析(附代碼)

作者:楊金珊 審校:陳之炎 本文約4300字,建議閱讀8分鐘 “Attention is all you need”一文在注意力機(jī)制的使用方面取得了很大的進(jìn)步,對Transformer模型做出了重大改進(jìn)。
目前NLP任務(wù)中的最著名模型(例如GPT-2或BERT),均由幾十個Transformer或它們的變體組成。
背景
減少順序算力是擴(kuò)展神經(jīng)網(wǎng)絡(luò)GPU、ByteNet和ConvS2S的基本目標(biāo),它們使用卷積神經(jīng)網(wǎng)絡(luò)作為基本構(gòu)建塊,并行計算所有輸入和輸出位置的隱含表示。在這些模型中,將來自兩個任意輸入或輸出位置的信號關(guān)聯(lián)起來,所需的操作數(shù)量隨著位置距離的增加而增加,對于ConvS2S來說,二者是線性增長的;對于ByteNet來說,二者是對數(shù)增長的。這使得學(xué)習(xí)遙遠(yuǎn)位置之間的依賴關(guān)系變得更加困難。在Transformer中,將操作數(shù)量減少到一個恒定數(shù)值,這是以降低有效分辨率為代價的,因為需要對注意力權(quán)重位置做平均,多頭注意力 (Multi-Head Attention)抵消了這一影響。
為什么需要transformer
在序列到序列的問題中,例如神經(jīng)機(jī)器翻譯,最初的建議是基于在編碼器-解碼器架構(gòu)中使用循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)。這一架構(gòu)在處理長序列時受到了很大的限制,當(dāng)新元素被合并到序列中時,它們保留來自第一個元素的信息的能力就喪失了。在編碼器中,每一步中的隱含狀態(tài)都與輸入句子中的某個單詞相關(guān)聯(lián),通常是最鄰近的那個單詞。因此,如果解碼器只訪問解碼器的最后一個隱含狀態(tài),它將丟失序列的第一個元素相關(guān)的信息。針對這一局限性,提出了注意力機(jī)制的概念。
與通常使用RNN時關(guān)注編碼器的最后狀態(tài)不同,在解碼器的每一步中我們都關(guān)注編碼器的所有狀態(tài),從而能夠訪問有關(guān)輸入序列中所有元素的信息。這就是注意力所做的,它從整個序列中提取信息,即過去所有編碼器狀態(tài)的加權(quán)和,解碼器為輸出的每個元素賦予輸入的某個元素更大的權(quán)重或重要性。從每一步中正確的輸入元素中學(xué)習(xí),以預(yù)測下一個輸出元素。
但是這種方法仍然有一個重要的限制,每個序列必須一次處理一個元素。編碼器和解碼器都必須等到t-1步驟完成后才能處理第t-1步驟。因此,在處理龐大的語料庫時,計算效率非常低。
什么是Transformer
Transformer是一種避免遞歸的模型架構(gòu),它完全依賴于注意力機(jī)制來繪制輸入和輸出之間的全局依賴關(guān)系。Transformer允許顯著的并行化……Transformer是第一個完全依靠自注意力來計算輸入和輸出的表示,而不使用序列對齊的RNN或卷積的傳導(dǎo)模型。

從圖1可以觀察到,左邊是一個編碼器模型,右邊是一個解碼器模型。兩者都包含一個重復(fù)N次的“一個注意力和一個前饋網(wǎng)絡(luò)”的核心塊。但為此,首先需要深入探討一個核心概念:自注意力機(jī)制。
Self-Attention基本操作
Self-attention是一個序列到序列的操作:一個向量序列進(jìn)去,一個向量序列出來。我們稱它們?yōu)檩斎胂蛄?/span>
,
,…,
和相應(yīng)的輸出向量
,
,…,
。這些向量的維數(shù)都是k。要產(chǎn)生輸出向量
,Self-attention操作只需對所有輸入向量取加權(quán)平均值,最簡單的選擇是點積。在我們的模型的Self-attention機(jī)制中,我們需要引入三個元素:查詢、值和鍵(Queries, Values and Keys)。
class SelfAttention(nn.Module):def __init__(self, embed_size, heads):super(SelfAttention,self).__init__()self.embed_size=embed_sizeself.heads=headsself.head_dim=embed_size//headsassert(self.head_dim*heads==embed_size),"Embed size needs to be div by heads"self.values=nn.Linear(self.head_dim, self.head_dim, bias=False)self.keys=nn.Linear(self.head_dim, self.head_dim, bias=False)self.queries=nn.Linear(self.head_dim, self.head_dim, bias=False)self.fc_out=nn.Linear(heads*self.head_dim, embed_size)def forward(self,values,keys,query,mask):N=query.shape[0]value_len,key_len,query_len=values.shape[1],keys.shape[1],query.shape[1]values=values.reshape(N,value_len,self.heads,self.head_dim)keys=keys.reshape(N,key_len,self.heads,self.head_dim)queries=query.reshape(N,query_len,self.heads,self.head_dim)values=self.values(values)keys=self.keys(keys)queries=self.queries(queries)energy=torch.einsum("nqhd,nkhd->nhqk",[queries,keys])if mask is not None:energy=energy.masked_fill(mask==0,float("-1e20"))attention=torch.softmax(energy/(self.embed_size**(1/2)),dim=3)out=torch.einsum("nhql,nlhd->nqhd",[attention,values]).reshape(N,query_len,self.heads*self.head_dim)out=self.fc_out(out)return out
Queries,Values和Keys
在自注意力機(jī)制中,通常輸入向量以三種不同的方式使用:查詢、鍵和值。在每個角色中,它將與其他向量進(jìn)行比較,以獲得自己的輸出
(Query),獲得第j個輸出
(Key),并在權(quán)重建立后計算每個輸出向量(Value)。為了得到這些,我們需要三個維數(shù)為k * k的權(quán)重矩陣,并為每個
計算三個線性變換:


圖2 查詢、值和鍵(Queries, Values and Keys)三元素
通常稱這三個矩陣為K、Q和V,這三個可學(xué)習(xí)權(quán)值層應(yīng)用于相同的編碼輸入。因此,由于這三個矩陣都來自相同的輸入,可以應(yīng)用輸入向量本身的注意力機(jī)制,即“Self-attention”。
TheScaledDot-ProductAttention(帶縮放的點積注意力)
輸入由維
的“查詢”和“鍵”以及維
的“值”值組成。我們用所有“鍵”計算“查詢”的點積,每個“鍵”除以
的平方根,應(yīng)用一個softmax函數(shù)來獲得值的權(quán)重。
使用Q, K和V矩陣來計算注意力分?jǐn)?shù)。分?jǐn)?shù)衡量的是對輸入序列的其他位置或單詞的關(guān)注程度。也就是說,查詢向量與要評分的單詞的鍵向量的點積。對于位置1,我們計算
和
的點積,然后是
、
2,
、
等等,…
接下來應(yīng)用“縮放”因子來獲得更穩(wěn)定的梯度。softmax函數(shù)在大的值下無法正常工作,會導(dǎo)致梯度消失和減慢學(xué)習(xí)速度[1]。在“softmax”之后,我們乘以“值”矩陣,保留想要關(guān)注的單詞的值,并最小化或刪除無關(guān)單詞的值(它在V矩陣中的值應(yīng)該非常小)。
這些操作的公式為:

Multi-head Attention(多頭注意力)
在前面的描述中,注意力分?jǐn)?shù)一次集中在整個句子上,即使兩個句子包含相同的單詞,但順序不同,也將產(chǎn)生相同的結(jié)果。相反,如果想關(guān)注單詞的不同部分,”self-attention”的辨別能力則比較大,通過組合幾個自注意力頭,將單詞向量分成固定數(shù)量(h,頭的數(shù)量)的塊,然后使用Q, K和V子矩陣將自注意力應(yīng)用到相應(yīng)的塊。

圖3 多頭注意力機(jī)制
由于下一層(前饋層)只需要一個矩陣,每個單詞的一個向量,所以“在計算每個頭部的點積之后,需要連接輸出矩陣,并將它們乘以一個附加的權(quán)重矩陣Wo”[2]。最后輸出的矩陣從所有的注意力頭部獲取信息。
PositionalEncoding(位置編碼)
前文已經(jīng)簡單地提到,由于網(wǎng)絡(luò)和self-attention機(jī)制是排列不變的,句子中單詞的順序是該模型中需要解決的問題。如果我們打亂輸入句子中的單詞,會得到相同的解。需要創(chuàng)建單詞在句子中位置的表示,并將其添加到單詞嵌入(embedding)中。
為此,我們在編碼器和解碼器棧底部的輸入嵌入中添加了“位置編碼”。位置編碼與嵌入具有相同的維數(shù),因此兩者可以求和,位置編碼有多種選擇。
應(yīng)用一個函數(shù)將句子中的位置映射為實值向量之后,網(wǎng)絡(luò)將學(xué)習(xí)如何使用這些信息。另一種方法是使用位置嵌入,類似于單詞嵌入,用向量對每個已知位置進(jìn)行編碼。“它需要訓(xùn)練循環(huán)中所有被接受的位置的句子,但位置編碼允許模型外推到比訓(xùn)練中遇到的序列長度更長的序列”,[1]。

TransformerBlock(Transformer代碼塊)
class TransformerBlock(nn.Module):def __init__(self, embed_size,heads,dropout,forward_expansion):super(TransformerBlock,self).__init__()self.attention=SelfAttention(embed_size,heads)self.norm1=nn.LayerNorm(embed_size)self.norm2=nn.LayerNorm(embed_size)self.feed_forward=nn.Sequential(nn.Linear(embed_size, forward_expansion*embed_size),nn.ReLU(),nn.Linear(forward_expansion*embed_size,embed_size))self.dropout=nn.Dropout(dropout)def forward(self,values,keys,query,mask):attention=self.attention(values,keys,query,mask)x=self.dropout(self.norm1(attention+query))forward=self.feed_forward(x)out=self.dropout(self.norm2(forward+x))return out
Theencoder(編碼器)
位置編碼:將位置編碼添加到輸入嵌入(將輸入單詞被轉(zhuǎn)換為嵌入向量)。
N=6個相同的層,包含兩個子層:一個多頭自注意力機(jī)制,和一個全連接的前饋網(wǎng)絡(luò)(兩個線性轉(zhuǎn)換與一個ReLU激活)。它按位置應(yīng)用于輸入,這意味著相同的神經(jīng)網(wǎng)絡(luò)會應(yīng)用于屬于句子序列的每一個“標(biāo)記”向量。

每個子層(注意和FC網(wǎng)絡(luò))周圍都有一個殘余連接,將該層的輸出與其輸入相加,然后進(jìn)行歸一化。
在每個殘余連接之前,應(yīng)用正則化:“對每個子層的輸出應(yīng)用dropout,然后將其添加到子層輸入并正則化。

圖4 編碼器結(jié)構(gòu)
class Encoder(nn.Module):def __init__(self,src_vocab_size,embed_size,num_layers,heads,device,forward_expansion,dropout,max_length):super(Encoder,self).__init__()self.embed_size=embed_sizeself.device=deviceself.word_embedding=nn.Embedding(src_vocab_size,embed_size)self.position_embedding=nn.Embedding(max_length,embed_size)self.layers=nn.ModuleList([TransformerBlock(embed_size,heads,dropout=dropout,forward_expansion=forward_expansion) for _ in range(num_layers)])self.dropout=nn.Dropout(dropout)def forward(self,x,mask):N,seq_length=x.shapepositions=torch.arange(0,seq_length).expand(N,seq_length).to(self.device)out=self.dropout(self.word_embedding(x)+self.position_embedding(positions))for layer in self.layers:out=layer(out,out,out,mask) #key,query,value all the samereturn out
DecoderBlock(解碼器代碼塊)
位置編碼:編碼器的編碼相類似。
N=6個相同的層,包含3個子層。第一,屏蔽多頭注意力或屏蔽因果注意力,以防止位置注意到后續(xù)位置。禁用點積注意力模塊的軟最大層,對應(yīng)的值設(shè)置為?∞。第二個組件或“編碼器-解碼器注意力”對解碼器的輸出執(zhí)行多頭注意力,“鍵”和“值”向量來自編碼器的輸出,但“查詢”來自前面的解碼器層,使得解碼器中的每個位置都能覆蓋輸入序列中的所有位置。,最后是完連接的網(wǎng)絡(luò)。
每個子層周圍的殘差連接和層歸一化,類似于編碼器。
然后重復(fù)在編碼器中執(zhí)行的相同殘差dropout。
class DecoderBlock(nn.Module):def __init__(self, embed_size,heads,dropout,forward_expansion,device):super(DecoderBlock,self).__init__()self.attention=SelfAttention(embed_size,heads)self.norm=nn.LayerNorm(embed_size)self.transformer_block=TransformerBlock(embed_size, heads, dropout, forward_expansion)self.dropout=nn.Dropout(dropout)def forward(self,x,value,key,src_mask,trg_mask):#source mask and target maskattention=self.attention(x,x,x,trg_mask)#trg_mask is the mask mult-headed attention the first one in decoder blockquery=self.dropout(self.norm(attention+x))out=self.transformer_block(value,key,query,src_mask)return out
在N個堆疊的解碼器的最后,線性層,一個全連接的網(wǎng)絡(luò),將堆疊的輸出轉(zhuǎn)換為一個更大的向量,logits。

圖5 解碼器結(jié)構(gòu)
Joining all the pieces: the Transformer(全部拼接起來構(gòu)成Transformer)
定義并創(chuàng)建了編碼器、解碼器和linear-softmax最后一層等部件之后,便可以將這些部件連接起來,形成Transformer模型。
值得一提的是,創(chuàng)建了3個掩碼,包括:
編碼器掩碼:它是一個填充掩碼,從注意力計算中丟棄填充標(biāo)記。
解碼器掩碼1:該掩碼是填充掩碼和前向掩碼的結(jié)合,它將幫助因果注意力丟棄“未來”的標(biāo)記,我們?nèi)√畛溲诖a和前向掩碼之間的最大值。
解碼器掩碼2:為填充掩碼,應(yīng)用于編碼器-解碼器注意力層。
class Transformer(nn.Module):def __init__(self,src_vocab_size,trg_vocab_size,src_pad_idx,trg_pad_idx,embed_size=256,num_layers=6,forward_expansion=4,heads=8,dropout=0,device="cuda",max_length=100):super(Transformer,self).__init__()self.encoder=Encoder(src_vocab_size,embed_size,num_layers,heads,device,forward_expansion,dropout,max_length)self.decoder=Decoder(trg_vocab_size,embed_size,num_layers,heads,forward_expansion,dropout,device,max_length)self.src_pad_idx=src_pad_idxself.trg_pad_idx=trg_pad_idxself.device=devicedef make_src_mask(self,src):src_mask=(src!= self.src_pad_idx).unsqueeze(1).unsqueeze(2)#(N,1,1,src_len)return src_mask.to(self.device)def make_trg_mask(self,trg):N,trg_len=trg.shapetrg_mask=torch.tril(torch.ones((trg_len,trg_len))).expand(N,1,trg_len,trg_len)return trg_mask.to(self.device)def forward(self,src,trg):src_mask=self.make_src_mask(src)trg_mask=self.make_trg_mask(trg)enc_src=self.encoder(src,src_mask)out=self.decoder(trg, enc_src,src_mask, trg_mask)return out
數(shù)據(jù)派研究部介紹
數(shù)據(jù)派研究部成立于2017年初,以興趣為核心劃分多個組別,各組既遵循研究部整體的知識分享和實踐項目規(guī)劃,又各具特色:
算法模型組:積極組隊參加kaggle等比賽,原創(chuàng)手把手教系列文章;
調(diào)研分析組:通過專訪等方式調(diào)研大數(shù)據(jù)的應(yīng)用,探索數(shù)據(jù)產(chǎn)品之美;
系統(tǒng)平臺組:追蹤大數(shù)據(jù)&人工智能系統(tǒng)平臺技術(shù)前沿,對話專家;
自然語言處理組:重于實踐,積極參加比賽及策劃各類文本分析項目;
制造業(yè)大數(shù)據(jù)組:秉工業(yè)強(qiáng)國之夢,產(chǎn)學(xué)研政結(jié)合,挖掘數(shù)據(jù)價值;
數(shù)據(jù)可視化組:將信息與藝術(shù)融合,探索數(shù)據(jù)之美,學(xué)用可視化講故事;
網(wǎng)絡(luò)爬蟲組:爬取網(wǎng)絡(luò)信息,配合其他各組開發(fā)創(chuàng)意項目。
點擊文末“閱讀原文”,報名數(shù)據(jù)派研究部志愿者,總有一組適合你~
轉(zhuǎn)載須知
如需轉(zhuǎn)載,請在開篇顯著位置注明作者和出處(轉(zhuǎn)自:數(shù)據(jù)派THUID:DatapiTHU),并在文章結(jié)尾放置數(shù)據(jù)派醒目二維碼。有原創(chuàng)標(biāo)識文章,請發(fā)送【文章名稱-待授權(quán)公眾號名稱及ID】至聯(lián)系郵箱,申請白名單授權(quán)并按要求編輯。
未經(jīng)許可的轉(zhuǎn)載以及改編者,我們將依法追究其法律責(zé)任。
點擊“閱讀原文”加入組織~
