transformer架构
主要包括编码器和解码器的内部结构

编码器
编码器包括词元嵌入和位置编码,多头自注意力,前馈层,残差连接和层归一化,以下就是下面的相关代码解析。
位置编码和词元嵌入
将每个单词转化为向量位置,因为Transformer取消了RNN的循环方式进行文本输入,所以使用位置编码(Position Encoding)来表示每个词的位置,向量位置的计算是一种位置(pos)和维度的sin或cos的计算,这样pos和pos+k的位置编码就存在一种线性组合的关系,其实也是一种关系的表示。

偶数维度采用sin函数计算,奇数维度采用cos函数计算,d表示整体维度的大小。将单词与相同长度的向量进行相加,并返回计算后的位置,用于后续的输入。
python
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
# 定义位置编码类,继承自nn.Module,作用将词的位置信息转换为位置向量
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=512):
super(PositionalEncoding, self).__init__()
self.d_model = d_model
# 创建一个常量的矩阵,
pe = torch.zeros(max_len, d_model)
# 对位置进行编码
for pos in range(max_len):
# 单次遍历两个位置,偶数位置使用sin函数,奇数位置使用cos函数
for i in range(0, d_model, 2):
theta = pos / (10000 ** (i / d_model))
pe[pos, i] = math.sin(theta)
pe[pos, i + 1] = math.cos(theta)
# 扩展维度以便于后续操作,在0的位置加一个新的维度
pe = pe.unsqueeze(0)
self.register_buffer('pe',pe)
def forward(self, x):
# 对输入的x进行缩放,乘以根号d_model,可以使得单词嵌入表示相对大一些
x = x * math.sqrt(self.d_model)
# 获取输入x的长度
seq_len = x.size(1)
# 将位置编码矩阵和x进行相加
x = x + Variable(self.pe[:,:seq_len],requires_grad=False).cuda()
return x
自注意力层
上一步中将单词与位置向量叠加作为输入,而在自注意力层,将输入的x转换为Q(Query)查询,K(Query)关键字,V(value)值,用于表示每个单词在上下文中的权重关系,也就是每个单词在此位置所需要的关注程度。 计算的大体过程为:将Q和K进行点积操作,除以缩放因子实现稳定,再用softmax将结果处理,避免梯度爆炸和收敛效率差等问题。之后的结果与V进行点积得出自注意力公式。
为了进一步增强自注意力机制增强聚合上下文信息的能力,提出了多头自注意力机制,就是将经过线形变换的Q,K,V与输入的xi从多维度多个侧面做计算,将结果映射到多组的子空间中。
ini
# 多头自注意力机制
class MutiHeadAttention(nn.Module):
def __init__(self,heads,d_model,drouout = 0.1):
super(MutiHeadAttention,self).__init__()
self.d_model = d_model
self.d_k = d_model // heads
self.h = heads
self.q_linear = nn.Linear(d_model,d_model)
self.v_linear = nn.Linear(d_model,d_model)
self.k_linear = nn.Linear(d_model,d_model)
self.dropout = nn.Dropout(drouout)
self.out = nn.Linear(d_model,d_model)
# d_k表示缩放因子,此处为讲述中的注意力公式计算实现
def attention(q,k,v,d_k,mask=None,dropout=None):
# 此处为softmax计算之前的点积缩放计算
scores = torch.matmul(q,k.transpose(-2,-1)) / math.sqrt(d_k)
# 掩盖掉那些为了填补长度而增加的单元,使其通过softmax计算后为0
if mask is not None:
mask = mask.unsqueeze(1)
scores = scores.masked_fill(mask == 0, -1e9)
scores = F.softmax(scores,dim=-1)
if dropout is not None:
scores = dropout(scores)
# socres与value的矩阵乘法
output = torch.matmul(scores,v)
return output
def forward(self,q,k,v,mask=None):
bs = q.size(0)
# 线性层,将输入的q、k、v分别映射到d_model维度,分成h个头,每个头维度为d_k
k = self.k_linear(k).view(bs,-1,self.h,self.d_k)
q = self.q_linear(q).view(bs,-1,self.h,self.d_k)
v = self.v_linear(v).view(bs,-1,self.h,self.d_k)
# 转置操作,方便后续计算
k = k.transpose(1,2)
q = q.transpose(1,2)
v = v.transpose(1,2)
# 多头注意力计算
scores = self.attention(q,k,v,self.d_k,mask,self.dropout)
# 连接多个头并输入到最后线性层
scores = scores.transpose(1,2).contiguous().view(bs,-1,self.d_model)
output = self.out(scores)
return output
前馈层
前馈层接收自注意力层的输出作为输入,并通过Relu激活函数的两层全连接网络对输入做非线性变换,变换对模型的结果产生了很重要的结果。前馈层的隐状态的层数要比自注意力的多。

