transformer first time

ref :https://jishuin.proginn.com/p/763bfbd37c6f

      https://zhuanlan.zhihu.com/p/46990010

      https://zhuanlan.zhihu.com/p/48508221

Transformer是Google在2017年提出的用於機器翻譯的模型:

Transformer內部本質上是一個Encoder-Decoder(編碼器-解碼器)結構:

 

Transformer中拋棄了傳統的CNN和RNN,整個網絡結構完全由Attention機制組成,並且採用了6層Encoder-Decoder結構:

 

很顯然了,Transformer只有分爲兩大部分:編碼器、解碼器。這裏只看其中一個Encoder-Decoder結構,剩餘5個一樣一樣的。

以一個簡單的例子進行說明:

輸入: Why do we work?(我們爲何工作? 後面以中文字符分析)

上圖左右兩個紅框,左側紅框是編碼器Encoder,右側紅框是解碼器Decoder。

編碼器負責將自然語言序列映射爲隱藏層(上圖第2步),即含有自然語言序列的數學表達

解碼器負責將隱藏層再映射爲自然語言序列,從而使我們可以解決各種問題,如情感分析、機器翻譯、摘要生成、語義關係抽取等

簡單說下,上圖每一步都做了什麼:

  輸入自然語言序列到編碼器:Why do we work?(爲啥要工作);

  編碼器輸出得到隱藏層,再將其輸入到解碼器;

  輸入<start>(起始)符號到解碼器;

  解碼器得到第一個字: “爲”;

  將解碼器得到的第一個字“爲”再次從上圖解碼器結構的下端輸入解碼器;

  解碼器得到第二個字:“什”;

  同樣將解碼器得到的第二個字“什”再次從上圖解碼器結構的下端輸入解碼器;

  如此,直到解碼器輸出<end>終止符,即序列生成完成。

解碼器和編碼器的結構類似,本文對編碼器部分進行講解:把自然語言序列映射爲隱藏層的數學表達的過程。

爲便於理解學習,將編碼器分爲4部分依次講解。

 

1. 位置嵌入

輸入維度爲[batch size, sequence length]的數據X,例如:我們爲什麼工作。

batch size就是一個batch的大小,這裏只有一句話,所以batch size爲1,sequence length是句子長度,共7個字,所以輸入數據維度是[1,7].

我們不能直接將這句話輸入到編碼器中,因爲transformer不認識,需要先進行字嵌入操作,得到上圖中的Xembedding.

簡單點說,就是文字-->字向量的轉換,這種轉換是將文字轉換爲計算機認識的數學表示,用到的方法就是Word2Vec,Word2Vec的具體細節可以暫時不用瞭解,知道要用到,先拿來用就好。

得到的Xembedding的維度是:[batch size, sequence length, embedding dimension]。enbedding dimension的大小由Word2Vec決定,transformer採用512長度的字向量。所以Xembedding的維度是[1, 7, 512]。

至此,輸入的“我們爲什麼工作”,可以用一個矩陣來表示:

 

我們知道,文字的先後順序,很重要。

比如喫飯沒沒喫飯沒飯喫飯喫沒飯沒喫,同樣三個字,順序顛倒,所表達的含義就不同了。

--->關於Position Embedding:

因爲前面說過模型本身並不包含RNN、CNN,因此無法捕捉到序列的順序/位置信息,例如將K, V(需要先驗知識:關於attention機制中Q K V三矩陣的常識)按行打亂,那麼attention之後的順序是一樣的。

但是序列信息又非常重要,代表全局結構,因此必須將序列的token相對或絕對position信息利用起來。

這裏每個token的position embedding 向量維度也是 [公式] 然後將原本的input embedding和position embedding加起來組成最終的embedding作爲encoder/decoder的輸入。其中position embedding計算公式如下:

其中 [公式] 表示位置index, [公式] 表示dimension index。

Position Embedding本身是一個絕對位置的信息,但在語言中,相對位置也很重要,Google選擇前述的位置向量公式的一個重要原因是:由於我們有:

這表明位置p+k的向量可以表示成位置p的向量的線性變換,這提供了表達相對位置信息的可能性。

在其他NLP論文中,大家也都看過position embedding,通常是一個訓練的向量,但是position embedding只是extra features,有該信息會更好,但是沒有性能也不會產生極大下降,因爲RNN、CNN本身就能夠捕捉到位置信息,但是在Transformer模型中,Position Embedding是位置信息的唯一來源,因此是該模型的核心成分,並非是輔助性質的特徵。

