第十二周周报:Transformer

目录

摘要

Abstract

Transformer

1、Embedding

2、位置编码

3、编码器

4、解码器

5、输出层

6、代码

总结


摘要

本周跟着李沐老师的课程详细学习了Transformer的原理,明白了编码器和解码器的工作原理,以及数据从输入到输出整个过程的参数传递。同时跟着视频讲解,也理解了Transformer为什么会选择多头自注意力机制,以及解码器中的掩码自注意力机制。本博客会根据自己的理解逐一编写,后会附上Transformer的PyTorch代码。

Abstract

This week, I followed Professor Li Mu's course to learn in detail the principles of Transformer, understand the working principles of encoder and decoder, and the parameter transfer of the entire process from input to output of data. At the same time, following the video explanation, I also understood why Transformer chose multi head self attention mechanism and the mask self attention mechanism in the decoder. This blog will be written one by one based on one's own understanding, and the PyTorch code for Transformer will be attached later.

Transformer

该模型整体架构如下图所示:

该网络大致流程如下:

数据输入经过Embedding后,该网络会进行一个位置编码;然后,将编码后的数据传入编码器,经过多头自注意力,再经过前馈神经网络;然后再将编码器的输出依次传入解码器,解码器先采用掩码自注意力,后面部分和编码器相同;最后,通过全连接层输出结果。在编码器和解码器中采用残差链接,类似于ResNet。

1、Embedding

将输入数据先进行词嵌入,该网络采用Embedding。Embedding是将高维的、稀疏的、离散的数据(如单词、用户ID、商品ID等)映射到一个低维的、密集的、连续的向量空间中,以便能够用数学的方式对这些数据进行处理和分析。

Embedding通过捕捉单词间的语义和句法关系,为自然语言处理任务提供有效特征表示,也根据预测单词上下文或全局词频统计来学习,可使用深度神经网络捕获更复杂的语言特征。 

为什么选用Embedding而不是one-hot编码:

One-hot编码是一种将类别变量转换为机器学习算法易于处理的形式的方法。它通过将每个类别值映射到一个除了该类别索引位置为1,其余位置都为0的二进制向量上,来实现对类别变量的编码。当类别数量很多时,会导致编码后的向量非常稀疏,且维度很高,增加了计算复杂度。无法表示类别之间的语义关系,即默认假设词与词之间是没有关系的。

而Embedding通过大量的数据训练之后,正好解决了这些问题。

2、位置编码

通过1中Embedding处理后的数据需要进行位置编码,用以表示序列的顺序。

论文中提到“由于我们的模型不包含递归和卷积,为了使模型能够利用序列的顺序,我们必须注入一些关于序列的相对或绝对位置的信息。为此,我们将“位置编码”嵌入到编码器和解码器堆栈的底部。位置编码和嵌入具有相同的维度d,从而可以将两者相加。位置编码有很多选择, 学习和固定。”

位置编码加上Embedding后的词嵌入就是基于时间步的词嵌入,就可以将其依次传入第一个编码器了。如果不添加位置编码,那么无论单词在什么位置,它的注意力分数都是确定的。因为单词不同的排列顺序意思是不同的。

3、编码器

论文中提到该模型的编码器模块是通过6个编码器堆叠而成。所有的编码器在结构上是相同的,但是没有进行参数共享,后期参数单独更新。

2中基于时间步的词嵌入输入到编码器时,首先经过一个多头自注意力层(Multi-Head Attention),然后通过残差连接(Add & Norm)进入全连接层(Feed Forward),最后再经过一次残差连接进入下一个编码器。

  • Multi-Head Attention

self-attention是只使用了一组W^{Q}W^{K}W^{V}来进行变换得到查询、键、值矩阵,而Multi-Head Attention使用多组W^{Q}W^{K}W^{V}得到多组查询、键、值矩阵,然后每组分别计算得到一个Z矩阵。

论文中提到:多头自注意力允许模型同时关注来自不同表示子空间、不同位置的信息;而使用单头自注意力会抑制这一点。也就是说多头自注意力考虑的信息更加全面。

该模型采用了8个头,X分别乘以各权重矩阵得到Q_{0}K_{0}V_{0}等,再通过如下公式:

softmax(\frac{Q\times K}{\sqrt{d_{k}}})V=Z

得到Z矩阵;最后,将所有Z拼接起来乘以权重矩阵W得到最终的输出Z。

这种X矩阵与权重矩阵相乘的自注意力方法为:缩放点积注意力

论文中提到:为了防止softmax将矩阵点乘结果过大的情况推入梯度极小的情况,统一除以d_{k}对矩阵进行缩放消除这种影响。

  • Add & Norm

论文提到:在多头自注意力层和全连接层之后采用残存连接和层归一化处理,所有的维度都将会升至512维。Add和Norm两部分公式如下:

LayerNorm(X+MultiHeadAttention(X))

LayerNorm(X+FeedForward(X))

