使用Pytorch从零开始构建Transformer

news2024/9/29 13:16:41

在本教程中,我们将使用 PyTorch 从头开始​​构建一个基本的 Transformer 模型。Vaswani 等人提出的 Transformer 模型。在论文“Attention is All You Need”中,是一种专为序列到序列任务(例如机器翻译和文本摘要)而设计的深度学习架构。它基于自注意力机制,已成为许多最先进的自然语言处理模型(如 GPT 和 BERT)的基础。

要详细了解 Transformer 模型,请访问这两篇文章:

  1. All you need to know about ‘Attention’ and ‘Transformers’ — In-depth Understanding — Part 1
  2. All you need to know about ‘Attention’ and ‘Transformers’ — In-depth Understanding — Part 2

要构建 Transformer 模型,我们将按照以下步骤操作:

  1. 导入必要的库和模块;
  2. 定义基本构建块:多头注意力、位置前馈网络、位置编码;
  3. 构建编码器和解码器层;
  4. 组合编码器和解码器层以创建完整的Transformer 模型;
  5. 准备样本数据;
  6. 训练模型

让我们首先导入必要的库和模块。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import math
import copy

现在,我们将定义 Transformer 模型的基本构建块。

多头注意力

在这里插入图片描述
多头注意力机制计算序列中每对位置之间的注意力。它由多个“注意力头”组成,捕获输入序列的不同方面。

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)
        
    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        if mask is not None:
            attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
        attn_probs = torch.softmax(attn_scores, dim=-1)
        output = torch.matmul(attn_probs, V)
        return output
        
    def split_heads(self, x):
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
        
    def combine_heads(self, x):
        batch_size, _, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
        
    def forward(self, Q, K, V, mask=None):
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))
        
        attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
        output = self.W_o(self.combine_heads(attn_output))
        return output

MultiHeadAttention 代码使用输入参数和线性变换层初始化模块。它计算注意力分数,将输入张量重塑为多个头,并组合所有头的注意力输出。前向方法计算多头自注意力,使模型能够关注输入序列的某些不同方面。

位置式前馈网络

class PositionWiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(PositionWiseFeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

PositionWiseFeedForward 类扩展了 PyTorch 的 nn.Module 并实现了位置明智的前馈网络。该类使用两个线性变换层和一个 ReLU 激活函数进行初始化。前向方法依次应用这些变换和激活函数来计算输出。此过程使模型能够在进行预测时考虑输入元素的位置。

位置编码

位置编码用于注入输入序列中每个标记的位置信息。它使用不同频率的正弦和余弦函数来生成位置编码。

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_seq_length):
        super(PositionalEncoding, self).__init__()
        
        pe = torch.zeros(max_seq_length, d_model)
        position = torch.arange(0, max_seq_length, 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)
        
        self.register_buffer('pe', pe.unsqueeze(0))
        
    def forward(self, x):
        return x + self.pe[:, :x.size(1)]

PositionalEncoding 类使用输入参数 d_model 和 max_seq_length 进行初始化,创建一个张量来存储位置编码值。该类根据缩放因子 div_term 分别计算偶数和奇数索引的正弦和余弦值。前向方法通过将存储的位置编码值添加到输入张量来计算位置编码,从而使模型能够捕获输入序列的位置信息。

现在,我们将构建编码器和解码器层。

编码器层

在这里插入图片描述
编码器层由多头注意力层、位置前馈层和两个层归一化层组成。

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        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 = self.norm1(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        return x

EncoderLayer 类使用输入参数和组件进行初始化,包括 MultiHeadAttention 模块、PositionWiseFeedForward 模块、两层归一化模块和 dropout 层。前向方法通过应用自注意力、将注意力输出添加到输入张量并对结果进行归一化来计算编码器层输出。然后,它计算位置前馈输出,将其与归一化的自注意力输出相结合,并在返回处理后的张量之前对最终结果进行归一化。

解码层

在这里插入图片描述
解码器层由两个多头注意力层、一个位置前馈层和三个层归一化层组成。

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.cross_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        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, src_mask, tgt_mask):
        attn_output = self.self_attn(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout(attn_output))
        attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
        x = self.norm2(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))
        return x

