第26周:Pytorch复现Transformer

目录

前言

一、基本概念

1.1 Seq2Seq与Transformer

1.2  Transformer结构详解

1.3 自注意机制(Self-Attention)

二、搭建Transformer模型

2.1 多头注意力机制

2.2 前馈传播

2.3 位置编码

2.4 编码层

2.5 解码层

2.6 Transformer模型搭建

三、使用示例

总结


前言

说在前面:

  • 从整体上把握Transformer模型,明白它是个什么东西,可以干嘛
  • 读懂Transformer的复现代码

一、基本概念

1.1 Seq2Seq与Transformer

Seq2Seq是一种用于序列到序列任务的模型架构,最初用于机器翻译,意味着它可以处理输入序列,并生成相应的输出序列

  • 结构:Seq2Seq模型通常由两个主要部分组成--编码器和解码器。编码器是负责将输入序列编码为固定大小的向量,而解码器则使用此向量生成输出序列
  • 问题:传统的Seq2Seq模型在处理长序列时可能会遇到梯度消失/爆炸等问题,而Transformer模型的提出正是为了解决这些问题

Transformer是一种更现代的深度学习模型,专为处理序列数据而设计,最初用于自然语言处理任务,它不依赖于RNN或CNN等传统结构,而是引入了注意力机制

  • 结构:Transformer模型主要由编码器和解码器组成,它们由自注意力层和全连接前馈网络组成。它使用注意力机制来捕捉输入序列中不同位置之间的依赖关系,同时通过多头注意力来提高模型的表达能力
  • 优势:Transformer的设计使其能够更好地处理长距离依赖关系,同时具有更好的并行性。

     在某种程度上,可以将Transformer看作是Seq2Seq的一种演变,Transformer可以执行Seq2Seq任务,并且相对于传统的Seq2Seq模型具有更好的性能和可扩展性。

     与RNN这类神经网络结构相比,Transformer一个巨大的优点是:模型在处理序列输入时,可以对整个序列输入进行并行计算,不需要按照时间步循环递归处理输入序列。下图Transformer整体结构图,左半部分为编码器,右半部分为解码器。

1.2  Transformer结构详解

       Transformer可以看作是Seq2Seq模型的一种,因此,先从seq2seq的角度对Transformer进行宏观结构的学习,以机器翻译任务为例,先将Transformer看作一个黑盒,黑盒的输入是法语文本序列,输出是英文文本序列。

       将上图中的中间部分“THE TRANSFORMER"拆开成seq2seq标准结构,得到下图:左边是编码器部分encoders,右边是解码部分decoders

       下面,再将上图中的编码器和解码器细节绘出,得到下图,我们可以看到,编码部分由多层编码器(Encoder)组成。解码部分也是由多层的解码器(Decoder)组成,每层编码器、解码器网络结构是一样的,但是不同层编码器、解码器网络结构不共享参数。

       其中,单层编码器主要由自注意力层(Self-Attention Layer)和全连接前馈网络(Feed Forward Neural Network, FFNN)组成,如下如所示:

       其中,解码器在编码器的自注意力层和全连接前馈网络中间插了一个Encoder-Decoder Attention层,这个层帮助解码器聚集于输入序列最相关的部分。

总结一下,Transformer由编码部分和解码部分组成,而编码部分和解码部分又由多个网络结构相同的编码层和解码层组成。每个编码层由自注意力层和全连接前馈网络组成,每个解码层由自注意力层、全连接前馈网络和encoder-decoder attention组成

1.3 自注意机制(Self-Attention)

       Self-Attention是在2017年Google机器翻译团队发表的《Attention is All You Need》中被提出来的,它完全抛弃了RNN和CNN等网络结构,而仅仅采用Attention机制来进行机器翻译任务,并且取得了很好的效果。

例如:在翻译的场景中,Source是一种语言,Target是另一种语言,Attention机制发生在Target元素Query和Source中所有元素之间;而Self-Attention指的不是Target和Source之间的Attention机制,指的是Source内部元素之间或者Target内部元素之间发生的Attention机制。

二、搭建Transformer模型

2.1 多头注意力机制

代码如下:

