Transformer實戰

引言:Transformer自從2017年提出到現在,已經被廣泛應用於NLP各項任務中,尤其是NMT,取得的效果最明顯。前面講了Transformer模型,下面就來介紹一下Transformer實戰。已經有很多大神寫了Transformer的各種實現,本文參考的是哈佛大學2018年4月的一個實現版本。與以往純翻譯的博客不同,本文側重整體結構+細節分析,並附上了很多模型局部圖以及類的依賴關係圖,解釋的更加全面和詳細。

對於深度學習模型的學習,我個人採用的模式是 論文 + 圖 + 公式 + 代碼,四者互相參照,即可徹底搞清楚每個細節,以及每個模塊的交互方式。就像數學裏有句話,“數缺形時少直觀,形少數時難入微”,數就是代碼,形就是模型圖。

運行環境

  • Pytorch : 1.1.0
  • Python: 3.7

由於harvardnlp這個Transformer的實現版本是基於PyTorch0.3.0的,跑起來會有很多坑,我都列出來了(在遇到的問題標題下)。以下貼出來的代碼都是基於PyTorch 1.0及以上的版本。如果不想再踩一遍這些坑,就直接用我的代碼吧。

依賴的包

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.set_context(context="talk")
%matplotlib inline

模型架構

首先上一個Transformer的模型圖:

在這裏插入圖片描述

代碼整體架構圖:

在這裏插入圖片描述

Encoder-Decoder

由宏觀到微觀,整體上就是一個Encoder-Decoder的架構,兩個Embedding層。

class EncoderDecoder(nn.Module):
    """
        A standard Encoder-Decoder architecture. Base for this and many
        other models.
    """

    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator

    def forward(self, src, tgt, src_mask, tgt_mask):
        """Take in and process masked src and target sequences."""
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)

    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

以上就是Encoder-Decoder部分了,可以從上圖看到,模型最上面還有一個線性連接層和softmax層。

於是還需要一部分模塊完成概率歸一化輸出最終預測結果,命名爲Generator。

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):
        # 將最後一個維度進行softmax
        return F.log_softmax(self.proj(x), dim=-1)

Stacks

在Encoder和Decoder內部,都是用N=6個一模一樣的層堆疊而成,如圖:

在這裏插入圖片描述

Encoder

每個Encoder由6個相同結構的EncoderLayer組成。

def clones(module, N):
    """克隆出N個完全一樣的Module"""
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
class Encoder(nn.Module):
    """核心Encoder是由N層堆疊而成"""

    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        """
        將輸入x和掩碼mask逐層傳遞下去,
        最後再 LayerNorm 一下。
        """
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)
EncoderLayer

對於每個EncoderLayer來說,內部有兩個SublayerConnection。
EncoderLayer代碼,可以看到在forward方法裏,先進行了self-attention運算 ,再進行了feed-forwad,都使用的SublayerConnection模塊計算,只是函數F(x)F(x)不一樣而已。

class EncoderLayer(nn.Module):

    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)

    def forward(self, x, mask):
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)

在這裏插入圖片描述

SublayerConnection

SublayerConnection內部設計基於兩個核心:

    1. 殘差連接(Residual connection)
    1. 層級歸一化(Layer normalize)

這兩個點,分別對應Transformer模型圖裏的 AddNorm

殘差連接(Residual connection)

殘差連接詳細介紹,參考https://juejin.im/post/5b9f1af0e51d450e425eb32d#heading-10

如下圖所示,就是在原來正常的結構上添加一條“捷徑”x,這樣做的好處就是:反向傳播時,對x求偏導會多一個常數1,梯度連乘,不會造成梯度消失。

在這裏插入圖片描述

爲了適用這些殘差連接,所有的 子層(Sublayer)以及嵌入層(Embedding)的維度 dmodeld_{model}=512。

class SublayerConnection(nn.Module):
    """殘差連接"""

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))

層級歸一化(Layer normalize)

LayerNorm的詳細介紹 參考 https://juejin.im/post/5b9f1af0e51d450e425eb32d#heading-11。

模型裏這一層的作用可以概括爲:就是把輸入轉化成均值爲0方差爲1的數據。我們在把數據送入激活函數之前進行normalization(歸一化),因爲我們不希望輸入數據落在激活函數的飽和區。

公式表述就是:
LN(xi)=α×xiuLσL2+ϵ+β L N\left(x_{i}\right)=\alpha \times \frac{x_{i}-u_{L}}{\sqrt{\sigma_{L}^{2}+\epsilon}}+\beta

class LayerNorm(nn.Module):
    """構建一個 layernorm層,具體細節看論文 https://arxiv.org/abs/1607.06450"""

    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 = eps

    def 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_2