Add部分就是残差连接,残差连接类似于ResNet。采用残差连接主要是防止网络层数增加后引起的梯度爆炸和梯度消失问题。残差连接如下图所示:

Norm部分是层归一化,主要目的是加速训练,以及提高模型泛化能力。

  • Feed Forward

全连接层是一个两层的神经网络,先线性变换,然后ReLU非线性,再线性变换。全连接层是为了将输入的Z映射到更加高维的空间中然后通过非线性函数ReLU进行筛选,筛选完后再变回原来的维度。全连接层公式如下:

FFN(x)=max(0,xW_{1}+b_{1})W_{2}+b_{2}

4、解码器

该模型也是采用6个解码器进行堆叠,其前半部分比编码器多引入了一个掩码多头自注意力,以及一个Add & Norm层;后半部分组成结构与编码器相同。

  • Masked Multi-Head Attention

掩码多头自注意力与多头自注意力的区别在于掩码多头自注意力不能看见未来的信息,它只能依赖某个时刻t之前的数据进行分析,t时刻之后的数据是无法被看见的。

掩码多头自注意力与多头自注意力的对比如下图所示:

  • 编码器-解码器

编码器和解码器之间的数据传输方式不是在6个编码器输出之后直接传进第一个解码器,而是同时向6个解码器传输,如下图所示:

5、输出层

该网络模型的输出层主要是先经过一次全连接层的线性变化,然后再通过Softmax得到输出的概率分布;最后,通过字典查询输出概率最大的对应单词,即是该模型的预测结果。

6、代码
import math
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data

sentences = [
    # enc_input                   dec_input             dec_output
    ['ich mochte ein bier P', 'S i want a beer .', 'i want a beer . E'],
    ['ich mochte ein cola P', 'S i want a coke .', 'i want a coke . E']
]

src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4, 'cola': 5}
src_vocab_size = len(src_vocab)

tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'coke': 5, 'S': 6, 'E': 7, '.': 8}
idx2word = {i: w for i, w in enumerate(tgt_vocab)}
tgt_vocab_size = len(tgt_vocab)

src_len = 5
tgt_len = 6

# 模型参数
d_model = 512  # Embedding Size
d_ff = 2048  # 全连接层维度
d_k = d_v = 64
n_layers = 6  # 模块个数
n_heads = 8  # 多头自注意力头数

# 制作数据
def make_data(sentences):
    enc_inputs, dec_inputs, dec_outputs = [], [], []
    for i in range(len(sentences)):
        enc_input = [[src_vocab[n] for n in sentences[i][0].split()]]  # [[1, 2, 3, 4, 0], [1, 2, 3, 5, 0]]
        dec_input = [[tgt_vocab[n] for n in sentences[i][1].split()]]  # [[6, 1, 2, 3, 4, 8], [6, 1, 2, 3, 5, 8]]
        dec_output = [[tgt_vocab[n] for n in sentences[i][2].split()]]  # [[1, 2, 3, 4, 8, 7], [1, 2, 3, 5, 8, 7]]

        enc_inputs.extend(enc_input)
        dec_inputs.extend(dec_input)
        dec_outputs.extend(dec_output)

    return torch.LongTensor(enc_inputs), torch.LongTensor(dec_inputs), torch.LongTensor(dec_outputs)

enc_inputs, dec_inputs, dec_outputs = make_data(sentences)

class MyDataSet(Data.Dataset):
    def __init__(self, enc_inputs, dec_inputs, dec_outputs):
        super(MyDataSet, self).__init__()
        self.enc_inputs = enc_inputs
        self.dec_inputs = dec_inputs
        self.dec_outputs = dec_outputs

    def __len__(self):
        return self.enc_inputs.shape[0]

    def __getitem__(self, idx):
        return self.enc_inputs[idx], self.dec_inputs[idx], self.dec_outputs[idx]


loader = Data.DataLoader(MyDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)


# 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-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).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)


# padding mask:对输入序列进行对齐,给在较短的序列后面填充0;序列太长,则是截取左边的内容,把多余的直接舍弃
def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()

    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
    return pad_attn_mask.expand(batch_size, len_q, len_k)


# sequence mask:掩盖t时刻后的信息
def get_attn_subsequence_mask(seq):
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
    subsequence_mask = np.triu(np.ones(attn_shape), k=1)  # Upper triangular matrix
    subsequence_mask = torch.from_numpy(subsequence_mask).byte()
    return subsequence_mask


# 缩放点积注意力
class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()

    def forward(self, Q, K, V, attn_mask):
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
        scores.masked_fill_(attn_mask, -1e9)

        attn = nn.Softmax(dim=-1)(scores)
        context = torch.matmul(attn, V)
        return context, attn


