文章目录
- 1、基本介绍
- [2、使用 "对数概率" 的束搜索](#2、使用 “对数概率” 的束搜索)
- 3、为什么:概率要乘,而不直接相加
- [4、使用 "长度归一化" 的束搜索](#4、使用 “长度归一化” 的束搜索)
- [5、使用 "覆盖惩罚" 的束搜索](#5、使用 “覆盖惩罚” 的束搜索)
【束搜索】 对于 翻译模型 优于 【Top-k 采样 + Temperature 调节】
Hugging Face 有封装好的 "束搜索" API。
束搜索是一种 确定性(Deterministic)的搜索算法。
- 核心思想 :它不只看眼前哪条路最好,而是同时保留
k条(k为束宽)当前看起来最优的候选路径。在生成序列的每一步,它都会扩展这k条路径,并从所有新的可能性中再次选出最好的k条。最终,它会从所有完成的序列中选择整体概率最高的那一个作为输出。 - 主要特点 :
- 追求最优:目标是找到全局概率最高的翻译结果,比简单的"贪心解码"(每一步只选概率最高的词)效果更好。
- 结果稳定:对于同一个输入,束搜索通常会给出相同或非常相似的输出。
- 计算成本高 :束宽
k越大,搜索空间越大,效果可能越好,但计算量和内存消耗也呈指数级增长。
因此,束搜索常用于对准确性和稳定性要求极高的场景,如正式的文档翻译。
1、基本介绍
束搜索详解:从名字到原理
一、为什么叫"束搜索"?
这个名字翻译自英文 Beam Search。
- Beam(束/梁) :想象一束光或者一根梁,它的宽度是有限的,只能照亮一小片区域。在束搜索中,"束"代表了每一层保留的候选路径数量,这个数量是固定的,就像光束的宽度是固定的一样。
- Search(搜索):模型在生成序列时,每一步都在庞大的词汇空间中搜索最优的路径。
所以"束搜索"字面意思是:用固定宽度的"光束"去搜索最优的生成路径。这个名字非常形象地描述了算法的核心特征------每一步只保留最亮(概率最高)的若干条路径,而不是探索所有可能性。
二、束搜索要解决什么问题?
问题背景
在序列生成任务(如机器翻译、文本摘要)中,模型需要逐个词地生成输出:
输入:"我 爱 你" → 模型要输出:"I love you"
模型的工作方式是:
- 第1步:根据输入,预测第一个词的概率分布(比如 P(I)=0.6, P(Me)=0.3, P(We)=0.1)
- 第2步:根据输入 + 已生成的第一个词,预测第二个词
- 第3步:根据输入 + 已生成的前两个词,预测第三个词
- ...直到生成结束符
核心困境
每一步都有很多种选择。如果词汇表有3万个词,生成10个词的句子,理论上就有 30000^10 种可能的组合------这个数字大到宇宙容不下。
我们必须在搜索质量 和计算效率之间做权衡:
| 方法 | 做什么 | 优点 | 缺点 |
|---|---|---|---|
| 穷举搜索 | 枚举所有可能的序列 | 保证找到全局最优 | 计算量爆炸,不可能实现 |
| 贪心搜索 | 每步只取概率最高的1个词 | 速度快 | 容易陷入局部最优,错过全局好解 |
| 束搜索 | 每步保留概率最高的k个候选 | 效率和质量平衡 | 需要调参k值 |
束搜索就是贪心搜索的"升级版"------贪心只保留1个候选,束搜索保留k个候选(k叫"束宽")。
三、束搜索的核心原理(结合实例)
用一个具体例子,假设:
- 要翻译 "我 爱 你" → 英文
- 词汇表:
{I, love, like, you, her, we, ...} - 设置束宽 beam_width = 2(每步保留2个候选)
- 模型每一步都会输出所有词的概率
[注]使用对数概率避免数值下溢:实际实现中会将概率取对数后相加(连乘变连加),但为直观理解,此处仍用原始概率相乘。
第1步:生成第一个词
模型输出每个词的概率:
| 词 | 概率 |
|---|---|
| I | 0.6 |
| We | 0.3 |
| They | 0.07 |
| ... | ... |
取概率最高的 2 个(束宽=2):
- 候选1:
[I],累积概率 = 0.6 - 候选2:
[We],累积概率 = 0.3
注意:没有选中的词(如They)直接丢弃,不再考虑。这就是"束"的作用------只让最亮的两束光继续前进。
第2步:生成第二个词
现在有2个候选序列,每个序列都要独立地预测下一个词。
对候选1 [I] :模型根据[I]预测下一个词
| 词 | 概率 |
|---|---|
| love | 0.7 |
| like | 0.2 |
| hate | 0.05 |
| ... | ... |
对候选2 [We] :模型根据[We]预测下一个词
| 词 | 概率 |
|---|---|
| love | 0.5 |
| are | 0.3 |
| will | 0.1 |
| ... | ... |
现在我们需要扩展每个候选:
- 从
[I]扩展出的新候选:[I, love],累积概率 = 0.6 × 0.7 = 0.42 (为什么不用加法把每个概率直接相加?后面有详情)[I, like],累积概率 = 0.6 × 0.2 = 0.12[I, hate],累积概率 = 0.6 × 0.05 = 0.03
- 从
[We]扩展出的新候选:[We, love],累积概率 = 0.3 × 0.5 = 0.15[We, are],累积概率 = 0.3 × 0.3 = 0.09[We, will],累积概率 = 0.3 × 0.1 = 0.03
现在一共有 2×3 = 6 个新候选。从中取概率最高的2个(束宽=2):
| 排名 | 候选序列 | 累积概率 |
|---|---|---|
| 1 | [I, love] |
0.42 |
| 2 | [We, love] |
0.15 |
| 3 | [I, like] |
0.12 ← 淘汰 |
| 4 | [We, are] |
0.09 ← 淘汰 |
| ... | ... | ... |
保留的候选:
- 候选1:
[I, love],累积概率 0.42 - 候选2:
[We, love],累积概率 0.15
关键观察 :注意
[We, love]虽然概率较低,但它被保留下来了,而[I, like]被淘汰了。这就是束搜索比贪心强大的地方------贪心在第1步选了[I]后,第2步一定会选love,得到[I, love];但束搜索同时保留了[We, love]这条"备胎"路径。
第3步:生成第三个词
继续扩展当前保留的2个候选:
从[I, love]扩展:预测第三个词
| 词 | 概率 |
|---|---|
| you | 0.9 |
| her | 0.08 |
| ... | ... |
[I, love, you],累积概率 = 0.42 × 0.9 = 0.378[I, love, her],累积概率 = 0.42 × 0.08 = 0.0336
从[We, love]扩展:预测第三个词
| 词 | 概率 |
|---|---|
| you | 0.7 |
| her | 0.2 |
| ... | ... |
[We, love, you],累积概率 = 0.15 × 0.7 = 0.105[We, love, her],累积概率 = 0.15 × 0.2 = 0.03
取最高的2个:
| 排名 | 候选序列 | 累积概率 |
|---|---|---|
| 1 | [I, love, you] |
0.378 |
| 2 | [We, love, you] |
0.105 |
[注] 补充停止条件说明:当所有候选序列都以结束符 <EOS>结尾,或达到最大长度限制时,搜索提前终止。
结束:选择最终答案
假设遇到了结束符<EOS>,或者达到最大长度,我们选择累积概率最高的序列:
最终输出 :[I, love, you],累积概率 0.378
[注] 重要补充------长度归一化问题 :上述过程直接比较累积概率,会导致模型偏好短句子(因为乘了更少的小于1的数)。实际使用时通常会对长句子做补偿,例如将累积概率除以句子长度的α次方(α常取0.6~1)。本示例句子很短,未体现该问题,但实际任务中务必注意。
四、束搜索 vs 贪心搜索:一个例子看出差距
假设正确的翻译是 "I love you",但存在一个"陷阱":
| 步骤 | 贪心搜索 | 束搜索(束宽=2) |
|---|---|---|
| 第1步 | 选概率最高的 "I" (0.6) | 保留 "I"(0.6) 和 "We"(0.3) |
| 第2步 | 从"I"扩展,选"love"(0.7),得"I love" | 保留"I love"(0.42) 和 "We love"(0.15) |
| 第3步 | 从"I love"扩展,选"you"(0.9),得"I love you" | 得"I love you"(0.378),同时"备胎"路径可能产生更好的结果 |
看起来贪心也对了?但考虑这个更刁钻的情况:
正确翻译:"The cat sat on the mat"
如果某句话中,"The cat sat"这个正确路径在第2步时的概率稍低于一个"陷阱"路径,贪心会直接走向陷阱,再也回不来;而束搜索因为保留了多个候选,有可能在后续步骤中发现陷阱路径走不下去,转而选择正确路径。
核心优势:束搜索允许模型"反悔"------当前看起来不是最优的选择,因为后面的词可能让它逆袭。
五、束宽的影响
| 束宽 | 特点 |
|---|---|
| 1 | 退化为贪心搜索 |
| 2-5 | 机器翻译常用的值,效果提升明显 |
| 5-10 | 效果进一步提升,但计算量线性增加 |
| >10 | 收益递减,边际效用很低 |
实际建议:
-
学习阶段:用
beam_width = 3或4 -
生产环境:根据速度要求,常用
4-8 -
每增加1倍束宽,计算量大约增加1倍
[注]严格说是"每增加1倍束宽,计算量约增加1倍"是指从k到2k,而不是从1到2。更准确:计算量正比于束宽×每步计算开销。
六、束搜索的变体(进阶)
-
长度归一化:长句子概率连乘后会非常小(因为乘了很多个小于1的数),导致模型偏好短句子。解决方法是除以句子长度的α次方。(已在第三节补充说明)
-
覆盖惩罚:防止模型重复翻译同一个词(常见于长文本),惩罚那些过多关注输入中同一位置的词。
-
分组束搜索:将输出分成若干组,每组独立做束搜索,增加多样性。
七、实现上的两个重要提醒
-
使用对数概率 :多个概率连乘极易超出浮点数精度范围(如0.5^100 ≈ 10^-30),实际实现中会取对数,将连乘变为连加:
log(P1*P2) = log(P1) + log(P2)。 -
处理结束符 :一旦某个候选生成了
<EOS>,它就不再参与后续扩展,直接进入最终候选池等待比较。
总结一句话
束搜索 = 每步保留k个概率最高的候选路径,让模型有机会在后续步骤中"反悔",从而找到比贪心更好的全局解。
2、使用 "对数概率" 的束搜索
使用对数概率的束搜索详解
一、为什么要用对数概率?
问题:数值下溢
在束搜索中,我们需要计算序列的累积概率。假设一个句子有10个词,每个词的概率平均为0.1:
累积概率 = 0.1 × 0.1 × 0.1 × ... (10次) = 0.1^10 = 1 × 10^(-10)
这是一个非常小的数字。如果是20个词的句子:
0.1^20 = 1 × 10^(-20)
计算机的浮点数精度有限。以常见的32位浮点数(float32)为例:
- 最小能表示的正数大约是
1 × 10^(-38) - 但实际操作中,连续乘很多个小于1的小数,很快就会下溢(underflow)------数值小到计算机直接当作0处理
一旦变成0,后续的比较就全乱了:0.0 和 0.0 无法区分大小,模型无法判断哪个候选更好。
解决方案:取对数
对数有一个非常好的性质:
log(a × b) = log(a) + log(b)
把乘法变成加法,数值范围就舒服多了。
| 原始概率 | 取自然对数 |
|---|---|
| 0.1 | -2.30 |
| 0.01 | -4.61 |
| 0.001 | -6.91 |
| 1 × 10^(-10) | -23.0 |
| 1 × 10^(-20) | -46.1 |
对数值的绝对值虽然也会增大,但不会变成0,始终在可安全计算的范围内(float32可以安全处理到约-1000)。
二、核心变化:从乘法到加法
原始版本(不用对数)
序列 [I, love] 的累积概率 = 0.6 × 0.7 = 0.42
对数版本
取自然对数 ln()(也可以用log10,原理一样):
ln(0.6) ≈ -0.511
ln(0.7) ≈ -0.357
序列 [I, love] 的累积对数概率 = (-0.511) + (-0.357) = -0.868
关键理解:原始概率越高 → 对数概率越接近0(因为ln(1)=0,ln(小于1的数)是负数)
原始概率 0.42 → 对数概率 -0.868
原始概率 0.15 → 对数概率 -1.897
因为 -0.868 > -1.897,所以比较时数值越大(负得越少)表示概率越高
三、完整的对数束搜索流程(接之前的例子)
设置
- 束宽 = 2
- 使用自然对数
ln()
第1步:生成第一个词
模型输出原始概率和对数概率:
| 词 | 原始概率 | 对数概率 ln§ |
|---|---|---|
| I | 0.6 | -0.511 |
| We | 0.3 | -1.204 |
| They | 0.07 | -2.659 |
取对数概率最高的2个(即数值最大的,因为负得越少越大):
- 候选1:
[I],累积对数概率 = -0.511 - 候选2:
[We],累积对数概率 = -1.204
第2步:生成第二个词
对候选1 [I]:预测下一个词的原始概率和对数概率
| 词 | 原始概率 | ln§ |
|---|---|---|
| love | 0.7 | -0.357 |
| like | 0.2 | -1.609 |
| hate | 0.05 | -2.996 |
对候选2 [We]:预测下一个词的原始概率和对数概率
| 词 | 原始概率 | ln§ |
|---|---|---|
| love | 0.5 | -0.693 |
| are | 0.3 | -1.204 |
| will | 0.1 | -2.302 |
扩展新候选(对数概率相加):
从 [I] 扩展:
[I, love]:累积 = (-0.511) + (-0.357) = -0.868[I, like]:累积 = (-0.511) + (-1.609) = -2.120[I, hate]:累积 = (-0.511) + (-2.996) = -3.507
从 [We] 扩展:
[We, love]:累积 = (-1.204) + (-0.693) = -1.897[We, are]:累积 = (-1.204) + (-1.204) = -2.408[We, will]:累积 = (-1.204) + (-2.302) = -3.506
保留对数概率最高的2个(数值最大):
| 排名 | 候选序列 | 累积对数概率 | 原始累积概率 |
|---|---|---|---|
| 1 | [I, love] |
-0.868 | e^(-0.868)=0.42 |
| 2 | [We, love] |
-1.897 | e^(-1.897)=0.15 |
| 3 | [I, like] |
-2.120 | 0.12 ← 淘汰 |
| 4 | [We, are] |
-2.408 | 0.09 ← 淘汰 |
注意:比较结果和原始概率版本完全一致。对数只是改变了数值表示,不改变相对大小。
[注]补充------关于 log(0) 的处理:
实际模型输出中,softmax 理论上不会产生绝对的0(因为 exp 后永远是正数),但 float32 精度下可能产生极小的值,取 log 后得到 -inf。此时需要在代码中处理,例如给 logits 加上一个极小值 epsilon 防止下溢,或在扩展时跳过 -inf 的候选。
不过 log_softmax 内部已做了数值稳定处理,通常不会出现 -inf。
第3步:生成第三个词
继续扩展:
从 [I, love](累积 -0.868)扩展:
- 预测 you: ln(0.9) = -0.105
[I, love, you]:累积 = -0.868 + (-0.105) = -0.973
从 [We, love](累积 -1.897)扩展:
- 预测 you: ln(0.7) = -0.357
[We, love, you]:累积 = -1.897 + (-0.357) = -2.254
取最高的2个(假设没有其他候选):
- 候选1:
[I, love, you],对数概率 = -0.973 - 候选2:
[We, love, you],对数概率 = -2.254
[注] 补充------停止条件 :当所有候选序列都已 EOS 结尾,或达到 max_len,或只剩一个活跃候选时,可以提前终止搜索。
最终选择
选对数概率最大的序列(最接近0):
- 最终输出:
[I, love, you],对数概率 = -0.973
对应原始概率:e^(-0.973) = 0.378,和之前一致。
四、对数版本的完整伪代码
python
import math
import torch
def beam_search_decoder_log(model, source, beam_width=3, max_len=50):
"""
使用对数概率的束搜索
"""
# 起始token(通常是 <sos> 或 <bos>)
start_token = sos_token_id
# 初始化:每个候选是一个元组 (序列, 累积对数概率)
candidates = [([start_token], 0.0)] # log(1) = 0
for step in range(max_len):
all_candidates = []
for seq, log_score in candidates:
# 如果序列已经以EOS结尾,不再扩展
if seq[-1] == eos_token_id:
all_candidates.append((seq, log_score))
continue
# 模型前向,获取下一个token的logits
# 输入:source(编码器输入)+ seq(已生成的序列)
logits = model.forward(source, seq) # shape: (vocab_size,)
# 计算对数概率(log_softmax = log(softmax))
log_probs = torch.log_softmax(logits[-1], dim=-1) # 注意:直接取log_softmax
# 取top-k个(k = beam_width)
top_k_log_probs, top_k_indices = torch.topk(log_probs, beam_width)
# 扩展候选
for i in range(beam_width):
new_seq = seq + [top_k_indices[i].item()]
new_log_score = log_score + top_k_log_probs[i].item()
all_candidates.append((new_seq, new_log_score))
# 排序:对数概率越大(数值越大)越好
all_candidates.sort(key=lambda x: x[1], reverse=True)
# 保留前 beam_width 个
candidates = all_candidates[:beam_width]
# 提前终止条件:所有候选都已EOS结尾
if all(seq[-1] == eos_token_id for seq, _ in candidates):
break
# 返回对数概率最高的序列
best_seq, best_log_score = candidates[0]
return best_seq, best_log_score
[注] 伪代码补充说明:
- 上述伪代码假设
model.forward()对每个候选独立调用。实际工程中,为了效率,会将所有候选序列批量输入模型,一次前向同时处理多个候选。学习阶段先理解逻辑即可。 sos_token_id(起始符)和eos_token_id(结束符)需要根据你的任务预先定义。
五、关键细节补充
- 为什么用
log_softmax而不是先softmax再log?
数学上等价,但 log_softmax 数值更稳定:
python
# 不推荐(可能下溢)
probs = torch.softmax(logits, dim=-1)
log_probs = torch.log(probs) # 如果probs有0,log(0) = -inf
# 推荐
log_probs = torch.log_softmax(logits, dim=-1) # 一步到位,数值稳定
- 长度归一化的对数版本
在原始概率版本中,长度归一化是:
归一化概率 = (累积概率)^(1/长度)
取对数后变成:
log(归一化概率) = (1/长度) × log(累积概率) = log(累积概率) / 长度
实现时在排序前除以长度:
python
# 长度归一化后的得分
length = len(seq)
normalized_score = log_score / (length ** alpha) # alpha通常取0.6~1.0
[注] 为什么要长度归一化:直接比较累积对数概率会偏好短句子,因为加的次数少,负得少(数值更大)。长度归一化是对此的补偿。
- 数值范围参考
| 序列长度 | 典型对数概率范围(自然对数) |
|---|---|
| 5 | -5 ~ -15 |
| 10 | -10 ~ -30 |
| 20 | -20 ~ -60 |
| 50 | -50 ~ -150 |
float32 可以安全处理到 -1000 左右,完全够用。
六、总结对比表
| 方面 | 原始概率版本 | 对数概率版本 |
|---|---|---|
| 运算 | 乘法 (×) | 加法 (+) |
| 数值范围 | 极小 (10^-20 ~ 10^-100) | 适中 (-10 ~ -100) |
| 下溢风险 | 高 | 几乎为零 |
| 比较方式 | 数值大的更好 | 数值大的更好(注意都是负数) |
| 与EOS处理 | 遇到0概率直接变0 | 可安全处理(log(0) = -inf) |
| 长度归一化 | p^(1/L) |
log_p / L |
一句话总结 :用对数概率就是将累积概率的连乘变成连加,彻底解决数值下溢问题,且不改变候选之间的相对排序关系。
实际实现中,一定要用对数概率,这是工程实践的标准做法。
3、为什么:概率要乘,而不直接相加
为什么用乘法而不是加法?
这是一个很好的问题!它触及了概率论中的一个核心原理。
简短回答
因为我们要找的是"整个句子同时出现的概率",这个概率必须用乘法计算,而不是加法。
如果你把概率相加,得到的东西根本就不是"概率",而是一个没有概率意义的数字,用它来比较会得出错误结论。
详细解释
一、概率的基本规则
假设一个句子由三个词组成:A, B, C
模型生成这个句子的过程是:
- 第一步:在给定输入的条件下,预测第一个词是
A的概率 →P(A) - 第二步:在给定输入和
A的条件下,预测第二个词是B的概率 →P(B | A) - 第三步:在给定输入和
A, B的条件下,预测第三个词是C的概率 →P(C | A, B)
整个句子 [A, B, C] 同时出现的概率是多少?
根据概率论中的链式法则:
P(A, B, C) = P(A) × P(B | A) × P(C | A, B)
这就是乘法的来源。这不是人为规定的,而是概率的数学定义。
[注] 直观理解 :要整个句子成功,必须第一步成功 并且 第二步成功 并且 第三步成功。在概率中,"并且"对应的是乘法(假设条件概率关系正确)。
二、为什么加法不行?
假设我们有两条候选路径:
| 路径 | 各步概率 | 乘积(正确) | 和(错误) |
|---|---|---|---|
| 路径1 | 0.6, 0.7, 0.9 | 0.378 | 2.2 |
| 路径2 | 0.9, 0.1, 0.9 | 0.081 | 1.9 |
用乘积比较:路径1更好(0.378 > 0.081)✅
用和比较:路径1也更好(2.2 > 1.9)✅
这个例子看起来没问题,但换一个例子:
| 路径 | 各步概率 | 乘积(正确) | 和(错误) |
|---|---|---|---|
| 路径1 | 0.9, 0.9, 0.1 | 0.081 | 1.9 |
| 路径2 | 0.5, 0.5, 0.5 | 0.125 | 1.5 |
用乘积比较:路径2更好(0.125 > 0.081)✅
用和比较:路径1更好(1.9 > 1.5)❌ 结论相反!
问题出在哪里?
路径1的第三步概率只有0.1(非常差),但因为前两步的0.9很高,用"和"计算时,第三步的"差"被掩盖了。用"乘积"计算时,0.1直接把整个结果拉低了。
逻辑上的原因 :一个句子必须每一步都正确才能算成功。只要某一步概率很低,整个句子的概率就应该很低。乘法天然体现了这一点(任何一个因子小 → 乘积小),而加法做不到。
[注] 补充------加法的数学错误:如果尝试用加法定义"句子得分",那么一个包含100个概率为0.5的词的句子得分是50,而一个包含3个概率为0.9的词的句子得分是2.7。按加法比较,长句子会永远优于短句子,这显然不合理。
三、用极端例子加深理解
假设两个句子:
- 句子A:第1步概率 1.0,第2步概率 1.0,第3步概率 0.000001
- 句子B:第1步概率 0.5,第2步概率 0.5,第3步概率 0.5
哪个句子更好?
从常识判断:句子B每一步都有50%的成功率,而句子A虽然前两步完美,但第三步几乎不可能成功(百万分之一的概率)。句子A整体几乎不可能被生成出来,所以句子B应该更好。
| 方法 | 句子A | 句子B | 结论 |
|---|---|---|---|
| 乘积 | 1.0 × 1.0 × 0.000001 = 0.000001 | 0.125 | B更好 ✅ |
| 和 | 1.0 + 1.0 + 0.000001 = 2.000001 | 1.5 | A更好 ❌ |
乘积正确反映了一个事实:一个链条的强度取决于它最薄弱的一环。加法则错误地让"长度"和"高概率步骤"掩盖了致命缺陷。
四、那对数是怎么回事?
你可能会困惑:既然你说要用乘法,那为什么前面又讲用对数变成加法?
这其实是两回事:
| 场景 | 操作 | 目的 |
|---|---|---|
| 数学上计算概率 | 乘法 | 这是概率的定义 |
| 计算机实现时 | 取对数后加法 | 防止数值下溢,不改变比较结果 |
因为 log(a × b × c) = log(a) + log(b) + log(c),所以:
- 比较
a × b × c的大小 ⇔ 比较log(a) + log(b) + log(c)的大小
对数加法只是乘法的一种数值稳定的实现方式,不是用加法替代乘法的逻辑。
[注] 补充------为什么不能直接用加法 :数学上的加法(不用对数)和取对数后的加法是两回事。前者是 a+b+c,后者是 log(a)+log(b)+log(c),两者完全不同。不要把"取对数后求和"误解为"直接对原概率求和"。
五、总结
| 问题 | 回答 |
|---|---|
| 为什么要用乘法? | 因为链式法则定义联合概率就是连乘 |
| 加法为什么不行? | 加法会掩盖低概率步骤的影响,得出错误结论 |
| 那对数加法是怎么回事? | 那是数值实现技巧,不是改变数学逻辑 |
a+b+c 和 log(a)+log(b)+log(c) 一样吗? |
完全不同。后者是乘法的等价变形,前者不是 |
一句话:乘法是概率的数学定义,取对数后求和是工程上为了避免下溢而对乘法做的等价变形。两者不是"替代"关系,而是"实现"关系。直接对原概率求和则是错误的做法。
4、使用 "长度归一化" 的束搜索
使用长度归一化的束搜索详解
一、为什么要用长度归一化?
问题1:束搜索天然偏好短句子
在束搜索中,我们比较的是累积概率(或累积对数概率):
累积概率 = P(词1) × P(词2) × P(词3) × ... × P(词n)
由于每个概率都小于1,乘的次数越多,结果就越小。
数值示例:
| 句子长度 | 假设每步概率0.5 | 累积概率 |
|---|---|---|
| 5个词 | 0.5 5 = 0.5 × 0.5 × 0.5 × 0.5 × 0.5 0.5^5 = 0.5 \times 0.5 \times 0.5 \times 0.5 \times 0.5 0.55=0.5×0.5×0.5×0.5×0.5 | 0.03125 0.03125 0.03125 |
| 10个词 | 0.5 10 0.5^{10} 0.510 | 0.0009765625 0.0009765625 0.0009765625 |
| 20个词 | 0.5 20 0.5^{20} 0.520 | 0.00000095367431640625 0.00000095367431640625 0.00000095367431640625 |
观察 :20个词的累积概率大约是5个词的 1 / 32768 1/32768 1/32768。这意味着:即使长句子的每一步质量都和短句子一样好,它的累积概率也会小得多。
核心问题 :同样的平均质量下,短句子的累积概率天然比长句子大。这导致束搜索会不公平地偏爱短句子,即使长句子的翻译更完整、更准确。
问题2:实际例子说明
假设我们要翻译以下中文句子:
输入:"那只猫坐在垫子上"
正确翻译:"The cat sat on the mat"
假设模型生成了三个候选翻译:
| 候选翻译 | 长度(词数) | 各步概率(示意) | 累积概率 | 无归一化排名 |
|---|---|---|---|---|
| A | 3 | 0.5 , 0.6 , 0.4 0.5, 0.6, 0.4 0.5,0.6,0.4 | 0.5 × 0.6 × 0.4 = 0.12 0.5 \times 0.6 \times 0.4 = 0.12 0.5×0.6×0.4=0.12 | 第1名 ❌ |
| B | 4 | 0.5 , 0.6 , 0.4 , 0.3 0.5, 0.6, 0.4, 0.3 0.5,0.6,0.4,0.3 | 0.12 × 0.3 = 0.036 0.12 \times 0.3 = 0.036 0.12×0.3=0.036 | 第2名 |
| C | 6 | 0.5 , 0.6 , 0.4 , 0.3 , 0.5 , 0.4 0.5, 0.6, 0.4, 0.3, 0.5, 0.4 0.5,0.6,0.4,0.3,0.5,0.4 | 0.036 × 0.5 × 0.4 = 0.0072 0.036 \times 0.5 \times 0.4 = 0.0072 0.036×0.5×0.4=0.0072 | 第3名 |
问题暴露:
- 候选A("The cat sat")是不完整的翻译,漏掉了"on the mat"
- 候选C("The cat sat on the mat")是完整正确的翻译
- 但候选A的累积概率最高,束搜索会错误地选择不完整的翻译
为什么会这样? 因为候选C多了3个词,每个词的概率( 0.5 , 0.4 0.5, 0.4 0.5,0.4)都小于1,乘了3次后,累积概率被大幅拉低。
问题3:对数版本下的同样问题
用对数概率表示(取自然对数):
| 候选 | 各步对数概率(数值) | 累积对数概率 | 无归一化排名 |
|---|---|---|---|
| A | − 0.693 , − 0.511 , − 0.916 -0.693,\ -0.511,\ -0.916 −0.693, −0.511, −0.916 | − 0.693 − 0.511 − 0.916 = − 2.120 -0.693 - 0.511 - 0.916 = -2.120 −0.693−0.511−0.916=−2.120 | 第1名 |
| B | − 0.693 , − 0.511 , − 0.916 , − 1.204 -0.693,\ -0.511,\ -0.916,\ -1.204 −0.693, −0.511, −0.916, −1.204 | − 2.120 − 1.204 = − 3.324 -2.120 - 1.204 = -3.324 −2.120−1.204=−3.324 | 第2名 |
| C | − 0.693 , − 0.511 , − 0.916 , − 1.204 , − 0.693 , − 0.916 -0.693,\ -0.511,\ -0.916,\ -1.204,\ -0.693,\ -0.916 −0.693, −0.511, −0.916, −1.204, −0.693, −0.916 | − 3.324 − 0.693 − 0.916 = − 4.933 -3.324 - 0.693 - 0.916 = -4.933 −3.324−0.693−0.916=−4.933 | 第3名 |
说明:
- ln ( 0.5 ) ≈ − 0.693 \ln(0.5) \approx -0.693 ln(0.5)≈−0.693, ln ( 0.6 ) ≈ − 0.511 \ln(0.6) \approx -0.511 ln(0.6)≈−0.511, ln ( 0.4 ) ≈ − 0.916 \ln(0.4) \approx -0.916 ln(0.4)≈−0.916, ln ( 0.3 ) ≈ − 1.204 \ln(0.3) \approx -1.204 ln(0.3)≈−1.204
- 所有对数概率均为负数,累积时不断相加(即越来越小)
虽然负数的比较是"越大越好"( − 2.120 > − 3.324 > − 4.933 -2.120 > -3.324 > -4.933 −2.120>−3.324>−4.933),但问题依然存在:更长的句子因为加了更多负数,得分自然更低。
长度归一化的目的 :消除长度对累积概率的负面影响,让不同长度的句子可以在公平的尺度上比较。
二、长度归一化的数学原理
核心思想:几何平均(几何平均是开根号,算术平均才是除以总数)
我们真正关心的是每一步的平均概率,而不是累积概率。
什么是几何平均?对于一组正数,几何平均是它们乘积的L次方根:
几何平均 = ( p 1 × p 2 × p 3 × ⋯ × p L ) 1 / L (p_1 \times p_2 \times p_3 \times \dots \times p_L)^{1/L} (p1×p2×p3×⋯×pL)1/L
几何平均的性质:
- 不受序列长度影响
- 如果每一步概率都是 0.5 0.5 0.5,无论L是多少,几何平均都是 0.5 0.5 0.5
- 如果每一步概率都是 0.8 0.8 0.8,几何平均就是 0.8 0.8 0.8
与算术平均的对比:
| 序列 | 算术平均 | 几何平均 |
|---|---|---|
| 0.9 , 0.9 , 0.1 0.9, 0.9, 0.1 0.9,0.9,0.1 | ( 0.9 + 0.9 + 0.1 ) / 3 = 0.633 (0.9 + 0.9 + 0.1)/3 = 0.633 (0.9+0.9+0.1)/3=0.633 | ( 0.9 × 0.9 × 0.1 ) 1 / 3 = 0.432 (0.9 \times 0.9 \times 0.1)^{1/3} = 0.432 (0.9×0.9×0.1)1/3=0.432 |
| 0.5 , 0.5 , 0.5 0.5, 0.5, 0.5 0.5,0.5,0.5 | 0.5 0.5 0.5 | 0.5 0.5 0.5 |
关键区别 :几何平均对低值更敏感。 0.1 0.1 0.1的存在让几何平均从 0.633 0.633 0.633降到 0.432 0.432 0.432,而算术平均只降到 0.633 0.633 0.633。这符合我们的直觉:一个链条的强度取决于它最薄弱的一环。
长度归一化的基本公式
将几何平均作为评分标准:
score = ( p 1 × p 2 × ⋯ × p L ) 1 / L (p_1 \times p_2 \times \dots \times p_L)^{1/L} (p1×p2×⋯×pL)1/L
比较时,我们比较的是每一步的平均表现,而不是总乘积。
对数版本(实际实现用)
取对数后:
log ( score ) = log ( p 1 × p 2 × ⋯ × p L ) 1 / L = 1 L × ( log p 1 + log p 2 + ⋯ + log p L ) \log(\text{score}) = \log\left(p_1 \\times p_2 \\times \\dots \\times p_L)\^{1/L}\\right = \frac{1}{L} \times (\log p_1 + \log p_2 + \dots + \log p_L) log(score)=log(p1×p2×⋯×pL)1/L=L1×(logp1+logp2+⋯+logpL)
-
特别注意:其中 ( log p 1 + log p 2 + ⋯ + log p L ) (\log p_1 + \log p_2 + \dots + \log p_L) (logp1+logp2+⋯+logpL) 是个负数 ,假设 ( log p 1 + log p 2 + ⋯ + log p L ) (\log p_1 + \log p_2 + \dots + \log p_L) (logp1+logp2+⋯+logpL) 不变:
L ≥ 1 {L} \geq 1 L≥1,分母 L {L} L 越大, 1 L \frac{1}{L} L1 越小, log ( score ) \log(\text{score}) log(score) 越大;
L ≥ 1 {L} \geq 1 L≥1,分母 L {L} L 越小, 1 L \frac{1}{L} L1 越大, log ( score ) \log(\text{score}) log(score) 越小;
即:归一化对数概率 = 累积对数概率 / L L L
引入调节参数 α \alpha α
实践中,完全消除长度影响(除以 L L L)不一定最优。更通用的形式是引入一个长度惩罚指数 α \alpha α:
normalized_score = 累积对数概率 / L α L^\alpha Lα
log ( score ) = log ( p 1 × p 2 × ⋯ × p L ) 1 / L α = 1 L α × ( log p 1 + log p 2 + ⋯ + log p L ) \log(\text{score}) = \log\left(p_1 \\times p_2 \\times \\dots \\times p_L)\^{1/L\^\\alpha}\\right = \frac{1}{L^\alpha} \times (\log p_1 + \log p_2 + \dots + \log p_L) log(score)=log(p1×p2×⋯×pL)1/Lα=Lα1×(logp1+logp2+⋯+logpL)
其中:
-
L L L : L ≥ 1 {L} \geq 1 L≥1,句子长度(通常不包含起始符和结束符)
-
α \alpha α :长度惩罚系数, α ≥ 0 \alpha \geq 0 α≥0。注意, α = 0 \alpha = 0 α=0 时, L α = 1 L^\alpha = 1 Lα=1
-
特别注意:其中 ( log p 1 + log p 2 + ⋯ + log p L ) (\log p_1 + \log p_2 + \dots + \log p_L) (logp1+logp2+⋯+logpL) 是个负数
L {L} L 不变, α ≥ 0 \alpha \geq 0 α≥0,分母 L α ≥ 1 {L^\alpha}\geq 1 Lα≥1 ,α \alpha α 越小 , L α {L^\alpha} Lα 越小, 1 L α \frac{1}{L^\alpha} Lα1 越大,log ( score ) \log(\text{score}) log(score) 越小;
L {L} L 不变, α ≥ 0 \alpha \geq 0 α≥0,分母 L α ≥ 1 {L^\alpha}\geq 1 Lα≥1 ,α \alpha α 越大 , L α {L^\alpha} Lα 越大, 1 L α \frac{1}{L^\alpha} Lα1 越小,log ( score ) \log(\text{score}) log(score) 越大
长度归一化的束搜索: α \alpha α 参数详解
在束搜索中,长度归一化用于消除句子长度对得分的影响,公式为:
score = ∑ i = 1 L log p i L α \text{score} = \frac{\sum_{i=1}^{L} \log p_i}{L^{\alpha}} score=Lα∑i=1Llogpi
其中 ∑ log p i \sum \log p_i ∑logpi 是累积对数概率(负数), L L L 是句子长度, α \alpha α 控制长度惩罚的强度。
α \alpha α 的作用与效果对照表
| α \alpha α 值 | 公式 | 效果 | 详细解析 |
|---|---|---|---|
| α = 0 \alpha = 0 α=0 | score = ∑ log p i \text{score} = \sum \log p_i score=∑logpi | 强偏好短句 | 对数概率为负,句子越长,累加的负数越多,得分越负(越差)。模型倾向于选择更短的句子。 |
| 0 < α < 1 0 < \alpha < 1 0<α<1 (如 0.6 ∼ 0.7 0.6\sim0.7 0.6∼0.7) | score = ∑ log p i L α \text{score} = \frac{\sum \log p_i}{L^{\alpha}} score=Lα∑logpi | 相对偏好长句 | 分母的增长慢于分子绝对值的增长,虽然长句得分仍是负数,但比 α = 0 \alpha=0 α=0 时受到的惩罚更小。相对而言,长句的排名被提升,模型倾向于选择更长、更完整的句子(经验最佳区间)。 |
| α = 1 \alpha = 1 α=1 | score = ∑ log p i L \text{score} = \frac{\sum \log p_i}{L} score=L∑logpi | 长度中立 | 得分等价于几何平均概率,长短句在长度上无偏差。 |
| α > 1 \alpha > 1 α>1 | score = ∑ log p i L α \text{score} = \frac{\sum \log p_i}{L^{\alpha}} score=Lα∑logpi | 绝对偏好长句 | 分母增长快于分子绝对值的增长,使得长句的得分高于短句(更接近0)。模型会过度奖励长度,可能导致生成冗长、重复甚至不终止的句子。 |
为什么 α > 1 \alpha > 1 α>1 会导致问题?
虽然 α > 1 \alpha > 1 α>1 使长句得分更高,但在实际应用中几乎不使用,因为:
- 它会鼓励模型为刷高分而添加无意义的词(每增加一个词,分母变大带来的"提分"可能超过该词本身的概率惩罚)。
- 输出容易变得冗长、重复,甚至陷入死循环。
为什么引入 α \alpha α 而不直接用 α = 1 \alpha=1 α=1?
α = 1 \alpha=1 α=1 看似"公平",但在机器翻译等任务中,经验表明 α ≈ 0.6 ∼ 0.7 \alpha \approx 0.6\sim0.7 α≈0.6∼0.7 效果更好,原因如下:
- 对抗天然短句偏好 : α = 0 \alpha=0 α=0 时模型天生倾向于短句。适度降低惩罚( α < 1 \alpha<1 α<1)可以平衡这种倾向。
- 鼓励完整性:轻微"奖励"长句(即让长句得分相对提高)有助于模型生成语法完整、语义通顺的句子,避免过早截断。
- 经验最优 :Google NMT、OpenNMT 等框架的实验表明, α = 0.6 ∼ 0.7 \alpha=0.6\sim0.7 α=0.6∼0.7 在 BLEU 分数上表现最佳。
数学原理: α \alpha α 如何影响长度偏好
考虑两个质量相同但长度不同的句子:
- 短句 :长度 L L L,累积对数概率 S S S( S < 0 S<0 S<0)。
- 长句 :长度 2 L 2L 2L,每一步生成质量相同,故累积对数概率为 2 S 2S 2S(这是理想化假设,实际长句往往包含更多低概率词,但此处用于说明趋势)。
应用归一化公式:
短句得分:
A = S L α A = \frac{S}{L^{\alpha}} A=LαS
长句得分:
B = 2 S ( 2 L ) α = 2 S 2 α L α = 2 1 − α ⋅ S L α = 2 1 − α ⋅ A B = \frac{2S}{(2L)^{\alpha}} = \frac{2S}{2^{\alpha}L^{\alpha}} = 2^{1-\alpha} \cdot \frac{S}{L^{\alpha}} = 2^{1-\alpha} \cdot A B=(2L)α2S=2αLα2S=21−α⋅LαS=21−α⋅A
由于 A < 0 A<0 A<0,比较 A A A 与 B B B 的大小:
- 若 2 1 − α > 1 2^{1-\alpha} > 1 21−α>1(即 α < 1 \alpha < 1 α<1),则 B < A B < A B<A,短句得分更高 → 模型相对偏好短句 。但注意,当 α \alpha α 从 0 增加到 0.6 时, 2 1 − α 2^{1-\alpha} 21−α 从 2 下降到约 1.32,长句的劣势在缩小,因此长句的相对排名上升,这就是"相对偏好长句"的含义。
- 若 2 1 − α = 1 2^{1-\alpha} = 1 21−α=1( α = 1 \alpha=1 α=1),则 B = A B = A B=A,长度中立。
- 若 2 1 − α < 1 2^{1-\alpha} < 1 21−α<1( α > 1 \alpha>1 α>1),则 B > A B > A B>A,长句得分更高 → 模型绝对偏好长句。
实际中,由于长句的累积概率绝对值通常比 2 ∣ S ∣ 2|S| 2∣S∣ 更大, α \alpha α 在 0.6 ∼ 0.7 0.6\sim0.7 0.6∼0.7 时已能有效抵消天然短句偏好,使模型选择更完整的输出。
三、完整示例:逐步对比有无归一化
场景设置
- 翻译任务:中译英
- 输入句子:"我爱机器学习"
- 束宽 = 2
- 使用自然对数, α = 0.6 \alpha=0.6 α=0.6 用于归一化(对比时也展示 α = 0 \alpha=0 α=0 和 α = 1 \alpha=1 α=1)
- 模型每步输出的对数概率(示例数值)
第1步:生成第一个词
模型输出的对数概率:
| 词 | 对数概率 |
|---|---|
| I | − 0.5 -0.5 −0.5 |
| We | − 1.2 -1.2 −1.2 |
| They | − 2.5 -2.5 −2.5 |
取对数概率最高的2个(束宽=2):
- 候选1:
[I],累积对数概率 = − 0.5 -0.5 −0.5,长度=1 - 候选2:
[We],累积对数概率 = − 1.2 -1.2 −1.2,长度=1
第2步:生成第二个词
对候选1 [I]:预测下一个词的对数概率
| 词 | 对数概率 |
|---|---|
| love | − 0.3 -0.3 −0.3 |
| like | − 1.0 -1.0 −1.0 |
| enjoy | − 2.1 -2.1 −2.1 |
对候选2 [We]:预测下一个词的对数概率
| 词 | 对数概率 |
|---|---|
| love | − 0.4 -0.4 −0.4 |
| are | − 1.1 -1.1 −1.1 |
| will | − 1.8 -1.8 −1.8 |
扩展新候选(对数概率相加):
从 [I] 扩展:
[I, love]:累积 = − 0.5 + ( − 0.3 ) = − 0.8 -0.5 + (-0.3) = -0.8 −0.5+(−0.3)=−0.8[I, like]:累积 = − 0.5 + ( − 1.0 ) = − 1.5 -0.5 + (-1.0) = -1.5 −0.5+(−1.0)=−1.5[I, enjoy]:累积 = − 0.5 + ( − 2.1 ) = − 2.6 -0.5 + (-2.1) = -2.6 −0.5+(−2.1)=−2.6
从 [We] 扩展:
[We, love]:累积 = − 1.2 + ( − 0.4 ) = − 1.6 -1.2 + (-0.4) = -1.6 −1.2+(−0.4)=−1.6[We, are]:累积 = − 1.2 + ( − 1.1 ) = − 2.3 -1.2 + (-1.1) = -2.3 −1.2+(−1.1)=−2.3[We, will]:累积 = − 1.2 + ( − 1.8 ) = − 3.0 -1.2 + (-1.8) = -3.0 −1.2+(−1.8)=−3.0
现在有6个长度2的候选。保留累积对数概率最高的2个作为下一轮前缀:
- 候选1:
[I, love],累积 = − 0.8 -0.8 −0.8 - 候选2:
[I, like],累积 = − 1.5 -1.5 −1.5
([We, love] 得分为 − 1.6 -1.6 −1.6,被淘汰。)
第3步:生成第三个词
从 [I, love] 扩展:
| 词 | 对数概率 |
|---|---|
| machine | − 0.4 -0.4 −0.4 |
| studying | − 1.5 -1.5 −1.5 |
| learning | − 1.8 -1.8 −1.8 |
[I, love, machine]:累积 = − 0.8 + ( − 0.4 ) = − 1.2 -0.8 + (-0.4) = -1.2 −0.8+(−0.4)=−1.2[I, love, studying]:累积 = − 0.8 + ( − 1.5 ) = − 2.3 -0.8 + (-1.5) = -2.3 −0.8+(−1.5)=−2.3[I, love, learning]:累积 = − 0.8 + ( − 1.8 ) = − 2.6 -0.8 + (-1.8) = -2.6 −0.8+(−1.8)=−2.6
从 [I, like] 扩展:
| 词 | 对数概率 |
|---|---|
| machine | − 0.6 -0.6 −0.6 |
| studying | − 1.2 -1.2 −1.2 |
| learning | − 1.5 -1.5 −1.5 |
[I, like, machine]:累积 = − 1.5 + ( − 0.6 ) = − 2.1 -1.5 + (-0.6) = -2.1 −1.5+(−0.6)=−2.1[I, like, studying]:累积 = − 1.5 + ( − 1.2 ) = − 2.7 -1.5 + (-1.2) = -2.7 −1.5+(−1.2)=−2.7[I, like, learning]:累积 = − 1.5 + ( − 1.5 ) = − 3.0 -1.5 + (-1.5) = -3.0 −1.5+(−1.5)=−3.0
现在有6个长度3的候选。保留累积对数概率最高的2个作为下一轮前缀:
- 候选1:
[I, love, machine],累积 = − 1.2 -1.2 −1.2 - 候选2:
[I, like, machine],累积 = − 2.1 -2.1 −2.1
([I, love, studying] 得分为 − 2.3 -2.3 −2.3,被淘汰。)
第4步:生成第四个词(假设达到最大长度,且模型允许提前结束)
从 [I, love, machine] 扩展:
| 词 | 对数概率 |
|---|---|
| learning | − 0.2 -0.2 −0.2 |
| studying | − 1.8 -1.8 −1.8 |
[I, love, machine, learning]:累积 = − 1.2 + ( − 0.2 ) = − 1.4 -1.2 + (-0.2) = -1.4 −1.2+(−0.2)=−1.4,长度=4[I, love, machine, studying]:累积 = − 1.2 + ( − 1.8 ) = − 3.0 -1.2 + (-1.8) = -3.0 −1.2+(−1.8)=−3.0,长度=4
从 [I, like, machine] 扩展:
| 词 | 对数概率 |
|---|---|
| learning | − 0.3 -0.3 −0.3 |
| studying | − 1.5 -1.5 −1.5 |
[I, like, machine, learning]:累积 = − 2.1 + ( − 0.3 ) = − 2.4 -2.1 + (-0.3) = -2.4 −2.1+(−0.3)=−2.4,长度=4[I, like, machine, studying]:累积 = − 2.1 + ( − 1.5 ) = − 3.6 -2.1 + (-1.5) = -3.6 −2.1+(−1.5)=−3.6,长度=4
此时,我们还有第3步结束时保留的两个长度3的候选 [I, love, machine] 和 [I, like, machine]。在标准束搜索中,如果允许提前结束(例如遇到句尾标记或达到最大长度),这些未扩展的候选也被视为"已完成"句子,参与最终排序。
因此,最终候选池包含:
| 候选序列 | 长度 L L L | 累积对数概率 |
|---|---|---|
[I, love, machine] |
3 | − 1.2 -1.2 −1.2 |
[I, like, machine] |
3 | − 2.1 -2.1 −2.1 |
[I, love, machine, learning] |
4 | − 1.4 -1.4 −1.4 |
[I, love, machine, studying] |
4 | − 3.0 -3.0 −3.0 |
[I, like, machine, learning] |
4 | − 2.4 -2.4 −2.4 |
[I, like, machine, studying] |
4 | − 3.6 -3.6 −3.6 |
(注:实际束宽=2时,在每一步扩展后只保留2个前缀,但最终排序时所有已完成的候选都会被考虑,上述列表包含了所有在第3、4步产生的候选。)
比较结果
- 无归一化( α = 0 \alpha = 0 α=0)
直接按累积对数概率排序(数值越大越好):
| 候选序列 | 累积对数概率 | 排名 |
|---|---|---|
[I, love, machine] |
− 1.2 -1.2 −1.2 | 1 ❌(不完整) |
[I, love, machine, learning] |
− 1.4 -1.4 −1.4 | 2 ✅(完整正确) |
[I, like, machine] |
− 2.1 -2.1 −2.1 | 3 |
[I, like, machine, learning] |
− 2.4 -2.4 −2.4 | 4 |
[I, love, machine, studying] |
− 3.0 -3.0 −3.0 | 5 |
[I, like, machine, studying] |
− 3.6 -3.6 −3.6 | 6 |
→ 束搜索会选择不完整的短句 [I, love, machine],尽管完整翻译 [I, love, machine, learning] 语义更优。
- 有归一化( α = 0.6 \alpha = 0.6 α=0.6)
计算归一化得分:
score = 累积对数概率 L 0.6 \text{score} = \frac{\text{累积对数概率}}{L^{0.6}} score=L0.6累积对数概率
L 0.6 L^{0.6} L0.6 值:
- L = 3 L=3 L=3: 3 0.6 ≈ 1.933 3^{0.6} \approx 1.933 30.6≈1.933
- L = 4 L=4 L=4: 4 0.6 ≈ 2.297 4^{0.6} \approx 2.297 40.6≈2.297
| 候选序列 | 累积对数概率 | 归一化得分( α = 0.6 \alpha=0.6 α=0.6) | 排名 |
|---|---|---|---|
[I, love, machine, learning] |
− 1.4 -1.4 −1.4 | − 1.4 / 2.297 ≈ − 0.609 -1.4 / 2.297 \approx -0.609 −1.4/2.297≈−0.609 | 1 ✅ |
[I, love, machine] |
− 1.2 -1.2 −1.2 | − 1.2 / 1.933 ≈ − 0.621 -1.2 / 1.933 \approx -0.621 −1.2/1.933≈−0.621 | 2 |
[I, like, machine, learning] |
− 2.4 -2.4 −2.4 | − 2.4 / 2.297 ≈ − 1.045 -2.4 / 2.297 \approx -1.045 −2.4/2.297≈−1.045 | 3 |
[I, like, machine] |
− 2.1 -2.1 −2.1 | − 2.1 / 1.933 ≈ − 1.086 -2.1 / 1.933 \approx -1.086 −2.1/1.933≈−1.086 | 4 |
[I, love, machine, studying] |
− 3.0 -3.0 −3.0 | − 3.0 / 2.297 ≈ − 1.306 -3.0 / 2.297 \approx -1.306 −3.0/2.297≈−1.306 | 5 |
[I, like, machine, studying] |
− 3.6 -3.6 −3.6 | − 3.6 / 2.297 ≈ − 1.567 -3.6 / 2.297 \approx -1.567 −3.6/2.297≈−1.567 | 6 |
→ 完整且高质量的句子 [I, love, machine, learning] 胜出 ✅
- 有归一化( α = 1.0 \alpha = 1.0 α=1.0)
归一化得分 = 累积对数概率 / L L L
| 候选序列 | 累积对数概率 | 归一化得分( α = 1.0 \alpha=1.0 α=1.0) | 排名 |
|---|---|---|---|
[I, love, machine, learning] |
− 1.4 -1.4 −1.4 | − 1.4 / 4 = − 0.350 -1.4 / 4 = -0.350 −1.4/4=−0.350 | 1 ✅ |
[I, love, machine] |
− 1.2 -1.2 −1.2 | − 1.2 / 3 = − 0.400 -1.2 / 3 = -0.400 −1.2/3=−0.400 | 2 |
[I, like, machine, learning] |
− 2.4 -2.4 −2.4 | − 2.4 / 4 = − 0.600 -2.4 / 4 = -0.600 −2.4/4=−0.600 | 3 |
[I, like, machine] |
− 2.1 -2.1 −2.1 | − 2.1 / 3 = − 0.700 -2.1 / 3 = -0.700 −2.1/3=−0.700 | 4 |
[I, love, machine, studying] |
− 3.0 -3.0 −3.0 | − 3.0 / 4 = − 0.750 -3.0 / 4 = -0.750 −3.0/4=−0.750 | 5 |
[I, like, machine, studying] |
− 3.6 -3.6 −3.6 | − 3.6 / 4 = − 0.900 -3.6 / 4 = -0.900 −3.6/4=−0.900 | 6 |
→ 同样,完整句子胜出 ✅
小结
- 无归一化时:即使完整句子质量更高,也会因长度吃亏而排第二。
- 有归一化后(无论 α = 0.6 \alpha=0.6 α=0.6 或 1.0 1.0 1.0) :候选池中得分最高的都是完整且语义正确的
[I, love, machine, learning]。 - α = 0.6 \alpha=0.6 α=0.6 时,第一名与第二名得分接近( − 0.609 -0.609 −0.609 vs − 0.621 -0.621 −0.621,差 0.012 0.012 0.012),体现"温和调整";
- α = 1.0 \alpha=1.0 α=1.0 时,差距更大( − 0.350 -0.350 −0.350 vs − 0.400 -0.400 −0.400,差 0.05 0.05 0.05),调整更彻底。
关键点 :长度归一化不是"让长句一定赢",而是让平均质量更高的句子胜出 。本例中,
[I, love, machine, learning]不仅完整,而且每步概率都很高(累积仅 − 1.4 -1.4 −1.4),因此在公平比较下自然胜出。
四、参数 α \alpha α 的深入讨论
α \alpha α 的实验结果
来自机器翻译领域的经典论文(Wu et al., 2016, Google's Neural Machine Translation System):
| α \alpha α 值 | BLEU分数(相对于 α = 0 \alpha=0 α=0 的提升) | 观察 |
|---|---|---|
| 0 | 基准值 | 明显偏低 |
| 0.2 | +0.5 | 略有提升 |
| 0.4 | +1.2 | 明显提升 |
| 0.6 | +1.8 | 最佳 |
| 0.8 | +1.5 | 略低于0.6 |
| 1.0 | +1.0 | 仍优于基准 |
结论 : α = 0.6 \alpha=0.6 α=0.6 在多个翻译任务上表现最佳。
α \alpha α 的直观理解
α = 0 \alpha=0 α=0(不归一化)
得分 = 累积对数概率
- 短句子天然占优(因为累加负对数概率,句子越长得分越负)。
- 模型倾向于输出过短的翻译,信息不完整。
α = 0.6 \alpha=0.6 α=0.6(轻度归一化)
得分 = 累积对数概率 / L 0.6 L^{0.6} L0.6
- 部分消除了长度偏见,长句子的得分相对提升(但仍可能略低于同等质量的短句)。
- 平衡了"完整性"(鼓励更长、更完整的翻译)和"简洁性"(避免过度冗余)。
- 这是实践中表现最好的区间。
α = 1.0 \alpha=1.0 α=1.0(完全几何平均)
得分 = 累积对数概率 / L L L
- 完全消除了长度偏见,不同长度句子的平均质量直接可比。
- 模型不再因为长度而天然偏向短句或长句,选择完全基于每一步的平均概率。
α > 1 \alpha>1 α>1(过度归一化)
得分 = 累积对数概率 / L α L^\alpha Lα
- 长句子反而占优,模型会倾向于输出过长的翻译,可能包含冗余信息甚至无意义的重复。
长度 L L L 的计数方式
不同实现中, L L L 的计数方式有细微差别:
| 方式 | L L L 的定义 | 说明 |
|---|---|---|
| 方式1 | 句子中的词数(包括结束符) | 最简单,最常用 |
| 方式2 | 句子中的词数 − 1 - 1 −1 | 减去起始符(如 <s>) |
| 方式3 | 句子中的词数 − 2 - 2 −2 | 减去起始符和结束符(如 <s> 和 </s>) |
| 方式4 | 使用平滑惩罚项(见下方公式) | Google NMT 的工程优化,对极短句惩罚更温和 |
方式4详解 :
Google NMT 并没有改变 L L L 的定义( L L L 仍为包含结束符的句子长度),而是将归一化分母改为一个平滑的惩罚项,避免极短句子被过度放大:
penalty ( y ) = ( 5 + L 5 + 1 ) α \text{penalty}(y) = \left( \frac{5 + L}{5 + 1} \right)^\alpha penalty(y)=(5+15+L)α
score ( y ) = log P ( y ∣ x ) penalty ( y ) = log P ( y ∣ x ) ( ( 5 + L ) / 6 ) α \text{score}(y) = \frac{\log P(y|x)}{\text{penalty}(y)} = \frac{\log P(y|x)}{\bigl((5+L)/6\bigr)^\alpha} score(y)=penalty(y)logP(y∣x)=((5+L)/6)αlogP(y∣x)
当 L = 1 L=1 L=1 时, penalty = ( 6 / 6 ) α = 1 \text{penalty} = (6/6)^\alpha = 1 penalty=(6/6)α=1,不会因为长度过短而得到极端高分;当 L L L 较大时,惩罚项接近 ( L / 6 ) α (L/6)^\alpha (L/6)α,与常规长度归一化趋于一致。
这个公式来自 Google NMT 论文(Wu et al., 2016),数字 5 和 6 是经验设计的平滑常数 ,目的是为了避免极短句子被过度奖励。
设计动机
如果使用原始的长度归一化:
score = log P ( y ∣ x ) L α \text{score} = \frac{\log P(y|x)}{L^\alpha} score=LαlogP(y∣x)
当句子长度 L = 1 L=1 L=1 时,分母为 1 α = 1 1^\alpha = 1 1α=1,得分就是 log P \log P logP。
当 L = 2 L=2 L=2 时,分母为 2 α 2^\alpha 2α,得分是 log P / 2 α \log P / 2^\alpha logP/2α。
问题在于:对于极短的句子(尤其是长度1),分母太小,导致得分可能虚高。比如一个长度为1但概率很低的句子,得分可能比一个长度为2且质量不错的句子还高,这不符合直觉。
平滑公式的作用
Google NMT 将分母改为一个平滑的惩罚项:
penalty = ( 5 + L 5 + 1 ) α = ( 5 + L 6 ) α \text{penalty} = \left( \frac{5 + L}{5 + 1} \right)^\alpha = \left( \frac{5 + L}{6} \right)^\alpha penalty=(5+15+L)α=(65+L)α
- 分子 5 + L 5 + L 5+L:给长度加上一个常数(这里是5),使短句的惩罚项不会太小。
- 分母 6 6 6:归一化因子,确保当 L = 1 L=1 L=1 时惩罚项为 1 1 1。
当 L = 1 L=1 L=1 时: ( 5 + 1 ) / 6 = 1 (5+1)/6 = 1 (5+1)/6=1,惩罚项 = 1 α = 1 = 1^\alpha = 1 =1α=1。
当 L = 2 L=2 L=2 时: ( 5 + 2 ) / 6 = 7 / 6 ≈ 1.167 (5+2)/6 = 7/6 \approx 1.167 (5+2)/6=7/6≈1.167,惩罚项 ≈ 1.167 α \approx 1.167^\alpha ≈1.167α。
当 L L L 较大时(如 L = 10 L=10 L=10): ( 5 + 10 ) / 6 = 15 / 6 = 2.5 (5+10)/6 = 15/6 = 2.5 (5+10)/6=15/6=2.5,与原始 L α L^\alpha Lα( 10 α 10^\alpha 10α)相比更温和,但相对关系保持。
为什么说平滑公式"避免极短句子被过度奖励"?
关键在于比较不同长度句子的得分 ,而不是只看 L = 1 L=1 L=1 时分母是否为 1。
原始归一化( α = 1 \alpha=1 α=1 为例)
score = log P L \text{score} = \frac{\log P}{L} score=LlogP
- L = 1 L=1 L=1:分母 = 1 =1 =1
- L = 2 L=2 L=2:分母 = 2 =2 =2
长度为 2 的句子,分母是长度为 1 的句子的 2 倍 。
因此,即使两个句子的平均质量相同(即 log P \log P logP 与长度成正比),短句的得分也会被过度放大(因为分母小)。
平滑归一化( α = 1 \alpha=1 α=1 为例)
score = log P ( 5 + L ) / 6 \text{score} = \frac{\log P}{(5+L)/6} score=(5+L)/6logP
- L = 1 L=1 L=1:分母 = ( 5 + 1 ) / 6 = 1 =(5+1)/6 = 1 =(5+1)/6=1
- L = 2 L=2 L=2:分母 = ( 5 + 2 ) / 6 = 7 / 6 ≈ 1.167 =(5+2)/6 = 7/6 \approx 1.167 =(5+2)/6=7/6≈1.167
长度为 2 的句子,分母仅为长度为 1 的句子的 1.167 倍,远小于原始归一化中的 2 倍。
结果 :在平滑归一化下,短句( L = 1 L=1 L=1)相对于稍长的句子( L = 2 L=2 L=2)不再拥有巨大的分母优势,因此短句被过度奖励的情况得到了缓解。
- 平滑公式并没有改变 L = 1 L=1 L=1 时的分母(仍然是 1)。
- 它改变了分母随 L L L 增长的速度:原始归一化分母增长快,平滑归一化分母增长慢。
- 因此,短句相对于稍长句子的"分母优势"被削弱,模型不再因为句子极短而获得不公平的高分。
这就是"避免极短句子被过度奖励"的真正含义。
为什么选 5 和 6?
这是经验调参的结果,并非理论推导。选择 5 作为加性常数,使得:
- L = 1 L=1 L=1 时惩罚项为 1(最短句无惩罚放大)
- L = 2 L=2 L=2 时惩罚项约为 1.167 α 1.167^\alpha 1.167α,仍较小
- 随着 L L L 增长,惩罚项逐渐接近 ( L / 6 ) α (L/6)^\alpha (L/6)α,与原始归一化趋势一致
常数 6 是为了让 L = 1 L=1 L=1 时分母恰好为 1 而设( 5 + 1 = 6 5+1=6 5+1=6)。如果加性常数选其他值(如 3 或 7),分母会相应调整,但核心思想不变:给短句一个"缓冲",避免它们因为分母太小而获得不合理的高分。
总结
数字 5 和 6 是工程上的平滑常数,目的是:
- 防止极短句子( L = 1 L=1 L=1 或 2 2 2)被过度奖励。
- 使长度惩罚在短句区间更平滑,在长句区间接近原始归一化。
- 这是经验最优值,而非必须遵循的规则;不同实现可能选用不同的平滑常数。
建议:学习和理解阶段使用方式1(或方式2)即可,方式4是工程优化细节,暂时不需要深究。
五、完整伪代码(带长度归一化)
python
import torch
import math
def beam_search_decoder_with_length_norm(model, source, beam_width=4, max_len=50, alpha=0.6):
"""
带长度归一化的束搜索
参数:
model: 训练好的Transformer模型
source: 源语言输入(已预处理)
beam_width: 束宽,常用4-8
max_len: 最大生成长度
alpha: 长度惩罚系数,常用0.6~0.7
返回:
best_seq: 最优序列(token id列表,不含起始符和结束符)
"""
start_token = sos_token_id
eos_token = eos_token_id
# 初始化:每个候选是一个元组 (序列, 累积对数概率, 是否已结束)
# 起始时,累积对数概率为 log(1) = 0
candidates = [([start_token], 0.0, False)]
for step in range(max_len):
all_candidates = []
for seq, log_score, finished in candidates:
# 如果候选已经结束,不再扩展,直接保留
if finished or seq[-1] == eos_token:
all_candidates.append((seq, log_score, True))
continue
# 模型前向:获取下一个token的logits
# 注意:实际实现中,应批量处理所有活跃候选以提高效率
logits = model.forward(source, seq) # shape: (seq_len, vocab_size)
# 取最后一个位置的logits,计算对数概率
log_probs = torch.log_softmax(logits[-1], dim=-1) # shape: (vocab_size,)
# 取top-k个(k = beam_width)
top_k_log_probs, top_k_indices = torch.topk(log_probs, beam_width)
# 扩展候选
for i in range(beam_width):
new_seq = seq + [top_k_indices[i].item()]
new_log_score = log_score + top_k_log_probs[i].item()
finished = (new_seq[-1] == eos_token)
all_candidates.append((new_seq, new_log_score, finished))
# ========== 关键步骤:长度归一化排序 ==========
def get_normalized_score(candidate):
"""计算长度归一化后的得分"""
seq, log_score, _ = candidate
# 计算有效长度:减去起始符,不计结束符
# 例如:序列 [sos, w1, w2, eos] 的有效长度为 2
length = len(seq) - 1 # 去掉起始符
if seq[-1] == eos_token:
length -= 1 # 去掉结束符(若存在)
length = max(length, 1) # 防止长度为0(如仅包含[sos,eos]的极端情况)
# 归一化得分 = 累积对数概率 / (长度^alpha)
return log_score / (length ** alpha)
# 按归一化得分排序(降序,得分越高越好)
all_candidates.sort(key=get_normalized_score, reverse=True)
# 保留前 beam_width 个候选
candidates = all_candidates[:beam_width]
# 提前终止条件:所有候选都已结束
if all(finished for _, _, finished in candidates):
break
# 从最终候选池中选取得分最高的序列
best_candidate = max(candidates, key=get_normalized_score)
best_seq, best_score, _ = best_candidate
# 去除起始符和结束符,只返回有效词序列
if best_seq and best_seq[0] == start_token:
best_seq = best_seq[1:]
if best_seq and best_seq[-1] == eos_token:
best_seq = best_seq[:-1]
return best_seq
六、长度归一化的常见变体
变体1:Google NMT 的平滑长度惩罚
Google 的神经机器翻译系统使用了更复杂的公式:
score = 累积对数概率 ( 5 + L 6 ) α \text{score} = \frac{\text{累积对数概率}}{\left( \frac{5 + L}{6} \right)^\alpha} score=(65+L)α累积对数概率
其中 L L L 是句子长度(包含结束符 ,但不包含起始符 ,即有效词数 + 结束符)。在 Google 的实现中, L L L 的定义与其内部代码一致,此处我们沿用常见简化: L L L 为有效词数(不含起始符,不含结束符)时,常数 5 和 6 的取值可能需要微调,但核心思想不变。
为什么这样设计?
原始的归一化分母为 L α L^\alpha Lα。当 L L L 很小时(如 L = 1 , 2 , 3 L=1,2,3 L=1,2,3),分母非常小,使得极短句子获得不合理的高分(因为负对数概率除以一个很小的正数,得分更接近0)。这导致模型过度偏爱短句。
平滑版本通过给长度加一个常数(5),再整体除以 6,使得分母在 L L L 较小时不会太小,从而削弱短句的天然优势,让模型更公平地评估长短句。
对比效果( α = 0.6 \alpha=0.6 α=0.6 为例):
| 有效长度 L L L | L 0.6 L^{0.6} L0.6(原始) | ( ( 5 + L ) / 6 ) 0.6 ((5+L)/6)^{0.6} ((5+L)/6)0.6(平滑) | 说明 |
|---|---|---|---|
| 1 | 1.000 | ( 6 / 6 ) 0.6 = 1.000 (6/6)^{0.6}=1.000 (6/6)0.6=1.000 | 相同 |
| 2 | 1.516 | ( 7 / 6 ) 0.6 ≈ 1.097 (7/6)^{0.6} \approx 1.097 (7/6)0.6≈1.097 | 平滑后分母更小,对 L = 2 L=2 L=2 的相对惩罚 (相对于 L = 1 L=1 L=1)减弱 |
| 3 | 1.933 | ( 8 / 6 ) 0.6 ≈ 1.183 (8/6)^{0.6} \approx 1.183 (8/6)0.6≈1.183 | 平滑后分母增长更慢,长句与短句的差距缩小 |
| 4 | 2.297 | ( 9 / 6 ) 0.6 ≈ 1.258 (9/6)^{0.6} \approx 1.258 (9/6)0.6≈1.258 | 同样,平滑使分母增长放缓 |
注意 :分母数值变小意味着对同一长度的句子,平滑后的得分更负(更差),但平滑的真正目的是改变不同长度之间的相对得分关系,使短句不再过分占优。
实现示例:
python
def get_normalized_score_google(seq, log_score, alpha=0.6, constant=5):
# 有效长度:减去起始符,不计结束符
length = len(seq) - 1 # 去掉 start_token
if seq[-1] == eos_token:
length -= 1 # 去掉 eos_token
length = max(length, 1)
# 平滑长度惩罚
penalty = ((constant + length) / (constant + 1)) ** alpha
return log_score / penalty
变体2:加法形式的长度惩罚
少数实现使用加法而非除法:
score = 累积对数概率 + β × L \text{score} = \text{累积对数概率} + \beta \times L score=累积对数概率+β×L
其中 β \beta β 是负数(例如 − 0.5 -0.5 −0.5),每增加一个词就减去一个固定值。
优缺点:
- 优点:计算简单,容易理解。
- 缺点:缺乏概率解释(不是几何平均), β \beta β 难以调优,且与概率尺度耦合。
一般不推荐使用。
变体3:长度归一化 + 覆盖惩罚
覆盖惩罚(Coverage Penalty)用于防止模型重复翻译源语言的同一部分,常见于机器翻译和文本生成任务。它可以与长度归一化结合使用:
score = 累积对数概率 L α + 覆盖惩罚项 \text{score} = \frac{\text{累积对数概率}}{L^\alpha} + \text{覆盖惩罚项} score=Lα累积对数概率+覆盖惩罚项
覆盖惩罚项通常是负值,惩罚那些过多关注输入中同一位置的候选,从而鼓励模型均匀覆盖源端信息。
学习阶段可以先忽略覆盖惩罚,等基础版本跑通后再考虑引入,以降低调试复杂性。
七、实际使用建议
参数选择
| 场景 | 推荐 beam_width | 推荐 α \alpha α | 说明 |
|---|---|---|---|
| 学习/调试 | 3-4 | 0.6 | 平衡速度与效果,便于观察 |
| 翻译质量优先 | 5-8 | 0.6~0.7 | 更大束宽提升多样性, α \alpha α 取经验最优区间 |
| 速度优先 | 2-4 | 0.6 | 小束宽加速, α \alpha α 保持常用值 |
| 极短句子任务 | 3-4 | 1.0 或平滑版本 | 当句子本身就很短时,使用 α = 1.0 \alpha=1.0 α=1.0 完全消除长度偏差,或使用 Google 平滑版本避免短句被过度惩罚 |
说明 :极短句子任务(如单轮对话、短语翻译)中,原始归一化 α = 0.6 \alpha=0.6 α=0.6 可能仍偏向长句,而 α = 1.0 \alpha=1.0 α=1.0 更公平;若担心极短句被过度惩罚,可使用平滑版本。
调试建议
- 先不用归一化 :实现基础束搜索( α = 0 \alpha=0 α=0),观察模型是否偏好短句子(通常是的)。这一步验证基础流程正确。
- 加入 α = 1.0 \alpha=1.0 α=1.0:观察长句子是否被正确选择(与无归一化对比,长句排名应提升)。这一步确认归一化逻辑正确。
- 调优 α \alpha α :在验证集上测试不同的 α \alpha α(0.4, 0.6, 0.8, 1.0),选 BLEU 或人工评估最高的。注意 α \alpha α 不是越大越好, α > 1 \alpha>1 α>1 通常效果变差。
常见问题
Q:归一化后,累积对数概率为负,除以正数后还是负,怎么比较?
A:直接比较数值大小。例如 − 0.35 > − 0.40 -0.35 > -0.40 −0.35>−0.40,所以 − 0.35 -0.35 −0.35 的候选更好。不需要取绝对值,也不需要转换成正数。
Q:长度 L L L 应该包含结束符吗?
A:通常不包含。结束符 </s> 没有语义信息,不应计入长度。起始符 <s> 也不应计入。实践中常用"有效词数"(即去掉起始符和结束符后的 token 数)。
Q:如果两个候选长度相同,归一化是否有效果?
A:长度相同时,除以相同的 L α L^\alpha Lα,不影响相对顺序。归一化只影响不同长度的候选之间的比较。
Q: α = 0.6 \alpha=0.6 α=0.6 时,长句子反而得分更高,这合理吗?
A:合理。这反映了"更完整的翻译应该被鼓励"的直觉。例如,一个 6 词的完整翻译 vs 一个 3 词的不完整翻译,即使每步平均概率相同,完整翻译也应该被选中。 α = 0.6 \alpha=0.6 α=0.6 实现了这种"相对奖励"(长句得分相对于短句被提升),而 α = 1.0 \alpha=1.0 α=1.0 则是完全公平比较。
八、总结对比表
| 方面 | 无长度归一化( α = 0 \alpha=0 α=0) | 有长度归一化( α > 0 \alpha>0 α>0) |
|---|---|---|
| 评分公式 | 累积对数概率 | 累积对数概率 / L α L^\alpha Lα |
| 对短句偏好 | 强 ❌ | 弱( 0 < α < 1 0<\alpha<1 0<α<1)或消除( α = 1 \alpha=1 α=1)或反向( α > 1 \alpha>1 α>1) |
| 对长句惩罚 | 重 ❌ | 减弱( 0 < α < 1 0<\alpha<1 0<α<1)或消除( α = 1 \alpha=1 α=1)或奖励( α > 1 \alpha>1 α>1) |
| 能否选出完整翻译 | 常失败(偏好截断) | 能正确比较不同长度的候选 |
| 概率解释 | 联合概率 | 几何平均( α = 1 \alpha=1 α=1)或加权几何平均( α ≠ 1 \alpha\neq1 α=1) |
| 常用 α \alpha α 值 | - | 0.6~0.7 |
| 计算开销 | 无额外开销 | 极小(每步对候选做除法和幂运算) |
一句话总结
长度归一化 = 用几何平均(或其推广)替代累积乘积,消除句子长度对评分的天然偏见,让不同长度的翻译能在公平尺度上比较。 实践中通常用 α = 0.6 ∼ 0.7 \alpha=0.6\sim0.7 α=0.6∼0.7,不完全消除长度影响,而是适度减弱,在"完整性"和"简洁性"之间取得平衡。
5、使用 "覆盖惩罚" 的束搜索
覆盖惩罚(Coverage Penalty)在束搜索中的应用
在生成任务(尤其是机器翻译)中,即使使用了长度归一化,模型仍然可能出现两种典型问题:重复翻译 (反复生成相似内容)和漏译(忽略源端部分信息)。这些问题往往源于解码过程中注意力机制对源端某些位置的过度关注或关注不足。
覆盖惩罚正是为解决这类问题而设计的。它通过惩罚那些未能均匀覆盖源端信息或重复关注同一位置的候选,促使模型更完整地利用输入信息。
一、覆盖惩罚的基本思想
在基于注意力机制的生成模型中,每一步解码器都会计算一个注意力分布 a t , j a_{t,j} at,j,表示在生成第 t t t 个词时对源端第 j j j 个词的关注程度。理想的注意力应该随时间推移覆盖源端所有位置,且每个位置被关注的次数大致均衡。
覆盖惩罚的核心是维护一个覆盖向量 (coverage vector) c j c_j cj,它累积了到当前步为止源端第 j j j 个词被关注的总和(或某种累计量)。常见定义有:
- 累积注意力 : c j = ∑ t ′ = 1 t a t ′ , j c_j = \sum_{t'=1}^{t} a_{t',j} cj=∑t′=1tat′,j,即从第1步到当前步,源端第 j j j 个位置被关注的总权重。
- 其他变体(如对数累加等)。
在每一步,覆盖惩罚会根据当前注意力与历史覆盖的差异,给予一个负向奖励,鼓励模型:
- 对已经覆盖较多的位置减少关注(避免重复翻译);
- 对尚未覆盖的位置增加关注(鼓励完整翻译)。
二、常见的覆盖惩罚形式
- 覆盖惩罚项(Tu et al., 2016)
Tu 等人(2016)在《Modeling Coverage for Neural Machine Translation》中提出了两种覆盖模型:
- 覆盖向量 : c t = c t − 1 + a t c_t = c_{t-1} + a_t ct=ct−1+at,其中 a t a_t at 是第 t t t 步的注意力分布。
- 训练时的覆盖损失 : coverage_loss = ∑ j = 1 J min ( a t , j , c t − 1 , j ) \text{coverage\loss} = \sum{j=1}^{J} \min(a_{t,j}, c_{t-1,j}) coverage_loss=∑j=1Jmin(at,j,ct−1,j)。
在解码阶段 ,通常将覆盖惩罚作为得分的一项,常见形式为:
score = ∑ log p t L α − λ ⋅ penalty ( c , a ) \text{score} = \frac{\sum \log p_t}{L^\alpha} - \lambda \cdot \text{penalty}(c, a) score=Lα∑logpt−λ⋅penalty(c,a)
其中 λ > 0 \lambda > 0 λ>0 是惩罚强度, penalty ( c , a ) \text{penalty}(c, a) penalty(c,a) 是某个与覆盖向量和当前注意力相关的非负函数。
另一种写法是直接将对数概率加上一个负的惩罚项,再归一化。两种表述等价,核心都是减去一个正值以惩罚不良覆盖行为。
- 简单形式的覆盖惩罚(用于束搜索)
在束搜索中,常见简化版本为:
score = ∑ log p t L α − λ ⋅ cov_penalty \text{score} = \frac{\sum \log p_t}{L^\alpha} - \lambda \cdot \text{cov\_penalty} score=Lα∑logpt−λ⋅cov_penalty
其中 cov_penalty \text{cov\_penalty} cov_penalty 的具体定义可参考下文"四、覆盖惩罚的几种具体实现"。
三、在束搜索中实现覆盖惩罚
在束搜索的每一扩展步,对于每个候选序列,我们需要:
- 维护其覆盖向量 c c c(大小为源端长度 J J J)。
- 计算当前步的注意力分布 a a a(由解码器输出)。
- 更新覆盖向量: c new = c + a c_{\text{new}} = c + a cnew=c+a。
- 计算覆盖惩罚项,并将其减去(即降低得分)到该候选的得分上。
伪代码示意(在长度归一化基础上增加覆盖惩罚):
python
def beam_search_with_coverage(model, source, beam_width=4, max_len=50,
alpha=0.6, cov_lambda=0.5):
start_token = sos_token_id
eos_token = eos_token_id
J = len(source) # 源端长度
# 候选存储:(seq, log_score, coverage_vector, finished)
candidates = [([start_token], 0.0, [0.0]*J, False)]
for step in range(max_len):
all_candidates = []
for seq, log_score, cov, finished in candidates:
if finished or seq[-1] == eos_token:
all_candidates.append((seq, log_score, cov, True))
continue
# 前向得到 logits 和注意力权重
logits, attn = model.forward_with_attn(source, seq) # attn shape: (1, J)
log_probs = torch.log_softmax(logits[-1], dim=-1)
top_k_log_probs, top_k_indices = torch.topk(log_probs, beam_width)
for i in range(beam_width):
new_seq = seq + [top_k_indices[i].item()]
new_log_score = log_score + top_k_log_probs[i].item()
new_finished = (new_seq[-1] == eos_token)
# 更新覆盖向量
new_cov = [cov[j] + attn[0][j].item() for j in range(J)]
# 计算覆盖惩罚(示例:使用"累积注意力限制"形式)
# 惩罚任何位置覆盖超过1的部分
penalty = sum(max(0, new_cov[j] - 1.0) for j in range(J))
# 应用覆盖惩罚(减去)
new_log_score -= cov_lambda * penalty
all_candidates.append((new_seq, new_log_score, new_cov, new_finished))
# 排序:先长度归一化,再比较(覆盖惩罚已计入 log_score)
def get_total_score(candidate):
seq, log_score, cov, finished = candidate
length = len(seq) - 1
if seq[-1] == eos_token:
length -= 1
length = max(length, 1)
return log_score / (length ** alpha)
all_candidates.sort(key=get_total_score, reverse=True)
candidates = all_candidates[:beam_width]
if all(finished for _, _, _, finished in candidates):
break
# 最终选择最佳候选
best_candidate = max(candidates, key=get_total_score)
best_seq, best_score, _, _ = best_candidate
# 去除特殊标记...
return best_seq
说明 :上述示例采用"累积注意力限制"作为覆盖惩罚。实际中也可以使用其他形式(见第四节)。平方和形式的惩罚(如 ∑ c j 2 \sum c_j^2 ∑cj2)会鼓励覆盖向量尽可能小,与覆盖惩罚的目标相悖,因此不推荐作为覆盖惩罚,此处未采用。
四、覆盖惩罚的几种具体实现
-
累积注意力限制
penalty = ∑ j max ( 0 , c j − 1 ) \text{penalty} = \sum_j \max(0, c_j - 1) penalty=∑jmax(0,cj−1)
惩罚任何位置的总关注度超过1的情况,防止重复关注同一位置。这是最直观的覆盖惩罚之一,鼓励每个源端位置被覆盖的次数不超过1。
-
注意力与历史覆盖的交互 (Tu et al., 2016 训练时使用)
penalty = ∑ j min ( a t , j , c t − 1 , j ) \text{penalty} = \sum_j \min(a_{t,j}, c_{t-1,j}) penalty=∑jmin(at,j,ct−1,j)
该惩罚衡量当前步注意力与历史覆盖的重叠,重叠越多,惩罚越大,从而鼓励新词关注未覆盖的位置。
-
对数覆盖
penalty = ∑ j log ( 1 + c j ) \text{penalty} = \sum_j \log(1 + c_j) penalty=∑jlog(1+cj)
随着 c j c_j cj 增大,惩罚项也增大,但增长速度逐渐放缓。这种形式对已经覆盖较多的位置惩罚减弱,但整体仍鼓励总覆盖不要过大,常用于某些变体。
-
平方覆盖
penalty = ∑ j c j 2 \text{penalty} = \sum_j c_j^2 penalty=∑jcj2
平方和会强烈惩罚较大的覆盖值,导致模型倾向于让所有 c j c_j cj 尽可能小。这通常不符合覆盖惩罚的本意(我们希望每个位置至少被覆盖一次),但在某些场景下结合奖励机制(例如鼓励覆盖到一定程度后再惩罚过度)也可能使用。实际中较少单独使用。
实际使用时,覆盖惩罚往往设计为负向奖励 ,即覆盖越多、越均匀,惩罚越小(得分越高)。因此系数 λ \lambda λ 为正,公式中为 减去 λ ⋅ penalty \lambda \cdot \text{penalty} λ⋅penalty。
五、参数调优与注意事项
- 惩罚强度 λ \lambda λ:需要调优。太小则无效,太大会使模型过度保守,生成过短或过于"平均"的翻译。
- 与长度归一化的顺序 :通常先计算长度归一化得分,再加减覆盖惩罚;或者将覆盖惩罚作为额外项直接加到对数概率和上,再统一归一化。两者差异不大,但需保持一致。示例中覆盖惩罚直接减在 log _ s c o r e \log\_score log_score 上,然后再归一化,这样可以保证惩罚项与对数概率处于同一量级。
- 覆盖向量初始化:通常全零。
- 结束符处理:生成结束符后不再更新覆盖,也不计算惩罚(因为已结束的序列不再需要关注源端)。
六、覆盖惩罚的效果与局限
优点:
- 有效减少重复翻译(如"我我我"或"I I I")。
- 降低漏译概率,尤其对长文本或信息密度高的输入。
- 可与其他解码策略(如长度归一化、束搜索)无缝结合。
缺点:
- 增加了计算开销(需维护覆盖向量,每步计算惩罚)。
- 引入额外超参数,调优成本增加。
- 在某些任务(如短文本生成)中可能无明显收益甚至副作用。
七、总结
覆盖惩罚通过显式建模解码过程中对源端信息的覆盖情况,有效缓解了注意力机制的"过度关注"和"遗漏"问题。它与长度归一化相辅相成:一个保证长度公平,一个保证内容覆盖完整,共同提升生成质量。在实践中,覆盖惩罚常用于长文本翻译、摘要生成等任务,并常作为高级解码技巧之一。
如果你正在实现束搜索,建议先完成基础版(长度归一化),确保流程正确,再逐步引入覆盖惩罚,并在验证集上微调 λ \lambda λ 和覆盖函数形式。