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 +60 -0
- gleamlm/dataset/__init__.py +1 -0
- gleamlm/dataset/dataset.py +127 -0
- gleamlm/evaluation/__init__.py +11 -0
- gleamlm/evaluation/benchmark.py +185 -0
- gleamlm/evaluation/knowledge.py +212 -0
- gleamlm/evaluation/ppl.py +129 -0
- gleamlm/evaluation/runner.py +151 -0
- gleamlm/inference/__init__.py +3 -0
- gleamlm/inference/generate.py +67 -0
- gleamlm/inference/sampler.py +45 -0
- gleamlm/inference/streamer.py +101 -0
- gleamlm/models/__init__.py +1 -0
- gleamlm/models/config.py +21 -0
- gleamlm/models/model.py +286 -0
- gleamlm/tokenizer/__init__.py +1 -0
- gleamlm/tokenizer/checkpoints/bbpe_12k/bbpe_tokenizer.json +58711 -0
- gleamlm/tokenizer/tokenizer.py +482 -0
- gleamlm/types.py +7 -0
- gleamlm/utils/__init__.py +2 -0
- gleamlm/utils/config.py +261 -0
- gleamlm/utils/logging.py +20 -0
- gleamlm/utils/torch_utils.py +48 -0
- gleamlm-0.2.0.dist-info/METADATA +510 -0
- gleamlm-0.2.0.dist-info/RECORD +30 -0
- gleamlm-0.2.0.dist-info/WHEEL +5 -0
- gleamlm-0.2.0.dist-info/licenses/LICENSE +201 -0
- gleamlm-0.2.0.dist-info/scm_file_list.json +109 -0
- gleamlm-0.2.0.dist-info/scm_version.json +8 -0
- gleamlm-0.2.0.dist-info/top_level.txt +1 -0
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
|