基于Pytorch,从头开始实现Transformer(编码器部分)

news2024/11/29 20:56:43

Transformer理论部分参考知乎上的这篇文章

Transformer的Attention和Masked Attention部分参考知乎上的这篇文章

Transformer代码实现参考这篇文章,不过这篇文章多头注意力实现部分是错误的,需要注意。

完整代码放到github上了,链接


Transformer结构如下图所示:
在这里插入图片描述

(1)Self-Attention

在 Transformer 的 Encoder 中,数据首先会经过一个叫做 self-attention 的模块,得到一个加权后的特征向量 Z,这个 Z 就是论文公式1中的Attention(Q,K,V)
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T ( d k ) ) V Attention(Q, K, V) = softmax(\frac{QK^T}{\sqrt(d_k)})V Attention(Q,K,V)=softmax(( dk)QKT)V

在公式中,之所以要除以根号d_k(词向量或隐含层维度),原因有:1)防止输入softmax的数值过大,进而导致偏导数趋近于0;2)使得q*k的结果满足期望为0,方差为1,类似于归一化。可以参考这篇文章。

代码实现如下:

import torch
from torch import Tensor 
import torch.nn.functional as F

class SelfAttention(nn.Module):
    def __init__(self, input_vector_dim:int, dim_k=None, dim_v=None) -> None:
        """
        初始化SelfAttention,包含以下参数:
        input_vector_dim: 输入向量的维度,对应公式中的d_k。加入我们将单词编码为了10维的向量,则该值为10
        dim_k:矩阵W^k和W^q的维度
        dim_v:输出向量的维度。例如经过Attention后的输出向量,如果你想让它的维度是15,则该值为15;若不填,则取input_vector_dim,即与输入维度一致。
        """
        super().__init__()
        
        self.input_vector_dim = input_vector_dim
        
        # 如果dim_k和dim_v是None,则取输入向量维度
        if dim_k is None:
            dim_k = input_vector_dim
        if dim_v is None:
            dim_v = input_vector_dim
        
        """
        实际编写代码时,常用线性层来表示需要训练的矩阵,方便反向传播和参数更新
        """
        self.W_q = nn.Linear(input_vector_dim, dim_k, bias=False)
        self.W_k = nn.Linear(input_vector_dim, dim_k, bias=False)
        self.W_v = nn.Linear(input_vector_dim, dim_v, bias=False)
        
        # 这个是根号下d_k
        self._norm_fact = 1 / np.sqrt(dim_k)
    
    def forward(self, x):
        """ 
        进行前向传播
        x: 输入向量,size为(batch_size, input_num, input_vector_dim)
        """
        # 通过W_q, W_k, W_v计算出Q,K,V
        Q = self.W_q(x)
        K = self.W_k(x)
        V = self.W_v(x)
        
        """
        permute用于变换矩阵的size中对应元素的位置
        即:将K的size由(batch_size, input_num, output_vector_dim) 变为 (batch_size, output_vector_dim, input_num)
        ----
        0,1,2 代表各个元素的下标,即变换前 batch_size所在的位置是0,input_num所在的位置是1
        """
        K_T = K.permute(0, 2, 1)
        
        """ 
        bmm 是batch matrix-matrix product,即对一批矩阵进行矩阵相乘。相比于matmul,bmm不具备广播机制
        """
        atten = nn.Softmax(dim=-1)(torch.bmm(Q, K_T) * self._norm_fact)
        
        """ 
        最后再乘以 V
        """
        output = torch.bmm(atten, V)
        
        return output

上面的代码要注意 Tensor.bmm() 方法的应用。一般而言,我们输入的Q、K和V的数据形式为(Batchsize, Sequence_length, Feature_embedding),在进行矩阵乘法时,只对后两维执行。

(2)Multi-Head Attention

Multi-Head Attention 的示意图如下所示:

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 ) Q O MultiHead(Q, K, V) = Concat(head_1, ..., head_h)Q^O MultiHead(Q,K,V)=Concat(head1,...,headh)QO
在这里插入图片描述

