gleamlm 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gleamlm/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Tuple
4
+ from .models.model import GleamLMModel
5
+
6
+
7
+ def load_model_for_inference(model_path: str, device: str = 'cuda',
8
+ checkpoint: Optional[dict] = None
9
+ ) -> Tuple[GleamLMModel, dict]:
10
+ """从 checkpoint 加载模型用于推理/评估。"""
11
+ import torch
12
+
13
+ if checkpoint is None:
14
+ try:
15
+ checkpoint = torch.load(model_path, map_location=device, weights_only=False)
16
+ except TypeError:
17
+ checkpoint = torch.load(model_path, map_location=device)
18
+
19
+ if 'args' in checkpoint:
20
+ args = checkpoint['args']
21
+ config = {
22
+ 'vocab_size': args.vocab_size,
23
+ 'd_model': args.d_model,
24
+ 'num_layers': args.num_layers,
25
+ 'num_heads': args.num_heads,
26
+ 'num_kv_heads': getattr(args, 'num_kv_heads', args.num_heads),
27
+ 'd_ff': args.d_ff,
28
+ 'dropout': 0.0,
29
+ 'max_seq_len': args.max_seq_len,
30
+ 'pad_token_id': getattr(args, 'pad_token_id', 0),
31
+ 'tie_weights': getattr(args, 'tie_weights', True),
32
+ 'use_flash_attn': getattr(args, 'use_flash_attn', False),
33
+ }
34
+ elif 'config' in checkpoint:
35
+ config = checkpoint['config']
36
+ else:
37
+ raise ValueError(
38
+ "Checkpoint 缺少模型结构信息。"
39
+ "请确保 checkpoint 包含 'args' 或 'config' 字段。"
40
+ )
41
+
42
+ config['dropout'] = 0.0
43
+
44
+ model = GleamLMModel(**config).to(device)
45
+
46
+ if 'model_state_dict' in checkpoint:
47
+ state_dict = checkpoint['model_state_dict']
48
+ state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}
49
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
50
+ if missing:
51
+ print(f"Warning: missing keys in checkpoint: {missing}")
52
+ if unexpected:
53
+ print(f"Warning: unexpected keys in checkpoint: {unexpected}")
54
+
55
+ model.eval()
56
+
57
+ if checkpoint.get('dtype') == 'float16':
58
+ model = model.half()
59
+
60
+ return model, config
@@ -0,0 +1 @@
1
+ from .dataset import LMDataset, collate_fn
@@ -0,0 +1,127 @@
1
+ """GleamLM 数据集。滑动窗口切分 + 分词 + 动态 padding + numpy memmap。"""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from typing import List, Tuple
6
+ import torch
7
+ from torch.utils.data import Dataset
8
+ import numpy as np
9
+ import os
10
+ import random
11
+ from tqdm import tqdm
12
+
13
+ from gleamlm.tokenizer.tokenizer import BBPETokenizer
14
+
15
+
16
+ class LMDataset(Dataset):
17
+ """LM 数据集(memmap 版本)。分词后存磁盘,按索引切片"""
18
+ def __init__(self, data_dir: str, tokenizer: BBPETokenizer,
19
+ max_seq_len: int, split: str = "train",
20
+ stride: int | None = None,
21
+ max_chars: int | None = None,
22
+ ids_prefix: str = "", augment: bool = True) -> None:
23
+ self.tokenizer = tokenizer
24
+ self.max_seq_len = max_seq_len
25
+ self.stride = stride or max_seq_len * 3 // 4
26
+ self.augment = augment
27
+
28
+ text_file = os.path.join(data_dir, f"{split}.txt")
29
+ id_prefix = f"{ids_prefix}_" if ids_prefix else ""
30
+ ids_file = os.path.join(data_dir, f"{split}_{id_prefix}ids.npy")
31
+
32
+ if not os.path.exists(text_file):
33
+ raise FileNotFoundError(
34
+ f"Data file not found: {text_file}\n"
35
+ f"Please run data_tools/build_dataset.py first to prepare the data."
36
+ )
37
+
38
+ if os.path.exists(ids_file):
39
+ print(f"Loading pre-tokenized {split} data from {ids_file}...")
40
+ self.all_ids = np.load(ids_file, mmap_mode='r')
41
+ self.total_tokens = len(self.all_ids)
42
+ print(f"Loaded {split} data: {self.total_tokens} tokens")
43
+ else:
44
+ import torch.distributed as dist
45
+ rank = dist.get_rank() if dist.is_available() and dist.is_initialized() else 0
46
+ if rank == 0:
47
+ with open(text_file, 'r', encoding='utf-8') as f:
48
+ text = f.read() if max_chars is None else f.read(max_chars)
49
+ total_chars = len(text)
50
+
51
+ chunk_size = 256 * 1024
52
+ all_ids: list[int] = []
53
+ n_chunks = (total_chars + chunk_size - 1) // chunk_size
54
+
55
+ print(f"Tokenizing {split} data ({total_chars/1e9:.2f}B chars, "
56
+ f"{n_chunks} chunks)...")
57
+ sys.stdout.flush()
58
+ status_path = os.path.join(data_dir, f".tokenizing_{split}")
59
+ for i in tqdm(range(0, total_chars, chunk_size), desc=f" {split}",
60
+ file=sys.stdout):
61
+ chunk = text[i:i + chunk_size]
62
+ ids = tokenizer.encode(chunk, add_bos=False, add_eos=False)
63
+ all_ids.extend(ids)
64
+ if (i // chunk_size) % 10 == 0:
65
+ with open(status_path, "w") as sf:
66
+ sf.write(f"{i+chunk_size}/{total_chars}")
67
+
68
+ print(f" Done: {len(all_ids)} tokens")
69
+
70
+ ids_array = np.array(all_ids, dtype=np.uint32)
71
+ np.save(ids_file, ids_array)
72
+ del all_ids, text
73
+ print(f" Saved to {ids_file}")
74
+
75
+ try:
76
+ os.remove(status_path)
77
+ except OSError:
78
+ pass
79
+
80
+ if dist.is_available() and dist.is_initialized():
81
+ dist.barrier()
82
+
83
+ self.all_ids = np.load(ids_file, mmap_mode='r')
84
+ self.total_tokens = len(self.all_ids)
85
+ print(f"Loaded {split} data: {self.total_tokens} tokens")
86
+
87
+ self.num_samples = max(0, (self.total_tokens - self.max_seq_len - 1) // self.stride + 1)
88
+ if self.num_samples == 0:
89
+ print(f"*** WARNING: 0 samples for {split}! "
90
+ f"total_tokens={self.total_tokens}, max_seq_len={self.max_seq_len}. "
91
+ f"Data may be too small or max_seq_len too large.")
92
+ print(f"Created {self.num_samples} samples for {split}")
93
+
94
+ def __len__(self) -> int:
95
+ return self.num_samples
96
+
97
+ def __getitem__(self, idx: int) -> torch.Tensor:
98
+ start = idx * self.stride
99
+ end = start + self.max_seq_len + 1
100
+ ids = self.all_ids[start:end].astype(np.int64)
101
+ tensor = torch.from_numpy(ids)
102
+
103
+ if self.augment and random.random() < 0.10:
104
+ min_len = self.max_seq_len // 2
105
+ trunc = random.randint(min_len, self.max_seq_len)
106
+ tensor = tensor[:trunc]
107
+
108
+ return tensor
109
+
110
+
111
+ def collate_fn(batch: List[torch.Tensor], pad_id: int) -> Tuple[torch.Tensor, torch.Tensor]:
112
+ """Padding 到 batch 内最大长度,右移一位拆分为 input 和 target"""
113
+ max_len = max(len(sample) for sample in batch)
114
+
115
+ padded = []
116
+ for sample in batch:
117
+ if len(sample) < max_len:
118
+ padding = torch.full((max_len - len(sample),), pad_id, dtype=torch.long)
119
+ sample = torch.cat([sample, padding])
120
+ padded.append(sample)
121
+
122
+ batch_tensor = torch.stack(padded)
123
+
124
+ input_ids = batch_tensor[:, :-1]
125
+ target_ids = batch_tensor[:, 1:]
126
+
127
+ return input_ids, target_ids
@@ -0,0 +1,11 @@
1
+ """GleamLM 统一评估框架 — PPL / 知识探针 / CEVAL / CMMLU"""
2
+
3
+ from .ppl import evaluate_ppl, evaluate_multiple, PPLResult
4
+ from .knowledge import evaluate_knowledge, KnowledgeResult
5
+ from .benchmark import evaluate_ceval, evaluate_cmmlu, BenchmarkResult
6
+
7
+ __all__ = [
8
+ "evaluate_ppl", "evaluate_multiple", "PPLResult",
9
+ "evaluate_knowledge", "KnowledgeResult",
10
+ "evaluate_ceval", "evaluate_cmmlu", "BenchmarkResult",
11
+ ]
@@ -0,0 +1,185 @@
1
+ """基准测试评估 — CEVAL / CMMLU / 多选题"""
2
+ from __future__ import annotations
3
+
4
+ import os, json, re
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+ from gleamlm.utils.torch_utils import safe_autocast
12
+ from gleamlm.models.model import GleamLMModel
13
+ from gleamlm.tokenizer.tokenizer import BBPETokenizer
14
+
15
+
16
+ @dataclass
17
+ class BenchmarkResult:
18
+ """基准测试结果"""
19
+ name: str # ceval / cmmlu
20
+ accuracy: float
21
+ total: int
22
+ correct: int
23
+ subject_scores: Dict[str, float] = field(default_factory=dict)
24
+ extra: Dict = field(default_factory=dict)
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ "benchmark": self.name,
29
+ "accuracy": round(self.accuracy, 4),
30
+ "total": self.total,
31
+ "correct": self.correct,
32
+ "subjects": self.subject_scores,
33
+ **self.extra,
34
+ }
35
+
36
+ def __repr__(self) -> str:
37
+ return (f"BenchmarkResult({self.name}: "
38
+ f"acc={self.accuracy:.2%}, {self.correct}/{self.total})")
39
+
40
+
41
+ def _mc_generate(model: GleamLMModel, tokenizer: BBPETokenizer,
42
+ prompt: str, choices: List[str],
43
+ device: str) -> str:
44
+ """多选题生成:预填充 prompt KV Cache,仅对选项部分增量推理。
45
+
46
+ 将 prompt 预填充一次得到 KV Cache,然后每个选项仅输入选项 token
47
+ 并复用 Cache,避免重复计算 prompt 部分。
48
+ """
49
+ prompt_ids = tokenizer.encode(prompt)
50
+ prompt_t = torch.tensor([prompt_ids], device=device)
51
+
52
+ with torch.no_grad(), safe_autocast():
53
+ _, prompt_kv = model(prompt_t)
54
+
55
+ choice_scores = []
56
+
57
+ for choice in choices:
58
+ choice_ids = tokenizer.encode(choice)
59
+ if not choice_ids:
60
+ choice_scores.append(float('-inf'))
61
+ continue
62
+
63
+ choice_t = torch.tensor([choice_ids], device=device)
64
+
65
+ with torch.no_grad(), safe_autocast():
66
+ logits, _ = model(choice_t, past_kv_list=prompt_kv)
67
+
68
+ total_log_prob = 0.0
69
+ for i, token_id in enumerate(choice_ids):
70
+ pred_logits = logits[0, i, :]
71
+ log_prob = torch.nn.functional.log_softmax(pred_logits, dim=-1)[token_id].item()
72
+ total_log_prob += log_prob
73
+
74
+ avg_log_prob = total_log_prob / len(choice_ids)
75
+ choice_scores.append(avg_log_prob)
76
+
77
+ best_idx = choice_scores.index(max(choice_scores))
78
+ return chr(ord('A') + best_idx) if best_idx < 4 else str(best_idx)
79
+
80
+
81
+ def _load_ceval(data_dir: str) -> List[Dict]:
82
+ """加载 CEVAL 数据集。支持单文件 ceval.json 或按学科拆分。"""
83
+ ceval_file = os.path.join(data_dir, "ceval.json")
84
+ if os.path.exists(ceval_file):
85
+ with open(ceval_file, 'r', encoding='utf-8') as f:
86
+ return json.load(f)
87
+
88
+ # 按学科拆分:ceval/high_school_math.json, etc.
89
+ items = []
90
+ for fname in sorted(os.listdir(data_dir)):
91
+ if fname.endswith('.json') and fname != 'ceval.json':
92
+ path = os.path.join(data_dir, fname)
93
+ with open(path, 'r', encoding='utf-8') as f:
94
+ subject_items = json.load(f)
95
+ subject = os.path.splitext(fname)[0]
96
+ for item in subject_items:
97
+ item.setdefault('subject', subject)
98
+ items.extend(subject_items)
99
+ return items
100
+
101
+
102
+ def _build_prompt(item: Dict) -> str:
103
+ """构建多选题 prompt: 问题 + A/B/C/D 选项"""
104
+ question = item.get('question', '')
105
+ choices = []
106
+ for key in ['A', 'B', 'C', 'D']:
107
+ if key in item:
108
+ choices.append(item[key])
109
+
110
+ prompt = f"{question}\n"
111
+ for i, choice in enumerate(choices):
112
+ prompt += f"{chr(ord('A') + i)}. {choice}\n"
113
+ prompt += "答案:"
114
+ return prompt
115
+
116
+
117
+ def evaluate_ceval(model: GleamLMModel, tokenizer: BBPETokenizer,
118
+ data_dir: str, device: str = "cuda",
119
+ subjects: Optional[List[str]] = None,
120
+ max_samples: Optional[int] = None,
121
+ verbose: bool = True) -> BenchmarkResult:
122
+ """CEVAL 中文综合能力评估。
123
+
124
+ 数据集格式: [{question, A, B, C, D, answer, subject}, ...]
125
+ """
126
+ items = _load_ceval(data_dir)
127
+ if not items:
128
+ raise FileNotFoundError(f"No CEVAL data found in {data_dir}")
129
+
130
+ if subjects:
131
+ items = [it for it in items if it.get('subject', '') in subjects]
132
+ if max_samples:
133
+ items = items[:max_samples]
134
+
135
+ model.eval()
136
+ subject_correct = {}
137
+ subject_total = {}
138
+ total_correct = 0
139
+
140
+ for item in items:
141
+ subject = item.get('subject', 'general')
142
+ choices = [item.get(k, '') for k in ['A', 'B', 'C', 'D'] if k in item]
143
+ prompt = _build_prompt(item)
144
+ predicted = _mc_generate(model, tokenizer, prompt, choices, device)
145
+ expected = item.get('answer', '')
146
+
147
+ correct = predicted == expected
148
+ if correct:
149
+ total_correct += 1
150
+ subject_correct[subject] = subject_correct.get(subject, 0) + 1
151
+ subject_total[subject] = subject_total.get(subject, 0) + 1
152
+
153
+ if verbose and len(items) <= 20:
154
+ mark = "✓" if correct else "✗"
155
+ print(f" [{mark}] {subject}: pred={predicted}, ans={expected} | {item.get('question', '')[:50]}...")
156
+
157
+ # 计算各学科分数
158
+ subject_scores = {}
159
+ for subj in subject_total:
160
+ subject_scores[subj] = round(subject_correct.get(subj, 0) / max(1, subject_total[subj]), 4)
161
+
162
+ accuracy = total_correct / max(1, len(items))
163
+ result = BenchmarkResult(
164
+ name="ceval",
165
+ accuracy=accuracy,
166
+ total=len(items),
167
+ correct=total_correct,
168
+ subject_scores=subject_scores,
169
+ )
170
+
171
+ if verbose:
172
+ print(f"\nCEVAL: {result.correct}/{result.total} = {accuracy:.2%}")
173
+ if len(subject_scores) > 1:
174
+ for subj, acc in sorted(subject_scores.items()):
175
+ print(f" {subj}: {acc:.2%}")
176
+ return result
177
+
178
+
179
+ def evaluate_cmmlu(model: GleamLMModel, tokenizer: BBPETokenizer,
180
+ data_dir: str, device: str = "cuda",
181
+ **kwargs: Any) -> BenchmarkResult:
182
+ """CMMLU 评估 — 同 CEVAL 格式,可直接复用。"""
183
+ result = evaluate_ceval(model, tokenizer, data_dir, device, **kwargs)
184
+ result.name = "cmmlu"
185
+ return result
@@ -0,0 +1,212 @@
1
+ """知识探针评估 — 填空测试 + 实体一致性检查
2
+ """
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ import torch
8
+ import random
9
+ from dataclasses import dataclass, field
10
+
11
+ from gleamlm.utils.torch_utils import safe_autocast
12
+ from gleamlm.models.model import GleamLMModel
13
+ from gleamlm.tokenizer.tokenizer import BBPETokenizer
14
+ from gleamlm.inference.sampler import sample_token
15
+
16
+
17
+ @dataclass
18
+ class KnowledgeResult:
19
+ correct: int = 0
20
+ wrong: int = 0
21
+ hallucination: int = 0
22
+ detailed: List[Dict] = field(default_factory=list)
23
+ entity_consistency: Dict[str, int] = field(default_factory=dict)
24
+
25
+ @property
26
+ def total(self):
27
+ return self.correct + self.wrong + self.hallucination
28
+
29
+ @property
30
+ def accuracy(self):
31
+ return self.correct / max(1, self.total)
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ return {
35
+ "accuracy": round(self.accuracy, 4),
36
+ "correct": self.correct,
37
+ "wrong": self.wrong,
38
+ "hallucination": self.hallucination,
39
+ "total": self.total,
40
+ "entity_consistency": self.entity_consistency,
41
+ "n_consistent": sum(1 for v in self.entity_consistency.values() if v >= 2),
42
+ }
43
+
44
+ def __repr__(self) -> str:
45
+ return (f"KnowledgeResult(acc={self.accuracy:.2%}, "
46
+ f"{self.correct}/{self.total} correct, "
47
+ f"{self.hallucination} hallucinations)")
48
+
49
+
50
+ # 默认知识探针池
51
+ DEFAULT_FACT_PROMPTS = [
52
+ ("世界上最高的山峰是", "珠穆朗玛峰"),
53
+ ("水的化学式是", "H2O"),
54
+ ("中国的首都是", "北京"),
55
+ ("爱因斯坦提出了", "相对论"),
56
+ ("光的速度大约是每秒", "30万公里"),
57
+ ("地球绕着什么转", "太阳"),
58
+ ("人类登上月球是在", "1969"),
59
+ ("DNA的全称是", "脱氧核糖核酸"),
60
+ ("第一个进入太空的人是", "加加林"),
61
+ ("太阳系有几大行星", "八"),
62
+ ("诺贝尔奖的创立者是", "诺贝尔"),
63
+ ("光合作用需要", "阳光"),
64
+ ("地球的卫星是", "月球"),
65
+ ("世界上最大的海洋是", "太平洋"),
66
+ ("一年有多少天", "365"),
67
+ ("人体最大的器官是", "皮肤"),
68
+ ("鲨珑GleamLM是什么", "轻量级开源对话模型"),
69
+ ]
70
+
71
+
72
+ DEFAULT_ENTITY_PROBES = {
73
+ "北京": ["中国的首都是哪里", "天安门在哪个城市", "故宫位于"],
74
+ "光速": ["光的速度是多少", "真空中最快的是什么"],
75
+ "爱因斯坦": ["爱因斯坦提出了什么", "相对论是谁提出的", "E=mc^2是谁的公式"],
76
+ "太阳": ["太阳是什么", "地球绕着什么转", "离我们最近的恒星是"],
77
+ "DNA": ["DNA是什么", "遗传物质叫什么"],
78
+ "诺贝尔": ["诺贝尔奖是谁设立的", "诺贝尔发明了什么"],
79
+ "太平洋": ["最大的海洋", "太平洋在哪"],
80
+ "月球": ["月球的别称", "地球的卫星是什么"],
81
+ }
82
+
83
+
84
+ def _simple_generate(model: GleamLMModel, tokenizer: BBPETokenizer,
85
+ prompt: str, device: str,
86
+ max_new_tokens: int = 64, temperature: float = 0.7,
87
+ top_k: int = 50) -> str:
88
+ """独立生成函数 — 使用 KV Cache 逐 token 增量推理。
89
+
90
+ 首步预填充 prompt,后续每步仅输入新 token 并复用 KV Cache,
91
+ RoPE 通过 offset 自动递增位置编码。
92
+ """
93
+ prompt_ids = tokenizer.encode(prompt)
94
+ input_ids = torch.tensor([prompt_ids], device=device)
95
+ generated_ids = prompt_ids.copy()
96
+ past_kv = None
97
+
98
+ with torch.no_grad():
99
+ for _ in range(max_new_tokens):
100
+ with safe_autocast():
101
+ logits, past_kv = model(input_ids, past_kv_list=past_kv)
102
+ next_token = sample_token(
103
+ logits[:, -1, :],
104
+ temperature=temperature,
105
+ top_k=top_k,
106
+ top_p=0.0,
107
+ generated_ids=generated_ids,
108
+ )
109
+ token_id = next_token.item()
110
+ if token_id in {tokenizer.eos_id, tokenizer.special_tokens.get("<|im_end|>")}:
111
+ break
112
+ generated_ids.append(token_id)
113
+ input_ids = torch.tensor([[token_id]], device=device)
114
+
115
+ full = tokenizer.decode(generated_ids)
116
+ if full.startswith(prompt):
117
+ generated = full[len(prompt):].strip()
118
+ else:
119
+ generated = full.strip()
120
+ return generated[:200]
121
+
122
+
123
+ def _check_answer(generated: str, expected: str,
124
+ hallucination_keywords: Optional[List[str]] = None) -> str:
125
+ """检查生成文本是否包含预期答案。返回 CORRECT / WRONG / HALLUCINATION"""
126
+ gen_lower = generated.lower().replace(" ", "")
127
+ answers = [a.strip().lower() for a in expected.split(",")]
128
+ for ans in answers:
129
+ if ans in gen_lower:
130
+ return "CORRECT"
131
+ if hallucination_keywords:
132
+ for hw in hallucination_keywords:
133
+ if hw in generated:
134
+ return "HALLUCINATION"
135
+ return "WRONG"
136
+
137
+
138
+ def evaluate_knowledge(model: GleamLMModel, tokenizer: BBPETokenizer,
139
+ device: str = "cuda",
140
+ fact_prompts: Optional[List[Tuple[str, str]]] = None,
141
+ entity_probes: Optional[Dict[str, List[str]]] = None,
142
+ hallucination_keywords: Optional[List[str]] = None,
143
+ max_new_tokens: int = 64,
144
+ temperature: float = 0.7,
145
+ verbose: bool = True) -> KnowledgeResult:
146
+ """运行知识探针评估。"""
147
+ if fact_prompts is None:
148
+ fact_prompts = DEFAULT_FACT_PROMPTS
149
+ if entity_probes is None:
150
+ entity_probes = DEFAULT_ENTITY_PROBES
151
+
152
+ model.eval()
153
+ result = KnowledgeResult()
154
+
155
+ # A1: 事实填空
156
+ if verbose:
157
+ print(f"\n{'='*60}")
158
+ print(f"A1: FACT FILL-IN ({len(fact_prompts)} prompts)")
159
+ print(f"{'='*60}")
160
+
161
+ for prompt, expected in fact_prompts:
162
+ generated = _simple_generate(model, tokenizer, prompt, device,
163
+ max_new_tokens, temperature)
164
+ label = _check_answer(generated, expected, hallucination_keywords)
165
+
166
+ if label == "CORRECT":
167
+ result.correct += 1
168
+ elif label == "HALLUCINATION":
169
+ result.hallucination += 1
170
+ else:
171
+ result.wrong += 1
172
+
173
+ result.detailed.append({
174
+ "prompt": prompt, "expected": expected,
175
+ "generated": generated[:100], "result": label
176
+ })
177
+
178
+ if verbose:
179
+ symbols = {"CORRECT": "OK", "HALLUCINATION": "HALU", "WRONG": "WRONG"}
180
+ print(f" [{symbols[label]:>4}] {prompt} → {generated[:60]}...")
181
+
182
+ if verbose:
183
+ print(f"\n Accuracy: {result.correct}/{result.total} ({result.accuracy:.1%}), "
184
+ f"{result.hallucination} hallucinations")
185
+
186
+ # A2: 实体探针
187
+ if verbose:
188
+ print(f"\n{'='*60}")
189
+ print(f"A2: ENTITY PROBE ({len(entity_probes)} entities)")
190
+ print(f"{'='*60}")
191
+
192
+ for entity, questions in entity_probes.items():
193
+ correct = 0
194
+ for q in questions:
195
+ generated = _simple_generate(model, tokenizer, q, device,
196
+ max_new_tokens, temperature)
197
+ if entity.lower() in generated.lower():
198
+ correct += 1
199
+ result.entity_consistency[entity] = correct
200
+
201
+ if verbose:
202
+ icon = "***" if correct == len(questions) else ("*" if correct > 0 else " ")
203
+ print(f" [{icon}] {entity}: {correct}/{len(questions)}")
204
+
205
+ if verbose:
206
+ consistent = sum(
207
+ 1 for entity, v in result.entity_consistency.items()
208
+ if entity in entity_probes and v >= len(entity_probes[entity]) * 0.67
209
+ )
210
+ print(f"\n Consistent entities: {consistent}/{len(entity_probes)}")
211
+
212
+ return result