DecoderLayer 使用输入参数和组件进行初始化,例如用于屏蔽自注意力和交叉注意力的 MultiHeadAttention 模块、PositionWiseFeedForward 模块、三层归一化模块和 dropout 层。

前向方法通过执行以下步骤来计算解码器层输出:

计算屏蔽自注意力输出并将其添加到输入张量,然后进行 dropout 和层归一化。
计算解码器和编码器输出之间的交叉注意力输出,并将其添加到归一化屏蔽自注意力输出中,然后进行 dropout 和层归一化。
计算位置前馈输出并将其与归一化交叉注意力输出相结合,然后进行 dropout 和层归一化。
返回处理后的张量。
这些操作使解码器能够基于输入和编码器输出生成目标序列。

现在,让我们组合编码器层和解码器层来创建完整的 Transformer 模型。

Transformer Model

在这里插入图片描述

将它们合并在一起:

class Transformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
        super(Transformer, self).__init__()
        self.encoder_embedding = nn.Embedding(src_vocab_size, d_model)
        self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model)
        self.positional_encoding = PositionalEncoding(d_model, max_seq_length)

        self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
        self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])

        self.fc = nn.Linear(d_model, tgt_vocab_size)
        self.dropout = nn.Dropout(dropout)

    def generate_mask(self, src, tgt):
        src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
        tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(3)
        seq_length = tgt.size(1)
        nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
        tgt_mask = tgt_mask & nopeak_mask
        return src_mask, tgt_mask

    def forward(self, src, tgt):
        src_mask, tgt_mask = self.generate_mask(src, tgt)
        src_embedded = self.dropout(self.positional_encoding(self.encoder_embedding(src)))
        tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))

        enc_output = src_embedded
        for enc_layer in self.encoder_layers:
            enc_output = enc_layer(enc_output, src_mask)

        dec_output = tgt_embedded
        for dec_layer in self.decoder_layers:
            dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)

        output = self.fc(dec_output)
        return output

Transformer 类组合了前面定义的模块来创建完整的 Transformer 模型。在初始化期间,Transformer 模块设置输入参数并初始化各种组件,包括源序列和目标序列的嵌入层、PositionalEncoding 模块、用于创建堆叠层的 EncoderLayer 和 DecoderLayer 模块、用于投影解码器输出的线性层以及 dropout 层。

generate_mask 方法为源序列和目标序列创建二进制掩码,以忽略填充标记并防止解码器关注未来的标记。forward方法通过以下步骤计算Transformer模型的输出:

  1. 使用generate_mask方法生成源和目标掩码。
  2. 计算源嵌入和目标嵌入,并应用位置编码和丢失。
  3. 通过编码器层处理源序列,更新enc_output 张量。
  4. 使用 enc_output 和掩码通过解码器层处理目标序列,并更新 dec_output 张量。
  5. 将线性投影层应用于解码器输出,获得输出 logits。

这些步骤使 Transformer 模型能够处理输入序列并根据其组件的组合功能生成输出序列。

准备样本数据

在此示例中,我们将创建一个玩具数据集用于演示目的。在实践中,您将使用更大的数据集,预处理文本,并为源语言和目标语言创建词汇映射。

src_vocab_size = 5000
tgt_vocab_size = 5000
d_model = 512
num_heads = 8
num_layers = 6
d_ff = 2048
max_seq_length = 100
dropout = 0.1

transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

# Generate random sample data
src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)
tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)

训练模型

现在我们将使用样本数据训练模型。在实践中,您将使用更大的数据集并将其拆分为训练集和验证集。

criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

transformer.train()

for epoch in range(100):
    optimizer.zero_grad()
    output = transformer(src_data, tgt_data[:, :-1])
    loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, 1:].contiguous().view(-1))
    loss.backward()
    optimizer.step()
    print(f"Epoch: {epoch+1}, Loss: {loss.item()}")

我们可以使用这种方式在 Pytorch 中从头开始构建一个简单的 Transformer。所有大型语言模型都使用这些 Transformer 编码器或解码器块进行训练。因此,了解基础Transformer网络非常重要。希望这篇文章对所有想要深入研究大语言模型(LLM)的人有所帮助。