# 多头自注意力
class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)
        self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
        self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
        self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)

    def forward(self, input_Q, input_K, input_V, attn_mask):
        residual, batch_size = input_Q, input_Q.size(0)
        # (B, S, D) -proj-> (B, S, D_new) -split-> (B, S, H, W) -trans-> (B, H, S, W)
        Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
        K = self.W_K(input_K).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
        V = self.W_V(input_V).view(batch_size, -1, n_heads, d_v).transpose(1,2)

        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)

        context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)
        context = context.transpose(1, 2).reshape(batch_size, -1, n_heads * d_v)
        output = self.fc(context)

        return nn.LayerNorm(d_model).cuda()(output + residual), attn


# 前馈神经网络
class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(d_model, d_ff, bias=False),
            nn.ReLU(),
            nn.Linear(d_ff, d_model, bias=False)
        )

    def forward(self, inputs):
        residual = inputs
        output = self.fc(inputs)

        return nn.LayerNorm(d_model).cuda()(output + residual)


# 编码器
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, enc_inputs, enc_self_attn_mask):
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)
        enc_outputs = self.pos_ffn(enc_outputs)
        return enc_outputs, attn


# 解码器
class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.dec_self_attn = MultiHeadAttention()
        self.dec_enc_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
        dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)
        dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)
        dec_outputs = self.pos_ffn(dec_outputs)
        return dec_outputs, dec_self_attn, dec_enc_attn


# 编码器模块
class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.src_emb = nn.Embedding(src_vocab_size, d_model)  # Embedding编码
        self.pos_emb = PositionalEncoding(d_model)  # 位置编码
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])

    def forward(self, enc_inputs):   # 向前传播
        enc_outputs = self.src_emb(enc_inputs)
        enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
        enc_self_attns = []
        for layer in self.layers:
            enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
            enc_self_attns.append(enc_self_attn)
        return enc_outputs, enc_self_attns


# 解码器模块
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

    def forward(self, dec_inputs, enc_inputs, enc_outputs):
        dec_outputs = self.tgt_emb(dec_inputs)
        dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1).cuda()
        dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).cuda()
        dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).cuda()
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0).cuda()

        dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)

        dec_self_attns, dec_enc_attns = [], []
        for layer in self.layers:
            dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask)
            dec_self_attns.append(dec_self_attn)
            dec_enc_attns.append(dec_enc_attn)
        return dec_outputs, dec_self_attns, dec_enc_attns


class Transformer(nn.Module):
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder = Encoder().cuda()
        self.decoder = Decoder().cuda()
        self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False).cuda()

    def forward(self, enc_inputs, dec_inputs):
        enc_outputs, enc_self_attns = self.encoder(enc_inputs)
        dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)
        dec_logits = self.projection(dec_outputs)
        return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns


model = Transformer().cuda()
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)

for epoch in range(1000):
    for enc_inputs, dec_inputs, dec_outputs in loader:
        enc_inputs, dec_inputs, dec_outputs = enc_inputs.cuda(), dec_inputs.cuda(), dec_outputs.cuda()

        outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
        loss = criterion(outputs, dec_outputs.view(-1))
        if epoch % 100 == 0:
            print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()


# 贪心解码器
def greedy_decoder(model, enc_input, start_symbol):
    enc_outputs, enc_self_attns = model.encoder(enc_input)
    dec_input = torch.zeros(1, 0).type_as(enc_input.data)
    terminal = False
    next_symbol = start_symbol
    while not terminal:
        dec_input = torch.cat([dec_input.detach(), torch.tensor([[next_symbol]], dtype=enc_input.dtype).cuda()], -1)
        dec_outputs, _, _ = model.decoder(dec_input, enc_input, enc_outputs)
        projected = model.projection(dec_outputs)
        prob = projected.squeeze(0).max(dim=-1, keepdim=False)[1]
        next_word = prob.data[-1]
        next_symbol = next_word
        if next_symbol == tgt_vocab["."]:
            terminal = True
        print(next_word)
    return dec_input


# 测试
enc_inputs, _, _ = next(iter(loader))
enc_inputs = enc_inputs.cuda()
for i in range(len(enc_inputs)):
    greedy_dec_input = greedy_decoder(model, enc_inputs[i].view(1, -1), start_symbol=tgt_vocab["S"])
    predict, _, _, _ = model(enc_inputs[i].view(1, -1), greedy_dec_input)
    predict = predict.data.max(1, keepdim=True)[1]
    print(enc_inputs[i], '->', [idx2word[n.item()] for n in predict.squeeze()])

代码运行结果如下图所示:

  • padding mask

因为每个批次输入序列长度是不一样,需要对输入序列进行对齐。给在较短的序列后面填充一个非常大的负数(经过 Softmax这些位置的概率就会接近0);较长的序列时,截取左边的内容,把多余的直接舍弃。

  • sequence mask 

解码输出只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出,不能看见未来的数据。预测的时候只能得到前一时刻预测出的输出。

总结

本周的学习到此结束,下周将会继续深入学习深度学习经典神经网络模型的理论知识。

如有错误,请各位大佬指出,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值