入门pytorch-Transformer

前言

虽然Transformer是2017年由Google推出,如果按照读论文只读近两年的思路看,那它无疑是过时的,但可惜的是,目前很多论文的核心依然是Transformer,或者由其进行改进的,故本文使用pytorch来搭建一下Transformer这个模型

全局分析

首先,我们要从整个模型架构入手,从大的层面看这块内容,然后再开始编写代码。欧克,这里默认大家掌握了一些基础知识,Transformer是由Google于2017年的Attention Is All You Need论文上所提出来的。如下图,则是论文中所提出的整个框架,可以很清晰的看出具体所使用的组件

总的来看,两个模块,编码器、解码器,特别有自编码器的思想(换句话说,Transformer借鉴了seq2sqe,而seq2sqe天然就是自编码器的思想),下面引用了论文当中的话.

  1. Encoder: 编码器由 N = 6 N = 6 N=6 个相同层的堆叠组成。每一层有两个子层。第一个是多头自注意机制,第二个是一个简单的,位置全连接前馈网络。我们在两个子层中的每一个周围使用残差连接,随后进行层归一化。也就是说,每个子层的输出是 L a y e r N o r m ( x + S u b l a y e r ( x ) ) LayerNorm(x + Sublayer(x)) LayerNorm(x+Sublayer(x)),其中 S u b l a y e r ( x ) Sublayer(x) Sublayer(x) 是由子层本身实现的函数。为了促进这些剩余连接,模型中的所有子层以及嵌入层产生维度 d m o d e l = 512 d_{model} = 512 dmodel=512 的输出。

  2. Decoder: 解码器也由 N = 6 N = 6 N=6 个相同层的堆栈组成。除了每个编码器层中的两个子层之外,解码器还插入第三子层,该第三子层对编码器堆栈的输出执行多头注意。与编码器类似,我们在每个子层周围使用残差连接,然后进行层归一化。我们还修改了解码器堆栈中的自注意子层,以防止位置注意到后续位置。该掩蔽与输出嵌入偏移一个位置的事实相结合,确保了位置i的预测可以仅依赖于小于 i i i 的位置处的已知输出。

好了,我们了解了整个大的框架,开始接触小的组件,由上图,我们可以进行拆分,其整个框架是由嵌入层、位置编码层、(掩码)多头注意力层、前馈神经网络层、残差连接+归一化层;接下来我们分别实现这些神经网络块(层),并将其堆叠在一起,其实就是Transformer了。

好了,接下来我们就开始编写代码吧

位置编码层

下面的就是位置编码。在这里打个比方,就比如机器翻译问题上,我们需要将一句话翻译成另一句话,即A --> B,那么A和B必然是长度随机的编码,这里输入的就是(想要翻译几句话,几个单词,每个单词的特征编码)

P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) PE_{(pos,2i)} = sin(pos/10000^{2i/d_{model}}) PE(pos,2i)=sin(pos/100002i/dmodel)
P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE_{(pos,2i+1)} = cos(pos/10000^{2i/d_{model}}) PE(pos,2i+1)=cos(pos/100002i/dmodel)

python 复制代码
# 位置编码
class PositionalEncoding(nn.Module):
    '''
    num_hiddens: 神经元数量(嵌入维度数量)
    dropout: 神经元的丢弃概率
    max_len: 序列最大长度
    '''
    def __init__(self, num_hiddens, dropout, max_len=1000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(dropout)
        self.P = torch.zeros((1, max_len, num_hiddens))
        # 等差数列
        X = (torch.arange(max_len, dtype=torch.float32).reshape(-1, 1)
             / torch.pow(10000, torch.arange(0, num_hiddens, 2, dtype=torch.float32) / num_hiddens))
        self.P[:, :, 0::2] = torch.sin(X)
        self.P[:, :, 1::2] = torch.cos(X)

    def forward(self, X): # X = (批次,序列长度,特征)
        X = X + self.P[:, :X.shape[1], :].to(X.device)
        return self.dropout(X)

残差连接+归一化层

欧克,接下来我们先来看一下简单理解的层。残差连接可以理解为防止梯度消失问题,以及加快收敛的一个有效的方法。而归一化就是防止梯度爆炸的问题

python 复制代码
# 残差连接 + 归一化层
class AddNorm(nn.Module):
    '''
    normalized_shape: 形状大小
    dropout: 神经元的丢弃概率
    '''
    def __init__(self, normalized_shape, dropout):
        super(AddNorm, self).__init__()
        self.drop = nn.Dropout(dropout) # 丢弃层
        self.layer = nn.LayerNorm(normalized_shape) # 对输入的小批量应用层规范化

    def forward(self, X, Y):
        return self.layer(self.drop(Y) + X)

前馈神经网络层

这块也比较简单,就是两层的感知机而已。

F F N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x) = max(0, xW_1 + b_1)W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2

