使用到的大语言模型:
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库得出的结果一致。
步骤总结:
- 分词:将参考句子和候选句子分割成单词列表;
- 生成n-gram:为参考句子和候选句子创建n-gram列表;
- 匹配n-gram计数:计算候选句子中与参考句子匹配的n-gram数量,评估候选句子在不同长度的n-gram上与参考句子的相似度;
- 计算n-gram精确度:对于每个n-gram级别,计算候选句子中匹配的n-gram与总n-gram的比例;
- 几何平均数计算:计算从1-gram到最大4-gram精确度的几何平均数。使用几何平均数来平衡不同长度n-gram的影响;
- 短句惩罚应用:引入惩罚因子,降低比参考句子短的候选句子的分数,防止短句得高分;
- 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之间的数值,可以直观地表示精确度的几何平均。