聊聊 Transformer

Attention is all you need 是一篇將 Attention 思想發揮到極致的論文,出自 Google。這篇論文中提出一個全新的模型,叫 Transformer。
論文地址:https://arxiv.org/abs/1706.03762
接下來我想講講自己對Transformer模型的理解:
**

1.模型結構

**
Transformer結構圖
Transformer模型有傳統的seq2seq一樣,也是由encoder和decoder組成。
**

1.1 Encoder

**
encoder由 6 層相同的層組成,每一個大模塊分別由兩部分組成:

第一部分是 multi-head self-attention
第二部分是 position-wise feed-forward network,是一個全連接層
這個兩個sub_layer部分,都有一個殘差連接(residual connection),然後接着一個 Layer Normalization,故每一個sub_layer輸出可表示爲:
sub_layer表示

**注意:**Encoder端每個大模塊接收的輸入是不一樣的,第一個大模塊(最底下的那個)接收的輸入是輸入序列的embedding(embedding可以通過word2vec預訓練得來),其餘大模塊接收的是其前一個大模塊的輸出,最後一個模塊的輸出作爲整個Encoder端的輸出。

1.2 Decoder

**
和 encoder 類似,decoder 也是由6個相同的層組成,每一個大模塊包括以下3個部分:

第一個部分是 multi-head self-attention mechanism
第二部分是 multi-head context-attention mechanism
第三部分是一個 position-wise feed-forward network和 encoder 一樣,上面三個部分的每一個部分,都有一個殘差連接,後接一個 Layer Normalization。

decoder 和 encoder 不同的地方在 multi-head context-attention mechanism

同樣需要注意的是,Decoder端每個大模塊接收的輸入也是不一樣的,其中第一個大模塊(最底下的那個)訓練時和測試時的接收的輸入是不一樣的,並且每次訓練時接收的輸入也可能是不一樣的(也就是模型總覽圖示中的"shifted right"),其餘大模塊接收的是同樣是其前一個大模塊的輸出,最後一個模塊的輸出作爲整個Decoder端的輸出。
**
對於第一個大模塊,簡而言之,其訓練及測試時接收的輸入爲:
訓練的時候每次的輸入爲上次的輸入加上輸入序列向後移一位的ground truth(例如每向後移一位就是一個新的單詞,那麼則加上其對應的embedding),特別地,當decoder的time step爲1時(也就是第一次接收輸入),其輸入爲一個特殊的token,可能是目標序列開始的token(如),也可能是源序列結尾的token(如),也可能是其它視任務而定的輸入等等,不同源碼中可能有微小的差異,其目標則是預測下一個位置的單詞(token)是什麼,對應到time step爲1時,則是預測目標序列的第一個單詞(token)是什麼,以此類推;
這裏需要注意的是,在實際實現中可能不會這樣每次動態的輸入,而是一次性把目標序列的embedding通通輸入第一個大模塊中,然後在多頭attention模塊對序列進行mask即可(後邊說的sentence mask)
而在測試的時候,是先生成第一個位置的輸出,然後有了這個之後,第二次預測時,再將其加入輸入序列,以此類推直至預測結束。

2.Attention

**

  • scaled dot-product attention

Tansformer模型採用的Attention是scaled dot-product attention,原始論文的描述爲:

An attention function can be described as a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility of the query with the corresponding key.

通過 query 和 key 的相似性程度來確定 value 的權重分佈,即:
Transformer公式
caled dot-product attention 和 dot-product attention 唯一的區別就是,scaled dot-product attention 有一個縮放因子, 叫1dk\frac{1}{\sqrt d_k}dk{ d_k}表示 Key 的維度,默認用 64。

論文裏對於 dk{ d_k}的作用這麼來解釋:對於dk{ d_k}很大的時候,點積得到的結果維度很大,使得結果處於softmax函數梯度很小的區域。這時候除以一個縮放因子,可以一定程度上減緩這種情況。caled dot-product attention結構圖如下所示:
caled dot-product attention
現在來說下 K、Q、V 分別代表什麼:

