zhan1206 commited on
Commit
0eeb85b
·
1 Parent(s): 68eedb0

fix: replace random embeddings in MoverScore with deterministic hash

Browse files

MoverScore was using torch.randn causing non-reproducible results each run.
Now uses the same hash-based pseudo-embedding as BERTScore for determinism.

Also created evaluation/benchmark_runner.py that was missing:
- Perplexity benchmark
- Generation quality (BERTScore/MoverScore)
- Inference speed benchmark
- Full benchmark suite CLI

Updated documentation headers to clarify these are simplified placeholder
implementations for demonstration, not production-quality metrics.

Verified: bertscore_simple and moverscore_simple both deterministic
25/25 pytest passed, all imports working.

evaluation/benchmark_runner.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 基准测试运行器
3
+ 支持多种评估任务和配置
4
+ """
5
+ import sys
6
+ import json
7
+ import time
8
+ import torch
9
+ import argparse
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional, Any
12
+ from datetime import datetime
13
+
14
+ sys.path.insert(0, '.')
15
+
16
+ from evaluation.metrics import ModelEvaluator, EvaluationMetrics
17
+ from evaluation.bertscore_moverscore import bertscore_simple, moverscore_simple
18
+
19
+
20
+ class BenchmarkRunner:
21
+ """
22
+ 基准测试运行器
23
+
24
+ 支持的测试类型:
25
+ - perplexity: 困惑度评估
26
+ - generation: 生成质量评估
27
+ - accuracy: 任务准确率
28
+ - speed: 推理速度基准
29
+ """
30
+
31
+ def __init__(self, model_path: str, device: str = "auto"):
32
+ """
33
+ 初始化基准测试运行器
34
+
35
+ Args:
36
+ model_path: 模型路径
37
+ device: 计算设备 (auto/cpu/cuda)
38
+ """
39
+ self.model_path = model_path
40
+ self.device = self._resolve_device(device)
41
+ self.model = None
42
+ self.config = None
43
+ self.tokenizer = None
44
+
45
+ def _resolve_device(self, device: str) -> str:
46
+ """解析设备字符串"""
47
+ if device == "auto":
48
+ return "cuda" if torch.cuda.is_available() else "cpu"
49
+ return device
50
+
51
+ def load_model(self):
52
+ """加载模型"""
53
+ print(f"[Loading] Model from {self.model_path} on {self.device}")
54
+
55
+ # 尝试加载 FusionMini
56
+ try:
57
+ from models.fusion_mini import FusionMini, FusionMiniConfig
58
+ self.model = FusionMini._load_from_safetensors(self.model_path)
59
+ self.config = self.model.config
60
+ print(f"[Loaded] FusionMini model")
61
+ except Exception as e:
62
+ # 回退到 FusionModel
63
+ from models.fusion_model import FusionModel, FusionConfig
64
+ self.model = FusionModel.from_pretrained(self.model_path)
65
+ self.config = self.model.config
66
+ print(f"[Loaded] FusionModel: {e}")
67
+
68
+ self.model.to(self.device)
69
+ self.model.eval()
70
+
71
+ # 创建简单 tokenizer
72
+ self.tokenizer = self._create_tokenizer()
73
+
74
+ def _create_tokenizer(self):
75
+ """创建简单 tokenizer(用于测试)"""
76
+ vocab_size = getattr(self.config, 'vocab_size', 10000)
77
+
78
+ class SimpleTokenizer:
79
+ def __init__(self, vs):
80
+ self.vocab_size = vs
81
+
82
+ def encode(self, text):
83
+ # 简单字符级编码
84
+ return [ord(c) % self.vocab_size for c in text[:512]]
85
+
86
+ def decode(self, ids):
87
+ return ''.join(chr(i % 128 + 32) for i in ids if 0 <= i < self.vocab_size)
88
+
89
+ return SimpleTokenizer(vocab_size)
90
+
91
+ def run_perplexity(self, texts: List[str]) -> Dict[str, float]:
92
+ """
93
+ 计算困惑度
94
+
95
+ Args:
96
+ texts: 测试文本列表
97
+
98
+ Returns:
99
+ 困惑度指标
100
+ """
101
+ print(f"\n[Benchmark] Perplexity on {len(texts)} texts")
102
+
103
+ self.model.eval()
104
+ total_loss = 0.0
105
+ total_tokens = 0
106
+
107
+ with torch.no_grad():
108
+ for text in texts:
109
+ ids = self.tokenizer.encode(text)
110
+ if len(ids) < 2:
111
+ continue
112
+
113
+ input_ids = torch.tensor([ids], device=self.device)
114
+ labels = input_ids.clone()
115
+
116
+ outputs = self.model(input_ids, labels=labels)
117
+ loss = outputs.loss
118
+
119
+ if loss is not None:
120
+ total_loss += loss.item() * len(ids)
121
+ total_tokens += len(ids)
122
+
123
+ if total_tokens == 0:
124
+ return {"perplexity": float('inf')}
125
+
126
+ avg_loss = total_loss / total_tokens
127
+ perplexity = torch.exp(torch.tensor(avg_loss)).item()
128
+
129
+ return {
130
+ "perplexity": perplexity,
131
+ "avg_loss": avg_loss,
132
+ "total_tokens": total_tokens
133
+ }
134
+
135
+ def run_generation_quality(
136
+ self,
137
+ prompts: List[str],
138
+ references: List[str],
139
+ max_new_tokens: int = 50
140
+ ) -> Dict[str, Any]:
141
+ """
142
+ 生成质量评估
143
+
144
+ Args:
145
+ prompts: 提示列表
146
+ references: 参考答案列表
147
+ max_new_tokens: 最大生成 token 数
148
+
149
+ Returns:
150
+ 生成质量指标
151
+ """
152
+ print(f"\n[Benchmark] Generation quality on {len(prompts)} prompts")
153
+
154
+ generations = []
155
+
156
+ self.model.eval()
157
+ with torch.no_grad():
158
+ for prompt in prompts:
159
+ ids = self.tokenizer.encode(prompt)
160
+ input_ids = torch.tensor([ids], device=self.device)
161
+
162
+ # 简单贪婪生成
163
+ generated = input_ids.clone()
164
+ for _ in range(max_new_tokens):
165
+ outputs = self.model(generated)
166
+ next_token = outputs.logits[:, -1, :].argmax(dim=-1, keepdim=True)
167
+ generated = torch.cat([generated, next_token], dim=1)
168
+
169
+ # EOS 检查(假设 2 是 EOS)
170
+ if next_token.item() == 2:
171
+ break
172
+
173
+ gen_text = self.tokenizer.decode(generated[0].tolist())
174
+ generations.append(gen_text)
175
+
176
+ # 计算 BERTScore 和 MoverScore
177
+ bert_scores = []
178
+ mover_scores = []
179
+
180
+ for gen, ref in zip(generations, references):
181
+ gen_ids = self.tokenizer.encode(gen)
182
+ ref_ids = self.tokenizer.encode(ref)
183
+
184
+ _, _, bert_f1 = bertscore_simple(gen_ids, ref_ids)
185
+ mover_score = moverscore_simple(gen_ids, ref_ids)
186
+
187
+ bert_scores.append(bert_f1)
188
+ mover_scores.append(mover_score)
189
+
190
+ return {
191
+ "bertscore_f1": sum(bert_scores) / len(bert_scores) if bert_scores else 0.0,
192
+ "moverscore": sum(mover_scores) / len(mover_scores) if mover_scores else 0.0,
193
+ "generations": generations[:5], # 只返回前5个样本
194
+ "num_samples": len(generations)
195
+ }
196
+
197
+ def run_speed_benchmark(
198
+ self,
199
+ batch_sizes: List[int] = [1, 2, 4],
200
+ seq_lengths: List[int] = [32, 64, 128, 256],
201
+ warmup: int = 3,
202
+ runs: int = 10
203
+ ) -> Dict[str, Any]:
204
+ """
205
+ 推理速度基准测试
206
+
207
+ Args:
208
+ batch_sizes: 批大小列表
209
+ seq_lengths: 序列长度列表
210
+ warmup: 预热次数
211
+ runs: 测试次数
212
+
213
+ Returns:
214
+ 速度指标
215
+ """
216
+ print(f"\n[Benchmark] Speed benchmark")
217
+
218
+ results = []
219
+ vocab_size = getattr(self.config, 'vocab_size', 10000)
220
+
221
+ self.model.eval()
222
+
223
+ for batch_size in batch_sizes:
224
+ for seq_len in seq_lengths:
225
+ # 预热
226
+ for _ in range(warmup):
227
+ dummy = torch.randint(0, vocab_size, (batch_size, seq_len), device=self.device)
228
+ with torch.no_grad():
229
+ _ = self.model(dummy)
230
+
231
+ # 计时
232
+ torch.cuda.synchronize() if self.device == "cuda" else None
233
+ start = time.perf_counter()
234
+
235
+ for _ in range(runs):
236
+ dummy = torch.randint(0, vocab_size, (batch_size, seq_len), device=self.device)
237
+ with torch.no_grad():
238
+ _ = self.model(dummy)
239
+
240
+ torch.cuda.synchronize() if self.device == "cuda" else None
241
+ end = time.perf_counter()
242
+
243
+ avg_time = (end - start) / runs
244
+ throughput = batch_size * seq_len / avg_time
245
+
246
+ results.append({
247
+ "batch_size": batch_size,
248
+ "seq_len": seq_len,
249
+ "latency_ms": avg_time * 1000,
250
+ "throughput_tokens_per_sec": throughput
251
+ })
252
+
253
+ print(f" batch={batch_size}, seq={seq_len}: {avg_time*1000:.2f}ms")
254
+
255
+ return {
256
+ "results": results,
257
+ "device": self.device,
258
+ "runs": runs
259
+ }
260
+
261
+ def run_full_benchmark(self, config: Dict[str, Any]) -> Dict[str, Any]:
262
+ """
263
+ 运行完整基准测试
264
+
265
+ Args:
266
+ config: 测试配置
267
+
268
+ Returns:
269
+ 完整测试结果
270
+ """
271
+ print("="*60)
272
+ print("Fusion-LLM Benchmark Runner")
273
+ print("="*60)
274
+
275
+ self.load_model()
276
+
277
+ results = {
278
+ "model_path": self.model_path,
279
+ "device": self.device,
280
+ "timestamp": datetime.now().isoformat(),
281
+ "config": self.config.to_dict() if hasattr(self.config, 'to_dict') else {}
282
+ }
283
+
284
+ # 困惑度
285
+ if config.get("perplexity", True):
286
+ test_texts = [
287
+ "The quick brown fox jumps over the lazy dog.",
288
+ "Machine learning models require large amounts of data.",
289
+ "Natural language processing enables computers to understand text."
290
+ ]
291
+ results["perplexity"] = self.run_perplexity(test_texts)
292
+
293
+ # 生成质量
294
+ if config.get("generation", True):
295
+ prompts = ["The future of AI is", "Machine learning helps"]
296
+ references = ["The future of AI is bright and transformative.", "Machine learning helps solve complex problems."]
297
+ results["generation"] = self.run_generation_quality(prompts, references)
298
+
299
+ # 速度基准
300
+ if config.get("speed", False):
301
+ results["speed"] = self.run_speed_benchmark(
302
+ batch_sizes=config.get("batch_sizes", [1]),
303
+ seq_lengths=config.get("seq_lengths", [32, 64, 128])
304
+ )
305
+
306
+ print("\n[Benchmark] Complete")
307
+ return results
308
+
309
+
310
+ def main():
311
+ parser = argparse.ArgumentParser(description="Fusion-LLM Benchmark Runner")
312
+ parser.add_argument("--model", required=True, help="Path to model checkpoint")
313
+ parser.add_argument("--device", default="auto", help="Device (auto/cpu/cuda)")
314
+ parser.add_argument("--output", default="benchmark_results.json", help="Output file")
315
+ parser.add_argument("--perplexity", action="store_true", help="Run perplexity benchmark")
316
+ parser.add_argument("--generation", action="store_true", help="Run generation benchmark")
317
+ parser.add_argument("--speed", action="store_true", help="Run speed benchmark")
318
+ parser.add_argument("--all", action="store_true", help="Run all benchmarks")
319
+
320
+ args = parser.parse_args()
321
+
322
+ # 配置
323
+ config = {
324
+ "perplexity": args.perplexity or args.all,
325
+ "generation": args.generation or args.all,
326
+ "speed": args.speed or args.all
327
+ }
328
+
329
+ # 运行
330
+ runner = BenchmarkRunner(args.model, args.device)
331
+ results = runner.run_full_benchmark(config)
332
+
333
+ # 保存
334
+ with open(args.output, 'w', encoding='utf-8') as f:
335
+ json.dump(results, f, indent=2, ensure_ascii=False)
336
+
337
+ print(f"\n[Saved] Results to {args.output}")
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()
evaluation/bertscore_moverscore.py CHANGED
@@ -1,7 +1,11 @@
1
  """
