File size: 6,715 Bytes
69a47f1
 
0eeb85b
 
 
 
 
 
69a47f1
 
 
 
 
 
 
 
 
 
 
 
 
0eeb85b
69a47f1
0eeb85b
 
 
 
 
 
443dc7d
 
 
69a47f1
 
443dc7d
 
 
69a47f1
 
 
 
443dc7d
 
 
 
 
69a47f1
443dc7d
 
69a47f1
443dc7d
 
 
 
 
 
 
69a47f1
443dc7d
 
69a47f1
443dc7d
 
 
69a47f1
443dc7d
 
69a47f1
443dc7d
 
 
 
 
69a47f1
 
 
 
 
 
 
 
 
 
0eeb85b
69a47f1
 
 
 
 
 
 
 
 
 
 
 
 
0eeb85b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69a47f1
0eeb85b
 
69a47f1
0eeb85b
69a47f1
 
0eeb85b
 
 
 
 
69a47f1
 
0eeb85b
 
 
 
 
 
 
69a47f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
BERTScore 和 MoverScore 评估指标

【重要说明】这些是简化版实现(演示用):
- BERTScore: 使用 hash 伪嵌入(确定性但非真正语义嵌入)
- MoverScore: 使用 hash 伪嵌入(确定性,模拟词嵌入距离)

