开始前,会这些就够了
- 知道平均数怎样计算
- 能阅读 Python 列表
假设一批输入里,第一条记录有三个数字,第二条只有一个。为了排成整齐的表,程序给第二条补上两个零。这种补齐叫 padding。但补出来的零不是第二条记录的新内容,只是暂时占个位置。
像登记表里的空格:为了让每一行同样宽而画出格子,不代表空格里真的记录了一次零分。机器不会凭外观猜出区别,因此通常还要保留一份“哪些格子有效”的标记。
零是原来的内容,还是后来补上的?
原始记录是 [4, 0, 8] 和 [6],都排成三格。第一条里的 0 是真实内容;第二条后面的两个 0 是填充。固定预测为 [3, 2, 9] 和 [4, 0, 0]。
| 位置 | 目标数字 | 来源 | 预测数字 | 误差平方 | 本次统计 |
|---|---|---|---|---|---|
| 第 1 行第 1 格 | 4 | 真实内容 | 3 | 1 | 纳入 |
| 第 1 行第 2 格 | 0 | 真实内容,确实为0 | 2 | 4 | 纳入 |
| 第 1 行第 3 格 | 8 | 真实内容 | 9 | 1 | 纳入 |
| 第 2 行第 1 格 | 6 | 真实内容 | 4 | 4 | 纳入 |
| 第 2 行第 2 格 | 0 | 补齐的占位 | 0 | 0 | 忽略 |
| 第 2 行第 3 格 | 0 | 补齐的占位 | 0 | 0 | 忽略 |
真实位置有 4 个;当前规则纳入 4 个位置。
目标均值:4.50(18 ÷ 4)。平均平方误差(MSE):2.50(10 ÷ 4)。
保留了真实的 0,同时排除了两个占位。每个有效位置按同样权重计算。
这里仅计算固定数字的均值与误差平方,没有运行神经网络。当前选择的是统计位置:注意力掩码控制哪些位置可被参考,损失掩码控制哪些目标计分,两者不能互相代替。误差平方越小也不能单独证明真实模型更好。
长度一样,不代表每个位置都有内容
掩码(mask)就是与数据对齐的一排标记。本例用1表示有效位置、0表示填充位置,而且根据原来的长度生成标记。第一行的数字0是真实内容,照样标成1;不能用“数值等于0”来猜哪个位置是补的。
文字模型通常用专门的填充符号及其编号补齐,编号不一定是0。Hugging Face 的文档列出了补到本批最长长度、补到指定长度等方式。补齐增加占位,截断则删掉超长部分,二者对内容的影响不同。
先排除占位,再数分母
下面是原创数字实验:两条记录分别为[4, 0, 8]和[6],统一补到三个位置。直接除以全部六个位置,会把平均值压低;只选有效位置后,应除以四。真实的零仍然算一个有效位置,不能顺手丢掉。
损失是给预测误差计分的数字。这里采用“预测减目标,再平方”的简单方法。两个填充位置的误差恰好为零,但把它们算进分母,仍会让平均损失显得更小。程序只演示按位置统计,没有训练模型,也没有实现注意力。
def masked_mean(values, mask):
if len(values) != len(mask) or any(m not in (0, 1) for m in mask):
raise ValueError('values and a 0/1 mask must align')
kept = [value for value, active in zip(values, mask) if active]
return sum(kept) / len(kept) if kept else None
rows = [[4, 0, 8], [6]]
width = max(map(len, rows))
padded = [row + [0] * (width - len(row)) for row in rows]
masks = [[1] * len(row) + [0] * (width - len(row)) for row in rows]
values = [value for row in padded for value in row]
valid = [active for row in masks for active in row]
predictions = [3, 2, 9, 4, 0, 0]
losses = [(pred - target) ** 2 for pred, target in zip(predictions, values)]
print('padded:', padded)
print('mask:', masks)
print(f'all-position mean: {sum(values) / len(values):.2f}')
print(f'valid-position mean: {masked_mean(values, valid):.2f}')
print(f'all-position loss: {sum(losses) / len(losses):.2f}')
print(f'valid-position loss: {masked_mean(losses, valid):.2f}')
empty = masked_mean([0, 0], [0, 0])
print('all-padding:', 'no valid positions' if empty is None else empty)怎样运行:保存为 padding_positions.py,用 Python 3.10 或更新版本运行:python padding_positions.py。仅用标准库,不联网。
运行后对照结果
padded: [[4, 0, 8], [6, 0, 0]]
mask: [[1, 1, 1], [1, 0, 0]]
all-position mean: 3.00
valid-position mean: 4.50
all-position loss: 1.67
valid-position loss: 2.50
all-padding: no valid positions能参考的位置,和要计分的位置
注意力掩码(attention mask)控制注意力计算能参考哪些位置,例如不把补齐的格子当作可读取的内容。损失掩码(loss mask)控制哪些目标位置参与误差计分。一个管信息怎样参与计算,一个管训练目标怎样计分,不能只设置其中一个就假定另一个也处理好了。
例如某项任务让前面的文字提供提示,只对后面的回答计分;提示仍可能需要被参考,却不作为本次损失的目标。具体规则要看模型与任务。所引 PyTorch 损失接口支持忽略指定的目标编号,但这不是所有损失函数通用的开关,也不等于把输入数字设成零。
全部都是占位时,别写成零误差
若没有任何有效位置,平均数就没有可用的分母。本例返回 None,并显示提示。实际训练应明确选择跳过、拒绝还是另行处理,不能把没有评分对象记成“预测全部正确”。不同任务的空样本规则需要单独确认。
还要说明你平均的单位:本例把每个有效位置看作一份;先算每条记录的平均,再平均各条记录,会给长短记录同样的权重,两种结果可能不同。继续读文档时,可分别寻找 padding、attention_mask 和忽略目标的参数,确认它们各自在处理什么。
记住这三点
- 补齐只解决长度对齐,还需要根据原始长度或明确标记识别有效位置。
- 忽略填充时,平均值的分子与分母都要按同一套有效位置统计。
- 注意力掩码与损失掩码职责不同,全填充输入和平均单位也要明确处理。
别把例子用过头
- 真实内容也可能是零;填充编号和掩码取值含义要以具体接口为准。
- 本例的误差平方不等于语言模型常用的分类损失,没有复现训练或完整 Transformer。
- 屏蔽注意力中的填充位置,不保证其输出自动为零,也不代表后续平均或损失已自动忽略它。
自己试一小步
当一条记录变长、另一条变空时,怎样确认填充没有偷偷影响你的统计?
- 在真实内容中再放入一个零,保持它的有效标记为1。
- 只增加填充位置,比较按有效位置计算的均值和损失。
- 加入空记录,分别写清按有效位置平均与按记录平均的处理规则。
做到这些,就算完成
- 真实的零是否仍被保留?
- 是否解释了每次除法的分母,并处理没有有效位置的情况?
- 是否能说明本例的统计掩码与模型中的注意力掩码有什么区别?
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Hugging Face:Padding and truncation (新标签页)↗
变长批次的补齐、特殊填充符号、补到批次最长或指定长度,以及补齐和截断的区别。
- Hugging Face:Glossary / attention mask (新标签页)↗
attention mask 指定可被参考的位置;BERT 分词示例中的1
/0标记与补齐编号。 - PyTorch:CrossEntropyLoss (新标签页)↗
目标为类别编号时
ignore_index排除目标位置,mean 按非忽略目标及相应权重汇总;与输入补齐分开设置。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Hugging Face:对照补齐位置与 Attention Mask (新标签页)↗
只看 attention mask 小节,把补齐位置与 mask 逐项连线,注意接口约定和损失忽略规则是不同问题。
查看学习准备与访问说明 →