背景图
大语言模型伴学之旅(一) —— BLEU值的计算

2024年04月22日 0 作者头像 作者头像 ArnoX

python_header.png

使用到的大语言模型:

Copilot,其整合有OpenAI的 GPT-4 Turbo
ChatGPT 3.5

提问方式:

“请帮我使用python写一段代码,用来计算两个英文句子的BLEU值。请注意:在保持你的代码简洁和高效的同时,不要使用任何python已有的库。”

第一次生成的代码如下:

  • def simple_bleu(candidate, reference):
  • candidate_words = candidate.split()
  • reference_words = reference.split()
  • reference_length = len(reference_words)
  • candidate_length = len(candidate_words)
  • # Count matches and calculate precision
  • matches = sum(1 for word in candidate_words if word in reference_words)
  • precision = matches / candidate_length
  • # Calculate brevity penalty
  • if candidate_length > reference_length:
  • bp = 1
  • else:
  • bp = candidate_length / reference_length
  • # Calculate BLEU score
  • bleu_score = bp * precision
  • # Example usage:
  • return bleu_score
  • candidate_sentence = "This is a test sentence."
  • reference_sentence = "This is the test sentence."

可以很明显看出,其第一次所给出的结果中只包含一个函数,该函数在进行BLEU计算时只考虑了单词级别的匹配和精确度,并没有考虑n-gram匹配、平滑、以及短句等更为复杂的因素。于是再次进行追问:

“请你基于以上给出的结果,编写一个更为复杂的BLEU计算函数,在符合BLEU值计算的原理并保持你的代码简洁和高效的同时,你要考虑到n-gram匹配对数函数平滑,以及短句惩罚等关键因素。”