def attention(query:Tensor, key:Tensor, value:Tensor):
    """ 
    计算Attention的结果。
    这里其实传入对的是Q,K,V;而Q,K,V的计算是放在模型中的,请参考后续的MultiHeadAttention类。
    
    这里的Q,K,V有两种shape,如果是Self-Attention,shape为(batch, 词数, d_model),
                            例如(1, 7, 128),表示batch_size为1,一句7个单词,每个单词128维
                            
                            但如果是Multi-Head Attention,则Shape为(batch, head数, 词数,d_model/head数),
                            例如(1, 8, 7, 16),表示batch_size为1,8个head,一句7个单词,128/8=16。
                            
                            这样其实也能看出来,所谓的MultiHead其实也就是将128拆开了。
                            
                            在Transformer中,由于使用的是MultiHead Attention,所以Q、K、V的shape只会是第二种。
    """

    """ 
    获取 d_model 的值。之所以这样可以获取,是因为 query 和输入的 shape 相同。
    若为Self-Attention,则最后一维都是词向量的维度,也就是 d_model 的值;
    若为MultiHead-Attention,则最后一维是 d_model/h,h表示head数。
    """
    d_k = query.size(-1)
    
    # 执行QK^T / 根号下d_k
    scores = torch.matmul(query, key.transpose(-2, -1)) / np.sqrt(d_k)
    
    """ 
    执行公式中的softmax
    这里的 p_attn 是一个方阵;若为Self-Attention,则shape为(batch, 词数, 词数);
    若为MultiHead-Attention,则shape为(batch, head数, 词数, 词数)
    """
    p_attn = scores.softmax(dim=-1)
    
    """ 
    最后再乘以 V.
    对于Self-Attention来说,结果 shape 为(batch, 词数, d_model),这也就是最终的结果了。
    对于MultiHead-Attention来说,结果 shape 为(batch, head数, 词数, d_model/head数)
    而这不是最终结果,后续还要将head合并,变为(batch, 词数, d_model)。不过这是MultiHeadAttention该做的事。
    """
    return torch.matmul(p_attn, value)


class MultiHeadAttention(nn.Module):
    def __init__(self, h:int, d_model:int) -> None:
        """ 
        h: head数
        d_model: d_model数
        """
        super().__init__()
        
        assert d_model % h == 0, "head number should be divided by d_model"
        
        self.d_k = d_model // h
        self.h = h

        # 定义W^q、W^k、W^v和W^o矩阵。
        self.linears = [
            nn.Linear(d_model, d_model),
            nn.Linear(d_model, d_model),
            nn.Linear(d_model, d_model),
            nn.Linear(d_model, d_model)
        ]
    
    def forward(self, x):
        # 获取batch_size
        batch_size = x.size(0)
        
        """ 
        1. 求出Q、K、V。这里是求MultiHead的Q、K、V,所以shape为(batch, head数, 词数, d_model/head数)
            1.1 首先,通过定义的W^q, W^k, W^v 求出Self-Attention的Q、K、V。此时,Q、K、V的shape为(batch, 词数, d_model)
                对应代码为 linear(x)
            1.2 分为多头,即将shape由(batch, 词数, d_model)变为(batch, 词数, head数, d_model/head数)
                对应代码为 .view(batch_size, -1, self.h, self.d_k)
            1.3 最终交换 词数 和 head数 这两个维度,将head数放在前面,最终shape变为(batch, head数, 词数, d_model/head数)
                对应代码为 .transpose(1,2)
        """
        query, key, value = [linear(x).view(batch_size, -1, self.h, self.d_k).transpose(1,2) for linear, x in zip(self.linears[:-1], (x, x, x))]

        """ 
        2. 求出Q、K、V后,通过Attention函数计算出Attention结果。
            这里x的shape为(batch, head数, 词数, d_model/head数)
            self.attn的shape为(batch, head数, 词数, 词数)
        """
        x = attention(query, key, value)
        
        """ 
        3. 将多个head再合并起来,即将x的shape由(batch, head数, 词数, d_model/head数)再变为(batch, 词数, d_model)
            3.1 首先, 交换 head数 和 词数 维度,结果为 (batch, 词数, head数, d_model/head数)
                对应代码为
        """
        x = x.transpose(1,2).reshape(batch_size, -1, self.h * self.d_k)

        """ 
        4. 最后,通过W^o矩阵再执行一次线性变换,得到最终结果
        """
        return self.linears[-1](x)

(3) Positional Encoding

