[深度学习] 自然语言处理---Transformer实现(二)
目錄
Encoder-Decoder框架
一 整體架構
動態流程圖
二 Encoder
2.1 Encoder Layer和殘差網絡
Residual Connection
2.2 Attention
Self Attention
Multi-head Attention
2.3 Add & Norm
LayerNormalization 層歸一化
2.4 前饋網絡 Feed Forward Neural Network
2.5 詞向量
Positional Encoding 位置編碼
三 Decoder
3.1 Masked Mutil-head Attention
3.2 線性層和softmax
3.3 完整模型代碼
四、相關問題
4.1 Transformer為什么需要進行Multi-head Attention?
4.2 Transformer相比于RNN/LSTM,有什么優勢?為什么?
4.3 為什么說Transformer可以代替seq2seq?
4.4 Transformer如何并行化的?
4.5 訓練-模型的參數在哪里?
參考文獻
看完本文,你大概能夠:
- 掌握Encoder-Decoder框架
- 掌握殘差網絡
- 掌握BatchNormalization(批歸一化)和LayerNormalization(層歸一化)
- 掌握Position Embedding(位置編碼)
Notes: 本文代碼?參考哈弗大學的The Annotated Transformer
引入必要的庫
import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import math, copy, time from torch.autograd import Variable import matplotlib.pyplot as plt import seaborn# Seaborn作為一個帶著定制主題和高級界面控制的Matplotlib擴展包,能讓繪圖變得更輕松. seaborn.set_context(context="talk")模型結構--Encoder-Decoder框架
Encoder-Decoder是為seq2seq(序列到序列)量身打造的一個深度學習框架,在機器翻譯、機器問答等領域有著廣泛的應用。這是一個抽象的框架,由兩個組件:Encoder(編碼器)和Decoder(解碼器)組成。
- encoder將使用符號表示的輸入inputs序列,映射到一個連續表示的序列Z。
- decoder一次一個元素地生成符號輸出序列Y。
在每一步模型均為自動回歸(auto-regressive),即在生成下一個符號時將先前生成的符號作為附加輸入。
上述代碼呈現了一個標準的Encoder-Decoder框架。在實際應用中,編碼器和解碼器可以有多種組合,比如(RNN, RNN)、(CNN,RNN)等等,這就是傳統的seq2seq框架。后來引入了attention機制,上述框架也被稱為”分心模型“。為什么說他”分心“呢?因為對于解碼器來說,他在生成每一個單詞的時候,中間向量的每一個元素對當前生成詞的貢獻都是一樣的。Attention的思想則是對于當前生成的單詞,中間向量z的每個元素對其貢獻的重要程度不同,跟其強相關的賦予更大的權重,無關的則給一個很小的權重。
class Generator(nn.Module):'''定義標準的線性+softmax生成步驟這是在8. Embeddings和Softmax中'''def __init__(self, d_model, vocab):super(Generator, self).__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):return F.log_softmax(self.proj(x), dim=1)再通俗一點的圖,可能你在其他博客里看到的圖,如下所示,Transformer由六個編碼器和六個解碼器組成。
??
動態流程圖
舉個例子介紹下如何使用這個Transformer Seq2Seq做翻譯
- 首先,Transformer對原語言的句子進行編碼,得到memory。
- 第一次解碼時輸入只有一個<SOS>標志,表示句子的開始。
- 解碼器通過這個唯一的輸入得到的唯一的輸出,用于預測句子的第一個詞。
編碼器通過處理輸入序列開啟工作。頂端編碼器的輸出之后會變轉化為一個包含向量K(鍵向量)和V(值向量)的注意力向量集 ,這是并行化操作。這些向量將被每個解碼器用于自身的“編碼-解碼注意力層”,而這些層可以幫助解碼器關注輸入序列哪些位置合適:
在完成編碼階段后,則開始解碼階段。解碼階段的每個步驟都會輸出一個輸出序列(在這個例子里,是英語翻譯的句子)的元素。
接下來的步驟重復了這個過程,直到到達一個特殊的終止符號,它表示transformer的解碼器已經完成了它的輸出。每個步驟的輸出在下一個時間步被提供給底端解碼器,并且就像編碼器之前做的那樣,這些解碼器會輸出它們的解碼結果 。
第二次解碼,將第一次的輸出Append到輸入中,輸入就變成了<SOS>和句子的第一個詞(ground truth或上一步的預測),解碼生成的第二個輸出用于預測句子的第二個詞。以此類推(過程與Seq2Seq非常類似)
二 Encoder
Encoder由N=6個相同的layer組成,layer指的就是上圖左側的單元,最左邊有個“Nx”,這里是x6個。
每個Layer由兩個sub-layer組成:
- 第一部分是一個multi-head self-attention mechanism
- 第二部分是一個position-wise feed-forward network,是一個全連接層
其中每個sub-layer都加了residual connection和normalisation,因此可以將sub-layer的輸出表示為:
def clones(module, N):"Produce N identical layers."return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])class LayerNorm(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 = epsdef forward(self, x):mean = x.mean(-1, keepdim=True)std = x.std(-1, keepdim=True)return self.a_2 * (x - mean) / (std + self.eps) + self.b_2class Encoder(nn.Module):"Core encoder is a stack of N layers"def __init__(self, layer, N):super(Encoder, self).__init__()self.layers = clones(layer, N)self.norm = LayerNorm(layer.size)def forward(self, x, mask):"Pass the input (and mask) through each layer in turn."for layer in self.layers:x = layer(x, mask)return self.norm(x)以上便是Encoder的核心實現。它由N個encoderLayer組成。輸入一次通過每個encoderLayer,然后經過一個歸一化層。下面來看下EncoderLayer和LayerNorm是什么樣子。
我們在每兩個子層之間都使用了殘差連接(Residual Connection) 和歸一化
2.1 Encoder Layer和殘差網絡
class SublayerConnection(nn.Module):"""A residual connection followed by a layer norm.Note for code simplicity the norm is first as opposed to last."""def __init__(self, size, dropout):super(SublayerConnection, self).__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self, x, sublayer):"Apply residual connection to any sublayer with the same size."return x + self.dropout(sublayer(self.norm(x)))每層都有兩個子層組成。第一個子層實現了“多頭”的 Self-attention,第二個子層則是一個簡單的Position-wise的全連接前饋網絡。
class EncoderLayer(nn.Module):"Encoder is made up of self-attn and feed forward (defined below)"def __init__(self, size, self_attn, feed_forward, dropout):super(EncoderLayer, self).__init__()self.self_attn = self_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 2)self.size = sizedef 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)這里的代碼初看上去有點繞,不過沒關系。我們先看什么是殘差網絡(即代碼中的SublayerConnection)。其實非常簡單,就是在正常的前向傳播基礎上開一個綠色通道,這個通道里x可以無損通過。這樣做的好處不言而喻,避免了梯度消失(求導時多了一個常數項)。最終的輸出結果就等于綠色通道里的x加上sublayer層的前向傳播結果。注意,這里輸入進來的時候做了個norm歸一化,關于norm我們后面再說。
Residual Connection
殘差連接其實很簡單!給你看一張示意圖你就明白了:
假設網絡中某個層對輸入x作用后的輸出是F(x),那么增加residual connection之后,就變成了:F(x) + x,這個+x操作就是一個shortcut。那么殘差結構有什么好處呢?顯而易見:因為增加了一項x,那么該層網絡對x求偏導的時候,多了一個常數項1!所以在反向傳播過程中,梯度連乘,也不會造成梯度消失!
所以,代碼實現residual connection很非常簡單:
def residual(sublayer_fn, x):return sublayer_fn(x)+x文章開始的transformer架構圖中的Add & Norm中的Add也就是指的這個shortcut。
理解了殘差網絡,EncoderLayer的代碼就很好看懂了。sublayer有兩個,一個是多頭self-attention層,另一個是前饋網絡(feed_forward)。輸入x先進入多頭self-attention,用一個殘差網絡加成,接著通過前饋網絡, 再用一個殘差網絡加成。
讓我們從輸入x開始,再從頭理一遍這個過程:
- 輸入x
- x做一個層歸一化: x1 = norm(x)
- 進入多頭self-attention: x2 = self_attn(x1)
- 殘差加成:x3 = x + x2
- 再做個層歸一化:x4 = norm(x3)
- 經過前饋網絡: x5 = feed_forward(x4)
- 殘差加成: x6 = x3 + x5
- 輸出x6
以上就是一個Encoder組件所做的全部工作了。里面有兩點暫未說明,一個是多頭attention, 另一個是層歸一化。
2.2??Self Attention
這里使用的是點乘attention,而不是加性(additive)attention。但是再提一點,在encoder和decoder的自注意力中,attention層的輸入分為self_attn(x, x, x, mask)和self_attn(t, t, t, mask), 這里的x和t分別為source和target輸入。后面會看到,從encoder到decoder層的注意力輸入時attn(t, m, m), 這里的m是Encoder的輸出。
def attention(query, key, value, mask=None, dropout=None):#因子化的點乘Attention-矩陣形式#Query: 查詢 (batch_size, heads, max_seq_len, d_k)#Key: 鍵 (batch_size, heads, max_seq_len_d_k)#Value: 值 (batch_size, heads, max_seq_len, d_v)#d_v = d_k#Q=K=Vd_k = query.size(-1)# (batch_size, heads, max_seq_len, d_k) * (batch_size, heads, d_k, max_seq_len)# = (batch_size, heads, max_seq_len, max_seq_len)# 為了方便說明,只看矩陣的后兩維 (max_seq_len, max_seq_len), 即# How are you# How [[0.8, 0.2, 0.3]# are [0.2, 0.9, 0.6]# you [0.3, 0.6, 0.8]]# 矩陣中每個元素的含義是,他對其他單詞的貢獻(分數)# 例如,如果我們想得到所有單詞對單詞“How”的打分,取矩陣第一列[0.8, 0.2, 0.3], 然后做softmaxscores = torch.matmul(query, key.transpose(-2, -1)) \/ math.sqrt(d_k)# 對于padding部分,賦予一個極大的負數,softmax后該項的分數就接近0了,表示貢獻很小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)# 接著與Value做矩陣乘法 和V做點積:# (batch_size, heads, max_seq_len, max_seq_len) * (batch_size, heads, max_seq_len, d_k)# = (batch_size, heads, max_seq_len, d_k)context = torch.matmul(p_attn, value)return context, p_attn class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):"Take in model size and number of heads."super(MultiHeadedAttention, self).__init__()assert d_model % h == 0, "heads is not a multiple of the number of the in_features"# We assume d_v always equals d_kself.d_k = d_model // hself.h = hself.linears = clones(nn.Linear(d_model, d_model), 4)self.attn = Noneself.dropout = nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):#這里的query, key, value與attention函數中的含義有所不同,這里指的是原始的輸入.#對于Encoder的自注意力來說,輸入query=key=value=x#對于Decoder的自注意力來說,輸入query=key=value=t#對于Encoder和Decoder之間的注意力來說, 輸入query=t, key=value=m#其中m為Encoder的輸出,即給定target,通過key計算出m中每個輸出對當前target的分數,在乘上mif mask is not None:# Same mask applied to all h heads.mask = mask.unsqueeze(1)nbatches = query.size(0)# 1) Do all the linear projections in batch from d_model => h x d_kquery, 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) Apply attention on all the projected vectors in batch.## x: (batch_size, heads, max_seq_len, d_k)x, self.attn = attention(query, key, value, mask=mask,dropout=self.dropout)# 3) "Concat" using a view and apply a final linear.## x: (batch_size, max_seq_len, d_k*h)x = x.transpose(1, 2).contiguous() \.view(nbatches, -1, self.h * self.d_k)## output: (batch_size, max_seq_len, d_model)return self.linears[-1](x) def clones(module, N):"Produce N identical layers."return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])2.3 Add & Norm
在Transformer中,每一個子層(self-attetion,Feed Forward Neural Network)之后都會接一個殘缺模塊,并且有一個Layer normalization。
一個殘差網絡,將一層的輸入與其標準化后的輸出進行相加即可。Transformer中每一個Self Attention層與FFN層后面都會連一個Add & Norm層。
LayerNormalization 層歸一化
Normalization有很多種,但是它們都有一個共同的目的,那就是把輸入轉化成均值為0方差為1的數據。我們在把數據送入激活函數之前進行normalization(歸一化),因為我們不希望輸入數據落在激活函數的飽和區。
BN的主要思想就是:在每一層的每一批數據上進行歸一化。我們可能會對輸入數據進行歸一化,但是經過該網絡層的作用后,我們的數據已經不再是歸一化的了。隨著這種情況的發展,數據的偏差越來越大,我的反向傳播需要考慮到這些大的偏差,這就迫使我們只能使用較小的學習率來防止梯度消失或者梯度爆炸。BN的具體做法就是對每一小批數據,在批這個方向上做歸一化。
Layer normalization 它也是歸一化數據的一種方式,不過LN 是在每一個樣本上計算均值和方差,而不是BN那種在批方向計算均值和方差!公式如下:
class LayerNorm(nn.Module):"""實現LayerNorm。其實PyTorch已經實現啦,見nn.LayerNorm。"""def __init__(self, features, eps=1e-6):"""Args:features: 就是模型的維度。論文默認512eps: 一個很小的數,防止數值計算的除0錯誤"""super(LayerNorm, self).__init__()self.gamma = nn.Parameter(torch.ones(features))self.beta = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self, x):"""Args:x: 輸入序列張量,形狀為[B, L, D]"""# 在X的最后一個維度求均值,最后一個維度就是模型的維度mean = x.mean(-1, keepdim=True)# 在X的最后一個維度求方差,最后一個維度就是模型的維度std = x.std(-1, keepdim=True)return self.gamma * (x - mean) / (std + self.eps) + self.beta2.4 前饋網絡 Feed Forward Neural Network
我們需要一種方式,把 8 個矩陣降為 1 個,首先,我們把 8 個矩陣連在一起,這樣會得到一個大的矩陣,再隨機初始化一個矩陣和這個組合好的矩陣相乘,最后得到一個最終的矩陣。
Encoder中和Decoder中經過Attention之后輸出的n個向量(這里n是詞的個數)都分別的輸入到一個全連接層中,完成一個逐個位置的前饋網絡。
每個encoderLayer中,多頭attention后會接一個前饋網絡。這個前饋網絡其實是兩個全連接層,進行了如下操作:
論文提到,這個公式還可以用兩個核大小為1的一維卷積來解釋,卷積的輸入輸出都是dmodel=512dmodel=512 dmodel?=512,中間層的維度是dff=2048
class PositionwiseFeedForward(nn.Module):'''Implements FFN equation.d_model=512d_ff=2048'''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_1 = nn.Conv1d(in_features=d_model, out_features=d_ff, kenerl_size=1)self.w_2 = nn.Linear(d_ff, d_model)# self.w_2 = nn.Conv1d(in_features=d_ff, out_features=d_model, kenerl_size=1)self.dropout = nn.Dropout(dropout)def forward(self, x):return self.w_2(self.dropout(F.relu(self.w_1(x))))這兩層的作用等價于兩個 kenerl_size=1的一維卷積操作。
2.5 詞向量
這里就是普通的不能再普通的詞向量,將詞語變成d_model維的向量。Word embedding應該是老生常談了,它實際上就是一個二維浮點矩陣,里面的權重是可訓練參數,我們只需要把這個矩陣構建出來就完成了word embedding的工作。
class Embeddings(nn.Module):def __init__(self, d_model, vocab):super(Embeddings, self).__init__()self.lut = nn.Embedding(vocab, d_model)self.d_model = d_modeldef forward(self, x):return self.lut(x) * math.sqrt(self.d_model)上面vocab_size就是詞典的大小,embedding_size就是詞嵌入的維度大小,論文里面就是等于dmodel=512,所以word embedding矩陣就是一個vocab_size*embedding_size的二維張量
Positional Encoding 位置編碼
由于Transformer沒有用到CNN和RNN,因此,句子單詞之間的位置信息就沒有利用到。顯然,這些信息對于翻譯來說是非常有用的,同樣一句話,每個單詞的意思能夠準確的翻譯出來,但如果順序不對,表達出來的意思就截然不同了。舉個栗子感受一下,原句:”A man went through the Big Buddhist Temple“, 翻譯成:”人過大佛寺“和”寺佛大過人“,意思就完全不同了。
那么如何表達一個序列的位置信息呢?為了解決這個問題,Transformer提出了Positional Encoding的方案,就是給每個輸入的詞向量疊加一個固定的向量來表示它的位置。
文中使用的Positional Encoding如下:
對于某一個單詞來說,他的位置信息主要有兩個方面:一是絕對位置,二是相對位置。絕對位置決定了單詞在一個序列中的第幾個位置,相對位置決定了序列的流向。作者利用了正弦函數和余弦函數來進行位置編碼:
其中pos是指當前詞在句子中的位置,i是指向量中每個值的index,可以看出,在偶數位置,使用正弦編碼,在奇數位置,使用余弦編碼。
最后把這個Positional Encoding與embedding的值相加,作為輸入送到下一層。
其中pos是詞在句子中的位置,i是詞向量中第i位,即將每個詞的詞向量為一行進行疊加,然后針對每一列都疊加上一個相位不同或波長逐漸增大的波,以此來唯一區分位置。
其中pos是單詞處于句子的第幾個位置。我們來考察一下第一個公式,看是否每個位置都能得到一個唯一的值作為編碼。為簡單起見,不妨令i=0,那么:
我們反過來想,假如存在位置j和k的編碼值相同,那么就有:
i,j 為非負整數且i不等于j, 以上兩式需要同時滿足,可等價為:
i,j為非負整數且i不等于j且k為整數
同時成立,這就意味著:
這顯然是不可能的,因為左邊是個無理數(無限不循環小數),而右邊是個有理數。通過反證法就證明了在這種表示下,每個位置確實有唯一的編碼。
上面的討論并未考慮i的作用。i決定了頻率的大小,不同的i可以看成是不同的頻率空間中的編碼,是相互正交的,通過改變i的值,就能得到多維度的編碼,類似于詞向量的維度。這里2i<=512(d_model), 一共512維。想象一下,當2i大于d_model時會出現什么情況,這時sin函數的周期會變得非常大,函數值會非常接近于0,這顯然不是我們希望看到的,因為這樣和詞向量就不在一個量級了,位置編碼的作用被削弱了。另外,值得注意的是,位置編碼是不參與訓練的,而詞向量是參與訓練的。作者通過實驗發現,位置編碼參與訓練與否對最終的結果并無影響。
class PositionalEncoding(nn.Module):"Implement the PE function."def __init__(self, d_model, dropout, max_len=5000):"""初始化。Args:d_model: 一個標量。模型的維度,論文默認是512max_seq_len: 一個標量。文本序列的最大長度"""super(PositionalEncoding, self).__init__()self.dropout = nn.Dropout(p=dropout)# Compute the positional encodings once in log space.pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2) *-(math.log(10000.0) / d_model))pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0)self.register_buffer('pe', pe)def forward(self, x):x = x + Variable(self.pe[:, :x.size(1)],requires_grad=False)return self.dropout(x)之所以對奇偶位置分別編碼,是因為編碼前一個位置是可以由另一個位置線性表示的(公差為1的等差數列),在編碼之后也希望能保留這種線性。我們以第1個位置和第k+1個位置為例,還是令i=0:
至此,我們就把Encoder部分的細節介紹完了,下面來看下Decoder部分
三 Decoder
Decoder和Encoder的結構差不多,但是多了一個attention的sub-layer,這里先明確一下decoder的輸入輸出和解碼過程:
- 輸出:對應i位置的輸出詞的概率分布
- 輸入:encoder的輸出 & 對應i-1位置decoder的輸出。所以中間的attention不是self-attention,它的K,V來自encoder,Q來自上一位置decoder的輸出
- 解碼:這里要特別注意一下,編碼可以并行計算,一次性全部encoding出來,但解碼不是一次把所有序列解出來的,而是像rnn一樣一個一個解出來的,因為要用上一個位置的輸入當作attention的query
明確了解碼過程之后最上面的圖就很好懂了,這里主要的不同就是新加的另外要說一下新加的attention多加了一個mask,因為訓練時的output都是ground truth,這樣可以確保預測第i個位置時不會接觸到未來的信息。
每一個層包括以下3個部分:
- 第一個部分是multi-head self-attention mechanism
- 第二部分是multi-head context-attention mechanism
- 第三部分是一個position-wise feed-forward network
還是和encoder類似,上面三個部分的每一個部分,都有一個殘差連接,后接一個Layer Normalization。
但是,decoder出現了一個新的東西multi-head context-attention mechanism。這個東西其實也不復雜,理解了multi-head self-attention你就可以理解multi-head context-attention。
通過觀察上面的結構圖我們還可以發現Decoder與Encoder的另外一個不同,就是每個Decoder單元的輸入層,要先經過一個Masked Attention層。那么Masked的與普通版本的Attention有什么區別呢?
3.1 Masked Mutil-head Attention
mask 表示掩碼,它對某些值進行掩蓋,使其在參數更新時不產生效果。
Encoder因為要編碼整個句子,所以每個詞都需要考慮上下文的關系。所以每個詞在計算的過程中都是可以看到句子中所有的詞的。但是Decoder與Seq2Seq中的解碼器類似,每個詞都只能看到前面詞的狀態,所以是一個單向的Self-Attention結構。
Masked Attention的實現也非常簡單,只要在普通的Self Attention的Softmax步驟之前,與(&)上一個下三角矩陣M就好了
需要說明的是,我們的Transformer模型里面涉及兩種mask。分別是padding mask和sequence mask。其中后者我們已經在decoder的self-attention里面見過啦!其中,padding mask在所有的scaled dot-product attention里面都需要用到,而sequence mask只有在decoder的self-attention里面用到。
所以,我們之前ScaledDotProductAttention的forward方法里面的參數attn_mask在不同的地方會有不同的含義。
padding mask
什么是padding mask呢?回想一下,我們的每個批次輸入序列長度是不一樣的!也就是說,我們要對輸入序列進行對齊!具體來說,就是給在較短的序列后面填充0。但是如果輸入的序列太長,則是截取左邊的內容,把多余的直接舍棄。因為這些填充的位置,其實是沒什么意義的,所以我們的attention機制不應該把注意力放在這些位置上,所以我們需要進行一些處理。
具體的做法是,把這些位置的值加上一個非常大的負數(可以是負無窮),這樣的話,經過softmax,這些位置的概率就會接近0!
而我們的padding mask實際上是一個張量,每個值都是一個Boolen,值為False的地方就是我們要進行處理的地方。
下面是實現:
# 參考實現代碼 def padding_mask(seq_k, seq_q):# seq_k和seq_q的形狀都是[B,L]len_q = seq_q.size(1)# `PAD` is 0pad_mask = seq_k.eq(0)pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1) # shape [B, L_q, L_k]return pad_maskSequence mask
文章前面也提到,sequence mask是為了使得decoder不能看見未來的信息。也就是對于一個序列,在time_step為t的時刻,我們的解碼輸出應該只能依賴于t時刻之前的輸出,而不能依賴t之后的輸出。因此我們需要想一個辦法,把t之后的信息給隱藏起來。
那么具體怎么做呢?也很簡單:產生一個上三角矩陣,上三角的值全為1,下三角的值權威0,對角線也是0。把這個矩陣作用在每一個序列上,就可以達到我們的目的啦。
具體的代碼實現如下:
#參考實現代碼 def sequence_mask(seq):batch_size, seq_len = seq.size()mask = torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),diagonal=1)mask = mask.unsqueeze(0).expand(batch_size, -1, -1) # [B, L, L]return mask對于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同時需要padding mask 和 sequence mask 作為 attn_mask,具體實現就是兩個mask相加作為attn_mask。其他情況,attn_mask 一律等于 padding mask。
注意下attention當中的mask。我們之前提到,在三個地方用到了attention。在Encoder的自注意力機制中,mask是用來過濾padding部分的作用,對于source中的每一個詞來講,其他的詞對他都是可見的,都可以做出貢獻的。但是在Decoder中,mask的作用就有所不同了。這可能又要從Encoder-Decoder框架說起。在這個框架下,解碼器實際上可看成一個神經網絡語言模型,預測的時候,target中的每一個單詞是逐個生成的,當前詞的生成依賴兩方面:
- 一是Encoder的輸出.
- 二是target的前面的單詞.
例如,在生成第一個單詞是,不僅依賴于Encoder的輸出,還依賴于起始標志[CLS];生成第二個單詞是,不僅依賴Encoder的輸出,還依賴起始標志和第一個單詞……依此類推。這其實是說,在翻譯當前詞的時候,是看不到后面的要翻譯的詞。由上可以看出,這里的mask是動態的。
def subsequent_mask(size):"Mask out subsequent positions."# size: 序列長度attn_shape = (1, size, size)# 生成一個上三角矩陣subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')return torch.from_numpy(subsequent_mask) == 0下面詳細介紹下subsequent_mask是如何起作用的。函數的參數size指的是target句子的長度。以”[CLS] That is it“這個長度為4的target輸入為例,這個函數的輸出是什么呢?
print(subsequent_mask(size=4))tensor([[[1, 0, 0, 0],[1, 1, 0, 0],[1, 1, 1, 0],[1, 1, 1, 1]]], dtype=torch.uint8)可以看到,輸出為一個下三角矩陣,維度為(1,4,4)。現在我們再來看下attention函數,mask起作用的地方是在Query和Key點乘后,結果矩陣的維度為(batch_size, heads, max_seq_len, max_seq_len)。為方便起見,我們只看一條數據,即batch_size=1。進入多頭attention時,注意到對mask做了一步操作:
mask = mask.unsqueeze(1) mask: tensor([[[[1, 0, 0, 0],[1, 1, 0, 0],[1, 1, 1, 0],[1, 1, 1, 1]]]], dtype=torch.uint8)這時mask的維度變成了(1,1,4,4).
target:CLS That is itCLS [[[[0.8, 0.2, 0.3, 0.9]That [0.2, 0.9, 0.6, 0.4]is [0.3, 0.6, 0.8, 0.7]it [1.2, 0.6, 2.1, 3.2]]]]mask:[[[[1, 0, 0, 0],[1, 1, 0, 0],[1, 1, 1, 0],[1, 1, 1, 1]]]]寫成了上面的樣子,mask的作用就很顯然了。例如,對于”CLS“來說,預測它下一個詞時,只有”CLS“參與了attention,其他的詞(相對于CLS為未來的詞)都被mask_fill掉了,不起作用。后面的情況依此類推。
細心的小伙伴可能發現了,這里的解釋并沒有考慮padding部分。事實上,就算加了padding部分(為0),也不影響上述過程,有興趣的話可以在上面it后面加上個0,下面的矩陣加一列[0 0 0 0 ], 就可以一目了然。
class Decoder(nn.Module):"Generic N layer decoder with masking."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)每個組件長什么樣子呢?首先輸入經過詞向量和位置編碼,進入target的自注意力層,這里和Encoder一樣,也是用了殘差和層歸一化。然后呢,這個輸出再和Encoder的輸出做一次context attention,相當于把上面的那層重復了一次,唯一不同的是,這次的attention有點不一樣的,不再是自注意力,所有的技術細節都可以參照Encoder部分,這里不再復述。
class DecoderLayer(nn.Module):"Decoder is made of self-attn, src-attn, and feed forward (defined below)"def __init__(self, size, self_attn, src_attn, feed_forward, dropout):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 3)def forward(self, x, memory, src_mask, tgt_mask):m = memoryx = 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)3.2 線性層和softmax
這是整個模型的最后一步了。從Decoder拿到的輸出是維度為(batch_size, max_seq_len, d_model)的浮點型張量,我們希望得到最終每個單詞預測的結果,首先用一個線性層將d_model映射到vocab的維度,得到每個單詞的可能性,然后送入softmax,找到最可能的單詞。
線性層的參數個數為d_mode ?? vocab_size, 一般來說,vocab_size會比較大,拿20000為例,那么只這層的參數就有512?20000512?20000個,約為10的8次方,非常驚人。而在詞向量那一層,同樣也是這個數值,所以,一種比較好的做法是將這兩個全連接層的參數共享,會節省不少內存,而且效果也不會差。
class Generator(nn.Module):"Define standard linear + softmax generation step."def __init__(self, d_model, vocab):super(Generator, self).__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):return F.log_softmax(self.proj(x), dim=-1)3.3 完整模型代碼
def transformer_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):"Helper: Construct a model from hyperparameters."c = copy.deepcopyattn = MultiHeadedAttention(h, d_model)ff = PositionwiseFeedForward(d_model, d_ff, dropout)position = PositionalEncoding(d_model, dropout)model = EncoderDecoder(Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),nn.Sequential(Embeddings(d_model, src_vocab), c(position)),nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),Generator(d_model, tgt_vocab))# This was important from their code. # Initialize parameters with Glorot / fan_avg.for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform(p)return modelmodel = transformer_model(10, 10, 2)詳細例子解釋: Attention Is All You Need(注意力模型)
GitHub鏈接:https://github.com/harvardnlp/annotated-transformer
代碼解讀:Transformer解析與tensorflow代碼解讀
GitHub - Kyubyong/transformer: A TensorFlow Implementation of the Transformer: Attention Is All You Need
?
總結
以上是生活随笔為你收集整理的[深度学习] 自然语言处理---Transformer实现(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于php二次开发的疑点
- 下一篇: 三证合一后税务登记号怎么查 三证合一后在