Spaces:
Running
Running
| """ | |
| BLEU、ROUGE、METEOR 评估指标 | |
| """ | |
| import math | |
| import sys | |
| from collections import Counter | |
| from pathlib import Path | |
| sys.path.insert(0, '.') | |
| def compute_bleu(references, hypotheses, max_n=4): | |
| """ | |
| 计算 BLEU 分数 | |
| Args: | |
| references: 参考文本列表(每个元素是字符串或token列表) | |
| hypotheses: 生成文本列表(每个元素是字符串或token列表) | |
| max_n: 最大 n-gram 阶数(默认4,即 BLEU-4) | |
| Returns: | |
| dict: {bleu_1, bleu_2, bleu_3, bleu_4, brevity_penalty} | |
| """ | |
| def tokenize(text): | |
| if isinstance(text, str): | |
| return text.lower().split() | |
| return [t.lower() for t in text] | |
| def get_ngrams(tokens, n): | |
| return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)) | |
| bleu_scores = {} | |
| for n in range(1, max_n + 1): | |
| total_clip = 0 | |
| total_count = 0 | |
| for ref, hyp in zip(references, hypotheses): | |
| ref_tokens = tokenize(ref) | |
| hyp_tokens = tokenize(hyp) | |
| ref_ngrams = get_ngrams(ref_tokens, n) | |
| hyp_ngrams = get_ngrams(hyp_tokens, n) | |
| clip = 0 | |
| for ngram, count in hyp_ngrams.items(): | |
| clip += min(count, ref_ngrams.get(ngram, 0)) | |
| total_clip += clip | |
| total_count += max(sum(hyp_ngrams.values()), 1) | |
| if total_count == 0: | |
| bleu_scores[f'bleu_{n}'] = 0.0 | |
| else: | |
| bleu_scores[f'bleu_{n}'] = total_clip / total_count | |
| # Brevity Penalty | |
| ref_lengths = [len(tokenize(r)) for r in references] | |
| hyp_lengths = [len(tokenize(h)) for h in hypotheses] | |
| bp = 1.0 | |
| if sum(hyp_lengths) < sum(ref_lengths): | |
| bp = math.exp(1 - sum(ref_lengths) / max(sum(hyp_lengths), 1)) | |
| # Combined BLEU score | |
| if all(bleu_scores[f'bleu_{n}'] > 0 for n in range(1, max_n + 1)): | |
| log_avg = sum(math.log(bleu_scores[f'bleu_{n}']) for n in range(1, max_n + 1)) / max_n | |
| bleu_scores['bleu'] = bp * math.exp(log_avg) | |
| else: | |
| bleu_scores['bleu'] = 0.0 | |
| bleu_scores['brevity_penalty'] = bp | |
| return bleu_scores | |
| def compute_rouge(references, hypotheses): | |
| """ | |
| 计算 ROUGE 分数(ROUGE-1, ROUGE-2, ROUGE-L) | |
| Args: | |
| references: 参考文本列表 | |
| hypotheses: 生成文本列表 | |
| Returns: | |
| dict: {rouge_1, rouge_2, rouge_l}(各含 precision, recall, f1) | |
| """ | |
| def tokenize(text): | |
| if isinstance(text, str): | |
| return text.lower().split() | |
| return [t.lower() for t in text] | |
| def get_ngrams(tokens, n): | |
| return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)) | |
| def f1_score(precision, recall): | |
| if precision + recall == 0: | |
| return 0.0 | |
| return 2 * precision * recall / (precision + recall) | |
| def lcs_length(x, y): | |
| """最长公共子序列长度""" | |
| m, n = len(x), len(y) | |
| dp = [[0] * (n + 1) for _ in range(m + 1)] | |
| for i in range(1, m + 1): | |
| for j in range(1, n + 1): | |
| if x[i-1] == y[j-1]: | |
| dp[i][j] = dp[i-1][j-1] + 1 | |
| else: | |
| dp[i][j] = max(dp[i-1][j], dp[i][j-1]) | |
| return dp[m][n] | |
| rouge_scores = { | |
| 'rouge_1': {'precision': 0, 'recall': 0, 'f1': 0}, | |
| 'rouge_2': {'precision': 0, 'recall': 0, 'f1': 0}, | |
| 'rouge_l': {'precision': 0, 'recall': 0, 'f1': 0}, | |
| } | |
| for ref, hyp in zip(references, hypotheses): | |
| ref_tokens = tokenize(ref) | |
| hyp_tokens = tokenize(hyp) | |
| # ROUGE-1 | |
| ref_unigrams = Counter(ref_tokens) | |
| hyp_unigrams = Counter(hyp_tokens) | |
| overlap = sum((ref_unigrams & hyp_unigrams).values()) | |
| r1_p = overlap / max(len(hyp_tokens), 1) | |
| r1_r = overlap / max(len(ref_tokens), 1) | |
| rouge_scores['rouge_1']['precision'] += r1_p | |
| rouge_scores['rouge_1']['recall'] += r1_r | |
| rouge_scores['rouge_1']['f1'] += f1_score(r1_p, r1_r) | |
| # ROUGE-2 | |
| ref_bigrams = get_ngrams(ref_tokens, 2) | |
| hyp_bigrams = get_ngrams(hyp_tokens, 2) | |
| overlap2 = sum((ref_bigrams & hyp_bigrams).values()) | |
| r2_p = overlap2 / max(sum(hyp_bigrams.values()), 1) | |
| r2_r = overlap2 / max(sum(ref_bigrams.values()), 1) | |
| rouge_scores['rouge_2']['precision'] += r2_p | |
| rouge_scores['rouge_2']['recall'] += r2_r | |
| rouge_scores['rouge_2']['f1'] += f1_score(r2_p, r2_r) | |
| # ROUGE-L | |
| lcs = lcs_length(ref_tokens, hyp_tokens) | |
| rl_p = lcs / max(len(hyp_tokens), 1) | |
| rl_r = lcs / max(len(ref_tokens), 1) | |
| rouge_scores['rouge_l']['precision'] += rl_p | |
| rouge_scores['rouge_l']['recall'] += rl_r | |
| rouge_scores['rouge_l']['f1'] += f1_score(rl_p, rl_r) | |
| # 平均 | |
| n = max(len(references), 1) | |
| for key in rouge_scores: | |
| for metric in rouge_scores[key]: | |
| rouge_scores[key][metric] /= n | |
| return rouge_scores | |
| def compute_meteor(references, hypotheses, alpha=0.9, beta=3, gamma=0.5): | |
| """ | |
| 计算 METEOR 分数 | |
| Args: | |
| references: 参考文本列表 | |
| hypotheses: 生成文本列表 | |
| alpha: 精确率权重(默认0.9) | |
| beta: 分段惩罚参数(默认3) | |
| gamma: 分段惩罚系数(默认0.5) | |
| Returns: | |
| dict: {meteor, precision, recall, fragmentation_penalty} | |
| """ | |
| def tokenize(text): | |
| if isinstance(text, str): | |
| return text.lower().split() | |
| return [t.lower() for t in text] | |
| def align(hyp_tokens, ref_tokens): | |
| """简单的对齐:贪心匹配""" | |
| aligned = set() | |
| ref_used = set() | |
| for i, h_token in enumerate(hyp_tokens): | |
| for j, r_token in enumerate(ref_tokens): | |
| if j not in ref_used and h_token == r_token: | |
| aligned.add((i, j)) | |
| ref_used.add(j) | |
| break | |
| return aligned | |
| def count_chunks(aligned, hyp_tokens, ref_tokens): | |
| """计算块数(连续对齐段数)""" | |
| if not aligned: | |
| return 0 | |
| # 按 hyp 索引排序 | |
| sorted_align = sorted(aligned, key=lambda x: x[0]) | |
| chunks = 1 | |
| for i in range(1, len(sorted_align)): | |
| prev_h, prev_r = sorted_align[i-1] | |
| curr_h, curr_r = sorted_align[i] | |
| if curr_h != prev_h + 1 or curr_r != prev_r + 1: | |
| chunks += 1 | |
| return chunks | |
| total_meteor = 0 | |
| for ref, hyp in zip(references, hypotheses): | |
| ref_tokens = tokenize(ref) | |
| hyp_tokens = tokenize(hyp) | |
| # 对齐 | |
| aligned = align(hyp_tokens, ref_tokens) | |
| # 精确率和召回率 | |
| m = len(aligned) | |
| precision = m / max(len(hyp_tokens), 1) | |
| recall = m / max(len(ref_tokens), 1) | |
| # F-mean | |
| if precision + recall == 0: | |
| f_mean = 0 | |
| else: | |
| f_mean = (precision * recall) / (alpha * precision + (1 - alpha) * recall) | |
| # 分段惩罚 | |
| chunks = count_chunks(aligned, hyp_tokens, ref_tokens) | |
| if m > 0: | |
| frag = chunks / m | |
| else: | |
| frag = 1.0 | |
| penalty = gamma * (frag ** beta) | |
| # METEOR | |
| meteor = f_mean * (1 - penalty) | |
| total_meteor += meteor | |
| n = max(len(references), 1) | |
| avg_meteor = total_meteor / n | |
| return { | |
| 'meteor': avg_meteor, | |
| 'note': f'alpha={alpha}, beta={beta}, gamma={gamma}' | |
| } | |
| if __name__ == "__main__": | |
| print("=" * 60) | |
| print("Fusion-LLM BLEU/ROUGE/METEOR 评估指标测试") | |
| print("=" * 60) | |
| print() | |
| # 测试数据 | |
| references = [ | |
| "The cat sat on the mat and looked at the window", | |
| "Machine learning is a subset of artificial intelligence", | |
| "The weather is nice today and I want to go outside", | |
| ] | |
| hypotheses = [ | |
| "The cat sat on the mat and looked outside", | |
| "Machine learning is a branch of artificial intelligence", | |
| "The weather is nice and I want to go out", | |
| ] | |
| # BLEU | |
| print("[1] BLEU 分数...") | |
| bleu = compute_bleu(references, hypotheses) | |
| for k, v in bleu.items(): | |
| print(f" {k}: {v:.4f}") | |
| print() | |
| # ROUGE | |
| print("[2] ROUGE 分数...") | |
| rouge = compute_rouge(references, hypotheses) | |
| for k, v in rouge.items(): | |
| print(f" {k}: P={v['precision']:.4f} R={v['recall']:.4f} F1={v['f1']:.4f}") | |
| print() | |
| # METEOR | |
| print("[3] METEOR 分数...") | |
| meteor = compute_meteor(references, hypotheses) | |
| for k, v in meteor.items(): | |
| print(f" {k}: {v:.4f}" if isinstance(v, float) else f" {k}: {v}") | |
| print() | |
| # 边缘情况测试 | |
| print("[4] 边缘情况测试...") | |
| # 空输入 | |
| bleu_empty = compute_bleu([""], [""]) | |
| assert bleu_empty['bleu'] == 0.0, "Empty BLEU should be 0" | |
| print(" 空输入测试通过") | |
| # 完全匹配(需要足够长的句子形成4-gram) | |
| long_sentence = "the cat sat on the mat and looked at the window" | |
| bleu_match = compute_bleu([long_sentence], [long_sentence]) | |
| assert bleu_match['bleu'] > 0, "Perfect match BLEU should be > 0" | |
| print(f" 完全匹配测试通过 (BLEU={bleu_match['bleu']:.4f})") | |
| # 无匹配 | |
| rouge_nomatch = compute_rouge(["aaa bbb"], ["ccc ddd"]) | |
| assert rouge_nomatch['rouge_1']['f1'] == 0.0, "No match ROUGE should be 0" | |
| print(" 无匹配测试通过") | |
| print() | |
| print("[PASS] BLEU/ROUGE/METEOR 评估指标测试全部通过") | |
| sys.exit(0) | |