在构建完整的 Transformer 之前,我们还需要一个组件—— Positional Encoding。请注意:MultiHeadAttention 没有在序列维度上运行,一起都是在特征维度上进行的,因此它与序列长度和顺序无关。

我们必须向模型提供位置信息,以便它知道输入序列中数据点的相对位置。

Transformer 论文中使用三角函数对位置进行编码:

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)

如何理解位置坐标编码? 参考这篇文章

在没有 Position embedding 的 Transformer 模型并不能捕捉序列的顺序,交换单词位置后 attention map 的对应位置数值也会进行交换,并不会产生数值变化,即没有词序信息。所以这时候想要将词序信息加入到模型中。

代码实现如下(参考这篇文章):

class PositionalEncoding(nn.Module):
    """
    基于三角函数的位置编码
    """
    def __init__(self, num_hiddens, dropout=0, max_len=1000):
        """
        num_hiddens:向量长度  
        max_len:序列最大长度
        dropout
        """
        super().__init__()
        self.dropout = nn.Dropout(dropout)
        
        # 创建一个足够长的P : (1, 1000, 32)
        self.P = torch.zeros((1, max_len, num_hiddens))
        
        # 本例中X的维度为(1000, 16)
        temp = 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(temp)   #::2意为指定步长为2 为[start_index : end_index : step]省略end_index的写法
        self.P[:, :, 1::2] = torch.cos(temp)

    def forward(self, X):
        X = X + self.P[:, :X.shape[1], :].to(X.device)  # torch 加法存在广播机制,因此可以避免batchsize不确定的问题
        return self.dropout(X)

(4) Encoder

Transformer采用的是编码器-解码器结构。编码器(左)处理输入序列并返回特征向量(或存储向量);解码器(右)处理目标序列,并合并来自编码器存储器的信息。解码器的输出是我们模型的预测结果。

在这里插入图片描述
我们可以彼此独立地对编码器和解码器进行编写代码,然后将它们组合。首先,我们先构建编码器(Encoder),具体也包括下述两个步骤,先编写Encoder layer,然后编写Encoder module

(4.1)Encoder layer

首先,构建残差连接功能模块:

class Residual(nn.Module):
    def __init__(self, sublayer: nn.Module, d_model: int, dropout: float = 0.1):
        """ 
        sublayer: Multi-Head Attention module 或者 Feed Forward module的一个.
        残差连接:上述两个module的输入x和module输出y相加,然后再进行归一化。
        """
        super().__init__()
        
        self.sublayer = sublayer  
        self.norm = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
 
    def forward(self, x: Tensor) -> Tensor:
        return self.norm(x + self.dropout(self.sublayer(x)))

然后,构建feed_forward功能模块:

class FeedForward(nn.Module):
    def __init__(self, d_model:int, hidden_num:int=2048) -> None:
        super().__init__()
        self.linear = nn.Sequential(
            nn.Linear(d_model, hidden_num),
            nn.ReLU(),
            nn.Linear(hidden_num, d_model)
        )
    
    def forward(self, x):
        return self.linear(x)    

最后,构建Encoder layer

class TransformerEncoderLayer(nn.Module):
    def __init__(
        self, 
        d_model: int = 512, 
        num_heads: int = 6, 
        dim_feedforward: int = 2048, 
        dropout: float = 0.1, 
     ):
        """ 
        d_model: 词向量维度数
        num_heads: 多头注意力机制的头数
        dim_feedforward: feedforward 模块的隐含层神经元数
        """
        super().__init__()
        
        """ 
        1. 进行多头注意力计算
        """
        self.multi_head_attention_module = Residual(
            sublayer=MultiHeadAttention(h=num_heads, d_model=d_model),
            d_model=d_model,
            dropout=dropout
        )
        
        """ 
        2. 进行前馈神经网络计算
        """
        self.feed_forward_module = Residual(
            sublayer=FeedForward(d_model=d_model, hidden_num=dim_feedforward),
            d_model=d_model,
            dropout=dropout
        )
        
    def forward(self, x:Tensor) -> Tensor:
        # 1. 多头注意力计算
        x = self.multi_head_attention_module(x)
        # 2. 前馈神经网络计算
        x = self.feed_forward_module(x)
        return x
(4.2) Encoder module

将残差连接、Encoder layer、feed forward功能模块拼接成为Encoder module

