Transformer

Transformer架构图

image-20251016105755863

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 = ['', 'a', '3', '', '']然后变成x = [0, 5, 6, 1, 2, ..., 2] ,长度 50,全是数字,y也是同理,这样完成了从“人类可读的文本”到“机器可算的数字”的关键转换

位置编码层 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 = ['', 'a', '3', '', ''],去词典里面查找,然后变成x = [0, 5, 6, 1, 2, ..., 2],假设我们现在是[8,50],八句话,每句话50个词

我们通过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矩阵相乘

  1. 传入的 Q, K, V当前层的输入,它们可能来自上一层的输出或原始 embedding。
  2. 它们首先经过 LayerNorm 进行归一化,稳定训练。
  3. 然后分别通过 fc_Q, fc_K, fc_V(即 Linear(32,32))进行线性变换:
    每个 Linear 层包含一个 32×32 的可学习权重矩阵 W 和一个 32 维的偏置向量 b
    变换公式为:output = input @ W^T + b
    这些参数初始化为随机值,但在训练中通过反向传播不断更新
  4. 这个线性变换的作用是:将输入投影到适合 Query、Key、Value 的专用语义空间,使模型能更好地计算注意力。

image-20251016154957061

全连接输出层”(即 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. 引入非线性(最重要的原因!)
  • 注意力机制本质是 线性操作(加权平均)
  • 如果没有非线性,整个网络就是“一堆线性变换的组合”,等价于一个线性模型
  • ReLUfc1fc2 之间引入了非线性激活,让模型能拟合复杂函数

“注意力负责‘看谁重要’,FFN 负责‘想得更深’。”

  1. 特征变换与增强

    fc1: 32 → 64:把 32 维的词向量“展开”到 64 维的高维空间
    在这个高维空间中,模型可以:
    组合原始特征
    发现新的语义模式
    学习更丰富的表示
    fc2: 128 → 32:再压缩回原始维度,保留精华
    这就像“先发散思维,再收敛总结”。

  2. 位置独立处理(与注意力互补)
    模块 处理方式 作用
    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()]))

image-20251016170015084

详解

y[:, :-1] 与 y[:, 1:] 的对齐,我对这个很迷惑,为什么输入的时候不要最后一个,我们要预测的标签是不要第一个?

首先,我们在输入的时候,我们是用输入来预测输出,如果这个时候输入的有最后一个,你用这最后一个来预测什么?所以不需要有最后一个

其次,为什么要预测的标签不要第一个,因为我们是用sos来预测下一个真正的词,要错位一下

image-20251016201215788

[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]

image-20251016202249624

posted @ 2025-10-21 19:32  小衿  阅读(1)  评论(0)    收藏  举报