Transformer
Transformer架构图
Input/Output数据定义
我们的Input sequence 通过分词等一系列操作之后变成很多的token,通过Embedding词编码之后,变成一个 [batch_size, seq_len, d_model]
的张量,其中每个词被映射为一个固定维度的向量(如 512 维),然后我们跟位置编码相加得到真正的输入,输入到Encoder中
# 定义字典
zidian_x = '<SOS>,<EOS>,<PAD>,0,1,2,3,4,5,6,7,8,9,q,w,e,r,t,y,u,i,o,p,a,s,d,f,g,h,j,k,l,z,x,c,v,b,n,m'
#enumerate(...)返回一个迭代器,每个元素是 (索引, 值) 的元组
zidian_x = {word: i for i, word in enumerate(zidian_x.split(','))}
'''
{
'<SOS>': 0,
'<EOS>': 1,
'<PAD>': 2,
'0': 3,
'1': 4,
...,
'm': 38
}
'''
zidian_xr = [k for k, v in zidian_x.items()]
zidian_y = {k.upper(): v for k, v in zidian_x.items()}
zidian_yr = [k for k, v in zidian_y.items()]
import random
import numpy as np
import torch
#生成和处理数据,把[a,b,c,d]->[0,1,2,3]
def get_data():
# 定义词集合
words = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'q', 'w', 'e', 'r',
't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',
'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'
]
# 定义每个词被选中的概率
p = np.array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
])
p = p / p.sum()
# 随机选n个词,句子的长度,句子最多是48个词,加上前后缀就是50个
n = random.randint(30, 48)
#replace=True 允许重复选择同一个词(即“放回抽样”)
x = np.random.choice(words, size=n, replace=True, p=p)
# 采样的结果就是x
x = x.tolist()
# y是对x的变换得到的-----------------------------------------
# 字母大写,数字取10以内的互补数
def f(i):
i = i.upper()
if not i.isdigit():#如果i不是数字
return i
i = 9 - int(i)
return str(i)
y = [f(i) for i in x]
#首字母双写,其实是逆序之前先把末尾字母重复一下加到列表后
y = y + [y[-1]]
# 逆序-------------------------------------------------------
y = y[::-1]
# 加上首尾符号
x = ['<SOS>'] + x + ['<EOS>']
y = ['<SOS>'] + y + ['<EOS>']
# 补pad到固定长度,先疯狂往后面加<PAD>,然后我们只需要前面50个
x = x + ['<PAD>'] * 50
y = y + ['<PAD>'] * 51
x = x[:50]
y = y[:51]
# 编码成数据,x = ['<SOS>', 'a', '3', '<EOS>', '<PAD>']---> x = [0, 5, 6, 1, 2, ..., 2] # 长度 50,全是数字,zidian_x[i]是取的value值,也就是词所对应的id
x = [zidian_x[i] for i in x]
y = [zidian_y[i] for i in y]
# 转tensor,PyTorch 模型只能接受 Tensor 作为输入,x = [0, 5, 6, 1, 2, ..., 2] # 每个数字代表一个词
#这些数字是离散的类别(category),不是连续的数值。在模型中,它们会被送入 Embedding 层,而 Embedding 层要求输入是 Long 类型的整数索引
x = torch.LongTensor(x)
y = torch.LongTensor(y)
return x, y
# 定义数据集
class Dataset(torch.utils.data.Dataset):
def __init__(self):
super(Dataset, self).__init__()
#它只是告诉 DataLoader:“我有 10 万个样本可以取”。
def __len__(self):
return 100000
def __getitem__(self, i):
return get_data()
# 数据加载器
'''
dataset=Dataset() DataLoader 会调用它的 __getitem__
batch_size=8 训练时一次输入 8 个句子给模型
drop_last=True 最后一个 batch 的样本数不足 8 个(比如只剩 5 个),就丢弃这个 batch
collate_fn=None 默认情况下,PyTorch 会自动将多个 Tensor 堆叠成一个 batch,所以 collate_fn=None 表示:使用默认的批处理函数
'''
loader = torch.utils.data.DataLoader(dataset=Dataset(),
batch_size=8,
drop_last=True,
shuffle=True,
collate_fn=None)
详解
把随机得到的x变成x = ['
位置编码层 Position Embedding
# 位置编码层
class PositionEmbedding(torch.nn.Module):
def __init__(self):
super().__init__()
# pos是第几个词,i是第几个维度,d_model是维度总数
def get_pe(pos, i, d_model):
fenmu = 1e4 ** (i / d_model)
pe = pos / fenmu
if i % 2 == 0:
return math.sin(pe)
return math.cos(pe)
# 初始化位置编码矩阵
pe = torch.empty(50, 32)
for i in range(50):
for j in range(32):
pe[i, j] = get_pe(i, j, 32)
#位置编码要跟词编码相加,所以他俩格式应该要一样的:[x,x,x],让位置编码能自动广播(broadcast)到任意 batch size 的输入上
pe = pe.unsqueeze(0)
# 定义为不更新的常量
self.register_buffer('pe', pe)
# 词编码层,词表的大小是39个,在data中定义过了,这里我们是创建了39个token向量组成一张表
self.embed = torch.nn.Embedding(39, 32)
# 初始化参数,表里面的数据是随机生成的
self.embed.weight.data.normal_(0, 0.1)
def forward(self, x):
# [8, 50] -> [8, 50, 32]
embed = self.embed(x)
# 词编码和位置编码相加
# [8, 50, 32] + [1, 50, 32] -> [8, 50, 32]
embed = embed + self.pe
return embed
详解
我们经过一系列的数据定义从Token → ID 映射(查词典),ID → Embedding 向量(查 embedding 表)最终输出形状
第一步也就是我们的 x经过拼接前缀与后缀,变成x = ['
我们通过torch.nn.Embedding(39, 32)创建了一个39x32的矩阵,也就是词向量表,里面是每个词所对应的维度(表里面的数据是随机的,这个表以后会发生变化,它会随着训练不断更新,让向量更好地表示语义)
第二步我们根据第一步的来的词后id,去向量表里面去查,得到每一个词对应的维度,我们现在就是变成了[8,50,32]
然后我们要加上位置编码,它是[1,50,32],我们给的最开始的话就是有顺序的,虽然[8,50,32]里面的数据都是随机的,但是每一句话中每一个词是都紧挨着旁边的词,但是机器不知道,位置编码是把这一句话中的词的位置说明白了
比如现在是
[[2,2],
[3,3]]这俩词里面的东西是随机的,但是他俩确实是在挨着的,现在位置编码跟他俩相加,因为靠近的位置之间编码是很像的,一旦相加就会把原来随机的数字加上一个有关系的数字
就比如一个平面图,我们先盖一个印章,每行就是一个词,一行挨着一行就是他们之间的位置信息,现在机器不知道这个位置信息,我们再在这个印章上面再印一个代表位置关系的印章,现在这个平面图就是机器能看懂的了
为什么位置编码能表示每个词之间的位置呢,因为 每个位置的编码是唯一的、有序的、且具有数学规律的
“位置靠前的词” → 编码向量相似
“相隔 k 个词” → 编码向量有固定模式
位置越靠后,编码向量缓慢变化
相邻位置的编码比较相似
相隔远的位置编码差异大
计算注意力--attention
# 注意力计算函数
def attention(Q: object, K: object, V: object, mask: object) -> Tensor:
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# Q,K,V = [b, 4, 50, 8]
# [b, 4, 50, 8] * [b, 4, 8, 50] -> [b, 4, 50, 50]
# Q,K矩阵相乘,求每个词相对其他所有词的注意力
#permute(0, 1, 3, 2)是把列的顺序进行调换,方便QK矩阵之间的相乘
score = torch.matmul(Q, K.permute(0, 1, 3, 2))
# 除以每个头维数的平方根,做数值缩放--------------------------------------------------------
#d_k(每个头的维度)较大时,Q 和 K 的点积结果会变得非常大,这会导致 softmax 函数进入饱和区,造成梯度消失。
score /= 8 ** 0.5
# mask遮盖,mask是true的地方都被替换成-inf,这样在计算softmax的时候,-inf会被压缩到0
# mask = [b, 1, 50, 50]
#这是让模型“知道什么不该看”的关键机制。
#原始序列中非 pad 的位置为 False,pad 位置为 True
score = score.masked_fill_(mask, -float('inf'))
score = torch.softmax(score, dim=-1)
# 以注意力分数乘以V,得到最终的注意力结果
# [b, 4, 50, 50] * [b, 4, 50, 8] -> [b, 4, 50, 8]
score = torch.matmul(score, V)
# 每个头计算的结果合一
# [b, 4, 50, 8] -> [b, 50, 32]
#-1:自动推断 batch size(即 b)
#将每个词的 4 个 8 维向量 拼接成一个 32 维向量
score = score.permute(0, 2, 1, 3).reshape(-1, 50, 32)
return score
详解
这个是只需要构建QKV是怎么相乘的,计算出得分的一个方法,不需要知道QKV是怎么来的,里面是什么内容
在这里我们遇见了mask,经过溯源,这个mask是model-->encoder-->multihead-->attention
model里面的生成mask,这个mask是50*50的矩阵,是50个词对50个词之间的注意力,pad列为true,在这个attention里面我们再把true变为负无穷,经过softmax变为0,得分矩阵score是50x50的
Multi-Head Attention
# 多头注意力计算层
class MultiHead(torch.nn.Module):
def __init__(self):
super().__init__()
#self.fc_Q = torch.nn.Linear(32, 32)之后就创建了那三个权重矩阵,里面的权重是随机的
#32x32 的权重矩阵 W 和一个长度为 32 的偏置向量 b,它们的值是随机的(通常是 Xavier/Glorot 初始化)。这些权重就是模型的“可学习参数”,训练过程中会不断更新。
self.fc_Q = torch.nn.Linear(32, 32)
self.fc_K = torch.nn.Linear(32, 32)
self.fc_V = torch.nn.Linear(32, 32)
#输出投影,保持输出维度与输入一致,用于整合多头信息(不同头可能关注不同模式(语法、语义等,学会如何组合它们)、增强表达能力、保持维度一致。
self.out_fc = torch.nn.Linear(32, 32)
# 规范化之后,均值是0,标准差是1
# BN是取不同样本做归一化
# LN是取不同通道做归一化
#bn是对一个batch的数据做归一化,ln是对每一个feature做归一化
#目的是稳定注意力机制的输入分布,防止训练发散
self.norm = torch.nn.LayerNorm(normalized_shape=32, elementwise_affine=True)
#用于防止模型过拟合,迫使模型不依赖于任何单个神经元
self.dropout = torch.nn.Dropout(p=0.1)
def forward(self, Q, K, V, mask):
# b句话,每句话50个词,每个词编码成32维向量
# Q,K,V = [b, 50, 32]
b = Q.shape[0]
# 保留下原始的Q,后面要做短接用
clone_Q = Q.clone()
# 规范化
Q = self.norm(Q)
K = self.norm(K)
V = self.norm(V)
# 线性运算,维度不变
# [b, 50, 32] -> [b, 50, 32]
K = self.fc_K(K)
V = self.fc_V(V)
Q = self.fc_Q(Q)
# 拆分成多个头
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# [b, 50, 32] -> [b, 4, 50, 8]
Q = Q.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
K = K.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
V = V.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
# 计算注意力
# [b, 4, 50, 8] -> [b, 50, 32]
score = attention(Q, K, V, mask)
# 计算输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
score = self.dropout(self.out_fc(score))
# 短接
score = clone_Q + score
return score
详解
在这个多头注意力计算的方法中,我们生成了wQ wK wV那三个矩阵,还有一个w0矩阵,我们的norm是选择的layernorm。
forward(self, Q, K, V, mask)这里面的其实就是三个处理后的input,线性运算就是让input与w矩阵相乘
- 传入的
Q, K, V
是当前层的输入,它们可能来自上一层的输出或原始 embedding。 - 它们首先经过
LayerNorm
进行归一化,稳定训练。 - 然后分别通过 fc_Q, fc_K, fc_V(即 Linear(32,32))进行线性变换:
每个 Linear 层包含一个 32×32 的可学习权重矩阵 W 和一个 32 维的偏置向量 b
变换公式为:output = input @ W^T + b
这些参数初始化为随机值,但在训练中通过反向传播不断更新 - 这个线性变换的作用是:将输入投影到适合 Query、Key、Value 的专用语义空间,使模型能更好地计算注意力。
全连接输出层”(即 out_fc)的作用是:把多头注意力的输出重新组合、映射回原始维度,以便进行残差连接和下一层处理。它不是“最终输出”,而是“内部整合器”。
Add & Norm
Add(残差连接)解决“梯度消失”问题,让信息能跨层流动
防止梯度消失
深层网络中,梯度在反向传播时会不断相乘
如果权重小于 1,梯度会指数级衰减 → 早期层几乎不更新
残差连接提供“捷径”:梯度可以直接从后面层“跳”到前面层
保留原始信息
Norm(层归一化)解决“数值不稳定”问题,让训练更平稳
每层输出的均值和方差可能变化很大,导致下一层输入不稳定,训练困难
对每个样本的特征维度做归一化,保证每层输入的分布稳定,加快收敛,防止爆炸/消失
Attention 输出可能很大或很小,加上 LayerNorm 后,输出被“拉回”到合理范围,避免 softmax 溢出或梯度爆炸
全连接输出层--FullyConnectedOutput
# 全连接输出层
class FullyConnectedOutput(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc = torch.nn.Sequential(
torch.nn.Linear(in_features=32, out_features=64),
torch.nn.ReLU(),
torch.nn.Linear(in_features=64, out_features=32),
torch.nn.Dropout(p=0.1),
)
self.norm = torch.nn.LayerNorm(normalized_shape=32,
elementwise_affine=True)
def forward(self, x):
# 保留下原始的x,后面要做短接用
clone_x = x.clone()
# 规范化
x = self.norm(x)
# 线性全连接运算
# [b, 50, 32] -> [b, 50, 32]
out = self.fc(x)
# 做短接
out = clone_x + out
return out
详解
FFN 的作用是:在每个位置独立地对注意力输出进行非线性变换和特征增强,让模型具备更强的表达能力。它由两个全连接层组成:第一个扩维(提升容量),第二个降维(恢复维度),中间用 ReLU 引入非线性。
为什么需要 FFN
1. 引入非线性(最重要的原因!)
- 注意力机制本质是 线性操作(加权平均)
- 如果没有非线性,整个网络就是“一堆线性变换的组合”,等价于一个线性模型
ReLU
在fc1
和fc2
之间引入了非线性激活,让模型能拟合复杂函数
“注意力负责‘看谁重要’,FFN 负责‘想得更深’。”
-
特征变换与增强
fc1: 32 → 64:把 32 维的词向量“展开”到 64 维的高维空间
在这个高维空间中,模型可以:
组合原始特征
发现新的语义模式
学习更丰富的表示
fc2: 128 → 32:再压缩回原始维度,保留精华
这就像“先发散思维,再收敛总结”。 -
位置独立处理(与注意力互补)
模块 处理方式 作用
Attention 跨位置交互 “这个词和那些词相关”
FFN 位置独立 “这个词本身意味着什么”
Encoder
Encoder中有两模块,一个是多头注意力计算层,一个是全连接输出层
# 编码器层
class EncoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, mask):
# 计算自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
score = self.mh(x, x, x, mask)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
out = self.fc(score)
return out
class Encoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = EncoderLayer()
self.layer_2 = EncoderLayer()
self.layer_3 = EncoderLayer()
def forward(self, x, mask):
x = self.layer_1(x, mask)
x = self.layer_2(x, mask)
x = self.layer_3(x, mask)
return x
Decoder
Decoder中有三个模块,一个是带Mask的多头注意力计算层,一个是多头注意力计算层,还一个是全连接输出层
# 解码器层
class DecoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh1 = MultiHead()
self.mh2 = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, y, mask_pad_x, mask_tril_y):
# 先计算y的自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.mh1(y, y, y, mask_tril_y)
# 结合x和y的注意力计算,维度不变
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.mh2(y, x, x, mask_pad_x)
# 全连接输出,维度变
# [b, 50, 32] -> [b, 50, 32]
y = self.fc(y)
return y
class Decoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = DecoderLayer()
self.layer_2 = DecoderLayer()
self.layer_3 = DecoderLayer()
def forward(self, x, y, mask_pad_x, mask_tril_y):
y = self.layer_1(x, y, mask_pad_x, mask_tril_y)
y = self.layer_2(x, y, mask_pad_x, mask_tril_y)
y = self.layer_3(x, y, mask_pad_x, mask_tril_y)
return y
计算Mask
def mask_pad(data):
# b句话,每句话50个词,这里是还没embed的
# data = [b, 50]
# 判断每个词是不是<PAD>
mask = data == zidian_x['<PAD>']
# [b, 50] -> [b, 1, 1, 50]
mask = mask.reshape(-1, 1, 1, 50)
# 在计算注意力时,是计算50个词和50个词相互之间的注意力,所以是个50*50的矩阵
# 是pad的列是true,意味着任何词对pad的注意力都是0
# 但是pad本身对其他词的注意力并不是0
# 所以是pad的行不是true
# 复制n次
# [b, 1, 1, 50] -> [b, 1, 50, 50]
mask = mask.expand(-1, 1, 50, 50)
return mask
def mask_tril(data):
# b句话,每句话50个词,这里是还没embed的
# data = [b, 50]
# 50*50的矩阵表示每个词对其他词是否可见
# 上三角矩阵,不包括对角线,意味着,对每个词而言,他只能看到他自己,和他之前的词,而看不到之后的词
# [1, 50, 50]
"""
[[0, 1, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 0]]"""
tril = 1 - torch.tril(torch.ones(1, 50, 50, dtype=torch.long))
# 判断y当中每个词是不是pad,如果是pad则不可见
# [b, 50]
mask = data == zidian_y['<PAD>']
# 变形+转型,为了之后的计算
# [b, 1, 50]
mask = mask.unsqueeze(1).long()
# mask和tril求并集
# [b, 1, 50] + [1, 50, 50] -> [b, 50, 50]
mask = mask + tril
# 转布尔型
mask = mask > 0
# 转布尔型,增加一个维度,便于后续的计算
mask = (mask == 1).unsqueeze(dim=1)
return mask
详解
mask_pad就是创造一个50x50的矩阵,是计算50个词和50个词相互之间的注意力,把pad列变为true
mask_tril因果掩码
tril = 1 - torch.tril(torch.ones(1, 50, 50, dtype=torch.long))
torch.tril(...):下三角矩阵(含对角线)
1 - tril:得到上三角(不含对角线),表示“未来位置”
Model
# 主模型
class Transformer(torch.nn.Module):
def __init__(self):
super().__init__()
self.embed_x = PositionEmbedding()
self.embed_y = PositionEmbedding()
self.encoder = Encoder()
self.decoder = Decoder()
self.fc_out = torch.nn.Linear(32, 39)
def forward(self, x, y):
# [b, 1, 50, 50]
mask_pad_x = mask_pad(x)
mask_tril_y = mask_tril(y)
# 编码,添加位置信息
# x = [b, 50] -> [b, 50, 32]
# y = [b, 50] -> [b, 50, 32]
x, y = self.embed_x(x), self.embed_y(y)
# 编码层计算
# [b, 50, 32] -> [b, 50, 32]
x = self.encoder(x, mask_pad_x)
# 解码层计算
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,维度变
# [b, 50, 32] -> [b, 50, 39]
y = self.fc_out(y)
return y
Main--主函数执行
# 预测函数
def predict(x):
# x = [1, 50]
model.eval()
# [1, 1, 50, 50]
mask_pad_x = mask_pad(x)
# 初始化输出,这个是固定值
# [1, 50]
# [[0,2,2,2...]]
target = [zidian_y['<SOS>']] + [zidian_y['<PAD>']] * 49
target = torch.LongTensor(target).unsqueeze(0)
# x编码,添加位置信息
# [1, 50] -> [1, 50, 32]
x = model.embed_x(x)
# 编码层计算,维度不变
# [1, 50, 32] -> [1, 50, 32]
x = model.encoder(x, mask_pad_x)
# 遍历生成第1个词到第49个词,很简单的逻辑,i=0,此时是拿sos预测,开始自己在脑海里面过一遍
for i in range(49):
# [1, 50]
y = target
# [1, 1, 50, 50]
mask_tril_y = mask_tril(y)
# y编码,添加位置信息
# [1, 50] -> [1, 50, 32]
y = model.embed_y(y)
# 解码层计算,维度不变
# [1, 50, 32],[1, 50, 32] -> [1, 50, 32]
y = model.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,39分类
# [1, 50, 32] -> [1, 50, 39]
out = model.fc_out(y)
# 取出当前词的输出
# [1, 50, 39] -> [1, 39],out:[1, 39],每个词的概率
out = out[:, i, :]
# 取出分类结果,.argmax(dim=1):在 39 个词中找概率最大的那个词的 ID,.detach():切断梯度
# [1, 39] -> [1]
out = out.argmax(dim=1).detach()
# 以当前词预测下一个词,填到结果中
target[:, i + 1] = out
return target
model = Transformer()
loss_func = torch.nn.CrossEntropyLoss()
optim = torch.optim.Adam(model.parameters(), lr=2e-3)
sched = torch.optim.lr_scheduler.StepLR(optim, step_size=3, gamma=0.5)
for epoch in range(1):
for i, (x, y) in enumerate(loader):
# x = [8, 50]
# y = [8, 51]
# 在训练时,是拿y的每一个字符输入,预测下一个字符,所以不需要最后一个字,为什么要忽略最后一个字,是因为最后一个字后面就没下一个可以预测了,所以不能作为输入给模型
# [8, 50, 39],8 个样本,每个样本 50 个位置,每个位置预测 39 个词的概率
pred = model(x, y[:, :-1])
# [8, 50, 39] -> [400, 39],为了把“序列生成任务”转化为“400 个独立的分类任务”,方便计算交叉熵损失(CrossEntropyLoss)。
pred = pred.reshape(-1, 39)
# [8, 51] -> [400],这正是我们想要的“下一个词”序列,
y = y[:, 1:].reshape(-1)
# 忽略pad,过滤掉所有 <PAD> 位置的预测和标签,确保模型只在“有意义的词”上计算损失,避免被填充符干扰
#[True, True, True, False, False, True, ...]
select = y != zidian_y['<PAD>']
# [400,39] -> [N,39]
pred = pred[select]
#[400] -> [N],损失只在“真实语义词”上计算
y = y[select]
loss = loss_func(pred, y)
#清空上一轮的“学习笔记”
optim.zero_grad()
#反向传播,计算梯度
loss.backward()
#根据梯度,更新模型参数
optim.step()#预测 → 算错 → 改参数 → 再预测 → 更准
if i % 200 == 0:
# [select, 39] -> [select],argmax(1):在词表维度取最大概率的词 ID,取预测结果
pred = pred.argmax(1)
#计算预测正确的词数
correct = (pred == y).sum().item()
#计算词级准确率(Token Accuracy)
accuracy = correct / len(pred)
#获取当前学习率
lr = optim.param_groups[0]['lr']
print(epoch, i, lr, loss.item(), accuracy)
sched.step()
# 测试
for i, (x, y) in enumerate(loader):
break
for i in range(8):
print(i)
'''
x[i].tolist():第 i 个样本的输入序列(ID 列表)
[zidian_xr[i] for i in ...]:把每个 ID 转成对应的字符
''.join(...):拼接成字符串
'''
print(''.join([zidian_xr[i] for i in x[i].tolist()]))
print(''.join([zidian_yr[i] for i in y[i].tolist()]))
'''
x[i].unsqueeze(0):把 [50] 变成 [1, 50],增加 batch 维度
predict(...):调用你的 predict 函数(应该是自回归生成)
[0]:取出第一个(也是唯一一个)样本的预测结果
.tolist() → 转列表
zidian_yr[i] → 转字符
''.join → 拼接
'''
print(''.join([zidian_yr[i] for i in predict(x[i].unsqueeze(0))[0].tolist()]))
详解
y[:, :-1] 与 y[:, 1:] 的对齐,我对这个很迷惑,为什么输入的时候不要最后一个,我们要预测的标签是不要第一个?
首先,我们在输入的时候,我们是用输入来预测输出,如果这个时候输入的有最后一个,你用这最后一个来预测什么?所以不需要有最后一个
其次,为什么要预测的标签不要第一个,因为我们是用sos来预测下一个真正的词,要错位一下
[8,50,39] -> [400,39] ,[8,50] -> [400]:为什么,因为我们要计算损失值
CrossEntropyLoss 要求:
pred: [N, C] → N 个样本,C 个类别
y: [N] → N 个真实标签(整数)
select = y != zidian_y['
pred = pred[select]
y = y[select]