也可以採用訓練的position embedding,但是試驗結果表明相差不大,因此論文選擇了sin position embedding,因爲

  1. 這樣可以直接計算embedding而不需要訓練,減少了訓練參數
  2. 這樣允許模型將position embedding擴展到超過了training set中最長position的position,例如測試集中出現了更大的position,sin position embedding依然可以給出結果,但不存在訓練到的embedding。

--->繼續:

所以我們知道文字的位置信息很重要,Tranformer 沒有類似 RNN 的循環結構,沒有捕捉順序序列的能力。

爲了保留這種位置信息提供給 Tranformer 進行學習,我們需要用到位置嵌入。

加入位置信息的方式非常多,最簡單的可以是直接將絕對座標 0,1,2 編碼。

Tranformer 採用的是 sin-cos 規則,使用了 sin 和 cos 函數的線性變換給模型提供位置信息:

 

 

上式中,pos指的是句中字的位置,取值範圍是[0, 𝑚𝑎𝑥 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ),i指的是字嵌入的維度,取值範圍是[0, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛)。 dmodel就是 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 的大小。

上面有 sin 和 cos 的一組公式,也就是對應着 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 維度的一組奇數和偶數的序號的維度,從而產生不同的週期性變化。

可以通過代碼簡單看下效果:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math

def get_positional_encoding(max_seq_len, embed_dim):
    # 初始化一個positional encoding
    # embed_dim: 字嵌入的維度
    # max_seq_len: 最大的序列長度
    positional_encoding = np.array([
        [pos / np.power(10000, 2 * i / embed_dim) for i in range(embed_dim)]
        if pos != 0 else np.zeros(embed_dim) for pos in range(max_seq_len)])
    positional_encoding[1:, 0::2] = np.sin(positional_encoding[1:, 0::2])  # dim 2i 偶數
    positional_encoding[1:, 1::2] = np.cos(positional_encoding[1:, 1::2])  # dim 2i+1 奇數
    # 歸一化, 用位置嵌入的每一行除以它的模長
    # denominator = np.sqrt(np.sum(position_enc**2, axis=1, keepdims=True))
    # position_enc = position_enc / (denominator + 1e-8)
    return positional_encoding
    
positional_encoding = get_positional_encoding(max_seq_len=100, embed_dim=16)
plt.figure(figsize=(10,10))
sns.heatmap(positional_encoding)
plt.title("Sinusoidal Function")
plt.xlabel("hidden dimension")
plt.ylabel("sequence length")
plt.legend()
plt.show()

可以看到,位置嵌入在 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛 (也是hidden dimension )維度上隨着維度序號增大,週期變化會越來越慢,而產生一種包含位置信息的紋理。

 

 

 這樣,產生獨一的紋理位置信息,模型從而學到位置之間的依賴關係和自然語言的時序特性。最後,將  Xembedding 和 位置嵌入 相加,輸入給下一層。

2. 自注意力層(self attention mechanism)

 

 

 多頭的意義在於,QKT得到的矩陣就叫做注意力矩陣,它可以表示,每個字與其他字的相似程度。因爲向量點積值越大,表明兩個向量越接近。

 

 

 我們的目的是,讓每個字都含有當前這個句子中所有字的信息,用注意力層,我們做到了。

需要注意的是,在上面 𝑠𝑒𝑙𝑓 𝑎𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛 的計算過程中,我們通常使用 𝑚𝑖𝑛𝑖 𝑏𝑎𝑡𝑐ℎ,也就是一次計算多句話,上文舉例只用了一個句子。

每個句子的長度是不一樣的,需要按照最長的句子的長度統一處理。對於較短的句子,做padding操作,一般用0填充。

3. 殘差鏈接和層歸一化

加入了殘差設計和層歸一化操作,目的是爲了防止梯度消失,加快收斂。

1)殘差設計

我們在上一步得到了經過注意力矩陣加權之後的V,也就是Attention(Q, K, V),將其轉置,使其和Xembeding維度一致,也就是[𝑏𝑎𝑡𝑐ℎ 𝑠𝑖𝑧𝑒, 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛𝑔𝑡ℎ, 𝑒𝑚𝑏𝑒𝑑𝑑𝑖𝑛𝑔 𝑑𝑖𝑚𝑒𝑛𝑠𝑖𝑜𝑛] ,然後把他們加起來做殘差連接,直接元素相加,因爲他們的維度一致:

Xembeding + Attention(Q, K, V)

在之後的運算裏,每經過一個模塊的運算,都要把運算之前的值和運算之後的值相加,從而得到殘差連接,訓練的時候可以使梯度直接走捷徑反傳到最初始層:

 

 

2)層歸一化

作用是把神經網絡中隱藏層歸一爲標準正態分佈,也就是 𝑖.𝑖.𝑑 獨立同分布, 以起到加快訓練速度, 加速收斂的作用。

 

 

 

