qinghuan
← 预训练与训练工程 / 全部笔记

单元 26 / 预训练与训练工程

P0 · 必会主线L2 · 进阶

预训练目标、序列打包与 Token 计数

把多篇文档装进同一训练序列来减少填充,并从“预测下一个 token”检查文档之间的注意力隔离、标签屏蔽和有效目标计数。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能手工生成文档分段掩码,验证打包与逐文档结果一致,区分输入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.pypython · 39 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

experiment.py · python
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

按顺序看,每一步做了什么

  1. 写明是否允许文档间关注,固定EOS与PAD含义。
  2. 输出打包输入、段编号和逐文档位置。
  3. 分别生成注意力允许矩阵和移位标签。
  4. 对照逐文档结果,核对有效目标数和求和损失。

03对照实现代码

语言建模整理器如何构造标签 (新标签页)↗

Transformers v4.57.1 · DataCollatorForLanguageModeling.torch_call;mlm=False 分支

以下为原创独立文档标签模型。真实整理器的mlm=False分支复制输入并屏蔽PAD,不负责本文额外的段边界隔离;本代码不是其原样节选。

核心机制代码 · python
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推断出来,必须分别检查模型调用参数。

代码先调用谁,再调用谁

  1. 样本文档进入整理器形成填充张量
  2. mlm=False复制输入并屏蔽PAD标签
  3. 模型损失或显式教学函数移位后计算交叉熵

04动手写一个例子

保序且不拆文档的容量打包

给定已含结束标记的正整数文档长度和容量,输出每个箱内的文档下标;超长文档明确拒绝。

输入 1
长度[3,2,4],容量5
输出 1
[[0,1],[2]]

维护当前箱已用容量,加入下一文档之前检查是否会超出容量;装不下时先关闭旧箱。

通过比较length > capacity-used避免整数相加溢出。输入长度一经校验,不会出现负长度导致容量倒退。

由于只允许保序、不拆文档、连续分组,尽可能填满当前箱不会增加箱数;这不等同于可任意重排的最优装箱问题。

为什么这样写是对的每个输出下标恰好出现一次且全局递增;已关闭箱和当前箱的总长度都不超过容量。

时间 O(n)额外空间 O(n) 输出,O(1)额外计数状态

每篇文档只访问一次,每个编号写入一次;输入总Token数不影响按长度进行的规划成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 34 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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加权归约

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    文档 A=[3,4,EOS]、B=[8,9,EOS]。分别为连续流和独立文档训练声明允许的上下文及目标,解释二者对应的建模任务。

    提交内容
    两份训练目标契约
    怎样算完成
    • 边界语义明确
    • EOS 与隔离职责区分

    练到的知识点因果语言建模目标 · 文档边界与结束标记 · 连续流与独立文档语义

  2. 作业 02代码推演

    将长度为 2、3、1 的三个文档放入容量 8 的块,画出逐位置段 ID、可见关系和目标标签;自行声明是否加入 BOS。

    提交内容
    打包布局及两种掩码
    怎样算完成
    • 标签和注意力分别设计
    • 填充位置处理完整

    练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽

  3. 作业 03代码推演

    给定两篇各含三个 token 的文档,分别画“可读取位置”和“需要预测位置”的二部关系;研究加入或不加入 BOS 时两张关系图如何变化。

    提交内容
    两类关系图与目标身份表
    怎样算完成
    • 读取与监督不混用
    • 边界 token 约定完整

    练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽

综合应用

  1. 作业 04编程实践

    实现保持原顺序的文档打包器,输入长度 [3,1,4,2]、容量 5;输出文档位置映射、有效目标计数和批量损失统计。

    提交内容
    CPU 实现与契约测试
    怎样算完成
    • 跨块政策明确
    • 每个目标归属可追踪

    练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约 · 顺序打包算法

  2. 作业 05排错实验

    构造两篇文档,分别只屏蔽损失和同时使用隔离注意力;扰动前一篇输入并检查后一篇输出与梯度。

    提交内容
    对照实验及依赖记录
    怎样算完成
    • 目标屏蔽与读取限制分开
    • 同一参数条件下比较

    练到的知识点连续流与独立文档语义 · 块对角因果掩码 · 标签移位与跨文档屏蔽

  3. 作业 06面试场景

    团队只用填充率评价打包收益。补充一套包含有效目标数、实际注意力工作量和每次更新数据量的 CPU 测量报告。

    提交内容
    测量脚本与指标表
    怎样算完成
    • 效率分母一致
    • 目标数量变化不被忽略

    练到的知识点有效Token加权归约 · 填充效率和二次注意力开销

  4. 作业 07编程实践

    CPU 小模型对三篇文档分别前向与独立文档打包前向,逐目标核对损失及梯度;固定位置编号策略并对账有效目标总数。

    提交内容
    双路径实现与逐目标差异
    怎样算完成
    • 等价所需条件显式
    • 整体均值不掩盖单目标错位

    练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽 · 有效Token加权归约

  5. 作业 08排错实验

    打包器在截断处重复一个边界 token,但标签计数仍按原文档长度。自建最小输入,审计重复监督、丢失监督与报告分母。

    提交内容
    边界故障案例及计数审计
    怎样算完成
    • 原文位置身份保留
    • 标签数来自实际参与目标

    练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约

  6. 作业 09设计评审

    文档长度差异很大,研究希望每篇文档等权而不是每个 token 等权。定义打包后的目标与评估,说明必须保存哪些归属信息。

    提交内容
    两种加权目标及数据契约
    怎样算完成
    • 文档权重与 token 权重区分
    • 目标改变有明确声明

    练到的知识点块对角因果掩码 · 有效Token加权归约

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    比较保持文档顺序与按长度重排的打包策略,评估填充、样本顺序和可复现性;提前规定不得跨越的来源分组边界。

    提交内容
    两策略实验与权衡报告
    怎样算完成
    • 顺序影响有记录
    • 效率不替代学习质量证据

    练到的知识点文档边界与结束标记 · 连续流与独立文档语义 · 顺序打包算法 · 填充效率和二次注意力开销

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    若只能使用普通下三角注意力,却要求独立文档训练语义,单改标签是否可能对任意模型完全补救?给出论证或最小反例。

    提交内容
    模型依赖图与反例
    怎样算完成
    • 比较针对相同参数输入
    • 信息路径与损失路径分别说明

    练到的知识点块对角因果掩码 · 标签移位与跨文档屏蔽

  2. 作业 12思维拓展 · 跨域迁移

    把打包视为数据库行批处理:重新排序或拆分记录何时改变统计结果?将目标 token 身份映射到带权记录,提出可自动检查的守恒量。

    提交内容
    映射、守恒契约与小检查器
    怎样算完成
    • 统计单位一致
    • 只声明实际可验证的守恒

    练到的知识点标签移位与跨文档屏蔽 · 有效Token加权归约

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