import math
import torch
import torch.nn as nn

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
class MultiHeadAttention(nn.Module):
    #n_heads: 多头注意力的数量
    #hid_dim:每个词输出的向量维度
    def __init__(self, hid_dim, n_heads):
        super(MultiHeadAttention, self).__init__()
        self.hid_dim = hid_dim
        self.n_heads = n_heads

        #强制hid_dim必须整除h
        assert hid_dim % n_heads == 0
        #定义W_q矩阵
        self.w_q = nn.Linear(hid_dim, hid_dim)
        # 定义w_k矩阵
        self.w_k = nn.Linear(hid_dim, hid_dim)
        # 定义w_v矩阵
        self.w_v = nn.Linear(hid_dim, hid_dim)
        self.fc = nn.Linear(hid_dim, hid_dim)
        #缩放
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim // n_heads]))

    def forward(self, query, key, value, mask=None):
        # 注意Q,K,V在句子长度这个维度的数值可以一样,可以不一样
        # K:[64,10,300],假设batch_size=64,有10个词,每个词的Query向量是300维
        # V:[64,10,300],假设batch_size=64,有10个词,每个词的Query向量是300维
        # Q:[64,12,300],假设batch_size=64,有12个词,每个词的Query向量是300维
        bsz = query.shape[0]
        Q = self.w_q(query)
        K = self.w_k(key)
        V = self.w_v(value)
        #这里把Q、K、V矩阵拆分为多组注意力
        #最后一个维度就是用self.hid_dim//self.n_heads得到的,表示每组注意力向量长度,每个head的向量长度是300/6=50
        # K:[64,10,300]拆分多组注意力->[64,10,6,50]转置得到[64,6,10,50]
        # V:[64,10,300]拆分多组注意力->[64,10,6,50]转置得到[64,6,10,50]
        # Q:[64,12,300]拆分多组注意力->[64,12,6,50]转置得到[64,6,12,50]
        #转置是为了把注意力的数量6放到前面。把10和50放到后面,方便下面计算
        Q = Q.view(bsz, -1, self.n_heads, self.hid_dim//self.n_heads).permute(0, 2, 1, 3)
        K = K.view(bsz, -1, self.n_heads, self.hid_dim//self.n_heads).permute(0, 2, 1, 3)
        V = V.view(bsz, -1, self.n_heads, self.hid_dim//self.n_heads).permute(0, 2, 1, 3)

        #第一步:Q乘以K的转置,除以scale
        # [64,6,12,50]*[64,6,50,10] = [64,6,12,10]
        # attention:[64,6,12,10]
        attention = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale

        #如果mask不为空,那么就把mask为0的位置的attention分数设置为-1额0,这里用“0”来指示哪些位置的词向量不能被attention到,比如padding位置
        if mask is not None:
            attention = attention.masked_fill(mask == 0, -1e10)
            # 第2步:计算上一步结果的softmax,再经过dropout,得到attention
            # 注意这里是对最后一维做softmax,也就是在输入序列的维度做softmax
            # attention:[64,6,12,10]
        attention = torch.softmax(attention, dim=-1)

        # 第三步1,attention结果与V相乘,得到多头注意力的结果
        # [64,6,12,10]*[64,6,10,50]=[64,6,12,50]
        # x:[64,6,12,50]
        x = torch.matmul(attention, V)

        # 因为query有12个词,所以把12放到前面,把50和6放到后面,方便下面拼多组的结果
        # x:[64,6,12,50]转置->[64,12,6,50]
        x = x.permute(0, 2, 1, 3).contiguous()
        # 这里的矩阵转换就是把多组注意力的结果拼接起来
        # 最终结果就是[64,12,300]
        # x:[64,12,6,50]->[64,12,300]
        x = x.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))
        x = self.fc(x)
        return x

2.2 前馈传播

代码如下:

#前馈传播
class Feedforward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(Feedforward, self).__init__()
        #两层线性映射和激活函数ReLU
        self.linear1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        x = torch.nn.functional.relu(self.linear1(x))
        x = self.dropout(x)
        x = self.linear2(x)
        return x

2.3 位置编码

代码如下:

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

        # 初始化Shape为(max_len, d_model)的PE (positional encoding)
        pe = torch.zeros(max_len, d_model)

        # 初始化一个tensor [[0, 1, 2, 3, ...]]
        position = torch.arange(0, max_len).unsqueeze(1)
        # 这里就是sin和cos括号中的内容,通过e和ln进行了变换
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))

        pe[:, 0::2] = torch.sin(position * div_term)  # 计算PE(pos, 2i)
        pe[:, 1::2] = torch.cos(position * div_term)  # 计算PE(pos, 2i+1)

        pe = pe.unsqueeze(0)  # 为了方便计算,在最外面在unsqueeze出一个batch

        # 如果一个参数不参与梯度下降,但又希望保存model的时候将其保存下来
        # 这个时候就可以用register_buffer
        self.register_buffer("pe", pe)

    def forward(self, x):
        """
        x 为embedding后的inputs,例如(1,7, 128),batch size为1,7个单词,单词维度为128
        """
        # 将x和positional encoding相加。
        print(x.device)
        x = x + self.pe[:, :x.size(1)].requires_grad_(False)
        return self.dropout(x)