上式中以矩陣的行 (𝑟𝑜𝑤) 爲單位求均值:

上式中以矩陣的行 (𝑟𝑜𝑤) 爲單位求方差:

 

然後用每一行每一個元素減去這行的均值,再除以這行的標準差,從而得到歸一化後的數值,ε是爲了防止除0

之後引入兩個可訓練參數來α、β來彌補歸一化過程中損失掉的信息,注意表示元素相乘而不是點積,我們一般初始化α爲全1,β爲全0

代碼層面非常簡單,單頭 attention 操作如下:

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):
        # self.temperature是論文中的d_k ** 0.5,防止梯度過大
        # QxK/sqrt(dk)
        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))

        if mask is not None:
            # 屏蔽不想要的輸出
            attn = attn.masked_fill(mask == 0, -1e9)
        # softmax+dropout
        attn = self.dropout(F.softmax(attn, dim=-1))
        # 概率分佈xV
        output = torch.matmul(attn, v)

        return output, attn

Multi-Head Attention 實現在 ScaledDotProductAttention 基礎上構建:

class MultiHeadAttention(nn.Module):
    ''' Multi-Head Attention module '''

    # n_head頭的個數,默認是8
    # d_model編碼向量長度,例如本文說的512
    # d_k, d_v的值一般會設置爲 n_head * d_k=d_model,
    # 此時concat後正好和原始輸入一樣,當然不相同也可以,因爲後面有fc層
    # 相當於將可學習矩陣分成獨立的n_head份
    def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
        super().__init__()
        # 假設n_head=8,d_k=64
        self.n_head = n_head
        self.d_k = d_k
        self.d_v = d_v
        # d_model輸入向量,n_head * d_k輸出向量
        # 可學習W^Q,W^K,W^V矩陣參數初始化
        self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
        # 最後的輸出維度變換操作
        self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
        # 單頭自注意力
        self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
        self.dropout = nn.Dropout(dropout)
        # 層歸一化
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

    def forward(self, q, k, v, mask=None):
        # 假設qkv輸入是(b,100,512),100是訓練每個樣本最大單詞個數
        # 一般qkv相等,即自注意力
        residual = q
        # 將輸入x和可學習矩陣相乘,得到(b,100,512)輸出
        # 其中512的含義其實是8x64,8個head,每個head的可學習矩陣爲64維度
        # q的輸出是(b,100,8,64),kv也是一樣
        q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
        k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
        v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)

        # 變成(b,8,100,64),方便後面計算,也就是8個頭單獨計算
        q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)

        if mask is not None:
            mask = mask.unsqueeze(1)   # For head axis broadcasting.
        # 輸出q是(b,8,100,64),維持不變,內部計算流程是:
        # q*k轉置,除以d_k ** 0.5,輸出維度是b,8,100,100即單詞和單詞直接的相似性
        # 對最後一個維度進行softmax操作得到b,8,100,100
        # 最後乘上V,得到b,8,100,64輸出
        q, attn = self.attention(q, k, v, mask=mask)

        # b,100,8,64-->b,100,512
        q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
        q = self.dropout(self.fc(q))
        # 殘差計算
        q += residual
        # 層歸一化,在512維度計算均值和方差,進行層歸一化
        q = self.layer_norm(q)

        return q, attn

前饋網絡:

class PositionwiseFeedForward(nn.Module):
    ''' A two-feed-forward-layer module '''

    def __init__(self, d_in, d_hid, dropout=0.1):
        super().__init__()
        # 兩個fc層,對最後的512維度進行變換
        self.w_1 = nn.Linear(d_in, d_hid) # position-wise
        self.w_2 = nn.Linear(d_hid, d_in) # position-wise
        self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        residual = x

        x = self.w_2(F.relu(self.w_1(x)))
        x = self.dropout(x)
        x += residual

        x = self.layer_norm(x)

        return x

最後,回顧下 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 𝑒𝑛𝑐𝑜𝑑𝑒𝑟 的整體結構。

經過上文的梳理,我們已經基本瞭解了 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 編碼器的主要構成部分,我們下面用公式把一個 𝑡𝑟𝑎𝑛𝑠𝑓𝑜𝑟𝑚𝑒𝑟 𝑏𝑙𝑜𝑐𝑘 的計算過程整理一下:

1)字向量與位置編碼

2)自注意力機制

3)殘差連接與層歸一化

4)前向網絡

其實就是兩層線性激活函數,比如ReLU:

5)repeat " 3)"

至此,我們已經講完了 Transformer 編碼器的全部內容,知道了如何獲得自然語言的位置信息,注意力機制的工作原理等。

 

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