观文听傑

返回

上一篇用滑动窗口、块稀疏与全局 token 规定了长序列中“谁能读谁”。但一个注意力结构即使信息边完全正确,也不会自己知道该学什么。给定“机器 学习 很 有趣”,训练代码必须把每个前缀变成监督信号:看到“机器”预测“学习”,看到“机器 学习”预测“很”。

这就是因果语言建模(Causal Language Modeling, CLM)的下一个 token 预测(Next-Token Prediction)。本文只追踪一条主线:同一条 token 序列怎样错开一位成为输入与标签,模型输出的 [N,L,V] logits 又怎样变成一个可反传的交叉熵标量。

01 语言模型的训练样本到底是什么?#

设词表大小为 VV,一段文本经分词器得到 x0,x1,,xL1x_0,x_1,\ldots,x_{L-1}。自回归分解(Autoregressive Factorization)把联合概率写成:

p(x0,,xL1)=t=0L1p(xtx<t)p(x_0,\ldots,x_{L-1})=\prod_{t=0}^{L-1}p(x_t\mid x_{<t})

若序列前放置开头符 <BOS>,训练对齐就是:

模型输入 input_ids:  [BOS, 机器, 学习, 很, 有趣]
监督标签 labels:     [机器, 学习, 很, 有趣, EOS]
                         ↑ 每一列预测右边那个 token

完整 tokens: [BOS, 机器, 学习, 很, 有趣, EOS]  长度 6
shift_logits: 位置 [0, 1, 2, 3, 4]              长度 5
shift_labels: id   [机器, 学习, 很, 有趣, EOS]   长度 5
text

最后一个输入位置没有序列内的下一个 token,第一个 token 也没有更早位置产生的预测,所以有效配对数是 L1L-1

02 因果掩码与标签右移解决不同问题#

  • 因果掩码(Causal Mask)限制隐藏状态 hth_t 只能由 xtx_{\le t} 计算,防止读取未来;
  • 标签右移(Label Shift)规定 hth_t 的输出要与 xt+1x_{t+1} 比较。
input_ids [N,L] ─► embedding [N,L,D] ─► causal Transformer


hidden [N,L,D] ─► linear W [V,D] ─► logits [N,L,V]
                                                │ shift one step

                                         labels [N,L]
text

只有右移而没有因果 mask,hth_t 可偷看 xt+1x_{t+1};只有因果 mask 而标签不右移,模型会学习复制当前位置。二者都正确,训练目标才和部署时“只给前缀、生成未来”一致。

03 logits 为什么不要先做 softmax?#

模型对位置 tt 输出未归一化分数 ztRVz_t\in\mathbb R^V。目标 token 为 yty_t 时:

t=logexp(zt,yt)v=0V1exp(zt,v)=zt,yt+logvexp(zt,v)\ell_t=-\log\frac{\exp(z_{t,y_t})}{\sum_{v=0}^{V-1}\exp(z_{t,v})} =-z_{t,y_t}+\log\sum_v\exp(z_{t,v})

第二种写法可用 LogSumExp 稳定计算。PyTorch 2.14 当前官方 torch.nn.functional.cross_entropy 直接接收 logits;若先 softmax 再传入,函数会把概率误当 logits,数值稳定性和梯度都会变差。

对有效位置集合 T\mathcal TL=T1(n,t)Tn,t\mathcal L=|\mathcal T|^{-1}\sum_{(n,t)\in\mathcal T}\ell_{n,t}。分母是有效 token 数,不是固定的 N(L1)N(L-1);padding 或边界位置被忽略后,分母也要改变。

04 用三个词和三个 logits 手算一次#

假设词表为 [甲, 乙, 丙],某位置的 logits 为 z=[2,1,0]z=[2,1,0],目标 y==1y=\text{乙}=1。归一化项为:

e2+e1+e07.389+2.718+1=11.107e^2+e^1+e^0\approx7.389+2.718+1=11.107

目标概率约为 2.718/11.107=0.2452.718/11.107=0.245,损失 log(0.245)1.408-\log(0.245)\approx1.408。梯度为:

zv=pv1(v=y)[0.665,0.755,0.090]\frac{\partial\ell}{\partial z_v}=p_v-\mathbb 1(v=y) \approx[0.665,-0.755,0.090]