参考文献

Attention is all you need
A. Vaswani, N. Shazeer, N. Parmar, J. Uszkoreit, L. Jones, A. Gomez, {. Kaiser, and I. Polosukhin. Advances in Neural Information Processing Systems , page 5998–6008. (2017)

博文译自Arjun Sarkar 的博客。

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

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

相关文章

C++算法入门练习——相同的二叉查找树

将第一组n​个互不相同的正整数先后插入到一棵空的二叉查找树中,得到二叉查找树T1​;再将第二组n个互不相同的正整数先后插入到一棵空的二叉查找树中,得到二叉查找树T2​。判断T1​和T2​​是否是同一棵二叉查找树。 二叉查找(搜索)树定义&am…

python变量、常量、数据类型

一、变量 变量是存储在内存中的值,这就意味着在创建变量时会在内存中开辟一个空间。 基于变量的数据类型,解释器会分配指定内存,并决定什么数据可以被存储在内存中。 因此,变量可以指定不同的数据类型,这些变量可以…

Delphi 12 Athens 发布了!

官方安装包 ☞ https://altd.embarcadero.com/download/radstudio/12.0/RADStudio_12_0_4915718.iso 安装辅助工具、控件可以戳这里 :Delphi 12 资源 RAD Stuido 12 Athens ,这次更新的细节还是比较多的,但主要还是多端(iOS、An…

来聊聊JVM中的类加载过程以及双亲委派模型(学习Java必知内容)

文章目录 1. 类加载过程加载验证准备解析初始化 2. 双亲委派模型一个类的加载流程双亲委派模型的优点 总结 1. 类加载过程 在整个 JVM 执行过程中, 和我们程序员关系最密切的就是类加载的过程, 所以接下来我们来看下类加载的执行流程. 对于一个类来说, 它的生命周期是这样的:…

盘点63个Python登录第三方源码Python爱好者不容错过

盘点63个Python登录第三方源码Python爱好者不容错过 学习知识费力气,收集整理更不易。 知识付费甚欢喜,为咱码农谋福利。 链接:https://pan.baidu.com/s/1l7oooH9YovHmWzQ_58FRdg?pwd8888 提取码:8888 项目名称 A headless…

安卓手机好用的清单软件有哪些?

生活中每个人都有丢三落四的习惯,伴随着生活节奏的加快,人们常忘事的情况会更加频繁的出现,这时候很多人就开始选择手机上记录清单类的软件,安卓手机在手机市场中占有很大的分量,在安卓手机上好用的记录清单的软件有哪…

1688商品详情数据接口(1688.item_get)

1688商品详情数据接口是一种程序化的接口,通过这个接口,商家或开发者可以使用自己的编程技能,对1688平台上的商品信息进行查询、获取和更新。这个接口允许商家根据自身的需求,获取商品的详细信息,例如价格、库存、描述…

没有PDF密码,如何解密?

PDF文件有两种密码,一个打开密码、一个限制编辑密码,因为PDF文件设置了密码,那么打开、编辑PDF文件就会受到限制。忘记了PDF密码该如何解密? PDF和office一样,可以对文件进行加密,但是没有提供恢复密码的功…

SQLite3 数据库学习(五):Qt 数据库高级操作

参考引用 SQLite 权威指南&#xff08;第二版&#xff09;SQLite3 入门 1. Qt 数据库密码加密 MD5 加密在线工具 1.1 加密流程 加密后的密码都是不可逆的 1.2 代码实现 loginsqlite.h #ifndef LOGINSQLITE_H #define LOGINSQLITE_H#include <QWidget> #include <Q…

第15届蓝桥杯Scratch选拔赛中级(STEMA)真题2023年10月

一、单选题 1.运行以下哪个程序后&#xff0c;巨嘴鸟会向下移动&#xff1f;&#xff08; &#xff09; A. B. C. D. 2.运行以下程序后&#xff0c; 能看到几只河豚鱼&#xff08; &#xff09;&#xff1f; A.3 B.4 C.6 D.7 3.以下运算结果为“False”的是&#xff08…

Python教程73:Pandas中一维数组Series学习

创建一维数据类型Series dataNone 要转化为Series的数据(也可用dict直接设置行索引) 若是标量则必须设置索引,该值会重复,来匹配索引的长度 indexNone 设置行索引 dtypeNone 设置数据类型(使用numpy数据类型) nameNone 设置Series的name属性 copyFalse 不复制 (当data为ndarray…

常用服务注册中心与发现(Eurake、zookeeper、Nacos)笔记(一)基础概念

基础概念 注册中心 在服务治理框架中&#xff0c;通常都会构建一个注册中心&#xff0c;每个服务单元向注册中心登记自己提供的服务&#xff0c;将主机与端口号、版本号、通信协议等一些附加信息告知注册中心&#xff0c;注册中心按照服务名分类组织服务清单&#xff0c;服务…

群晖NAS搭建WebDav服务做文件共享,可随时随地远程访问

文章目录 1. 在群晖套件中心安装WebDav Server套件1.1 安装完成后&#xff0c;启动webdav服务&#xff0c;并勾选HTTP复选框 2. 局域网测试WebDav服务2.1 下载RaiDrive客户端2.2 打开RaiDrive&#xff0c;设置界面语言可以选择中文2.3 点击添加按钮&#xff0c;新建虚拟驱动区2…

碳化硅MOS/超结MOS在直流充电桩上的应用-REASUNOS瑞森半导体

一、前言 直流充电桩是新能源汽车直流充电桩的简称&#xff0c;一般也被叫做“快充”。直流充电桩一般与交流电网连接&#xff0c;可作为非车载电动汽车的动力补充&#xff0c;是一种直流工作电源的电源控制装置&#xff0c;可以提供充足的电量&#xff0c;输出电压和电流可以…

.symtab ELF符号表(转载)

1. 符号表&#xff08;symbol table&#xff09;介绍 ELF文件中的“符号表&#xff08;symbol table&#xff09;”包含的是程序中的符号信息 – 这些符号代表的或许是定义&#xff08;例如定义全局变量时使用的变量名&#xff0c;或者定义函数时使用的函数名&#xff09;&…

探索网络模型与协议:从OSI到HTTPs的原理解析

一、OSI网络模型 OSI&#xff08;Open Systems Interconnection&#xff09;七层网络参考模型和TCP/IP四层模型都是用于理解和设计计算机网络的框架&#xff0c;但它们之间存在一些差异。 1、七层 vs 四层 OSI七层网络参考模型&#xff1a; 物理层&#xff08;Physical Laye…

博士研究生不会编程,也没有使用过Python,是否很失败

首先&#xff0c;对于博士研究生来说&#xff0c;虽然在学习和科研的过程中会涉猎到大量的专业知识&#xff0c;但是同样也会错过很多知识&#xff0c;对于非计算机相关专业的博士研究生来说&#xff0c;没有使用过Python&#xff0c;或者说编程能力比较弱也是比较正常的情况&a…

Linux CentOS+宝塔面板工具结合内网穿透实现网站发布至公网可访问

使用Typecho搭建个人博客网站&#xff0c;并内网穿透实现公网访问 文章目录 使用Typecho搭建个人博客网站&#xff0c;并内网穿透实现公网访问前言1. 安装环境2. 下载Typecho3. 创建站点4. 访问Typecho5. 安装cpolar6. 远程访问Typecho7. 固定远程访问地址8. 配置typecho 前言 …

「快学Docker」监控和日志记录容器的健康和性能

「快学Docker」监控和日志记录容器的健康和性能 1. 容器健康状态监控2. 性能监控3. 日志记录几种采集架构图 4. 监控工具和平台cAdvisor&#xff08;Container Advisor&#xff09;PrometheusGrafana 5. 自动化运维 1. 容器健康状态监控 方法1&#xff1a;需要实时监测容器的运…

医学检验(LIS)源码,实现检验结果审核自动化、检验无纸化、双向通讯

医学检验(LIS)管理系统 随着全自动生化分析仪、全自动免疫分析仪和全自动血球计数器等仪器的使用&#xff0c;检验科的大多数项目实现了全自动化分析。全自动化分析引入后&#xff0c;组合化验增多&#xff0c;更好的满足了临床需要&#xff0c;也使检验科的工作量和检验数据成…