minimind/scripts/eval_compare.py

182 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
GQA vs MLA 模型对比评估脚本
- Perplexity(在 SFT 数据子集上)
- 指定题目人工对比
"""
import torch, json, argparse, random, sys, os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from model.model_minimind_mla import MiniMindMLAConfig, MiniMindMLAForCausalLM
from transformers import AutoTokenizer
TEST_QUESTIONS = [
"请用Python写一个计算斐波那契数列的函数",
"解释一下机器学习和深度学习的区别",
"推荐三本经典的科幻小说,并说明理由",
"如果我想在北京开一家小餐馆,需要注意哪些方面",
"请解释什么是光合作用",
"比较一下燃油车和电动车的优缺点",
"写一首关于秋天的五言绝句",
"如何给孩子解释为什么天空是蓝色的",
"请用简单的语言说明什么是API",
"介绍一道你最喜欢的菜的做法",
"什么是区块链技术,它可以应用在哪些领域",
"如何有效地管理时间",
"请翻译:Artificial intelligence is transforming every industry",
"三个开关分别控制三个灯泡,你只能进房间一次,如何确定对应关系",
"请用200字以内介绍中国的四大发明",
]
def load_model(weight_path, config, device):
"""加载模型权重"""
if isinstance(config, MiniMindMLAConfig):
model = MiniMindMLAForCausalLM(config)
else:
model = MiniMindForCausalLM(config)
model = model.to(device)
if weight_path and os.path.exists(weight_path):
state_dict = torch.load(weight_path, map_location=device, weights_only=True)
model.load_state_dict(state_dict, strict=False)
print(f" 加载权重: {weight_path}")
model.eval()
return model
def compute_ppl(model, tokenizer, data_path, device, max_samples=200, max_length=512):
"""在数据集子集上计算 Perplexity"""
print(f"\n 数据: {data_path} (最多 {max_samples} 条, max_len={max_length})")
try:
with open(data_path) as f:
lines = f.readlines()
except FileNotFoundError:
print(f" [WARN] 数据文件不存在,跳过 PPL")
return None
random.shuffle(lines)
total_loss = 0.0
total_tokens = 0
count = 0
for line in lines[:max_samples]:
item = json.loads(line)
# SFT 格式:有 conversations 字段
if "conversations" in item:
text = tokenizer.apply_chat_template(item["conversations"], tokenize=False, add_generation_prompt=False)
else:
text = item.get("text", "")
if not text.strip():
continue
tokens = tokenizer(text, truncation=True, max_length=max_length, return_tensors="pt")
input_ids = tokens["input_ids"].to(device)
with torch.no_grad():
output = model(input_ids, labels=input_ids)
loss = output.loss
if loss is not None:
total_loss += loss.item() * input_ids.size(1)
total_tokens += input_ids.size(1)
count += 1
if total_tokens == 0:
return None
avg_loss = total_loss / total_tokens
ppl = torch.exp(torch.tensor(avg_loss)).item()
print(f" 处理 {count} 条, 总 token {total_tokens}, loss={avg_loss:.4f}, ppl={ppl:.2f}")
return ppl
@torch.inference_mode()
def generate_answer(model, tokenizer, prompt, device, max_new_tokens=512):
"""生成回答"""
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
input_ids = tokenizer(text, return_tensors="pt")["input_ids"].to(device)
output = model.generate(
inputs=input_ids,
max_new_tokens=max_new_tokens,
use_cache=True,
)
response = tokenizer.decode(output[0][input_ids.size(1):], skip_special_tokens=True)
return response.strip()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu")
parser.add_argument("--gqa_weight", type=str, default="../out/full_sft_768.pth")
parser.add_argument("--mla_weight", type=str, default="../out/full_sft_768_mla.pth")
parser.add_argument("--tokenizer_path", type=str, default="../model")
parser.add_argument("--sft_data", type=str, default="../dataset/sft_t2t_mini.jsonl")
parser.add_argument("--ppl_samples", type=int, default=200, help="PPL 采样条数")
parser.add_argument("--max_new_tokens", type=int, default=256)
parser.add_argument("--skip_ppl", action="store_true", help="跳过 PPL 计算")
parser.add_argument("--skip_qna", action="store_true", help="跳过问答对比")
parser.add_argument("--only_ppl", action="store_true", help="只算 PPL")
args = parser.parse_args()
device = args.device
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_path)
print("=" * 80)
print(" GQA vs MLA 模型对比评估")
print("=" * 80)
# ── 1. 加载模型 ──
print("\n[1] 加载 GQA 模型...")
gqa_config = MiniMindConfig()
gqa_model = load_model(args.gqa_weight, gqa_config, device)
print("\n[2] 加载 MLA 模型...")
mla_config = MiniMindMLAConfig()
mla_model = load_model(args.mla_weight, mla_config, device)
# ── 2. PPL ──
if not args.skip_ppl:
print("\n" + "=" * 80)
print(" Perplexity 对比")
print("=" * 80)
print("\n[GQA]")
gqa_ppl = compute_ppl(gqa_model, tokenizer, args.sft_data, device, max_samples=args.ppl_samples)
print("\n[MLA]")
mla_ppl = compute_ppl(mla_model, tokenizer, args.sft_data, device, max_samples=args.ppl_samples)
if gqa_ppl and mla_ppl:
better = "GQA" if gqa_ppl < mla_ppl else "MLA"
print(f"\n 结果: GQA PPL={gqa_ppl:.2f} MLA PPL={mla_ppl:.2f} → {better} 更低({abs(gqa_ppl - mla_ppl):.2f})")
if args.only_ppl:
return
# ── 3. 问答对比 ──
if not args.skip_qna:
print("\n" + "=" * 80)
print(" 问答对比")
print("=" * 80)
for i, question in enumerate(TEST_QUESTIONS, 1):
print(f"\n{'─' * 80}")
print(f" [{i}/{len(TEST_QUESTIONS)}] {question}")
print(f"{'─' * 80}")
print(f"\n [GQA]:")
gqa_answer = generate_answer(gqa_model, tokenizer, question, device, max_new_tokens=args.max_new_tokens)
for line in gqa_answer.split('\n'):
print(f" {line}")
print(f"\n [MLA]:")
mla_answer = generate_answer(mla_model, tokenizer, question, device, max_new_tokens=args.max_new_tokens)
for line in mla_answer.split('\n'):
print(f" {line}")
print("\n" + "=" * 80)
print(" 评估完成")
print("=" * 80)
if __name__ == "__main__":
main()