2
  BERTScore 和 MoverScore 评估指标
3
- 注意:这些是简化版实现,用于演示目的
4
- 际使时应该安装官方包
 
 
 
 
5
  - BERTScore: pip install bert-score
6
  - MoverScore: pip install moverscore
7
  """
@@ -15,9 +19,14 @@ sys.path.insert(0, '.')
15
 
16
  def bertscore_simple(candidate, reference, model_name="bert-base-uncased"):
17
  """
18
- Simplified BERTScore using cosine similarity on token ID embeddings.
19
 
20
- For production use, install the official package:
 
 
 
 
 
21
  pip install bert-score
22
  from bert_score import score
23
  P, R, F1 = score(candidates, references, lang="en")
@@ -72,7 +81,7 @@ def bertscore_simple(candidate, reference, model_name="bert-base-uncased"):
72
 
73
  def moverscore_simple(candidate, reference):
74
  """
75
- 简化版 MoverScore(演示用)
76
 
77
  实际使用时请安装官方包:pip install moverscore
78
  然后使用:
@@ -86,22 +95,43 @@ def moverscore_simple(candidate, reference):
86
  Returns:
87
  float: MoverScore
88
  """
89
- # 简化版:使用 token 嵌入Earth Mover's Distance
90
- # 实际 MoverScore 使BERT 嵌入的 WMD
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- # 转换为嵌入(简化版:随机嵌入)
93
- # 实际使用时应该使用 BERT 嵌入
94
- cand_embeddings = torch.randn(len(candidate), 768) # (seq_len, hidden_size)
95
- ref_embeddings = torch.randn(len(reference), 768)
96
 