class TransformerEncoder(nn.Module):
    def __init__(
        self, 
        num_layers: int = 6,
        d_model: int = 512, 
        num_heads: int = 8, 
        dim_feedforward: int = 2048, 
        dropout: float = 0.1, 
        max_len: int = 1000
    ):
        """ 
        Transformer 编码器
        num_layers: TransformerEncoderLayer 层数
        d_model: 词向量维度数
        num_heads: 多头注意力机制的头数
        dim_feedforward: 前馈神经网络的隐含层神经元数
        dropout: 
        max_len: 三角函数位置编码的最大单词数量,需要设置超过数据集中句子单词长度
        """
        super().__init__()
        """ 
        1. 实例化 num_layers 个TransformerEncoderLayer
        """
        self.layers = nn.ModuleList([
            TransformerEncoderLayer(d_model, num_heads, dim_feedforward, dropout)
            for _ in range(num_layers)
        ])
        
        """ 
        2. 初始化位置编码器
        """
        self.pe = PositionalEncoding(num_hiddens=d_model, max_len=max_len)
        
    def forward(self, x: Tensor) -> Tensor:
        """ 
        x: (batchsize, sequence_number, d_model),sequence_number 表示句子的单词数量,d_model表示每个词的编码维度
        """
        
        """ 
        1. 对输入x添加位置编码信息
        """
        x  = self.pe(x)
        
        """ 
        2. 逐层计算,最后输出特征提取后的values
        """
        for layer in self.layers:
            x = layer(x)
 
        return x

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ASE50N06-ASEMI低压MOS管ASE50N06

编辑-Z ASE50N06在TO-252-2L封装里的静态漏极源导通电阻(RDS(ON))为15mΩ,是一款N沟道低压MOS管。ASE50N06的最大脉冲正向电流ISM为200A,零栅极电压漏极电流(IDSS)为1uA,其工作时耐温度范围为-55~175摄氏度。ASE50N06…

2年手动测试,裸辞后找不到工作怎么办?

我们可以从以下几个方面来具体分析下,想通了,理解透了,才能更好的利用资源提升自己。一、我会什么?先说第一个我会什么?第一反应:我只会功能测试,在之前的4年的中我只做了功能测试。内心存在一种…

LDPC码的编译码原理简述

关于fpga调用ldpc IP core的相关参数问题可以看我的另一篇文章 LDPC码由Gallager在1962年提出,全称为 Low Density Parity-check Codes 低密度奇偶校验码 它的译码性能可以逼近Shannon信道容量限,广富盛名的Turbo码也被证明是LDPC码的一个特例。并且LDPC…

软件测试简单么,如何自学?

软件测试是不是简单其实需要自己学习了才知道,难易程度对于不同的人来说都是不一样的。都是需要实际去尝试了之后才知道。也要看是和谁对比,对于java这种来说肯定是容易多了。 软件测试其实算是互联网三大技术岗位中最轻松的工种,但是你学起…

idea中的Maven导包失败问题解决总结

idea中的Maven导包失败问题解决总结 先确定idea和Maven 的配置文件settings 没有问题 找到我们本地的maven仓库,默认的maven仓库路径是在\C:\Users\用户名.m2下 有两个文件夹,repositotry是放具体jar包的,根据报错包的名,找对应文…

重识html

html 重识html 万维网用url统一资源定位符标识分布因特网上的各种文档 各种概念 URL: 统一资源定位器 它是WWW的统一资源定位标志,就是指网络地址 在WWW上,每一信息资源都有统一的且在网上唯一的地址 网页: 由文字 图片 视频 音乐各种元素排列组…

面试热点题:stl中vector与list的优缺点对比、以及list的迭代器与vector迭代器的区别

vector的优点 下标随机访问 vector的底层是一段连续的物理空间,所以支持随机访问尾插尾删效率高 跟数组类似,我们能够很轻易的找到最后一个元素,并完成各种操作cpu高速缓存命中率高 因为系统在底层拿空间的时候,是拿一段进cpu&am…

软件测试5年,一路走来的艰辛路程

前言 不论你是什么时候开始接触测试这个行业的,你首先听说的应该是功能测试。通过一些测试手段来验证开发做出的代码是否符合产品的需求?当然你也有自己对功能测试的理解,但是最近两年感觉功能测试好像不太受欢迎,同时不少同学真的…