python 复制代码
# 前馈神经网络
class PostionWiseFFN(nn.Module):
    '''
    num_input: 输入形状
    num_hiddens: 隐藏层形状
    num_ouput: 输出层形状
    '''
    def __init__(self, num_input, num_hiddens, num_ouput):
        super(PostionWiseFFN, self).__init__()
        self.liner1 = nn.Linear(num_input, num_hiddens) # 线性层1
        self.relu = nn.ReLU() # 激活函数
        self.liner2 = nn.Linear(num_hiddens, num_ouput) # 线性层2

    def forward(self, X):
        return self.liner2(self.relu(self.liner1(X)))

点积注意力机制

这里给出点积注意力机制的公式

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d ) V Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d}})V Attention(Q,K,V)=softmax(d QKT)V

其中Q为查询矩阵,K键矩阵,V值矩阵,它们的维度是 ( n , d k ) (n,d_k) (n,dk), ( m , d k ) (m,d_k) (m,dk), ( m , d v ) (m,d_v) (m,dv),m是键值对的数量, d k d_k dk是键矩阵和查询矩阵的特征维度, d v d_v dv是值矩阵的特征维度。而除于 d \sqrt{d} d 是为了避免梯度消失问题

python 复制代码
class DotProductAttention(nn.Module):
    '''
    dropout: 神经元的丢弃概率
    '''
    def __init__(self, dropout):
        super(DotProductAttention, self).__init__()
        self.dropout = nn.Dropout(dropout)
    def sequence_mask(self, X, valid_lens, value=0): # 根据valid_lens生成掩码
        '''
        X: (批量, 最大序列长度, 特征)
        valid_lens: 有效长度
        value: 填充数据
        '''
        maxlen = X.size(1)
        mask = torch.arange((maxlen), dtype=torch.float32,
                            device=X.device)[None, :] < valid_lens[:, None]
        X[~mask] = value
        return X
    def masked_softmax(self, X, valid_lens):
        if valid_lens is None:
            return nn.functional.softmax(X, dim=-1)
        else:
            shape = X.shape
            if valid_lens.dim == 1:
                valid_lens = valid_lens.repeat_interleave(valid_lens, shape[1])
            else:
                valid_lens = valid_lens.reshape(-1)
            X = self.sequence_mask(X, valid_lens)
            return nn.functional.softmax(X.reshape(shape), dim=-1)
    def forward(self, Q, K, V, valid_lens=None):
        d = Q.shape[-1]
        scores = torch.bmm(Q, K.transpose(1, 2)) / math.sqrt(d) # 点积获取注意力分数
        self.attention_weights = self.masked_softmax(scores, valid_lens) # 获取注意力权重
        return torch.bmm(self.dropout(self.attention_weights), V) # 注意力权重 * 值V

到这里,我们由此便可以提出自注意力机制,而自注意力机制对上面做出了一个很简单的改变,就是 Q = K = V Q = K = V Q=K=V,换句话说Q、K、V同源

多头注意力

所谓多头注意力机制,就是有多个自注意力机制并行,然后将输出进行拼接送到线性层进一步整合

