4. 实现BLEU指标计算
任务目标
实现BLEU(Bilingual Evaluation Understudy)指标计算,输入机器翻译候选文本与参考文本,输出BLEU分数。通过代码理解两个核心机制:n-gram精度(modified precision)与简短惩罚(brevity penalty)。
核心机制说明
BLEU分数由两部分构成:
- n-gram精度:统计候选文本中n-gram(n=1~4)在参考文本中出现的次数,并对每个n-gram的计数进行截断(不超过其在任一参考文本中的最大出现次数),避免重复词带来的虚高精度。
- 简短惩罚(BP):当候选文本长度小于参考文本长度时,惩罚其分数。BP = exp(1 - r/c),其中r为最接近候选长度的参考长度,c为候选长度。若c > r,则BP = 1。
最终BLEU = BP × exp(Σ log p_n / N),其中p_n为第n阶的修正精度,N通常取4。
完整实现代码
import math
from collections import Counter
def tokenize(text):
"""简单分词:按空格切分并转为小写"""
return text.lower().split()
def modified_precision(candidate, references, n):
"""计算n-gram修正精度"""
cand_ngrams = Counter(
tuple(candidate[i:i+n]) for i in range(len(candidate)-n+1)
)
if not cand_ngrams:
return 0.0
# 统计每个n-gram在所有参考中的最大出现次数
max_ref_counts = Counter()
for ref in references:
ref_ngrams = Counter(
tuple(ref[i:i+n]) for i in range(len(ref)-n+1)
)
for ngram, count in ref_ngrams.items():
max_ref_counts[ngram] = max(max_ref_counts[ngram], count)
# 截断计数
clipped_count = 0
for ngram, count in cand_ngrams.items():
clipped_count += min(count, max_ref_counts.get(ngram, 0))
return clipped_count / sum(cand_ngrams.values())
def brevity_penalty(candidate_len, references):
"""计算简短惩罚"""
if candidate_len == 0:
return 0.0
# 选择与候选长度最接近的参考长度
ref_lens = [len(ref) for ref in references]
closest_ref_len = min(ref_lens, key=lambda x: abs(x - candidate_len))
if candidate_len > closest_ref_len:
return 1.0
return math.exp(1 - closest_ref_len / candidate_len)
def bleu_score(candidate, references, max_n=4):
"""计算BLEU分数"""
cand_tokens = tokenize(candidate)
ref_tokens = [tokenize(ref) for ref in references]
# 过滤空参考
ref_tokens = [ref for ref in ref_tokens if len(ref) > 0]
if not ref_tokens or len(cand_tokens) == 0:
return 0.0
precisions = []
for n in range(1, max_n+1):
p = modified_precision(cand_tokens, ref_tokens, n)
# 当某个n-gram精度为0时,用极小值避免log(0)
precisions.append(p if p > 0 else 1e-16)
# 几何平均精度
log_sum = sum(math.log(p) for p in precisions) / max_n
bp = brevity_penalty(len(cand_tokens), ref_tokens)
return bp * math.exp(log_sum)
# 测试示例
if __name__ == "__main__":
candidate = "the cat is on the mat"
references = [
"the cat is on the mat",
"there is a cat on the mat"
]
score = bleu_score(candidate, references)
print(f"BLEU score: {score:.4f}")
# 测试简短惩罚:候选过短
short_candidate = "the cat"
score_short = bleu_score(short_candidate, references)
print(f"Short candidate BLEU: {score_short:.4f}")
# 测试n-gram精度截断:候选重复词
repeat_candidate = "the the the the"
score_repeat = bleu_score(repeat_candidate, references)
print(f"Repeated candidate BLEU: {score_repeat:.4f}")
运行与验证
运行方式:将代码保存为 bleu.py,在Python 3.6+环境中执行 python bleu.py。
预期输出(由代码逻辑直接推出):
BLEU score: 1.0000
Short candidate BLEU: 0.0000
Repeated candidate BLEU: 0.0000
验证说明:
- 第一个用例中候选与参考完全一致,所有n-gram精度均为1,且无长度惩罚,BLEU应为1.0。
- 第二个用例候选过短,触发简短惩罚,BP趋近于0,BLEU接近0。
- 第三个用例候选全部为重复词"the",虽然1-gram精度可能较高,但2-gram及以上精度为0(参考中无"the the"),导致整体BLEU为0,验证了截断机制的作用。
手动验证方法:修改 candidate 和 references 变量为其他句子,观察分数变化。例如将候选改为与参考语义相近但用词不同的句子,BLEU会下降但通常高于完全无关的句子。
常见问题
- 分词差异:本实现使用简单空格分词,实际应用中需根据语言选择合适分词器(如英文用NLTK、中文用jieba),否则n-gram统计会失真。
- 多参考处理:BLEU标准实现中,修正精度取每个n-gram在所有参考中的最大出现次数,本代码已正确实现。
- n=1时的边界:当候选长度小于n时,
cand_ngrams为空,返回精度0,符合BLEU定义。