在 encoder 的 self-attention 中,Q、K、V 都來自同一個地方,它們是上一層 encoder 的輸出。對於第一層 encoder,它們就是 word embedding 和 positional encoding 相加得到的輸入。
在 decoder 的 self-attention 中,Q、K、V 也是自於同一個地方,它們是上一層 decoder 的輸出。對於第一層 decoder,同樣也是 word embedding 和 positional encoding 相加得到的輸入。但是對於 decoder,我們不希望它能獲得下一個 time step (即將來的信息,不想讓他看到它要預測的信息),因此我們需要進行 sequence masking。
在 encoder-decoder attention 中,Q 來自於 decoder 的上一層的輸出,K 和 V 來自於 encoder 的輸出,K 和 V 是一樣的。
Q、K、V 的維度都是一樣的,分別用 dq{ d_q}dk{ d_k}dv{ d_v} 來表示
目前可能描述有有點抽象,不容易理解。結合一些應用來說,比如,如果是在自動問答任務中的話,Q 可以代表答案的詞向量序列,取 K = V 爲問題的詞向量序列,那麼輸出就是所謂的 Aligned Question Embedding。

其pytorch實現爲:

import torch
import torch.nn as nn
import torch.functional as F
import numpy as np

class ScaledDotProductAttention(nn.Module):
    """Scaled dot-product attention mechanism."""

    def __init__(self, attention_dropout=0.0):
        super(ScaledDotProductAttention, self).__init__()
        self.dropout = nn.Dropout(attention_dropout)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, q, k, v, scale=None, attn_mask=None):
        """
        前向傳播.
        Args:
        	q: Queries張量,形狀爲[B, L_q, D_q]
        	k: Keys張量,形狀爲[B, L_k, D_k]
        	v: Values張量,形狀爲[B, L_v, D_v],一般來說就是k
        	scale: 縮放因子,一個浮點標量
        	attn_mask: Masking張量,形狀爲[B, L_q, L_k]

        Returns:
        	上下文張量和attention張量
        """
        attention = torch.bmm(q, k.transpose(1, 2))
        if scale:
            attention = attention * scale
        if attn_mask:
            # 給需要 mask 的地方設置一個負無窮
            attention = attention.masked_fill_(attn_mask, -np.inf)
	# 計算softmax
        attention = self.softmax(attention)
	# 添加dropout
        attention = self.dropout(attention)
	# 和V做點積
        context = torch.bmm(attention, v)
        return context, attention

- Multi-head attention

Multi-Head Attention相當於多個不同的self-attention的集成,論文中h=8。論文提到,他們發現將 Q、K、V 通過一個線性映射之後,分成 h 份,對每一份進行 scaled dot-product attention 效果更好。然後,把各個部分的結果合併起來,再次經過線性映射,得到最終的輸出。Multi-Head Attention圖如下所示:
Multi-head Attention
論文中的具體實現爲:
multi-head Attention

**

3.Layer normalization

Normalization 有很多種,但是它們都有一個共同的目的,那就是把輸入轉化成均值爲 0 方差爲 1 的數據。我們在把數據送入激活函數之前進行 normalization(歸一化),因爲我們不希望輸入數據落在激活函數的飽和區。
說到 normalization,那就肯定得提到 Batch Normalization。

BN 的主要思想就是:在每一層的每一批數據上進行歸一化。我們可能會對輸入數據進行歸一化,但是經過該網絡層的作用後,我們的數據已經不再是歸一化的了。隨着這種情況的發展,數據的偏差越來越大,我的反向傳播需要考慮到這些大的偏差,這就迫使我們只能使用較小的學習率來防止梯度消失或者梯度爆炸。

BN 的具體做法就是對每一小批數據,在批這個方向上做歸一化。
什麼是 Layer normalization 呢?它也是歸一化數據的一種方式,不過 LN 是在每一個樣本上計算均值和方差,而不是 BN 那種在批方向計算均值和方差!
在這裏插入圖片描述
下面看一下 LN 的公式:
在這裏插入圖片描述
**