得到的最终结果如下:

  • import math
  • from collections import Counter
  • def count_ngram(sentence, n):
  • # 将句子分割成单词列表,为了生成n-gram。
  • tokens = sentence.split()
  • # 创建n-gram列表。这里使用列表推导式,对于句子中的每个起始点i,创建一个从i到i+n的元组。
  • return [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
  • def calculate_bleu(reference, candidate, max_n=4):
  • # 初始化一个列表来存储每个n-gram级别的精确度。
  • precisions = []
  • # 对于从1到max_n的每个n,计算n-gram精确度。
  • for n in range(1, max_n + 1):
  • # 使用count_ngram函数计算候选句子的n-gram。
  • candidate_ngrams = count_ngram(candidate, n)
  • # 使用count_ngram函数计算参考句子的n-gram,并转换成集合去除重复项。
  • reference_ngrams = set(count_ngram(reference, n))
  • # 计算候选n-gram和参考n-gram的交集,得到匹配的n-gram数量。
  • common_ngrams = set(candidate_ngrams).intersection(reference_ngrams)
  • # 计算精确度,即匹配的n-gram数量除以候选n-gram的总数量。
  • precision = len(common_ngrams) / len(candidate_ngrams) if candidate_ngrams else 0
  • # 如果精确度大于0,使用对数函数进行平滑,否则设置为负无穷。
  • precision = math.log(precision) if precision > 0 else float('-inf')
  • # 将计算出的精确度添加到列表中。
  • precisions.append(precision)
  • # 计算所有n-gram精确度的几何平均数,使用指数函数来反转之前的对数操作。
  • geometric_mean = math.exp(sum(precisions) / max_n) if precisions else 0
  • # 计算短句惩罚,如果候选句子长度小于参考句子长度,应用惩罚。
  • reference_length = len(reference.split())
  • candidate_length = len(candidate.split())
  • # 如果候选句子长度小于参考句子长度,计算惩罚因子,否则设置为1。
  • brevity_penalty = math.exp(1 - float(reference_length) / candidate_length) if
  • candidate_length < reference_length else 1
  • # 最终计算BLEU分数,将几何平均数乘以短句惩罚因子。
  • bleu_score = brevity_penalty * geometric_mean
  • return bleu_score
  • reference_sentence = "The quick brown fox jumps over the lazy dog"
  • candidate_sentence = "A fast brown fox leaps over the lazy dog"
  • bleu = calculate_bleu(reference_sentence, candidate_sentence)
  • print(f"The BLEU score for the candidate sentence is: {bleu}")

在Colab中运行得到的结果为0.3549481056010053,同使用nltk库得出的结果一致。

步骤总结:

  1. 分词:将参考句子和候选句子分割成单词列表;
  2. 生成n-gram:为参考句子和候选句子创建n-gram列表;
  3. 匹配n-gram计数:计算候选句子中与参考句子匹配的n-gram数量,评估候选句子在不同长度的n-gram上与参考句子的相似度;
  4. 计算n-gram精确度:对于每个n-gram级别,计算候选句子中匹配的n-gram与总n-gram的比例;
  5. 几何平均数计算:计算从1-gram到最大4-gram精确度的几何平均数。使用几何平均数来平衡不同长度n-gram的影响;
  6. 短句惩罚应用:引入惩罚因子,降低比参考句子短的候选句子的分数,防止短句得高分;
  7. BLEU分数计算。

2024年3月26日更新:
受韩老师上课启发,感觉自己写的部分注释有些抽象,本着寻根问底的态度,现补充对一些关键代码行的详细解析与举例。

  • def count_ngram(sentence, n):
  • # 将句子分割成单词列表,为了生成n-gram。
  • tokens = sentence.split()
  • # 创建n-gram列表。这里使用列表推导式,对于句子中的每个起始点i,创建一个从i到i+n的元组。
  • return [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]

1. 对 count_ngram 函数所返回的结果进行说明:

  • tokens[i:i+n]:这部分是一个切片操作,它从索引i开始,取出连续的n个单词来创建一个n-gram。
  • tuple(...):将切片操作得到的单词列表转换成一个元组。这是因为元组是不可变的,适合用作字典的键或集合的元素。
  • for i in range(len(tokens)-n+1):这是一个循环,它从0开始,到len(tokens)-n+1结束。这确保了切片操作不会超出列表的边界,并且能够为句子中的每个可能的起始点生成n-gram。
  • [...]:最外层的方括号表示这是一个列表推导式,它会根据提供的表达式生成一个新的列表。

这段代码会返回一个包含句子所有可能n-gram的列表。每个n-gram都是一个元组形式,包含了连续的n个单词。例如,如果n 是2,那么对于句子"The quick brown fox",这段代码会生成以下n-gram列表:

  • [('The', 'quick'), ('quick', 'brown'), ('brown', 'fox')]

2. calculate_bleu 函数
对于这个函数的整体逻辑,在前文代码的注释中已经说得很详细了,这里就对其中自己一开始不甚了解的部分函数的功能进行举例说明:

  • # 使用count_ngram函数计算参考句子的n-gram,并转换成集合去除重复项。
  • reference_ngrams = set(count_ngram(reference, n))
  • # 计算候选n-gram和参考n-gram的交集,得到匹配的n-gram数量。
  • common_ngrams = set(candidate_ngrams).intersection(reference_ngrams)

这段代码的含义是在计算两组n-gram之间的交集,以确定有多少候选n-gram在参考n-gram中也出现了。

  • set(candidate_ngrams):将候选句子的n-gram列表转换成集合。集合中的元素是唯一的,这意味着即使在原始列表中出现多次,重复的n-gram也只会被计算一次;
  • .intersection(reference_ngrams):在代码中,* set(candidate_ngrams).intersection(reference_ngrams)计算了candidate_ngrams集合和reference_ngrams集合的交集。这个交集包含了所有既出现在候选n-gram集合中,也出现在参考n-gram集合中的元素;
  • common_ngrams:这是一个新的集合,包含了所有匹配的n-gram。这个集合的大小(即元素数量)就是匹配的n-gram的数量。

    # 如果精确度大于0,使用对数函数进行平滑,否则设置为负无穷。
    precision = math.log(precision) if precision > 0 else float('-inf')

  • precisions列表包含了每个n-gram级别的对数精确度值;
  • sum(precisions)计算了precisions列表中所有元素的总和。
  • 将总和除以max_n(最大的n-gram长度),即得到对数精确度的平均值。
  • math.exp(...)函数计算了这个对数平均值的指数,这样就可以得到几何平均数。这是因为在之前的步骤中,精确度被取了对数,现在需要通过取指数来还原它。
  • 如果precisions列表为空(即没有匹配的n-gram),则几何平均数为0。

另外,关于此处为什么要用取指数来还原对数,询问了Copilot,得到的回复如下:

在BLEU评分算法中,使用取指数来还原是因为之前计算n-gram精确度时采用了对数。这样做的原因有两个:

  • 防止下溢:当计算几何平均数时,如果n-gram精确度的乘积非常小,可能会导致数值下溢,即数值太小而无法在计算机中准确表示。通过先取对数,再求和,可以避免这个问题。
  • 运算简便:对数转换可以将乘法运算变为加法运算,这在数学上更为简便。因为几何平均数涉及到乘法,所以先对每个精确度取对数,然后求和,最后再通过指数函数还原,可以简化计算过程。

例如,我们计算1-gram到4-gram的精确度。假设我们得到以下精确度列表(这里只是为了示例,实际值可能不同):

1-gram精确度: 0.8
2-gram精确度: 0.5
3-gram精确度: 0.3
4-gram精确度: 0.1

接下来,我们取每个精确度的对数:

log(0.8)=−0.223

log(0.5)=−0.693

log(0.3)=−1.204

log(0.1)=−2.303

然后,我们计算这些对数值的平均数:

(−0.223−0.693−1.204−2.303) / 4 ​= −1.106

最后,我们使用指数函数来还原几何平均数:

exp(−1.106)=0.33

所以,几何平均数是0.33。这个值反映了候选句子在不同n-gram级别上与参考句子的匹配程度。通过取指数还原,我们得到了一个介于0和1之间的数值,可以直观地表示精确度的几何平均。

false