第TR3周:Pytorch复现Transformer

本人往期文章可查阅: 深度学习总结

📌本周任务详情:

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

一、Transformer 与 Seq2Seq

       在之前的任务重我们学习了 Seq2Seq,知晓了 Attention 为 RNN 带来的优点。那么有没有一种神经网络结构直接基于 attention 构造,并且不再依赖 RNN、LSTM 或者 CNN 网络结构了呢?答案便是:Transformer。Seq2Seq 和 Transformer 都是用于处理序列数据的深度学习模型,但它们是两种不同的架构。

1.Seq2Seq

  • 定义:Seq2Seq 是一种用于序列到序列任务的模型架构,最初用于机器翻译。这意味着它可以处理输入序列,并生成相应的输出序列。
  • 结构:Seq2Seq 模型通常由两个主要部分组成:编码器和解码器。编码器负责将输入序列编码为固定大小的向量,而解码器则使用此向量生成输出序列。
  • 问题:传统的Seq2Seq模型在处理长序列时可能会遇到梯度消失/爆炸等问题,而Transformer 模型的提出正是为了解决这些问题。

2.Transformer

  • 定义:Transformer是一种更现代的深度学习模型,专为处理序列数据而设计,最初用于自然语言处理任务。它不依赖于RNN或CNN等传统结构,而是引入了注意力机制。
  • 结构:Transformer 模型主要由编码器解码器组成,它们由自注意力层和全连接前馈网络组成。它使用注意力机制来捕捉输入序列中不同位置之间的依赖关系,同时通过多头注意力来提高模型的表达能力。
  • 优势:Transformer 的设计使其能够更好地处理长距离依赖关系,同时具有更好的并行性。

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

Transformer 论文原文:Attention Is All You Need

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

       上图便是 Transformer 整体结构图,与 Seq2Seq 模型类似,Transformer 模型结构中的左半部分为编码器,右半部分为解码器,下面我们来一步步拆解 Transformer。

二、Transformer 宏观结构

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

图2:Transformer黑盒输入和输出 

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

图3 :Encoders-Decoders

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

 图4:6层编码器和6层解码器

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

图5 :单层编码器

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

图6 :单层解码器

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

三、复现 Transformer

1.多头注意力机制

import math,torch
import torch.nn as nn

#device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
#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)
        # 这里把 K Q V 矩阵拆分为多组注意力
        # 最后一维就是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度
        # 每个 head 的向量长度是:300/6=50
        # 64 表示 batch size,6表示有6组注意力,10表示有10个词,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分数设置为-1e10,
        # 这里用“0”来指示哪些位置的词向量不能被attention到,比如padding位置,
        # 当然也可以用“1”或者其他数字来指示,主要涉及下面2行代码的改动
        if mask is not None:
            attention=attention.masked_fill(mask==0,-1e10)
            
            # 第二步:计算上一步结果的softmax,再经过dropout,得到attention
            # 注意,这里是对最后一维做softmax,也就是在输入序列的维度做softmax
            # attention:[64,6,12,10]
        attention=torch.softmax(attention,dim=-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.前馈传播

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

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(positonal 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和position encoding相加
        x=x+self.pe[:,:x.size(1)].requires_grad_(False)
        return self.dropout(x)

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):
        # 自注意力机制
        atten_output=self.self_attn(x,x,x,mask)
        x=x+self.dropout(atten_output)
        x=self.norm1(x)
        
        # 前馈神经网络
        ff_output=self.feedforward(x)
        x=x+self.dropout(ff_output)
        x=self.norm2(x)
        
        return x

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

6.Transformer模型构建

class Transformer(nn.Module):
    def __init__(self,vocab_size,d_model,n_heads,n_encoder_layer,
                 n_decoder_layer,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_layer)])
        self.decoder_layers=nn.ModuleList([DecoderLayer(d_model,n_heads,d_ff,dropout) for _ in range(n_decoder_layer)])
        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])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值