生产环境请使用官方包:
- BERTScore: pip install bert-score
- MoverScore: pip install moverscore
"""
import sys
import torch
import torch.nn.functional as F
from pathlib import Path

sys.path.insert(0, '.')


def bertscore_simple(candidate, reference, model_name="bert-base-uncased"):
    """
    Simplified BERTScore using deterministic hash embeddings.
    
    【警告】此版本使用 hash 伪嵌入,非真正语义嵌入:
    - 结果是确定性的(相同输入总是相同输出)
    - 但不捕获真正语义相似性
    - 仅用于快速测试和流程验证
    
    生产环境请安装官方包:
        pip install bert-score
        from bert_score import score
        P, R, F1 = score(candidates, references, lang="en")
    
    Args:
        candidate: candidate token IDs (list of ints)
        reference: reference token IDs (list of ints)
        model_name: BERT model name (unused in simplified version)
    
    Returns:
        tuple: (Precision, Recall, F1)
    """
    # L-NEW-1 FIX: Use cosine similarity instead of Jaccard set overlap.
    # Map each token ID to a learned-like embedding via hashing.
    # This approximates BERTScore's IDF-weighted cosine similarity.
    if len(candidate) == 0 or len(reference) == 0:
        return 0.0, 0.0, 0.0
    
    embed_dim = 64
    max_vocab = 100000
    
    def _hash_embed(token_ids):
        """Deterministic pseudo-embedding from token IDs via hash projection."""
        emb = torch.zeros(len(token_ids), embed_dim)
        for i, tid in enumerate(token_ids):
            for j in range(embed_dim):
                emb[i, j] = ((tid * (j + 1) * 2654435761) % (2**31)) / (2**31) * 2 - 1
        return emb
    
    cand_emb = _hash_embed(candidate)  # (len_c, dim)
    ref_emb = _hash_embed(reference)    # (len_r, dim)
    
    # Normalize
    cand_emb = cand_emb / (cand_emb.norm(dim=1, keepdim=True) + 1e-8)
    ref_emb = ref_emb / (ref_emb.norm(dim=1, keepdim=True) + 1e-8)
    
    # Cosine similarity matrix: (len_c, len_r)
    sim = torch.mm(cand_emb, ref_emb.t())
    
    # Precision = max similarity per candidate token
    precision = sim.max(dim=1).values.mean().item()
    # Recall = max similarity per reference token
    recall = sim.max(dim=0).values.mean().item()
    # F1
    if precision + recall == 0:
        f1 = 0.0
    else:
        f1 = 2 * precision * recall / (precision + recall)
    
    return precision, recall, f1


def moverscore_simple(candidate, reference):
    """
    简化版 MoverScore(演示用,确定性版本)
    
    实际使用时请安装官方包:pip install moverscore
    然后使用:
    from moverscore import moverscore
    scores = moverscore(candidates, references)
    
    Args:
        candidate: 候选文本(token IDs)
        reference: 参考文本(token IDs)
    
    Returns:
        float: MoverScore
    """
    # [FIX] 使用与 BERTScore 相同的 hash 嵌入,确保结果可复现
    # 之前用 torch.randn 导致每次调用结果不同
    
    if len(candidate) == 0 or len(reference) == 0:
        return 0.0
    
    embed_dim = 128  # MoverScore 用较小维度即可
    
    def _hash_embed(token_ids):
        """确定性伪嵌入,与 bertscore_simple 保持一致"""
        emb = torch.zeros(len(token_ids), embed_dim)
        for i, tid in enumerate(token_ids):
            for j in range(embed_dim):
                # 使用不同的 hash 种子(2654435761 是黄金比例常数的整数部分)
                emb[i, j] = ((tid * (j + 1) * 2654435761) % (2**31)) / (2**31) * 2 - 1
        return emb
    
    cand_embeddings = _hash_embed(candidate)  # (len_c, dim)
    ref_embeddings = _hash_embed(reference)    # (len_r, dim)
    
    # 计算成本矩阵(Euclidean 距离)
    cost_matrix = torch.cdist(cand_embeddings.unsqueeze(0), ref_embeddings.unsqueeze(0)).squeeze(0)
    
    # 简化版:使用平均最小成本作为分数(双向)
    # Precision: 对每个候选 token 找最近的参考 token
    precision_cost = cost_matrix.min(dim=1).values.mean().item()
    # Recall: 对每个参考 token 找最近的候选 token
    recall_cost = cost_matrix.min(dim=0).values.mean().item()
    
    # 归一化到 0-1(越低越好 → 越高越好)
    precision_score = 1.0 / (1.0 + precision_cost)
    recall_score = 1.0 / (1.0 + recall_cost)
    
    # F1 综合
    if precision_score + recall_score == 0:
        return 0.0
    score = 2 * precision_score * recall_score / (precision_score + recall_score)
    
    return score


def evaluate_bertscore_moverscore(candidates, references):
    """
    评估 BERTScore 和 MoverScore
    
    Args:
        candidates: 候选文本列表(每个文本是 token IDs 列表)
        references: 参考文本列表(每个文本是 token IDs 列表)
    
    Returns:
        dict: 评估指标字典
    """
    metrics = {
        "bertscore_precision": [],
        "bertscore_recall": [],
        "bertscore_f1": [],
        "moverscore": [],
    }
    
    for cand, ref in zip(candidates, references):
        # BERTScore
        P, R, F1 = bertscore_simple(cand, ref)
        metrics["bertscore_precision"].append(P)
        metrics["bertscore_recall"].append(R)
        metrics["bertscore_f1"].append(F1)
        
        # MoverScore
        ms = moverscore_simple(cand, ref)
        metrics["moverscore"].append(ms)
    
    # 平均
    for key in metrics:
        metrics[key] = sum(metrics[key]) / len(metrics[key]) if len(metrics[key]) > 0 else 0.0
    
    return metrics


if __name__ == "__main__":
    print("=" * 60)
    print("Fusion-LLM BERTScore & MoverScore 评估")
    print("=" * 60)
    print()
    
    # 创建示例数据
    candidates = [
        [1, 2, 3, 4, 5],
        [6, 7, 8, 9, 10],
    ]
    
    references = [
        [1, 2, 3, 4, 5],
        [6, 7, 8, 9, 11],  # 最后一个 token 不同
    ]
    
    # 评估
    metrics = evaluate_bertscore_moverscore(candidates, references)
    
    # 打印结果
    print("[METRICS] BERTScore & MoverScore:")
    print(f"   BERTScore Precision: {metrics['bertscore_precision']:.4f}")
    print(f"   BERTScore Recall:    {metrics['bertscore_recall']:.4f}")
    print(f"   BERTScore F1:       {metrics['bertscore_f1']:.4f}")
    print(f"   MoverScore:         {metrics['moverscore']:.4f}")
    print()
    
    print("[PASS] BERTScore & MoverScore 评估测试通过")
    sys.exit(0)