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定义。