JavaEE简单示例——动态SQL之更新操作<set>元素

简单介绍: 在之前我们做的学生管理系统的时候,曾经有一个环节是修改学生的数据。我们在修改的时候是必须将student对象的三个属性全部填入信息,然后全部修改才可以,这样会造成一个问题就是在我们明明只需要修改一个属性的时候却要…

华为外包测试2年,不甘被替换,168天的学习转岗成正式员工

我25岁的时候,华为外包测试,薪资13.5k,人在深圳。 内卷什么的就不说了,而且人在外包那些高级精英年薪大几十的咱也接触不到,就说说外包吧。假设以我为界限,25岁一线城市13.5k,那22-24大部分情况…

干货|最全焊接不良汇总,你知道如何避免吗?

良好的焊接,是保证电路稳定持久工作的前提。下面给出了常见的8种焊接缺陷,看看你遇到过多少种?焊接中的常见问题一、锡珠形成原因:渣或杂质:在焊接过程中,如果焊接区域附近有过多的杂质或者脏污&#xff0c…

毕业论文图片格式、分辨率选择及高质量Word转PDF方法

已知1:毕业论文盲评通常需要提交PDF文件。 已知2:PDF文件太大可能会导致翻页卡顿以及上传盲评网站失败。 已知3:Word转PDF方法不当可能会导致图像模糊。 已知4:打印机分辨率通常为300dpi。 问题1:论文插图分辨率设置…

分类预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆网络数据分类预测

分类预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆网络数据分类预测 目录分类预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆网络数据分类预测分类效果基本描述模型描述程序设计参考资料分类效果 基本描述 1.Matlab实现WOA-CNN-LSTM多特征分类预测&…

关于一个Java程序员马上要笔试了,临时抱佛脚,一晚上恶补45道简单SQL题,希望笔试能通过

MySQL随手练 / DQL篇 MySQL随手练——DQL篇 题目网盘下载:https://pan.baidu.com/s/1Ky-RJRNyfvlEJldNL_yQEQ?pwdlana 初始数据 表 course 表 student 表 teacher 表 sc 答案 :) —> :( —> :) 1. 查询 "01"课程比"02"课程成绩高的学生…

VXLAN基础介绍

VXLAN简介 VXLAN(Virtual eXtensible Local Area Network,虚拟扩展局域网)采用MAC in UDP封装方式,是NVO3(Network Virtualization over Layer 3)中的一种网络虚拟化技术。 VXLAN特性在本质上属于一种VPN技…

MySQl高可用集群搭建(MGR + ProxySQL + Keepalived)

前言 服务器规划(CentOS7.x) IP地址主机名部署角色192.168.x.101mysql01mysql192.168.x.102mysql02mysql192.168.x.103mysql03mysql192.168.x.104proxysql01proxysql、keepalived192.168.x.105proxysql02proxysql、keepalived 将安装包 mysql_cluster_…

这套软件测试试卷能打90分,直接入职字节吧

目录 一.填空 二、 判断题(正确的√,错误的╳)共10分,每小题1分 三、数据库部分:(共15分) 四、设计题。本题共 1 小题,满分 20分 一.填空 1、 系…

让ArcMap变得更加强大,用python执行地理处理以及编写自定义脚本工具箱

文章目录一、用python执行地理处理工具1.1 例:乘以0.00011.2 例:裁剪栅格1.3 哪里查看调用某工具的代码?二、用python批量执行地理处理工具2.1 必需的python语法知识for循环语句缩进的使用注释的使用2.2 一个批处理栅格的代码模板三、创建自定…

数组中的各种迭代API方法手写

js的数组上有很多实用的方法,不论是在遍历数组上,还是在操作数组内元素上,它有许多不同的遍历数组的方法,同时它还有着可以直接操作数组中间元素的方法。 接下来,我来带大家手写数组里的 遍历方法 。 Array.forEach(…

Elasticsearch在Linux中的单节点部署和集群部署

目录一、Elasticsearch简介二、Linux单节点部署1、软件下载解压2、创建用户3、修改配置文件4、切换到刚刚创建的用户启动软件5、测试三、Linux集群配置1、拷贝文件2、修改配置文件3、分别修改文件所有者4、启动三个软件5、测试四、问题总结1、在elasticsearch启动时如果报错内存…