Decoder

Decoder也是由N=6個相同結構的DecoderLayer 組成。

Decoder構造函數和Encoder一樣,forward接收的參數比Encoder多了Encoder生成的memory以及目標句子的掩碼tgt_mask。大體計算邏輯和Encoder基本一致,只是參數多了兩個。

class Decoder(nn.Module):

    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)
DecoderLayer

再來看一看DecoderLayer,由3個SublayerConnection構成。比EncoderLayer在構造函數上多了一個src_attn(即源語言句子的attention)。可以看到在forward方法裏,先進行了self-attention運算 ,再進行了與Encoder的Attention進行運算,最後再feed-forwad。

class DecoderLayer(nn.Module):

    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        m = memory
        x = 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)

在這裏插入圖片描述

Mask

https://jalammar.github.io/images/t/transformer_decoding_2.gif
這個動圖上傳不上來,只能補一個鏈接了。

從以上動圖可以看出,Decoder是自迴歸(step by step)的方式生成目標句子的。對於神經機器翻譯(NMT)訓練來說,採用的監督學習,也就意味着原句子和目標句子都是餵給模型的。那就需要保證,Decoder在生成當前單詞的時候,不能看到後面的單詞(以上面的例子來說,在翻譯 a 的時候,不能看到student以及後面的單詞)。這就好比,老師讓學生做試卷,不能同時把答案也給他,都有答案了還考個毛線,正確的做法是學生做完後給出答案讓他去對比,看看自己哪兒做錯了。

爲了不讓Decoder看到未來的信息,便引出了掩碼的概念,其實很簡單,代碼如下。

def subsequent_mask(size):
    """
    這個掩碼是Decoder用到的tgt_mask,因爲Decoder不允許看到未來的信息。
    例子:
    print(subsequent_mask(3))
    tensor([[[1, 0, 0],
             [1, 1, 0],
             [1, 1, 1]]], dtype=torch.uint8)

    """
    attn_shape = (1, size, size)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(subsequent_mask) == 0

Attention

這是整個模型最重要的創新點,Self-Attention,Multi-Head Attention。

Self-Attention

在這裏插入圖片描述

假設下面的句子就是我們需要翻譯的輸入句:

”The animal didn’t cross the street because it was too tired”

這句話中的"it"指的是什麼?它指的是“animal”還是“street”?對於人來說,這其實是一個很簡單的問題,但是對於一個算法來說,處理這個問題其實並不容易。Self-Attention的出現就是爲了解決這個問題,通過Self-Attention,我們能將“it”與“animal”聯繫起來。

Self-Attention的本質,簡單的來說,就是句子裏每個單詞受其他單詞的影響程度,可以學到語義依賴關係

在Transformer中,Self-Attention在Encoder、Decoder內部都有應用。

Self-Attention的計算公式如下,除以dk\sqrt{d_{k}}是爲了進行縮放,這裏就不介紹了。
(Q,K,V)=softmax(QKTdk)V (Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V
公式不好理解,來張圖:

在這裏插入圖片描述

如圖所示:

在這裏插入圖片描述

再看代碼就明瞭多了:

def attention(query, key, value, mask=None, dropout=None):
    """Compute 'Scaled Dot Product Attention"""
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask:
        # -1e9 是-INF的意思嗎???還是說近似於0的數值???
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(scores, dim=-1)
    if dropout:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value)

多頭計算只是多個Self-Attention並行計算的形式而已。

Multi-Head Attention

多個Self-Attention並行計算就叫多頭計算(Multi-Head Attention)模式,這就是Transformer訓練速度超快的原因。

多頭計算換一種理解,就是可以把它理解爲一個集成學習方法。

一個512維的向量,經過8組獨立WQW^QWKW^KWVW^V,得到了8組維度爲64的QKVQ、K、V向量。
WiQRd model ×dk,WiKRd model ×dk,WiVRd model ×dv,WORhdv×dmodel W_{i}^{Q} \in \mathbb{R}^{d_{\text { model }} \times d_{k}}, W_{i}^{K} \in \mathbb{R}^{d_{\text { model }} \times d_{k}}, W_{i}^{V} \in \mathbb{R}^{d_{\text { model }} \times d_{v}}, W^{O} \in \mathbb{R}^{h d_{v} \times d_{\mathrm{model}}}

dmodel=512dk=dv=64h=8 {d_{model}} = 512\\ {d_k} = {d_v} = 64\\ h = 8

在這裏插入圖片描述

在這裏插入圖片描述

8組並行計算Self-Attention的過程就是Multi-Head Attention計算,得到8組維度爲64的向量ZZ,然後綜合8組ZZ進行變換得到最終的向量ZZ,維度也由原來的8個 n×64n \times 64 變成 n×512n \times 512