M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) W O w h e r e h e a d i = A t t e n t i o n ( Q W i Q , K W i K , V W i V ) MultiHead(Q,K,V) = Concat(head_1,...,head_h)W^O\\ where \ head_i = Attention(QW_i^Q,KW_i^K,VW_i^V) MultiHead(Q,K,V)=Concat(head1,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)

python 复制代码
# 多头注意力
class MultiHeadAttention(nn.Module):
    '''
    key_size: K值形状大小
    query_size: Q值形状大小
    value_size: V值形状大小
    num_hiddens: 隐藏层神经元数量
    num_heads: 头数
    dropout: 神经元的丢弃概率
    bias: 是否学习加性偏差,默认不学习
    '''
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 num_heads, dropout, bias=False):
        super(MultiHeadAttention, self).__init__()
        assert num_hiddens % num_heads == 0
        self.num_heads = num_heads
        self.attention = DotProductAttention(dropout) # 这里使用到了点积注意力机制
        self.W_q = nn.Linear(query_size, num_hiddens, bias=bias) # Q矩阵权重
        self.W_k = nn.Linear(key_size, num_hiddens, bias=bias) # K矩阵权重
        self.W_v = nn.Linear(value_size, num_hiddens, bias=bias) # V矩阵权重
        self.W_o = nn.Linear(num_heads, num_hiddens, bias=bias) # 输出线性层

    def forward(self, Q, K, V, valid_lens): # valid_lens 有效长度
        Q = transpose_qkv(self.W_q(Q), self.num_heads)
        K = transpose_qkv(self.W_k(K), self.num_heads)
        V = transpose_qkv(self.W_v(V), self.num_heads)
        if valid_lens is not None:
            valid_lens = torch.repeat_interleave(valid_lens, repeats=self.num_heads, dim=0)
        output = self.attention(Q, K, V, valid_lens)
        output_concat = transpose_output(output, self.num_heads)
        return self.W_o(output_concat)

这里我们通过transpose_qkvtranspose_output函数来将数据进行改造,以此来进行并行操作

python 复制代码
def transpose_qkv(X, num_heads):
    """为了多注意力头的并行计算而变换形状"""
    # 输入X的形状:(batch_size,查询或者"键-值"对的个数,num_hiddens)
    # 输出X的形状:(batch_size,查询或者"键-值"对的个数,num_heads,num_hiddens/num_heads)
    X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)
    X = X.permute(0, 2, 1, 3)
    return X.reshape(-1, X.shape[2], X.shape[3])
def transpose_output(X, num_heads):
    """逆转transpose_qkv函数的操作"""
    X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
    X = X.permute(0, 2, 1, 3)
    return X.reshape(X.shape[0], X.shape[1], -1)

编码器

如下则是编码器的代码

python 复制代码
class EncoderBlock(nn.Module):
    '''
    K_size, Q_size, V_size: 键值对和查询的大小
    num_hiddens: 中间隐藏层神经元
    num_heads: 头数
    norm_shape: 残差连接+归一化层的输入形状
    num_input: FFN的输入形状
    ffn_num_hiddens: FFN隐藏层神经元
    dropout: 神经元的丢弃概率
    '''
    def __init__(self, K_size, Q_size, V_size, num_hiddens, num_heads,
                 norm_shape, num_input, ffn_num_hiddens, dropout, bias=False):
        super(EncoderBlock, self).__init__()
        self.attention = MultiHeadAttention(
            K_size, Q_size, V_size, num_hiddens, num_heads, dropout, bias
        ) # 多头注意力机制 
        self.addnorm1 = AddNorm(norm_shape, dropout) # 归一化+残差连接
        self.ffn = PostionWiseFFN(num_input, ffn_num_hiddens, num_hiddens) # 前馈神经网络
        self.addnorm2 = AddNorm(norm_shape, dropout) # 归一化+残差连接

    def forward(self, X, valid_lens):
        Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
        return self.addnorm2(Y, self.ffn(Y))