97
- # 计算成本矩阵
98
  cost_matrix = torch.cdist(cand_embeddings.unsqueeze(0), ref_embeddings.unsqueeze(0)).squeeze(0)
99
 
100
- # 简化版:使用最小成本作为分数
101
- min_cost = cost_matrix.min().item()
 
 
 
102
 
103
  # 归一化到 0-1(越低越好 → 越高越好)
104
- score = 1.0 / (1.0 + min_cost)
 
 
 
 
 
 
105
 
106
  return score
107
 
 
1
  """
2
  BERTScore 和 MoverScore 评估指标
3
+
4
+ 【重要说明】这些是简化版现(演示
5
+ - BERTScore: 使用 hash 伪嵌入(确定性但非真正语义嵌入)
6
+ - MoverScore: 使用 hash 伪嵌入(确定性,模拟词嵌入距离)
7
+
8
+ 生产环境请使用官方包:
9
  - BERTScore: pip install bert-score
10
  - MoverScore: pip install moverscore
11
  """
 
19
 
20
  def bertscore_simple(candidate, reference, model_name="bert-base-uncased"):
21
  """
22
+ Simplified BERTScore using deterministic hash embeddings.
23
 
24
+ 【警告】此版本使用 hash 伪嵌入,非真正语义嵌入:
25
+ - 结果是确定性的(相同输入总是相同输出)
26
+ - 但不捕获真正语义相似性
27
+ - 仅用于快速测试和流程验证
28
+
29
+ 生产环境请安装官方包:
30
  pip install bert-score
31
  from bert_score import score
32
  P, R, F1 = score(candidates, references, lang="en")
 
81
 
82
  def moverscore_simple(candidate, reference):
83
  """