梯度下降会提高“乙”的 logit,压低另外两项。若完全均匀猜测,loss 为 logV\log VV=50,000V=50,000 时约为 10.82。这是有用的初始化基线:一开始远低于它,要检查标签泄漏;远高于它,要检查 logit 尺度和数值异常。

05 [N,L,V] 怎样交给 Cross-Entropy?#

view(-1, V) 前调用 contiguous(),因为切片后的张量不保证内存连续。也可以用 reshape,但仍应理解最终把每个有效位置当成一次 VV 类分类。

06 padding 为什么不能当作普通词来学?#

A: [BOS, 甲, 乙, EOS, PAD, PAD]
B: [BOS, 丙, 丁, 戊, 己, EOS]
text

若直接使用所有 token id,A 会产生“EOS 后预测 PAD、PAD 后预测 PAD”的简单监督。模型被错误训练,长 padding 样本还会获得不成比例的权重。应把无需预测的位置替换成 ignore_index

def labels_from_attention_mask(input_ids, attention_mask):
    # input_ids/attention_mask: [N,L];1 表示真实 token
    labels = input_ids.clone()
    labels[attention_mask == 0] = -100
    return labels

labels = labels_from_attention_mask(input_ids, attention_mask)
loss = causal_lm_loss(logits, labels)
python

输入侧 padding attention mask 仍要交给 Transformer,阻止真实 Query 读取 padding Key。注意力 mask 控制信息流,loss mask 控制计分位置,两者不能替代。

07 拼接文档时,边界是否应该产生监督?#

数据管道常把文档拼接再切成定长 block。若没有边界符:

文档 A 末尾 ... a_last | b_first ... 文档 B 开头
                        ↑ 被迫学习 p(b_first | A)
text

至少应插入 <EOS>。是否训练 <EOS> → 下一文档首 token 取决于语义:连续章节可能允许跨块连续;无关网页通常应重置注意力,或忽略跨文档标签;packed sequence 中不同样本还必须使用块对角因果 mask,避免互相偷看。

def mask_cross_document_targets(labels, document_ids):
    # 两者 [N,L];labels[:,t+1] 是 logits[:,t] 的目标
    labels = labels.clone()
    crosses = document_ids[:, 1:] != document_ids[:, :-1]
    labels[:, 1:][crosses] = -100
    return labels
python

若保留 <EOS> 预测,要先决定忽略 a_last→EOS 还是 EOS→b_first,不要用一句“屏蔽边界”掩盖方向差异。

08 一个最小因果语言模型训练步#

真实模型还要加入位置表示,并把 padding mask 传入 block;此处省略是为了突出 loss,不能直接当作 packed-batch 生产实现。

09 从语料到更新的完整伪代码#

for documents in corpus:
    ids = tokenize(documents)
    ids = insert_boundary_tokens(ids)
    blocks, attention_mask, document_ids = pack(ids, max_length=L)

    logits = causal_transformer(blocks)          # [N,L,V]
    labels = copy(blocks)                        # [N,L]
    labels[padding_targets] = IGNORE
    labels[cross_document_targets] = IGNORE

    loss = CE(logits[:, :-1, :], labels[:, 1:])
    backward(loss)
    clip_gradients()
    optimizer_step()
text

相同模型、相同 token 数,如果 block 长度、边界处理、重复数据和有效 token 比例不同,学到的条件分布也会不同。

10 困惑度该怎样从 loss 得到?#

若 loss 是按 token 平均的自然对数负似然,PPL=exp(L)\mathrm{PPL}=\exp(\mathcal L)。loss 为 2 时困惑度约 7.39。它可理解为每一步的“有效候选规模”,但不能跨不同 tokenizer 直接比较。

跨 batch 也不能简单平均各 batch 的均值。应累加未约简 loss 与有效 token 数:

token_loss = F.cross_entropy(
    shift_logits.reshape(-1, vocab),
    shift_labels.reshape(-1),
    ignore_index=-100,
    reduction='sum',
)
valid_tokens = (shift_labels != -100).sum()

# 全部 batch 累加后
mean_nll = total_token_loss / total_valid_tokens
perplexity = torch.exp(mean_nll)
python

否则 padding 较多的小 batch 会和有效 token 很多的大 batch 获得相同权重。

11 怎样调试标签错位而不是猜?#

构造递增 id [10, 11, 12, 13],要求逐位置目标必须是 [11, 12, 13]

  1. 打印 input_ids[:, :-1]labels[:, 1:]
  2. 把正确目标 logit 设为 20、其余为 0,loss 应接近 0;
  3. 扰动未来 token,确认较早位置 logits 不变;
  4. 改变 padding token id,loss 必须不变;
  5. 全零 logits 的 loss 应接近 logV\log V
