读完这篇,你能做什么
能手工生成文档分段掩码,验证打包与逐文档结果一致,区分输入Token和有效目标Token。
学习理由、准备要求与知识点
为什么学它
打包后的文档边界如果处理错了,模型看到的上下文和学习目标就会改变。提速前要先确认它仍在做原定的预测任务。
先读这些内容
知识点清单
- 因果语言建模目标
- 文档边界与结束标记
- 连续流与独立文档语义
- 块对角因果掩码
- 标签移位与跨文档屏蔽
- 有效Token加权归约
- 顺序打包算法
- 填充效率和二次注意力开销
运行前先确认Java17、PyTorch2.9.1 CPU;固定小词表与无位置编码单层注意力。实验采用独立文档打包语义,不代表所有预训练框架的默认行为。
01先弄懂它是什么
计算语言模型的平均损失时,要数真正做了几次“预测下一个词元”。例如文档词元是 x0 到 x3,没有起始标记时,只有三个可监督位置:x0 预测 x1,x1 预测 x2,x2 预测 x3。设有效预测位置数为 M,损失就是这些位置的负对数概率之和除以 M。输入有多长、去掉填充后有多长、真正预测了多少个目标,是三个不同的数,不能直接拿张量元素数作分母。
很多短文分别放进固定长度序列,会留下大量空位。Packing 把几篇短文装进同一条序列来减少浪费,但先要决定文档能不能互相读取。连续流训练允许后文关注前文,通常用结束标记提示边界;独立文档训练则禁止跨文档关注,也不让上一篇末尾去预测下一篇开头。结束标记本身只是一个词元,不会自动阻断注意力,也不会替你修改哪些位置参与损失。
掩码的形状应明确。单条长度T序列的注意力允许矩阵是T乘T,第i行第j列为真表示位置i可读取j。独立文档因果掩码要求j不大于i且二者文档编号相同。损失掩码却按预测位置定义,长度T;二者解决不同问题,不能用一个替代另一个。
文中这几个词,先认一认
- 有效目标
- 完成标签移位后,真正参与交叉熵计算、没有被
ignore_index忽略的目标位置。 - 文档段编号
- 为每个输入位置标出“它属于第几篇独立文档”的编号,和词元 ID、位置 ID 各有不同用途。
- 顺序打包
- 按文档原顺序逐个放入固定容量的序列,不拆开文档;当前序列放不下下一篇时,再开一个新的。
02看看它怎么工作
实验将两个文档拼接,分别构造段编号,再取同段矩阵与下三角矩阵的交集。使用PyTorch的scaled_dot_product_attention时布尔真表示允许。本例不包含位置编码,因此单独计算和打包计算的位置语义天然相同;若加入绝对位置编码,要为每个独立文档重置位置,若框架采用其他位置策略,应重新验证等价条件。
标签先整体左移,再检查当前位置和目标位置属于同一文档,否则写入-100。这样文档末尾不会预测另一篇开头;最后一个位置也被屏蔽。若文档末尾包含EOS,则前一个内容词元预测EOS仍有效。填充位置既不能成为有效键值,也不能成为目标;必须防止整个查询行被遮住导致实现相关的数值行为。
若先算每篇文档的平均损失,再对这些平均数取平均,短文和长文就会各占一份。要让每个有效 token 贡献相同权重,应先把各篇的损失总和加起来,再除以全部有效目标数。实验做两层核对:先看打包与逐篇计算得到的原始预测分数(logits)是否一致,再看真正参与损失的目标是否仍是三项。
第一层帮助发现文档不该互相读取却读到了的问题,第二层帮助发现求和、平均时分母算错的问题;只看最终 loss 下降,分不清错在哪一层。
先跑通一个最小例子
运行 experiment.py。随机种子固定,单层注意力与线性词表头只做前向和反向核对,无训练语料下载。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import torch
import torch.nn.functional as F
torch.set_num_threads(1)
torch.manual_seed(26)
docs = [torch.tensor([1, 2, 3]), torch.tensor([4, 5])]
ids = torch.cat(docs)
seg = torch.tensor([0, 0, 0, 1, 1])
emb = torch.nn.Embedding(8, 4)
head = torch.nn.Linear(4, 8, bias=False)
def logits(tokens, allowed):
x = emb(tokens)[None, None, :, :]
out = F.scaled_dot_product_attention(x, x, x, attn_mask=allowed,
dropout_p=0.0)
return head(out[0, 0])
# 布尔真表示允许,只允许同文档的当前与过去位置
causal = torch.ones(5, 5, dtype=torch.bool).tril()
allowed = causal & (seg[:, None] == seg[None, :])
packed = logits(ids, allowed)
separate = torch.cat([logits(d, torch.ones(len(d), len(d),
dtype=torch.bool).tril()) for d in docs])
assert torch.allclose(packed, separate, atol=1e-6)
# 移位后仅保留同一文档的下一词元目标
labels = torch.full((5,), -100)
labels[:-1] = torch.where(seg[:-1] == seg[1:], ids[1:], -100)
assert labels.tolist() == [2, 3, -100, 5, -100]
valid = labels != -100
assert valid.sum().item() == 3
loss_sum = F.cross_entropy(packed, labels, ignore_index=-100, reduction="sum")
mean = F.cross_entropy(packed, labels, ignore_index=-100)
assert torch.allclose(mean, loss_sum / 3)
# 更改后一文档不会影响前一文档,反向也应没有跨段输入依赖
changed = ids.clone()
changed[3:] = torch.tensor([6, 7])
assert torch.allclose(logits(changed, allowed)[:3], packed[:3], atol=1e-6)
packed.retain_grad()
mean.backward()
assert torch.count_nonzero(packed.grad[~valid]) == 0
assert torch.isfinite(emb.weight.grad).all()
print("7 packed-document and token-count checks passed")
核对运行结果输出7项检查通过;标签为[2,3,-100,5,-100],有效预测数3。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["独立文档与词元"] --> B["容量内顺序打包"]
B --> C["输入和段编号"]
C --> D["同段因果注意力"]
C --> E["标签左移和边界屏蔽"]
D --> F["词表logits"]
E --> G["有效目标计数"]
F --> H["损失求和"]
G --> I["按有效数归一"]
H --> I按顺序看,每一步做了什么
- 写明是否允许文档间关注,固定EOS与PAD含义。
- 输出打包输入、段编号和逐文档位置。
- 分别生成注意力允许矩阵和移位标签。
- 对照逐文档结果,核对有效目标数和求和损失。
03对照实现代码
Transformers v4.57.1 · DataCollatorForLanguageModeling.torch_call;mlm=False 分支
以下为原创独立文档标签模型。真实整理器的mlm=False分支复制输入并屏蔽PAD,不负责本文额外的段边界隔离;本代码不是其原样节选。
import torch
def independent_targets(ids, segment):
# 这是已经移位的标签,不能再交给会二次移位的模型损失
target = torch.full_like(ids, -100)
# 只有相邻位置属于同一文档,才允许形成预测对
same = segment[:-1] == segment[1:]
target[:-1] = torch.where(same, ids[1:], -100)
# 最后一个位置没有后继目标,保持忽略标记
return target
真实torch_call先根据输入形式整理填充,再进入MLM或因果语言模型标签分支。MLM随机遮词和下一个词元目标不是同一任务。
mlm=False时复制input_ids,再按pad_token_id屏蔽;若PAD与EOS共享ID,这种按ID屏蔽也会影响真实EOS,需要检查具体数据整理策略。
整理器本身不实施下一Token移位。常见模型损失会移位;本文直接调用交叉熵,所以教学代码显式移位一次。
段边界与注意力隔离属于附加语义,不能由看到labels中存在-100推断出来,必须分别检查模型调用参数。
代码先调用谁,再调用谁
- 样本文档进入整理器形成填充张量
- mlm=False复制输入并屏蔽PAD标签
- 模型损失或显式教学函数移位后计算交叉熵
04动手写一个例子
保序且不拆文档的容量打包
给定已含结束标记的正整数文档长度和容量,输出每个箱内的文档下标;超长文档明确拒绝。
- 输入 1
长度[3,2,4],容量5- 输出 1
[[0,1],[2]]
维护当前箱已用容量,加入下一文档之前检查是否会超出容量;装不下时先关闭旧箱。
通过比较length > capacity-used避免整数相加溢出。输入长度一经校验,不会出现负长度导致容量倒退。
由于只允许保序、不拆文档、连续分组,尽可能填满当前箱不会增加箱数;这不等同于可任意重排的最优装箱问题。
为什么这样写是对的每个输出下标恰好出现一次且全局递增;已关闭箱和当前箱的总长度都不超过容量。
每篇文档只访问一次,每个编号写入一次;输入总Token数不影响按长度进行的规划成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static List<List<Integer>> pack(int[] lengths, int capacity) {
if(capacity<=0) throw new IllegalArgumentException("capacity");
List<List<Integer>> result=new ArrayList<>();
List<Integer> current=new ArrayList<>(); int used=0;
for(int i=0;i<lengths.length;i++){
int length=lengths[i];
// 超长文档交给独立切分策略,本算法不静默截断
if(length<=0 || length>capacity) throw new IllegalArgumentException("length");
if(length>capacity-used){
result.add(List.copyOf(current));
current.clear(); used=0;
}
// 保持顺序,每个文档编号只加入一次
current.add(i); used+=length;
}
// 空输入不能产生一个空箱
if(!current.isEmpty()) result.add(List.copyOf(current));
return result;
}
static void check(boolean ok){if(!ok)throw new AssertionError();}
public static void main(String[] args){
check(pack(new int[]{3,2,4},5).equals(List.of(List.of(0,1),List.of(2))));
check(pack(new int[]{5,5},5).size()==2);
check(pack(new int[]{},5).isEmpty());
check(pack(new int[]{Integer.MAX_VALUE,1},Integer.MAX_VALUE).size()==2);
try{pack(new int[]{6},5);throw new AssertionError();}
catch(IllegalArgumentException expected){}
try{pack(new int[]{0},5);throw new AssertionError();}
catch(IllegalArgumentException expected){}
System.out.println("6 ordered packing checks passed");
}
}
这些特殊情况也要试一试
- 空输入无输出箱。
- 超长文档先拒绝,由明确的切分或截断策略处理。
- 容量包含EOS等实际输入词元,而不是原文字数。
进阶选读 · 受约束的高利用率打包
场景长度差异大时,保序方案可能浪费容量,希望允许局部重排。
做法在固定大小窗口内按长度分桶或使用启发式装箱,再记录文档到箱的映射。重新测试位置编码、边界损失和数据恢复游标,避免为利用率牺牲可复现性。
复杂度窗口排序为O(w log w);任意最少箱装箱是组合优化问题,启发式不能保证全局最优。
好处与代价更高填充利用率可能增大完整注意力矩阵的计算;真正收益取决于内核是否利用文档块结构。
05项目里怎么用
训练日志应同时记录读入词元、非填充词元、参与监督词元和独立文档数。吞吐比较用相同口径,否则屏蔽大量标签也可能产生虚高的Token每秒速率。改变Packing策略后,应复查学习率日程是否按优化步还是有效词元累计,避免预算相同却学习率轨迹不同。
有些高效变长注意力内核,需要的是各段序列的累计长度,而不是直接传入 T乘T 的掩码矩阵。两者可以表达同样的读取限制,但输入形状和边界约定不同,应先用小张量的参考实现逐项比对结果。本篇只在 CPU 上用布尔矩阵核对这些规则,没有测 GPU 吞吐或变长内核的显存占用。
连续流语义本身可以是合理设计,但要写清它允许跨文档条件化。独立文档方案则要同时控制注意力、位置和损失。比较两个方案时固定总有效目标与数据顺序,单独报告文档打包差异,不能把由额外上下文获得的损失变化全归因于硬件优化。
容易踩的坑
- 把EOS当作自动注意力防火墙。
- 整理器和模型损失同时移位,导致预测隔两个词元。
- 用整个张量长度当交叉熵有效分母。
- 把PAD和EOS共用ID后按ID屏蔽全部结束标记。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 普通填充批次 | 语义直观便于调试 | 短文占比大时浪费容量 | 初始正确性基线 |
| 独立文档打包 | 减少填充并保留条件独立语义 | 需要额外段边界与位置处理 | 大量短文的严格对照 |
| 连续文本流 | 实现简单且上下文持续 | 相邻文档存在额外条件依赖 | 明确接受连续流目标的预训练 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01长度5的一篇文档没有BOS,最多几个下一Token目标?
展开参考答案与推导
四个;最后输入没有下一个标签。如果有额外起始标记,计数要按实际输入重新计算。
02为什么标签掩码不能防止跨文档读取?
展开参考答案与推导
它只屏蔽损失项,注意力仍可能把另一文档内容写进当前隐藏状态。
03两个文档的平均损失能直接平均吗?
展开参考答案与推导
仅在目标是文档等权或有效词元数相同时;Token等权应按各文档有效数加权。
换个情况,还会处理吗
01打包后loss降低但评测没提高怎么排查?
展开参考答案与推导
检查是否误预测跨文档开头、是否增加可见上下文、有效分母是否变化,再进行逐文档数值对照。
02全是长度1且没有BOS的文档怎么办?
展开参考答案与推导
独立语义下没有有效目标,批次应跳过或改变明确的输入构造,不能让零分母均值生成NaN。
03加绝对位置后对照失败怎么办?
展开参考答案与推导
检查独立文档的位置是否重置,并核查dropout、填充和训练模式;只有等价条件满足才期待数值一致。
写代码试一试
01构造同段因果矩阵。
- 输入 / 条件
段编号[0,0,1]- 输出 / 预期
[[1,0,0],[1,1,0],[0,0,1]]
提示 比较段编号并限制键位置。
展开参考答案与推导
允许条件为segment[i]==segment[j]且j<=i。
02构造移位标签。
- 输入 / 条件
IDs[2,3,4,5],段[0,0,1,1]- 输出 / 预期
[3,-100,5,-100]
提示 检查目标是否仍属同段。
展开参考答案与推导
前后段末端与整体末端都不能监督下一篇开头。
03合并有效词元损失。
- 输入 / 条件
总损失6和8,有效数2和4- 输出 / 预期
14/6
提示 先合计再相除。
展开参考答案与推导
全局均值为(6+8)/(2+4),而不是两个局部均值的平均。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能手工生成文档分段掩码,验证打包与逐文档结果一致,区分输入Token和有效目标Token。
本篇重点加练
隔离注意力、标签屏蔽和归约是三个独立约束,需逐个检验打包是否改变目标。
- 块对角因果掩码
- 标签移位与跨文档屏蔽
- 有效Token加权归约
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
文档 A=[3,4,EOS]、B=[8,9,EOS]。分别为连续流和独立文档训练声明允许的上下文及目标,解释二者对应的建模任务。
- 提交内容
- 两份训练目标契约
- 怎样算完成
- 边界语义明确
- EOS 与隔离职责区分
练到的知识点因果语言建模目标 · 文档边界与结束标记 · 连续流与独立文档语义
- 作业 02代码推演
将长度为 2、3、1 的三个文档放入容量 8 的块,画出逐位置段 ID、可见关系和目标标签;自行声明是否加入 BOS。
- 提交内容
- 打包布局及两种掩码
- 怎样算完成
- 标签和注意力分别设计
- 填充位置处理完整
练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽
- 作业 03代码推演
给定两篇各含三个 token 的文档,分别画“可读取位置”和“需要预测位置”的二部关系;研究加入或不加入 BOS 时两张关系图如何变化。
- 提交内容
- 两类关系图与目标身份表
- 怎样算完成
- 读取与监督不混用
- 边界 token 约定完整
练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽
综合应用
- 作业 04编程实践
实现保持原顺序的文档打包器,输入长度 [3,1,4,2]、容量 5;输出文档位置映射、有效目标计数和批量损失统计。
- 提交内容
- CPU 实现与契约测试
- 怎样算完成
- 跨块政策明确
- 每个目标归属可追踪
练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约 · 顺序打包算法
- 作业 05排错实验
构造两篇文档,分别只屏蔽损失和同时使用隔离注意力;扰动前一篇输入并检查后一篇输出与梯度。
- 提交内容
- 对照实验及依赖记录
- 怎样算完成
- 目标屏蔽与读取限制分开
- 同一参数条件下比较
练到的知识点连续流与独立文档语义 · 块对角因果掩码 · 标签移位与跨文档屏蔽
- 作业 06面试场景
团队只用填充率评价打包收益。补充一套包含有效目标数、实际注意力工作量和每次更新数据量的 CPU 测量报告。
- 提交内容
- 测量脚本与指标表
- 怎样算完成
- 效率分母一致
- 目标数量变化不被忽略
练到的知识点有效Token加权归约 · 填充效率和二次注意力开销
- 作业 07编程实践
CPU 小模型对三篇文档分别前向与独立文档打包前向,逐目标核对损失及梯度;固定位置编号策略并对账有效目标总数。
- 提交内容
- 双路径实现与逐目标差异
- 怎样算完成
- 等价所需条件显式
- 整体均值不掩盖单目标错位
练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽 · 有效Token加权归约
- 作业 08排错实验
打包器在截断处重复一个边界 token,但标签计数仍按原文档长度。自建最小输入,审计重复监督、丢失监督与报告分母。
- 提交内容
- 边界故障案例及计数审计
- 怎样算完成
- 原文位置身份保留
- 标签数来自实际参与目标
练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约
- 作业 09设计评审
文档长度差异很大,研究希望每篇文档等权而不是每个 token 等权。定义打包后的目标与评估,说明必须保存哪些归属信息。
- 提交内容
- 两种加权目标及数据契约
- 怎样算完成
- 文档权重与 token 权重区分
- 目标改变有明确声明
练到的知识点块对角因果掩码 · 有效Token加权归约
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
比较保持文档顺序与按长度重排的打包策略,评估填充、样本顺序和可复现性;提前规定不得跨越的来源分组边界。
- 提交内容
- 两策略实验与权衡报告
- 怎样算完成
- 顺序影响有记录
- 效率不替代学习质量证据
练到的知识点文档边界与结束标记 · 连续流与独立文档语义 · 顺序打包算法 · 填充效率和二次注意力开销
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
若只能使用普通下三角注意力,却要求独立文档训练语义,单改标签是否可能对任意模型完全补救?给出论证或最小反例。
- 提交内容
- 模型依赖图与反例
- 怎样算完成
- 比较针对相同参数输入
- 信息路径与损失路径分别说明
练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽
- 作业 12思维拓展 · 跨域迁移
把打包视为数据库行批处理:重新排序或拆分记录何时改变统计结果?将目标 token 身份映射到带权记录,提出可自动检查的守恒量。
- 提交内容
- 映射、守恒契约与小检查器
- 怎样算完成
- 统计单位一致
- 只声明实际可验证的守恒
练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Transformers4.57.1 数据整理器 (新标签页)↗
核对mlm=False复制标签与PAD屏蔽的实际行为。
- 深入参考文档英文
Transformers4.57.1 因果损失 (新标签页)↗
确认标签移位责任与有效项目归约方式。
- 深入参考文档英文
PyTorch2.9.1 SDPA 接口实现 (新标签页)↗
核对布尔掩码真值语义与缩放点积接口。