我们先看两句话:“甲把文件交给乙”和“乙把文件交给甲”。两句话使用了相同的词,但交文件的人和收文件的人发生了变化。因此,文本模型除了识别词,还需要理解词的位置及其上下文关系。

序列模型就用来处理这类有顺序的数据。分词器可以把文本切成字、词或子词等 Token,音频和传感器记录也可以按时间步排列。本课会说明序列任务怎样定义,以及语言模型怎样通过预测下一个 Token 学习。读者需要先了解神经网络训练中的批次、Logits 和交叉熵。

先看输出落在哪里

任务 输出单位 例子
序列分类 整段输入 判断反馈属于咨询还是报修
序列标注 输入中的位置 标出设备名和地点
条件生成 新的 Token 序列 根据维修记录写摘要

我们可以根据输出落在什么位置来区分任务。分类模型给整段文本一个类别,标注模型给选定位置分配标签,生成模型则产生新的 Token。对于标注任务,数据处理程序还要让标签与分词结果对齐:一个词拆成多个子词以后,原词标签分配到哪里、哪些位置忽略,都需要明确处理。Token 分类

从编号变成向量

分词器会为每个 Token 分配一个 ID,不过这个数字只用于查找词表。比如,编号 20 并不意味着它比编号 10 多一倍语义。Embedding 层会根据 ID 查出可学习向量,后续模型再处理这些向量。

RNN 将当前输入与上一步隐藏状态结合,可以写成 h_t=f(x_t,h_(t-1))。隐藏状态把前面的信息传到后面,LSTM 与 GRU 进一步用门控调节信息保留。Transformer 则通过注意力在位置之间交换信息,我们会在下一课展开。

这些结构最后还需要接任务对应的输出头。分类模型常从特殊位置或池化结果得到整段表示;标注模型为各有效位置输出类别;生成模型则在每一步产生词表分布。我们选择模型时,应当同时看输入结构与监督目标。

下一 Token 的答案要错开一位

因果语言模型学习的是“给定前文,下一个 Token 可能是什么”。我们可以把一句话的概率分解成多个条件概率,训练程序再提高真实后继 Token 的概率。下面这个示意序列展示了输入和答案怎样配对:

原序列: [BOS, 小灯, 亮了, EOS]
输入:   [BOS, 小灯, 亮了]
目标:   [小灯, 亮了, EOS]

这里的目标比输入向后错开一个位置。训练程序使用真实前缀,能够并行计算多个位置的损失;因果掩码则限制每个位置只使用当前与更早的有效信息。模型生成文本时,程序会把刚生成的 Token 接回前缀,再计算下一步。语言模型任务

部分框架会在模型内部完成标签移位,因此我们需要查看接口定义。假如数据处理与模型内部各移一次,实际训练目标就会偏离原来的后继预测。

PAD 和 EOS 怎样参与计算

不同文本长度不一,程序常用 PAD 把它们补成规整批次。PAD 主要用于排列数据,通常不参与目标损失;EOS 则告诉模型序列在这里结束,训练时常需要保留这个信号。

程序还会使用两类掩码。注意力掩码规定一个位置能读取哪些信息,损失掩码规定哪些目标参与计分。下面的例子让我们检查标签移位、EOS 数量和 PAD 排除是否按计划生效。

import numpy as np

PAD, BOS, EOS, VOCAB = 0, 1, 2, 8
tokens = np.array([[BOS, 3, 4, EOS, PAD], [BOS, 5, EOS, PAD, PAD]])
inputs, targets = tokens[:, :-1], tokens[:, 1:]
valid = targets != PAD
assert inputs.shape == targets.shape == (2, 4)
assert int(valid.sum()) == 5
assert int((targets[valid] == EOS).sum()) == 2

def masked_nll(logits, labels, mask):
    if not mask.any():
        raise ValueError("no supervised token")
    shifted = logits - logits.max(axis=-1, keepdims=True)
    log_probs = shifted - np.log(np.exp(shifted).sum(axis=-1, keepdims=True))
    nll = -np.take_along_axis(log_probs, labels[..., None], axis=-1)[..., 0]
    return float(nll[mask].mean())

logits = np.zeros((*targets.shape, VOCAB))
loss = masked_nll(logits, targets, valid)
changed = logits.copy()
changed[~valid, 0] = 100
assert np.isclose(loss, np.log(VOCAB))
assert np.isclose(masked_nll(changed, targets, valid), loss)
print("five targets counted; PAD ignored; two EOS targets retained")

代码统计出了五个有效目标,其中包含两个 EOS。我们改变 PAD 位置的 Logits 后,损失保持不变,说明程序确实排除了这些补齐目标。如果 PAD 与 EOS 共用 ID,程序就需要根据真实长度或掩码识别补齐位置,避免连结束目标也一起排除。

评估跟着任务走

任务目标也决定了评估方式。文本分类通常计算样本级指标,实体识别还要检查实体边界和类型,生成任务则需要观察实际回答质量、事实与指令遵循。语言模型的困惑度可以由平均负对数似然取指数得到,但研究者比较模型时,需要保持分词器、数据和计分方式一致。

练习:读者可以为一句话分别设计分类、实体标注与摘要任务,再手写两个不同长度样本的输入、标签和两类掩码。请逐个位置说明模型能看到什么,以及哪些答案参与训练。

下一篇:Transformer 的一次完整计算。

资料:《序列任务简介》第 1–10、47–50 页,作为疏锦行大模型专题的基础预习。示例只检查标签与损失,没有训练语言模型。