ruby
class FeedForward(nn.Module):
def __init__(self,d_model,d_ff=2048,drouout=0.1):
super(FeedForward,self).__init__()
# 定义第一个线性层(全连接层)
# 输入维度为 d_model,输出维度为 d_ff
# 这个线性层的作用是将输入的特征从 d_model 维度映射到 d_ff 维度
self.linear1 = nn.Linear(d_model,d_ff)
# 定义第二个线性层(全连接层)
# 输入维度为 d_ff,输出维度为 d_model
# 这个线性层的作用是将经过第一个线性层处理后的特征从 d_ff 维度映射回 d_model 维度
self.linear2 = nn.Linear(d_ff,d_model)
# 定义一个 Dropout 层,用于防止过拟合
# dropout 参数指定了在训练过程中随机丢弃神经元的概率
self.dropout = nn.Dropout(drouout)
def forward(self,x):
# 通过第一个线性层进行线性变换
# 然后使用 ReLU 激活函数引入非线性
# ReLU 函数将小于 0 的值置为 0,大于 0 的值保持不变
# 最后通过 Dropout 层随机丢弃一些神经元的输出,防止过拟合
x = self.dropout(F.relu(self.linear1(x)))
# 通过第二个线性层进行线性变换
# 将经过第一个线性层和 Dropout 层处理后的特征从 d_ff 维度映射回 d_model 维度
x = self.linear2(x)
return x
残差连接和层归一化
Transformer的网络结构庞大又复杂,编码器和解码器包含了很多Transformer块,中间的处理也有非线性变换,为了防止上一层的输出到该层输入过程的误解,采用了残差连接,直接用用直接的线性变换。层归一化借助标准差和均值的方式进行归一处理。


python
# 层归一化处理
class NormLayer(nn.Module):
def __init__(self,d_model,eps=1e-6):
super(NormLayer,self).__init__()
# 定义一个线性层,用于计算均值和方差
self.size = d_model
# 定义一个可训练的参数,用于调整归一化后的特征
self.alpha = nn.Parameter(torch.ones(self.size))
self.beta = nn.Parameter(torch.zeros(self.size))
self.eps = eps
def forward(self,x):
norm = self.alpha * (x - x.mean(dim=-1,keepdim=True)) / (x.std(dim=-1,keepdim=True) + self.eps) + self.beta
return norm
解码层
ini
# 定义解码器层类,继承自nn.Module
class DecoderLayer(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward):
super(DecoderLayer, self).__init__()
# 初始化第一个多头注意力机制(自注意力)
self.multihead_attention1 = nn.MultiheadAttention(d_model, nhead)
# 初始化第二个多头注意力机制(与编码器交互)
self.multihead_attention2 = nn.MultiheadAttention(d_model, nhead)
# 初始化前馈网络
self.feed_forward = nn.Sequential(
nn.Linear(d_model, dim_feedforward),
nn.ReLU(),
nn.Linear(dim_feedforward, d_model)
)
# 初始化第一层归一化层
self.layer_norm1 = nn.LayerNorm(d_model)
# 初始化第二层归一化层
self.layer_norm2 = nn.LayerNorm(d_model)
# 初始化第三层归一化层
self.layer_norm3 = nn.LayerNorm(d_model)
# 初始化dropout层
self.dropout = nn.Dropout(0.1)
def forward(self, tgt, memory, tgt_mask=None, memory_mask=None):
# 自注意力机制
attn_output1, _ = self.multihead_attention1(tgt, tgt, tgt, attn_mask=tgt_mask)
# 残差连接并应用dropout
tgt = tgt + self.dropout(attn_output1)
# 归一化
tgt = self.layer_norm1(tgt)
# 与编码器输出交互的注意力机制
attn_output2, _ = self.multihead_attention2(tgt, memory, memory, attn_mask=memory_mask)
# 残差连接并应用dropout
tgt = tgt + self.dropout(attn_output2)
# 归一化
tgt = self.layer_norm2(tgt)
# 前馈网络
ff_output = self.feed_forward(tgt)
# 残差连接并应用dropout
tgt = tgt + self.dropout(ff_output)
# 归一化
tgt = self.layer_norm3(tgt)
return tgt
学习资料来自于《大规模语言模型 从理论到实践》张奇 桂韬 郑锐 ⻩萱菁