有趣的是:

5128=64 \frac{{512}}{8} = 64

這不就是集成學習的套路嗎?以分類任務爲例,訓練數據有512個特徵,現訓練8個獨立的分類器,每個從數據中隨機抽取64個特徵訓練,得到各自分類結果,最後再綜合決策,得到最終分類結果。

在這裏插入圖片描述

class MultiHeadedAttention(nn.Module):

    def __init__(self, h, d_model, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        # d_model 必須要被 h 整除
        assert d_model % h == 0
        self.d_k = d_model // h
        self.h = h
        self.dropout = nn.Dropout(p=dropout)
        # Q K V 三個權重,最後合併Z,還有一個線性連接層,所以4個
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None

    def forward(self, query, key, value, mask=None):
        # 由於self-attention的q, k, v 都是有x經過線性變換得到的,
        # 所以調用forward的時候,傳遞的參數就是 (x, x, x, mask)
        if mask:
            # 若原來mask的維度是[n, n],經過unsqueeze後會變成[n, 1, n]
            # 爲何要如此操作???
            mask = mask.unsqueeze(1)
        # q, k, v 第一個維度是batch
        # 那就是說 q, k, v 是 [n, 64]???
        nbatches = query.size(0)
        # 真正的q,k,v是這一步計算出來的
        # 中間-1那個維度是幹嘛的????
        # n  h   d_k  這三個維度是確定的, 還有一個維度是幹嘛 的???
        query, 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))
        ]
        # attention 計算得到一堆z
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 將8個z拼接, 變換形態 n x ? x 512 !!!
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        # 得到最終的z
        return self.linears[-1](x)

Position-wise Feed-Forward Networks

上圖中很多模型中都有Feed Forward,用的都是如下的結構,只是各自參數獨立,兩個線性變換,一個Relu非線性函數,一個dropout。先上圖,再擺公式,最後代碼,是不是非常清晰明瞭了。

在這裏插入圖片描述
FFN(x)=max(0,xW1+b1)W2+b2 \operatorname{FFN}(x)=\max \left(0, x W_{1}+b_{1}\right) W_{2}+b_{2}

class PositionwiseFeedForward(nn.Module):

    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_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w_2(self.dropout(F.relu(self.w_1(x))))

Embeddings and Softmax

這個沒多少說的,有點不太明白的地方就是—爲什麼要乘以dmodel\sqrt{d_{\mathrm{model}}}???

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_model

    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)

Positional Encoding

我們可能忽略了去介紹一個重要的內容,就是怎麼考慮輸入序列中單詞順序的方法。
爲了解決這個問題,transformer爲每個輸入單詞的詞嵌入上添加了一個新向量-位置向量。

爲了讓模型捕捉到單詞的順序信息,我們添加位置編碼向量信息(POSITIONAL ENCODING)-位置編碼向量不需要訓練,它有一個規則的產生方式。

如果我們的嵌入維度爲4,那麼實際上的位置編碼就如下圖所示:

在這裏插入圖片描述

那麼生成位置向量需要遵循怎樣的規則呢?

觀察下面的圖形,每一行都代表着對一個矢量的位置編碼。因此第一行就是我們輸入序列中第一個字的嵌入向量,每行都包含512個值,每個值介於1和-1之間。我們用顏色來表示1,-1之間的值,這樣方便可視化的方式表現出來:

在這裏插入圖片描述

PE(pos,2i)=sin(pos/100002i/d model )PE(pos,2i+1)=cos(pos/100002i/d model ) \begin{aligned} P E_{(\text {pos}, 2 i)} &=\sin \left(\text {pos} / 10000^{2 i / d_{\text { model }}}\right) \\ P E_{(\text {pos}, 2 i+1)} &=\cos \left(\text {pos} / 10000^{2 i / d_{\text { model }}}\right) \end{aligned}

對於上述公式,代碼中進行了對數變換,很容易推導的。

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(dropout)
        # pe 初始化爲0,shape爲 [n, d_model] 的矩陣,用來存放最終的PositionalEncoding的
        pe = torch.zeros(max_len, d_model)
        # position 表示位置,shape爲 [max_len, 1],從0開始到 max_len
        position = torch.arange(0., max_len).unsqueeze(1)
        # 這個是變形得到的,shape 爲 [1, d_model//2]
        div_term = torch.exp(torch.arange(0., d_model, 2) * -(math.log(10000.0) / d_model))
        # 矩陣相乘 (max_len, 1) 與 (1, d_model // 2) 相乘,最後結果 shape   (max_len, d_model // 2)
        # 即所有行,一半的列。(行爲句子的長度,列爲向量的維度)
        boy = position * div_term
        # 偶數列  奇數列 分別賦值
        pe[:, 0::2] = torch.sin(boy)
        pe[:, 1::2] = torch.cos(boy)
        # 爲何還要在前面加一維???
        pe = pe.unsqueeze(0)
        # Parameter 會在反向傳播時更新
        # Buffer不會,是固定的,本文就是不想讓其被修改
        self.register_buffer('pe', pe)

    def forward(self, x):
        # x能和後面的相加說明shape是一致的,也是 (1, sentence_len, d_model)
        # forward 其實就是,將原來的Embedding 再加上 Positional Embedding
        x += Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

