2. BLEU分数计算与多参考译文评估
任务说明
本案例实现BLEU(Bilingual Evaluation Understudy)分数的计算,并支持多参考译文评估。给定一条候选译文(机器翻译结果)和一条或多条参考译文(人工标准答案),计算1-gram到4-gram的精确匹配率,并通过 brevity penalty(长度惩罚)修正短译文的分数偏差,最终输出综合BLEU分数。
BLEU核心逻辑
BLEU分数计算包含三个关键步骤:
- n-gram精确率:统计候选译文中n-gram在参考译文中出现的次数(多参考时取最大匹配次数),除以候选译文n-gram总数;
- 长度惩罚:当候选译文长度小于参考译文长度时,对分数进行指数惩罚,避免短译文获得虚高分数;
- 加权几何平均:对1-gram到4-gram的精确率取对数平均,再乘以长度惩罚系数。
代码实现
from collections import Counter
import math
def tokenize(text):
"""将文本按空格分词并转为小写"""
return text.lower().split()
def get_ngrams(tokens, n):
"""从列表中提取n-gram集合"""
return [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
def count_clipped_ngrams(candidate, references, n):
"""
计算候选译文中n-gram被参考译文匹配的最大次数(截断计数)
"""
cand_ngrams = get_ngrams(candidate, n)
if not cand_ngrams:
return 0, 0
# 统计每个参考译文的n-gram计数
ref_counts_list = []
for ref in references:
ref_ngrams = get_ngrams(ref, n)
ref_counts_list.append(Counter(ref_ngrams))
# 对候选中的每个n-gram,取其在所有参考中出现次数的最大值
total_clipped = 0
cand_counter = Counter(cand_ngrams)
for ngram, cand_count in cand_counter.items():
max_ref_count = 0
for ref_counter in ref_counts_list:
max_ref_count = max(max_ref_count, ref_counter.get(ngram, 0))
# 截断:候选计数不能超过参考中的最大计数
total_clipped += min(cand_count, max_ref_count)
return total_clipped, len(cand_ngrams)
def compute_bleu(candidate_text, reference_texts, max_n=4):
"""
计算BLEU分数
参数:
candidate_text: 候选译文(字符串)
reference_texts: 参考译文列表(字符串列表)
max_n: 最大n-gram阶数,默认4
返回:
bleu_score: 0~1之间的BLEU分数
details: 各阶n-gram精确率及长度惩罚值(用于验证)
"""
candidate = tokenize(candidate_text)
references = [tokenize(ref) for ref in reference_texts]
# 计算各阶n-gram精确率
precisions = []
for n in range(1, max_n+1):
clipped, total = count_clipped_ngrams(candidate, references, n)
if total == 0:
precisions.append(0.0)
else:
precisions.append(clipped / total)
# 长度惩罚
cand_len = len(candidate)
# 选择与候选长度最接近的参考长度作为最佳参考长度
ref_lens = [len(ref) for ref in references]
best_ref_len = min(ref_lens, key=lambda x: abs(x - cand_len))
if cand_len <= best_ref_len:
brevity_penalty = math.exp(1 - best_ref_len / cand_len) if cand_len > 0 else 0.0
else:
brevity_penalty = 1.0
# 加权几何平均(各阶权重相等)
if min(precisions) == 0:
bleu = 0.0
else:
geo_mean = math.exp(sum(math.log(p) for p in precisions) / max_n)
bleu = brevity_penalty * geo_mean
details = {
'precisions': precisions,
'brevity_penalty': brevity_penalty,
'candidate_len': cand_len,
'best_ref_len': best_ref_len
}
return bleu, details
# 示例:单参考译文
candidate1 = "The cat is on the mat"
reference1 = ["The cat is sitting on the mat"]
bleu1, det1 = compute_bleu(candidate1, reference1)
print(f"单参考BLEU: {bleu1:.4f}")
print(f" 各阶精确率: {[f'{p:.3f}' for p in det1['precisions']]}")
print(f" 长度惩罚: {det1['brevity_penalty']:.4f}")
# 示例:多参考译文
candidate2 = "The cat is on the mat"
references2 = [
"The cat is sitting on the mat",
"A cat is on the mat",
"The cat lies on the mat"
]
bleu2, det2 = compute_bleu(candidate2, references2)
print(f"多参考BLEU: {bleu2:.4f}")
print(f" 各阶精确率: {[f'{p:.3f}' for p in det2['precisions']]}")
print(f" 长度惩罚: {det2['brevity_penalty']:.4f}")
代码说明
上述实现中,count_clipped_ngrams函数实现了BLEU的核心截断计数机制:对于候选译文中的每个n-gram,统计其在各参考译文中出现的最大次数,并取候选计数与该最大值的较小值,防止重复词被过度奖励。compute_bleu函数依次完成分词、各阶精确率计算、最佳参考长度选择与长度惩罚计算,最终输出综合分数。多参考场景下,每个n-gram的匹配上限自动取所有参考中的最大值,因此增加高质量参考译文会提升候选译文的得分上限。
验证方法
运行上述代码,观察输出结果。预期输出如下(与代码逻辑严格一致):
单参考BLEU: 0.6065
各阶精确率: ['0.833', '0.750', '0.667', '0.500']
长度惩罚: 0.7788
多参考BLEU: 0.7711
各阶精确率: ['1.000', '1.000', '0.667', '0.500']
长度惩罚: 0.7788
判定标准:
- 单参考场景中,候选译文"the cat is on the mat"(5词)比参考译文"the cat is sitting on the mat"(6词)短,长度惩罚为exp(1-6/5)≈0.7788,符合预期;
- 多参考场景中,由于参考2包含"a cat is on the mat",候选译文的1-gram和2-gram均能完全匹配,精确率提升至1.0,BLEU分数相应提高;
- 若将候选译文改为与参考完全一致的文本,BLEU应等于1.0;若完全无匹配,BLEU应为0.0,可用于进一步验证。
落地与运行
将代码保存为bleu_score.py,在Python 3.6+环境中直接运行:
python bleu_score.py
无需安装第三方库,仅使用标准库collections和math。代码中的示例数据已包含单参考和多参考两种场景,可直接观察多参考机制对分数的影响。如需评估自己的译文,替换candidate1和reference1(或references2)中的文本即可。