4.Mask

**
mask 表示掩碼,它對某些值進行掩蓋,使其在參數更新時不產生效果。Transformer 模型裏面涉及兩種 mask,分別是 padding mask 和 sequence mask。

其中,padding mask 在所有的 scaled dot-product attention 裏面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 裏面用到。

  • Padding Mask

每個批次輸入序列長度是不一樣的,要對輸入序列進行對齊。具體來說,就是給在較短的序列後面填充 0。因爲這些填充的位置,其實是沒什麼意義的,所以我們的 attention 機制不應該把注意力放在這些位置上,所以我們需要進行一些處理。

具體的做法是,把這些位置的值加上一個非常大的負數(負無窮),這樣的話,經過 softmax,這些位置的概率就會接近0!

def padding_mask(seq_k, seq_q):
    # seq_k 和 seq_q 的形狀都是 [B,L]
    len_q = seq_q.size(1)
    # `PAD` is 0
    pad_mask = seq_k.eq(0)
    pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1)  # shape [B, L_q, L_k]
    return pad_mask
  • Sequence mask

Decoder端多頭self-attention模塊與Encoder端的一致,但是需要注意的是Decoder端的多頭self-attention需要做mask,因爲它在預測時,是“看不到未來的序列的”,所以要將當前預測的單詞(token)及其之後的單詞(token)全部mask掉。也就是對於一個序列,在 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。

5.Positional Embedding

我們對每一個 word 進行 embedding 作爲 input 表達。但是還有問題,embedding 本身不包含在句子中的相對位置信息。

那 RNN 爲什麼在任何地方都可以對同一個 word 使用同樣的向量呢?因爲 RNN 是按順序對句子進行處理的,一次一個 word。但是在 Transformer 中,輸入句子的所有 word 是同時處理的,沒有考慮詞的排序和位置信息。

對此,Transformer 的作者提出了加入 ”positional encoding“ 的方法來解決這個問題。”positional encoding“ 使得 Transformer 可以衡量 word 位置有關的信息。論文中的具體實現爲:
在這裏插入圖片描述
pos 指的是這個 word 在這個句子中的位置
i指的是 embedding 維度。比如選擇 d_model=512,那麼i就從1數到512

爲什麼選擇 sin 和 cos ?positional encoding 的每一個維度都對應着一個正弦曲線,作者假設這樣可以讓模型相對輕鬆地通過對應位置來學習。

其具體實現爲:

class PositionalEncoding(nn.Module):
    
    def __init__(self, d_model, max_seq_len):
        """初始化。
        Args:
            d_model: 一個標量。模型的維度,論文默認是512
            max_seq_len: 一個標量。文本序列的最大長度
        """
        super(PositionalEncoding, self).__init__()
        
        # 根據論文給的公式,構造出PE矩陣
        position_encoding = np.array([
          [pos / np.power(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
          for pos in range(max_seq_len)])
        # 偶數列使用sin,奇數列使用cos
        position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
        position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

        # 在PE矩陣的第一行,加上一行全是0的向量,代表這`PAD`的positional encoding
        # 在word embedding中也經常會加上`UNK`,代表位置單詞的word embedding,兩者十分類似
        # 那麼爲什麼需要這個額外的PAD的編碼呢?很簡單,因爲文本序列的長度不一,我們需要對齊,
        # 短的序列我們使用0在結尾補全,我們也需要這些補全位置的編碼,也就是`PAD`對應的位置編碼
        pad_row = torch.zeros([1, d_model])
        position_encoding = torch.cat((pad_row, position_encoding))
        
        # 嵌入操作,+1是因爲增加了`PAD`這個補全位置的編碼,
        # Word embedding中如果詞典增加`UNK`,我們也需要+1。看吧,兩者十分相似
        self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
        self.position_encoding.weight = nn.Parameter(position_encoding,
                                                     requires_grad=False)
    def forward(self, input_len):
        """神經網絡的前向傳播。

        Args:
          input_len: 一個張量,形狀爲[BATCH_SIZE, 1]。每一個張量的值代表這一批文本序列中對應的長度。

        Returns:
          返回這一批序列的位置編碼,進行了對齊。
        """
        
        # 找出這一批序列的最大長度
        max_len = torch.max(input_len)
        tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
        # 對每一個序列的位置進行對齊,在原序列位置的後面補上0
        # 這裏range從1開始也是因爲要避開PAD(0)的位置
        input_pos = tensor(
          [list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
        return self.position_encoding(input_pos)

**

6.Position-wise Feed-Forward network

**
這是一個全連接網絡,包含兩個線性變換和一個非線性函數(實際上就是 ReLU)。公式如下:
在這裏插入圖片描述
具體實現爲:

class PositionalWiseFeedForward(nn.Module):

    def __init__(self, model_dim=512, ffn_dim=2048, dropout=0.0):
        super(PositionalWiseFeedForward, self).__init__()
        self.w1 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.w2 = nn.Conv1d(ffn_dim, model_dim, 1)
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, x):
        output = x.transpose(1, 2)
        output = self.w2(F.relu(self.w1(output)))
        output = self.dropout(output.transpose(1, 2))

        # add residual and norm layer
        output = self.layer_norm(x + output)
        return output

對於Transformer的一些常見問題總結:

1.Transformer爲什麼需要進行Multi-head Attention?這樣做有什麼好處?Multi-head Attention的計算過程?各方論文的觀點是什麼?

Multi-head Attention的原因是將模型分爲多個頭,形成多個子空間,可以讓模型去關注不同方面的信息,最後再將各個方面的信息綜合起來。其實直觀上也可以想到,如果自己設計這樣的一個模型,必然也不會只做一次attention,多次attention綜合的結果至少能夠起到增強模型的作用,也可以類比CNN中同時使用多個卷積核的作用,直觀上講,多頭的注意力有助於網絡捕捉到更豐富的特徵/信息

2.Transformer相比於RNN/LSTM,有什麼優勢?爲什麼?

(1).RNN系列的模型,並行計算能力很差

RNN系列的模型T時刻隱層狀態的計算,依賴兩個輸入,一個是T時刻的句子輸入單詞X_t,另一個是T−1時刻的隱層狀態的輸出,這是最能體現RNN本質特徵的一點,RNN的歷史信息是通過這個信息傳輸渠道往後傳輸的。而RNN並行計算的問題就出在這裏,因爲T時刻的計算依賴T−1時刻的隱層計算結果,而T−1時刻的計算依賴T-2T−2時刻的隱層計算結果,如此下去就形成了所謂的序列依賴關係。

(2).Transformer的特徵抽取能力比RNN系列的模型要好
相關對比實驗可見:
放棄幻想,全面擁抱Transformer:自然語言處理三大特徵抽取器(CNN/RNN/TF)比較

3.Transformer是如何訓練的?測試階段如何進行測試呢?
Transformer訓練過程與seq2seq類似,首先Encoder端得到輸入的encoding表示,並將其輸入到Decoder端做交互式attention,之後在Decoder端接收其相應的輸入,經過多頭self-attention模塊之後,結合Encoder端的輸出,再經過FFN,得到Decoder端的輸出之後,最後經過一個線性全連接層,就可以通過softmax來預測下一個單(token),然後根據softmax多分類的損失函數,將loss反向傳播即可,所以從整體上來說,Transformer訓練過程就相當於一個有監督的多分類問題。
需要注意的是,Encoder端可以並行計算,一次性將輸入序列全部encoding出來,但Decoder端不是一次性把所有單詞(token)預測出來的,而是像seq2seq一樣一個接着一個預測出來的。
而對於測試階段,其與訓練階段唯一不同的是Decoder端最底層的輸入,具體地前邊講過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章