Spaces:
Running
Running
zhan1206 commited on
Commit ·
48aac3d
1
Parent(s): cb121a2
Feat: Add BLEU, ROUGE, METEOR evaluation metrics
Browse files- 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)
|