2.4 编码层

代码如下:

#编码层
class EncoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super(EncoderLayer, self).__init__()
        # 编码器包含自注意机制的前馈神经网络
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.feedforward = Feedforward(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask):
        # 自注意机制
        attn_output = self.self_attn(x, x, x, mask)
        x = x + self.dropout(attn_output)
        x = self.norm1(x)
        # 前馈神经网络
        ff_output = self.feedforward(x)
        x = x + self.dropout(ff_output)
        x = self.norm2(x)

        return x

2.5 解码层

代码如下:

#解码层
class DecoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super(DecoderLayer, self).__init__()
        # 解码器层包含自注意机制、编码器-解码器注意力机制和前馈神经网络
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.enc_attn = MultiHeadAttention(d_model, n_heads)
        self.feedforward = Feedforward(d_model, d_ff, dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, enc_output, self_mask, context_mask):
        attn_output = self.self_attn(x, x, x, self_mask)
        x = x + self.dropout(attn_output)
        x = self.norm1(x)

        attn_output = self.enc_attn(x, enc_output, enc_output, context_mask)
        x = x + self.dropout(attn_output)
        x = self.norm2(x)

        ff_output = self.feedforward(x)
        x = x + self.dropout(ff_output)
        x = self.norm3(x)

        return x

2.6 Transformer模型搭建

代码如下:

#Transformer模型构建
class Transformer(nn.Module):
    def __init__(self, vocab_size, d_model, n_heads, n_encoder_layers,
                 n_decoder_layers, d_ff, dropout=0.1):
        super(Transformer, self).__init__()
        # Transformer模型包括词嵌入、位置编码、编码器和解码器
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.positional_encoding = PositionalEncoding(d_model, dropout)
        self.encoder_layers = nn.ModuleList(
            [EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_encoder_layers)])
        self.decoder_layers = nn.ModuleList(
            [DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_decoder_layers)])
        self.fc_out = nn.Linear(d_model, vocab_size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, src, trg, src_mask, trg_mask):
        # 词嵌入和位置编码
        src = self.embedding(src)
        src = self.positional_encoding(src)
        trg = self.embedding(trg)
        trg = self.positional_encoding(trg)

        # 编码器
        for layer in self.encoder_layers:
            src = layer(src, src_mask)

        # 解码器
        for layer in self.decoder_layers:
            trg = layer(trg, src, trg_mask, src_mask)

        # 输出层
        output = self.fc_out(trg)
        return output

三、使用示例

代码如下:

#使用示例
vocab_size = 10000     #假设词汇表大小为10000
d_model = 512
n_heads = 8
n_encoder_layers = 6
n_decoder_layers = 6
d_ff = 2048
dropout = 0.1
transformer_model = Transformer(vocab_size, d_model, n_heads, n_encoder_layers,
                                n_decoder_layers, d_ff, dropout)
# 定义输入,这里的输入是假设的,需要根据实际情况修改
src = torch.randint(0, vocab_size, (32, 10))
trg = torch.randint(0, vocab_size, (32, 20))
src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
trg_mask = (trg != 0).unsqueeze(1).unsqueeze(2)

output = transformer_model(src, trg, src_mask, trg_mask)
print(output.shape)

打印输出:

torch.Size([32, 20, 10000])


总结

对Transformer和自注意力机制的基本概念进行了解,并结合代码进行实际学习操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值