接下来我们需要将位置编码层、嵌入层、残差连接+归一化层、前馈神经网络、编码器等封装起来

python 复制代码
class TransformerEncoder(nn.Module):
    '''
    vocab_size: 词典大小
    K_size, Q_size, V_size: 键值对和查询的大小
    num_hiddens: 中间隐藏层神经元
    num_heads: 头数
    norm_shape: 残差连接+归一化层的输入形状
    num_input: FFN的输入形状
    ffn_num_hiddens: FFN隐藏层神经元
    num_layers: 编码器的层数
    dropout: 神经元的丢弃概率
    '''
    def __init__(self, vocab_size, K_size, Q_size, V_size, num_hiddens, num_heads,
                 norm_shape, num_input, ffn_num_hiddens, num_layers, dropout, bias=False):
        super(TransformerEncoder, self).__init__()
        self.num_hiddens = num_hiddens
        #  num_embeddings: 嵌入词典的大小  embedding_dim: 每个嵌入向量的尺寸
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=num_hiddens) # 嵌入层
        self.pos_encoding = PositionalEncoding(num_hiddens, dropout) # 位置编码层
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block" + str(i),
                                 EncoderBlock(K_size, Q_size, V_size, num_hiddens, num_heads, norm_shape, num_input, ffn_num_hiddens, dropout, bias))

    def forward(self, X, valid_lens):
        # 输入 X (句子个数,单词个数)
        # 输出 ret (句子个数,单词个数,单词特征)
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self.attention_weights = [None] * len(self.blks)
        for i, blk in enumerate(self.blks):
            X = blk(X, valid_lens)
            self.attention_weights[i] = blk.attention.attention.attention_weights
        return X

解码器

python 复制代码
class DecodeBlock(nn.Module):
    '''
    K_size, Q_size, V_size: 键值对和查询的大小
    num_hiddens: 中间隐藏层神经元
    num_heads: 头数
    norm_shape: 残差连接+归一化层的输入形状
    num_input: FFN的输入形状
    ffn_num_hiddens: FFN隐藏层神经元
    dropout: 神经元的丢弃概率
    i:
    '''
    def __init__(self, K_size, Q_size, V_size, num_hiddens, num_heads,
                 norm_shape, num_input, ffn_num_hiddens, dropout, i):
        super(DecodeBlock, self).__init__()
        self.i = i
        self.attention1 = MultiHeadAttention(
            K_size, Q_size, V_size, num_hiddens, num_heads, dropout
        ) # 多头注意力机制
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.attention2 = MultiHeadAttention(
            K_size, Q_size, V_size, num_hiddens, num_heads, dropout
        ) # 多头注意力机制
        self.addnorm2 = AddNorm(norm_shape, dropout)
        self.ffn = PostionWiseFFN(num_input, ffn_num_hiddens, num_hiddens)
        self.addnorm3 = AddNorm(norm_shape, dropout)

    def forward(self, X, state):
        # state [编码器输入,编码器有效长度,中间状态用于记录]
        enc_outputs, enc_valid_lens = state[0], state[1]
        if state[2][self.i] is None:
            K = X
        else:
            K = torch.cat((state[2][self.i], X), axis=1)
        state[2][self.i] = K
        if self.training:
            batch_size, num_steps, _ = X.shape
            dec_valid_lens = torch.arange(
                1, batch_size + 1, device=X.device
            )
        else:
            dec_valid_lens = None

        X2 = self.attention1(X, K, K, dec_valid_lens)
        Y = self.addnorm1(X, X2)
        Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
        Z = self.addnorm2(Y, Y2)
        return self.addnorm3(Z, self.ffn(Z)), state

封装