84
+ 简化版 MoverScore(演示用,确定性版本
85
 
86
  实际使用时请安装官方包:pip install moverscore
87
  然后使用:
 
95
  Returns:
96
  float: MoverScore
97
  """
98
+ # [FIX] 使用 BERTScore 相同hash 嵌入,确保结果可复现
99
+ # 之前torch.randn 导致每次调用结果不同
100
+
101
+ if len(candidate) == 0 or len(reference) == 0:
102
+ return 0.0
103
+
104
+ embed_dim = 128 # MoverScore 用较小维度即可
105
+
106
+ def _hash_embed(token_ids):
107
+ """确定性伪嵌入,与 bertscore_simple 保持一致"""
108
+ emb = torch.zeros(len(token_ids), embed_dim)
109
+ for i, tid in enumerate(token_ids):
110
+ for j in range(embed_dim):
111
+ # 使用不同的 hash 种子(2654435761 是黄金比例常数的整数部分)
112
+ emb[i, j] = ((tid * (j + 1) * 2654435761) % (2**31)) / (2**31) * 2 - 1
113
+ return emb
114
 
115
+ cand_embeddings = _hash_embed(candidate) # (len_c, dim)
116
+ ref_embeddings = _hash_embed(reference) # (len_r, dim)
 
 
117
 
118
+ # 计算成本矩阵(Euclidean 距离)
119
  cost_matrix = torch.cdist(cand_embeddings.unsqueeze(0), ref_embeddings.unsqueeze(0)).squeeze(0)
120
 
121
+ # 简化版:使用平均最小成本作为分数(双向)
122
+ # Precision: 对每个候选 token 找最近的参考 token
123
+ precision_cost = cost_matrix.min(dim=1).values.mean().item()
124
+ # Recall: 对每个参考 token 找最近的候选 token
125
+ recall_cost = cost_matrix.min(dim=0).values.mean().item()
126
 
127
  # 归一化到 0-1(越低越好 → 越高越好)
128
+ precision_score = 1.0 / (1.0 + precision_cost)
129
+ recall_score = 1.0 / (1.0 + recall_cost)
130
+
131
+ # F1 综合
132
+ if precision_score + recall_score == 0:
133
+ return 0.0
134
+ score = 2 * precision_score * recall_score / (precision_score + recall_score)
135
 
136
  return score
137