拼裝模型

把Transformer這個輪子一塊一塊地拆開了,研究透了,現在我們再把它組裝起來。

def make_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.deepcopy
    attn = 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 model

模型訓練

遇到的問題及解決方案

1、RuntimeError: exp_vml_cpu not implemented for ‘Long’。

解決方案:將如下代碼:

position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / 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))

Thanks stackoverflow.

2、IndexError: invalid index of a 0-dim tensor. Use tensor.item() to convert a 0-dim tensor to a Python number。很多關於loss的地方,代碼都做了改動。

解決方案:將

return crit(Variable(predict.log()), Variable(torch.LongTensor([1]))).data[0]

改爲

return crit(Variable(predict.log()), Variable(torch.LongTensor([1]))).item()

其他的也是這麼改

Thanks for github issues

3、運行到 Greedy Decoding,jupyter notebook 的 kernel就掛掉了。是run_epoch的問題,後面有解決方案。

解決方案:

出錯的代碼是這段:

for epoch in range(10):
    model.train()
    run_epoch(data_gen(V, 30, 20), model, 
              SimpleLossCompute(model.generator, criterion, model_opt))
    model.eval()
    print(run_epoch(data_gen(V, 30, 5), model, 
                    SimpleLossCompute(model.generator, criterion, None)))

這段是個例子,註釋掉就行。

4、OSError: [E050] Can’t find model ‘de’. It doesn’t seem to be a shortcut link, a Python package or a valid path to a data directory.

解決方案:在命令行執行如下命令,先下載對應的資源。需要等幾分鐘。

python -m spacy download de
python -m spacy download en

5、de-en,德英文件下載很慢,還報錯。

解決方案:我用IDM(一個多線程下載器,很快)在本地下載好了,然後上傳到服務器。一共23MB。

下載地址是:https://wit3.fbk.eu/archive/2016-01//texts/de/en/de-en.tgz

自己下載也行。

上傳到項目路徑下的 .data/iwslt下面即可。

6、The device argument should be set by using torch.device or passing a string as an argument. This behavior will be deprecated soon and currently defaults to cpu.

解決方案:將model_par = nn.DataParallel(model, device_ids=devices)改爲model_par = nn.DataParallel(model)。默認使用全部顯卡。

7、又缺少文件。FileNotFoundError: [Errno 2] No such file or directory: ‘iwslt.pt

解決方案:下載唄。https://s3.amazonaws.com/opennmt-models/iwslt.pt

8、loss 均爲整數。

解決方案:將norm轉爲float再進行乘法運算:

return loss.item() * norm.float()

9、 run_epoch有大問題,跑着跑着,就崩了。全部改一下,複製下面的就好了。

def run_epoch(data_iter, model, loss_compute, epoch = 0):
    "Standard Training and Logging Function"
    start = time.time()
    total_tokens = 0
    total_loss = 0
    tokens = 0
    for i, batch in enumerate(data_iter):
        out = model.forward(batch.src, batch.trg, batch.src_mask, batch.trg_mask)
        loss = loss_compute(out, batch.trg_y, batch.ntokens)

        total_loss += loss.detach().cpu().numpy()
        total_tokens += batch.ntokens.cpu().numpy()
        tokens += batch.ntokens.cpu().numpy()
        if i % 50 == 1:
            elapsed = time.time() - start
            print("Epoch Step: %d Loss: %f Tokens per Sec: %f" % (i, loss.detach().cpu().numpy() / batch.ntokens.cpu().numpy(), tokens / elapsed))
            start = time.time()
            tokens = 0
    return total_loss / total_tokens

10、RuntimeError: Expected object of backend CUDA but got backend CPU for argument #3 ‘index’。

解決方案:將如下代碼

src = batch.src.transpose(0, 1)[:1]
src_mask = (src != SRC.vocab.stoi["<blank>"]).unsqueeze(-2)

改爲:

src = batch.src.transpose(0, 1)[:1].cuda()
src_mask = (src != SRC.vocab.stoi["<blank>"]).unsqueeze(-2).cuda()

總結

參考鏈接

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