zhan1206 commited on
Commit
48aac3d
·
1 Parent(s): cb121a2

Feat: Add BLEU, ROUGE, METEOR evaluation metrics

Browse files
Files changed (1) hide show
  1. evaluation/bleu_rouge_meteor.py +315 -0
evaluation/bleu_rouge_meteor.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BLEU、ROUGE、METEOR 评估指标
3
+ """
4
+ import math
5
+ import sys
6
+ from collections import Counter
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, '.')
10
+
11
+
12
+ def compute_bleu(references, hypotheses, max_n=4):
13
+ """
14
+ 计算 BLEU 分数
15
+
16
+ Args:
17
+ references: 参考文本列表(每个元素是字符串或token列表)
18
+ hypotheses: 生成文本列表(每个元素是字符串或token列表)
19
+ max_n: 最大 n-gram 阶数(默认4,即 BLEU-4)
20
+
21
+ Returns:
22
+ dict: {bleu_1, bleu_2, bleu_3, bleu_4, brevity_penalty}
23
+ """
24
+ def tokenize(text):
25
+ if isinstance(text, str):
26
+ return text.lower().split()
27
+ return [t.lower() for t in text]
28
+
29
+ def get_ngrams(tokens, n):
30
+ return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1))
31
+
32
+ bleu_scores = {}
33
+
34
+ for n in range(1, max_n + 1):
35
+ total_clip = 0
36
+ total_count = 0
37
+
38
+ for ref, hyp in zip(references, hypotheses):
39
+ ref_tokens = tokenize(ref)
40
+ hyp_tokens = tokenize(hyp)
41
+
42
+ ref_ngrams = get_ngrams(ref_tokens, n)
43
+ hyp_ngrams = get_ngrams(hyp_tokens, n)
44
+
45
+ clip = 0
46
+ for ngram, count in hyp_ngrams.items():
47
+ clip += min(count, ref_ngrams.get(ngram, 0))
48
+
49
+ total_clip += clip
50
+ total_count += max(sum(hyp_ngrams.values()), 1)
51
+
52
+ if total_count == 0:
53
+ bleu_scores[f'bleu_{n}'] = 0.0
54
+ else:
55
+ bleu_scores[f'bleu_{n}'] = total_clip / total_count
56
+
57
+ # Brevity Penalty
58
+ ref_lengths = [len(tokenize(r)) for r in references]
59
+ hyp_lengths = [len(tokenize(h)) for h in hypotheses]
60
+
61
+ bp = 1.0
62
+ if sum(hyp_lengths) < sum(ref_lengths):
63
+ bp = math.exp(1 - sum(ref_lengths) / max(sum(hyp_lengths), 1))
64
+
65
+ # Combined BLEU score
66
+ if all(bleu_scores[f'bleu_{n}'] > 0 for n in range(1, max_n + 1)):
67
+ log_avg = sum(math.log(bleu_scores[f'bleu_{n}']) for n in range(1, max_n + 1)) / max_n
68
+ bleu_scores['bleu'] = bp * math.exp(log_avg)
69
+ else:
70
+ bleu_scores['bleu'] = 0.0
71
+
72
+ bleu_scores['brevity_penalty'] = bp
73
+
74
+ return bleu_scores
75
+
76
+
77
+ def compute_rouge(references, hypotheses):
78
+ """
79
+ 计算 ROUGE 分数(ROUGE-1, ROUGE-2, ROUGE-L)
80
+
81
+ Args:
82
+ references: 参考文本列表
83
+ hypotheses: 生成文本列表
84
+
85
+ Returns:
86
+ dict: {rouge_1, rouge_2, rouge_l}(各含 precision, recall, f1)
87
+ """
88
+ def tokenize(text):
89
+ if isinstance(text, str):
90
+ return text.lower().split()
91
+ return [t.lower() for t in text]
92
+
93
+ def get_ngrams(tokens, n):
94
+ return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1))
95
+
96
+ def f1_score(precision, recall):
97
+ if precision + recall == 0:
98
+ return 0.0
99
+ return 2 * precision * recall / (precision + recall)
100
+
101
+ def lcs_length(x, y):
102
+ """最长公共子序列长度"""
103
+ m, n = len(x), len(y)
104
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
105
+ for i in range(1, m + 1):
106
+ for j in range(1, n + 1):
107
+ if x[i-1] == y[j-1]:
108
+ dp[i][j] = dp[i-1][j-1] + 1
109
+ else:
110
+ dp[i][j] = max(dp[i-1][j], dp[i][j-1])
111
+ return dp[m][n]
112
+
113
+ rouge_scores = {
114
+ 'rouge_1': {'precision': 0, 'recall': 0, 'f1': 0},
115
+ 'rouge_2': {'precision': 0, 'recall': 0, 'f1': 0},
116
+ 'rouge_l': {'precision': 0, 'recall': 0, 'f1': 0},
117
+ }
118
+
119
+ for ref, hyp in zip(references, hypotheses):
120
+ ref_tokens = tokenize(ref)
121
+ hyp_tokens = tokenize(hyp)
122
+
123
+ # ROUGE-1
124
+ ref_unigrams = Counter(ref_tokens)
125
+ hyp_unigrams = Counter(hyp_tokens)
126
+ overlap = sum((ref_unigrams & hyp_unigrams).values())
127
+
128
+ r1_p = overlap / max(len(hyp_tokens), 1)
129
+ r1_r = overlap / max(len(ref_tokens), 1)
130
+ rouge_scores['rouge_1']['precision'] += r1_p
131
+ rouge_scores['rouge_1']['recall'] += r1_r
132
+ rouge_scores['rouge_1']['f1'] += f1_score(r1_p, r1_r)
133
+
134
+ # ROUGE-2
135
+ ref_bigrams = get_ngrams(ref_tokens, 2)
136
+ hyp_bigrams = get_ngrams(hyp_tokens, 2)
137
+ overlap2 = sum((ref_bigrams & hyp_bigrams).values())
138
+
139
+ r2_p = overlap2 / max(sum(hyp_bigrams.values()), 1)
140
+ r2_r = overlap2 / max(sum(ref_bigrams.values()), 1)
141
+ rouge_scores['rouge_2']['precision'] += r2_p
142
+ rouge_scores['rouge_2']['recall'] += r2_r
143
+ rouge_scores['rouge_2']['f1'] += f1_score(r2_p, r2_r)
144
+
145
+ # ROUGE-L
146
+ lcs = lcs_length(ref_tokens, hyp_tokens)
147
+ rl_p = lcs / max(len(hyp_tokens), 1)
148
+ rl_r = lcs / max(len(ref_tokens), 1)
149
+ rouge_scores['rouge_l']['precision'] += rl_p
150
+ rouge_scores['rouge_l']['recall'] += rl_r
151
+ rouge_scores['rouge_l']['f1'] += f1_score(rl_p, rl_r)
152
+
153
+ # 平均
154
+ n = max(len(references), 1)
155
+ for key in rouge_scores:
156
+ for metric in rouge_scores[key]:
157
+ rouge_scores[key][metric] /= n
158
+
159
+ return rouge_scores
160
+
161
+
162
+ def compute_meteor(references, hypotheses, alpha=0.9, beta=3, gamma=0.5):
163
+ """
164
+ 计算 METEOR 分数
165
+
166
+ Args:
167
+ references: 参考文本列表
168
+ hypotheses: 生成文本列表
169
+ alpha: 精确率权重(默认0.9)
170
+ beta: 分段惩罚参数(默认3)
171
+ gamma: 分段惩罚系数(默认0.5)
172
+
173
+ Returns:
174
+ dict: {meteor, precision, recall, fragmentation_penalty}
175
+ """
176
+ def tokenize(text):
177
+ if isinstance(text, str):
178
+ return text.lower().split()
179
+ return [t.lower() for t in text]
180
+
181
+ def align(hyp_tokens, ref_tokens):
182
+ """简单的对齐:贪心匹配"""
183
+ aligned = set()
184
+ ref_used = set()
185
+
186
+ for i, h_token in enumerate(hyp_tokens):
187
+ for j, r_token in enumerate(ref_tokens):
188
+ if j not in ref_used and h_token == r_token:
189
+ aligned.add((i, j))
190
+ ref_used.add(j)
191
+ break
192
+
193
+ return aligned
194
+
195
+ def count_chunks(aligned, hyp_tokens, ref_tokens):
196
+ """计算块数(连续对齐段数)"""
197
+ if not aligned:
198
+ return 0
199
+
200
+ # 按 hyp 索引排序
201
+ sorted_align = sorted(aligned, key=lambda x: x[0])
202
+ chunks = 1
203
+
204
+ for i in range(1, len(sorted_align)):
205
+ prev_h, prev_r = sorted_align[i-1]
206
+ curr_h, curr_r = sorted_align[i]
207
+ if curr_h != prev_h + 1 or curr_r != prev_r + 1:
208
+ chunks += 1
209
+
210
+ return chunks
211
+
212
+ total_meteor = 0
213
+
214
+ for ref, hyp in zip(references, hypotheses):
215
+ ref_tokens = tokenize(ref)
216
+ hyp_tokens = tokenize(hyp)
217
+
218
+ # 对齐
219
+ aligned = align(hyp_tokens, ref_tokens)
220
+
221
+ # 精确率和召回率
222
+ m = len(aligned)
223
+ precision = m / max(len(hyp_tokens), 1)
224
+ recall = m / max(len(ref_tokens), 1)
225
+
226
+ # F-mean
227
+ if precision + recall == 0:
228
+ f_mean = 0
229
+ else:
230
+ f_mean = (precision * recall) / (alpha * precision + (1 - alpha) * recall)
231
+
232
+ # 分段惩罚
233
+ chunks = count_chunks(aligned, hyp_tokens, ref_tokens)
234
+ if m > 0:
235
+ frag = chunks / m
236
+ else:
237
+ frag = 1.0
238
+
239
+ penalty = gamma * (frag ** beta)
240
+
241
+ # METEOR
242
+ meteor = f_mean * (1 - penalty)
243
+ total_meteor += meteor
244
+
245
+ n = max(len(references), 1)
246
+ avg_meteor = total_meteor / n
247
+
248
+ return {
249
+ 'meteor': avg_meteor,
250
+ 'note': f'alpha={alpha}, beta={beta}, gamma={gamma}'
251
+ }
252
+
253
+
254
+ if __name__ == "__main__":
255
+ print("=" * 60)
256
+ print("Fusion-LLM BLEU/ROUGE/METEOR 评估指标测试")
257
+ print("=" * 60)
258
+ print()
259
+
260
+ # 测试数据
261
+ references = [
262
+ "The cat sat on the mat and looked at the window",
263
+ "Machine learning is a subset of artificial intelligence",
264
+ "The weather is nice today and I want to go outside",
265
+ ]
266
+
267
+ hypotheses = [
268
+ "The cat sat on the mat and looked outside",
269
+ "Machine learning is a branch of artificial intelligence",
270
+ "The weather is nice and I want to go out",
271
+ ]
272
+
273
+ # BLEU
274
+ print("[1] BLEU 分数...")
275
+ bleu = compute_bleu(references, hypotheses)
276
+ for k, v in bleu.items():
277
+ print(f" {k}: {v:.4f}")
278
+ print()
279
+
280
+ # ROUGE
281
+ print("[2] ROUGE 分数...")
282
+ rouge = compute_rouge(references, hypotheses)
283
+ for k, v in rouge.items():
284
+ print(f" {k}: P={v['precision']:.4f} R={v['recall']:.4f} F1={v['f1']:.4f}")
285
+ print()
286
+
287
+ # METEOR
288
+ print("[3] METEOR 分数...")
289
+ meteor = compute_meteor(references, hypotheses)
290
+ for k, v in meteor.items():
291
+ print(f" {k}: {v:.4f}" if isinstance(v, float) else f" {k}: {v}")
292
+ print()
293
+
294
+ # 边缘情况测试
295
+ print("[4] 边缘情况测试...")
296
+
297
+ # 空输入
298
+ bleu_empty = compute_bleu([""], [""])
299
+ assert bleu_empty['bleu'] == 0.0, "Empty BLEU should be 0"
300
+ print(" 空输入测试通过")
301
+
302
+ # 完全匹配(需要足够长的句子形成4-gram)
303
+ long_sentence = "the cat sat on the mat and looked at the window"
304
+ bleu_match = compute_bleu([long_sentence], [long_sentence])
305
+ assert bleu_match['bleu'] > 0, "Perfect match BLEU should be > 0"
306
+ print(f" 完全匹配测试通过 (BLEU={bleu_match['bleu']:.4f})")
307
+
308
+ # 无匹配
309
+ rouge_nomatch = compute_rouge(["aaa bbb"], ["ccc ddd"])
310
+ assert rouge_nomatch['rouge_1']['f1'] == 0.0, "No match ROUGE should be 0"
311
+ print(" 无匹配测试通过")
312
+
313
+ print()
314
+ print("[PASS] BLEU/ROUGE/METEOR 评估指标测试全部通过")
315
+ sys.exit(0)