def perfect_logits(labels: torch.Tensor, vocab: int):
    n, length = labels.shape
    logits = torch.zeros(n, length - 1, vocab)
    targets = labels[:, 1:].clamp_min(0)
    logits.scatter_(2, targets[..., None], 20.0)
    return logits
python

-100 不能直接 scatter;这里 clamp_min(0) 只负责构造,最终 loss 仍用原始 labels 忽略对应位置。

12 常见的“loss 会降,但目标错了”#

错误表面现象真正后果
logits 与同位置 token 比较loss 很快下降学会复制输入
外部和模型内部各 shift 一次也可能收敛实际预测 t+2t+2
先 softmax 再交叉熵数值仍能运行把概率当 logits
padding 未忽略loss 很漂亮大量学习 PAD 转移
batch loss 直接平均曲线平滑不同长度权重错误
packed 文档共用普通 causal mask吞吐高不相关样本泄漏
EOSPAD 混用生成异常结束与补齐无法区分

13 训练与推理为何仍有差异?#

训练时使用教师强制(Teacher Forcing):位置 tt 总能看到真实历史 x<tx_{<t};推理时看到模型自己采样的历史。一次错误会改变后续条件,形成暴露偏差(Exposure Bias)。

Scheduled Sampling、序列级强化学习等方法试图缩小差异,却各自引入偏差或高方差。标准 next-token loss 的优势是目标明确、容易规模化、梯度稳定;它不保证事实正确、长程规划或人类偏好一致。

14 失败场景与相近目标#

  • 语料包含错误、偏见或重复时,最大似然会拟合这些分布;
  • 稀有但重要事实只贡献很少 token,平均 loss 对它不敏感;
  • 多种合理表达只给一条参考时,目标只奖励其中一路;
  • 长链推理按局部 token 计分,不直接优化最终任务成功;
  • 低 loss 可能来自记忆训练文本,需去污染测试区分。

掩码语言模型从双向上下文恢复遮盖 token;序列到序列 Decoder 还条件于源序列;对比学习比较表示的相对相似性;偏好优化比较整段回答。它们都不能简单等同于从左到右 next-token loss。

15 今天真正需要记住什么?#

  1. 因果 mask 决定“能看什么”,标签右移决定“预测什么”。
  2. [N,L,V] logits 与右移后的整数标签在有效位置做词表交叉熵。
  3. padding、文档边界和 packed sequence 要分别处理信息 mask 与 loss mask。
  4. 验证指标必须按有效 token 加权;困惑度只在 tokenizer 和协议一致时可比。

16 思考题与小练习#

  1. [BOS, A, B, EOS, PAD] 写出 shift_logits 位置、shift_labels 和有效 mask。若 EOS 也忽略,模型失去什么能力?
  2. 构造 V=4V=4 的两位置 logits,手算每项 loss、平均 loss 与 perplexity;再屏蔽一项,说明分母变化。
  3. 写单元测试识别“双重 shift”:让位置 tt 的最高 logit 指向 xt+1x_{t+1},比较正确标签和再右移标签的 loss。

相关工作#

  1. Bengio et al., A Neural Probabilistic Language Model,早期神经自回归语言模型。
  2. Radford et al., Improving Language Understanding by Generative Pre-Training,生成式预训练再微调。
  3. Radford et al., Language Models are Unsupervised Multitask Learners,规模化 next-token 训练的任务行为。
  4. Kaplan et al., Scaling Laws for Neural Language Models,模型、数据、计算与交叉熵的关系。
  5. Hoffmann et al., Training Compute-Optimal Large Language Models,计算预算下参数与 token 配比。

17 下一篇预告#

next-token loss 已把每个位置变成一次 VV 类预测,但词表可能有数万到数十万项,Embedding 与输出头随之成为巨大的参数和计算入口。下一篇将拆解子词分词:BPE 怎样从字符对合并出词表、未知词如何被组合,以及 tokenizer 为什么会改变长度、成本与 loss 的可比性。

一句文本怎样变成训练信号?因果语言模型的标签右移与 Next-Token Loss
https://zwjcode.cn/blog/causal-language-model-next-token-loss-label-shift
作者
发布于 2026年9月10日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。