python 复制代码
class TransformerDecoder(nn.Module):
    '''
    vocab_size: 词典大小
    K_size, Q_size, V_size: 键值对和查询的大小
    num_hiddens: 中间隐藏层神经元
    num_heads: 头数
    norm_shape: 残差连接+归一化层的输入形状
    num_input: FFN的输入形状
    ffn_num_hiddens: FFN隐藏层神经元
    num_layers: 编码器的层数
    dropout: 神经元的丢弃概率
    '''
    def __init__(self, vocab_size, K_size, Q_size, V_size, num_hiddens, num_heads,
                 norm_shape, num_input, ffn_num_hiddens, num_layers, dropout):
        super(TransformerDecoder, self).__init__()
        self.num_hiddens = num_hiddens
        self.num_layers = num_layers
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=num_hiddens)
        self.pos_encoding = PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block" + str(i),
                                 DecodeBlock(K_size, Q_size, V_size, num_hiddens, num_heads, norm_shape, num_input, ffn_num_hiddens, dropout, i))
        self.linear = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, enc_valid_lens):
        return [enc_outputs, enc_valid_lens, [None] * self.num_layers]

    def forward(self, X, state):
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self._attention_weights = [[None] * len(self.blks) for _ in range(2)]
        for i, blk in enumerate(self.blks):
            X, state = blk(X, state)
            self._attention_weights[0][i] = blk.attention1.attention.attention_weights
            self._attention_weights[1][i] = blk.attention2.attention.attention_weights
        return self.linear(X), self._attention_weights

    @property
    def attention_weights(self):
        return self._attention_weights

Transformer

最后整合一下

python 复制代码
class EncoderDecoder(nn.Module):
    """编码器-解码器架构的基类"""
    def __init__(self, encoder, decoder, **kwargs):
        super(EncoderDecoder, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_X, dec_X, *args):
        enc_outputs = self.encoder(enc_X, *args)
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_X, dec_state)

Transformer以三种不同的方式使用多头注意力:(引自论文)

  • 在 "encoder-decoder attention" 层中,查询 Q Q Q来自先前的解码器层,并且存储器键 K K K和值 V V V来自编码器的输出。这使得解码器中的每个位置都能处理输入序列中的所有位置。这模拟了序列到序列模型中的典型编码器-解码器注意机制。
  • 编码器包含自我注意层。在自关注层中,所有的键、值和查询都来自同一个地方,在这种情况下,是编码器中前一层的输出。编码器中的每个位置都可以处理编码器的前一层中的所有位置。
  • 类似地,解码器中的自关注层允许解码器中的每个位置关注解码器中的直到并且包括该位置的所有位置。我们需要防止解码器中的冗余信息流,以保持自回归特性。我们通过屏蔽(设置为 − ∞ -\infty −∞) s o f t m a x softmax softmax 输入中对应于非法连接的所有值来实现这一点。

最后,大家再来回顾一下Transformer模型整体架构

相关推荐
ws2019078 分钟前
AUTO TECH China 2025华南展:一起探索汽车零部件的未来
大数据·人工智能·汽车
WBingJ11 分钟前
李宏毅机器学习-局部最小值与鞍点
人工智能·深度学习·机器学习
城市数据研习社16 分钟前
论文概览 |《Urban Analytics and City Science》2023.03 Vol.50 Issue.3
大数据·人工智能·chatgpt·数据分析
L_cl20 分钟前
【NLP 14、激活函数 ② tanh激活函数】
人工智能·深度学习·自然语言处理
WBingJ28 分钟前
李宏毅机器学习-批次 (batch)和动量(momentum)
人工智能·机器学习·batch
DevUI团队36 分钟前
开源开发者获奖,MateChat即将发布,快来看DevUI在华为云开源开发者论坛的精彩回顾吧
前端·人工智能
过去式的马马马1 小时前
aippt:AI 智能生成 PPT 的开源项目
人工智能·开源·powerpoint
池央1 小时前
AlexNet:开启深度学习图像识别新纪元
人工智能·深度学习
剑盾云安全专家1 小时前
让PPT不再“难搞”:智能工具如何改变办公体验
人工智能·aigc·powerpoint·软件
封步宇AIGC2 小时前
量化交易系统开发-实时行情自动化交易-8.25.真格(澎博财经旗下)平台
人工智能·python·机器学习·数据挖掘