verify.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. """
  2. LLM 有效秩谱收敛验证实验主流程
  3. 验证文档《基于 SVCCA 的数据集和模型评价.md》附录 C 中的实验设计
  4. 核心研究问题:
  5. Q1: 不同 LLM 在相同文本集上的谱有效秩是否趋同?
  6. Q2: 不仅有效秩,特征值分布的整体形状是否收敛?
  7. Q3: 收敛程度是否随文本领域(新闻/代码/对话/学术)变化?
  8. Q4: 模型规模越大,谱越接近某个"吸引子"?
  9. 实验类型:
  10. - MVP 实验:3 个模型 × 2 个领域,快速验证
  11. - 完整实验:7 个模型 × 6 个领域,全面验证
  12. - 规模效应实验:同家族不同规模模型对比
  13. """
  14. import torch
  15. import numpy as np
  16. import json
  17. import os
  18. from pathlib import Path
  19. from typing import Dict, List, Optional, Tuple, Any
  20. from dataclasses import dataclass, field
  21. from datetime import datetime
  22. # 导入核心模块
  23. import sys
  24. sys.path.insert(0, str(Path(__file__).parent.parent))
  25. from model.spectrum import (
  26. compute_gram_spectrum,
  27. compute_spectrum_array,
  28. wasserstein1_distance,
  29. compute_spectrum_distance_matrix,
  30. spectrum_summary,
  31. effective_rank_from_eigvals,
  32. )
  33. from model.extractor import (
  34. RepresentationLoader,
  35. load_H,
  36. load_all_H,
  37. )
  38. from database.corpus import (
  39. CorpusLoader,
  40. load_texts,
  41. load_all_texts,
  42. list_domains,
  43. )
  44. @dataclass
  45. class ExperimentConfig:
  46. """实验配置"""
  47. # 模型配置
  48. model_names: List[str] = field(default_factory=list)
  49. # 数据集配置
  50. domains: List[str] = field(default_factory=list)
  51. # 实验参数
  52. limit_per_domain: Optional[int] = None # 每个领域限制多少文本
  53. # 计算参数
  54. normalize_method: str = "per_sample_l2"
  55. # 输出配置
  56. output_dir: str = "experiments/output"
  57. @classmethod
  58. def mvp_config(cls) -> "ExperimentConfig":
  59. """MVP 实验配置(最小可行实验)"""
  60. return cls(
  61. model_names=["qwen_7b", "mistral_7b", "llama3_8b"],
  62. domains=["news_en", "news_zh", "academic", "code", "dialogue", "literature"],
  63. limit_per_domain=500,
  64. output_dir="experiments/output/mvp",
  65. )
  66. @classmethod
  67. def glue_config(cls, limit_per_domain: Optional[int] = 5000) -> "ExperimentConfig":
  68. """GLUE Benchmark 完整数据集配置"""
  69. return cls(
  70. model_names=["qwen_7b", "mistral_7b", "llama3_8b"],
  71. domains=[
  72. "glue_mnli", # ~285K - 多段落推理
  73. "glue_qnli", # ~98K - 问答推理
  74. "glue_sst2", # ~24K - 情感分析
  75. "glue_stsb", # ~3.5K - 语义相似度
  76. "glue_cola", # ~2K - 语法判断
  77. ],
  78. limit_per_domain=limit_per_domain,
  79. output_dir="experiments/output/glue",
  80. )
  81. @classmethod
  82. def full_config(cls) -> "ExperimentConfig":
  83. """完整实验配置"""
  84. return cls(
  85. model_names=[
  86. "qwen_7b", "mistral_7b",
  87. "gemma_9b", "deepseek_16b", "internlm_7b"
  88. ],
  89. domains=[
  90. "news_en", "news_zh", "academic",
  91. "code", "dialogue", "literature"
  92. ],
  93. limit_per_domain=None,
  94. output_dir="experiments/output/full",
  95. )
  96. @dataclass
  97. class ModelSpectrumResult:
  98. """单个模型的谱计算结果"""
  99. model_name: str
  100. domain: str
  101. r_eff: float
  102. prob: np.ndarray
  103. eigvals: np.ndarray
  104. d: int # 特征维度
  105. N: int # 样本数
  106. @dataclass
  107. class ExperimentResult:
  108. """完整实验结果"""
  109. config: ExperimentConfig
  110. timestamp: str
  111. results: Dict[str, ModelSpectrumResult] # key = "{model}__{domain}"
  112. # 距离矩阵
  113. distance_matrices: Dict[str, np.ndarray] # key = domain, value = n×n W1 距离矩阵
  114. model_names: List[str]
  115. # 汇总统计
  116. summary: Dict[str, Any] = field(default_factory=dict)
  117. def save(self, output_dir: str) -> str:
  118. """保存实验结果"""
  119. output_path = Path(output_dir)
  120. output_path.mkdir(parents=True, exist_ok=True)
  121. # 保存配置
  122. with open(output_path / "config.json", "w") as f:
  123. json.dump({
  124. "model_names": self.config.model_names,
  125. "domains": self.config.domains,
  126. "limit_per_domain": self.config.limit_per_domain,
  127. "normalize_method": self.config.normalize_method,
  128. }, f, indent=2)
  129. # 保存谱结果
  130. spectrum_results = {}
  131. for key, result in self.results.items():
  132. spectrum_results[key] = {
  133. "model_name": result.model_name,
  134. "domain": result.domain,
  135. "r_eff": result.r_eff,
  136. "d": result.d,
  137. "N": result.N,
  138. "prob": result.prob.tolist(),
  139. "eigvals": result.eigvals.tolist(),
  140. }
  141. with open(output_path / "spectrum_results.json", "w") as f:
  142. json.dump(spectrum_results, f, indent=2)
  143. # 保存距离矩阵
  144. dist_dir = output_path / "distance_matrices"
  145. dist_dir.mkdir(exist_ok=True)
  146. for domain, matrix in self.distance_matrices.items():
  147. np.save(dist_dir / f"{domain}_w1_matrix.npy", matrix)
  148. # 保存汇总统计
  149. with open(output_path / "summary.json", "w") as f:
  150. json.dump(self.summary, f, indent=2, default=str)
  151. # 保存元信息
  152. with open(output_path / "metadata.json", "w") as f:
  153. json.dump({
  154. "timestamp": self.timestamp,
  155. "num_models": len(self.model_names),
  156. "num_domains": len(self.config.domains),
  157. }, f, indent=2)
  158. return str(output_path)
  159. # 导入 numpy(在 dataclass 之后)
  160. import numpy as np
  161. class VerificationExperiment:
  162. """
  163. LLM 有效秩谱收敛验证实验
  164. """
  165. def __init__(self, config: ExperimentConfig):
  166. self.config = config
  167. self.rep_loader = RepresentationLoader("model/representations")
  168. self.corpus_loader = CorpusLoader("database/corpus")
  169. # 结果存储
  170. self.results: Dict[str, ModelSpectrumResult] = {}
  171. self.distance_matrices: Dict[str, np.ndarray] = {}
  172. def run(self) -> ExperimentResult:
  173. """运行完整实验流程"""
  174. timestamp = datetime.now().isoformat()
  175. print(f"=== LLM 有效秩谱收敛验证实验 ===")
  176. print(f"时间:{timestamp}")
  177. print(f"模型数:{len(self.config.model_names)}")
  178. print(f"领域数:{len(self.config.domains)}")
  179. print(f"归一化方法:{self.config.normalize_method}")
  180. print()
  181. # Step 1: 计算每个模型的谱
  182. self._compute_spectra()
  183. # Step 2: 计算每个领域内的跨模型谱距离矩阵
  184. self._compute_distance_matrices()
  185. # Step 3: 生成汇总统计
  186. summary = self._generate_summary()
  187. return ExperimentResult(
  188. config=self.config,
  189. timestamp=timestamp,
  190. results=self.results,
  191. distance_matrices=self.distance_matrices,
  192. model_names=self.config.model_names,
  193. summary=summary,
  194. )
  195. def _compute_spectra(self) -> None:
  196. """Step 1: 计算每个模型的谱有效秩"""
  197. print("Step 1: 计算谱有效秩...")
  198. for model_name in self.config.model_names:
  199. for domain in self.config.domains:
  200. key = f"{model_name}__{domain}"
  201. print(f" 处理:{key}")
  202. # 加载表示矩阵
  203. H = self.rep_loader.load_representation(model_name, domain)
  204. # 计算谱
  205. r_eff, prob = compute_gram_spectrum(
  206. H,
  207. normalize=self.config.normalize_method,
  208. return_eigvals=True
  209. )
  210. # 计算完整谱信息
  211. eigvals, _ = compute_spectrum_array(H, normalize=self.config.normalize_method)
  212. # 存储结果
  213. self.results[key] = ModelSpectrumResult(
  214. model_name=model_name,
  215. domain=domain,
  216. r_eff=r_eff,
  217. prob=prob,
  218. eigvals=eigvals,
  219. d=H.shape[0],
  220. N=H.shape[1],
  221. )
  222. print(f" r_eff = {r_eff:.4f}, d = {H.shape[0]}, N = {H.shape[1]}")
  223. print()
  224. def _compute_distance_matrices(self) -> None:
  225. """Step 2: 计算每个领域内的跨模型谱距离矩阵"""
  226. print("Step 2: 计算谱距离矩阵...")
  227. for domain in self.config.domains:
  228. print(f" 领域:{domain}")
  229. # 提取该领域所有模型的谱分布
  230. prob_dict = {}
  231. for model_name in self.config.model_names:
  232. key = f"{model_name}__{domain}"
  233. if key in self.results:
  234. prob_dict[model_name] = self.results[key].prob
  235. # 计算距离矩阵
  236. if len(prob_dict) >= 2:
  237. dist_matrix, model_names = compute_spectrum_distance_matrix(
  238. prob_dict, distance_fn="wasserstein1"
  239. )
  240. self.distance_matrices[domain] = dist_matrix
  241. print(f" 距离矩阵形状:{dist_matrix.shape}")
  242. print(f" 平均距离:{dist_matrix[np.triu_indices(len(model_names), k=1)].mean():.4f}")
  243. else:
  244. print(f" 跳过(模型数不足)")
  245. print()
  246. def _generate_summary(self) -> Dict[str, Any]:
  247. """Step 3: 生成汇总统计"""
  248. print("Step 3: 生成汇总统计...")
  249. summary = {
  250. "spectrum_statistics": {},
  251. "convergence_analysis": {},
  252. "per_domain": {},
  253. }
  254. # 谱统计
  255. for model_name in self.config.model_names:
  256. model_results = [
  257. r for key, r in self.results.items()
  258. if r.model_name == model_name
  259. ]
  260. if model_results:
  261. r_effs = [r.r_eff for r in model_results]
  262. summary["spectrum_statistics"][model_name] = {
  263. "r_eff_mean": float(np.mean(r_effs)),
  264. "r_eff_std": float(np.std(r_effs)),
  265. "r_eff_min": float(np.min(r_effs)),
  266. "r_eff_max": float(np.max(r_effs)),
  267. }
  268. # 跨模型收敛分析
  269. for domain, dist_matrix in self.distance_matrices.items():
  270. n = dist_matrix.shape[0]
  271. if n >= 2:
  272. upper_tri = dist_matrix[np.triu_indices(n, k=1)]
  273. summary["per_domain"][domain] = {
  274. "mean_w1_distance": float(upper_tri.mean()),
  275. "std_w1_distance": float(upper_tri.std()),
  276. "max_w1_distance": float(upper_tri.max()),
  277. "min_w1_distance": float(upper_tri.min()),
  278. }
  279. # 整体收敛判断
  280. all_distances = []
  281. for dist_matrix in self.distance_matrices.values():
  282. n = dist_matrix.shape[0]
  283. if n >= 2:
  284. all_distances.extend(dist_matrix[np.triu_indices(n, k=1)].tolist())
  285. if all_distances:
  286. overall_mean = float(np.mean(all_distances))
  287. summary["convergence_analysis"] = {
  288. "overall_mean_w1_distance": overall_mean,
  289. "convergence_judgment": "收敛" if overall_mean < 0.05 else "部分收敛" if overall_mean < 0.2 else "发散",
  290. "num_domain_matrices": len(self.distance_matrices),
  291. }
  292. print(f" 整体平均 W1 距离:{summary['convergence_analysis'].get('overall_mean_w1_distance', 'N/A')}")
  293. print(f" 收敛判断:{summary['convergence_analysis'].get('convergence_judgment', 'N/A')}")
  294. print()
  295. return summary
  296. def run_mvp_experiment(
  297. output_dir: Optional[str] = None,
  298. ) -> ExperimentResult:
  299. """
  300. 运行 MVP 实验(最小可行实验)
  301. 配置:
  302. - 2 个模型:qwen_7b, mistral_7b
  303. - 6 个领域:news_en, news_zh, academic, code, dialogue, literature
  304. - 每个领域 500 条文本
  305. """
  306. config = ExperimentConfig.mvp_config()
  307. if output_dir:
  308. config.output_dir = output_dir
  309. experiment = VerificationExperiment(config)
  310. result = experiment.run()
  311. # 保存结果
  312. output_path = result.save(config.output_dir)
  313. print(f"结果已保存到:{output_path}")
  314. return result
  315. def run_full_experiment(
  316. output_dir: Optional[str] = None,
  317. ) -> ExperimentResult:
  318. """
  319. 运行完整实验
  320. 配置:
  321. - 7 个模型
  322. - 6 个领域
  323. - 无文本数量限制
  324. """
  325. config = ExperimentConfig.full_config()
  326. if output_dir:
  327. config.output_dir = output_dir
  328. experiment = VerificationExperiment(config)
  329. result = experiment.run()
  330. # 保存结果
  331. output_path = result.save(config.output_dir)
  332. print(f"结果已保存到:{output_path}")
  333. return result
  334. def run_scale_analysis(
  335. model_family: str = "llama",
  336. output_dir: str = "experiments/output/scale_analysis",
  337. ) -> Dict[str, Any]:
  338. """
  339. 运行规模效应分析
  340. 分析同一家族不同规模模型的谱收敛趋势
  341. """
  342. # 需要预先定义模型家族配置
  343. scale_configs = {
  344. "llama": [
  345. "llama_1b", "llama_3b", "llama_8b", "llama_70b"
  346. ],
  347. "qwen": [
  348. "qwen_0.5b", "qwen_1.5b", "qwen_3b", "qwen_7b", "qwen_72b"
  349. ],
  350. }
  351. if model_family not in scale_configs:
  352. raise ValueError(f"Unknown model family: {model_family}")
  353. model_names = scale_configs[model_family]
  354. domains = ["news_en", "academic"] # 简化版
  355. config = ExperimentConfig(
  356. model_names=model_names,
  357. domains=domains,
  358. output_dir=output_dir,
  359. )
  360. experiment = VerificationExperiment(config)
  361. result = experiment.run()
  362. # 分析规模与谱收敛的关系
  363. scale_analysis = {
  364. "model_family": model_family,
  365. "model_names": model_names,
  366. "r_eff_by_model": {},
  367. "cross_model_distances": {},
  368. }
  369. for model_name in model_names:
  370. model_results = [
  371. r for key, r in result.results.items()
  372. if r.model_name == model_name
  373. ]
  374. r_effs = [r.r_eff for r in model_results]
  375. scale_analysis["r_eff_by_model"][model_name] = {
  376. "mean": float(np.mean(r_effs)),
  377. "std": float(np.std(r_effs)),
  378. }
  379. for domain, dist_matrix in result.distance_matrices.items():
  380. scale_analysis["cross_model_distances"][domain] = {
  381. "matrix": dist_matrix.tolist(),
  382. "mean": float(dist_matrix[np.triu_indices(len(model_names), k=1)].mean()),
  383. }
  384. # 保存规模分析结果
  385. output_path = Path(output_dir)
  386. output_path.mkdir(parents=True, exist_ok=True)
  387. with open(output_path / "scale_analysis.json", "w") as f:
  388. json.dump(scale_analysis, f, indent=2)
  389. return scale_analysis
  390. def run_glue_experiment(
  391. output_dir: Optional[str] = None,
  392. limit_per_domain: Optional[int] = 5000,
  393. ) -> ExperimentResult:
  394. """
  395. 运行 GLUE Benchmark 实验
  396. 配置:
  397. - 3 个模型:qwen_7b, mistral_7b, llama3_8b
  398. - 5 个 GLUE 子集:MNLI, QNLI, SST-2, STS-B, CoLA
  399. - 每个子集限制 5000 条样本(默认)
  400. """
  401. config = ExperimentConfig.glue_config(limit_per_domain=limit_per_domain)
  402. if output_dir:
  403. config.output_dir = output_dir
  404. experiment = VerificationExperiment(config)
  405. result = experiment.run()
  406. # 保存结果
  407. output_path = result.save(config.output_dir)
  408. print(f"结果已保存到:{output_path}")
  409. return result
  410. # ============= 命令行入口 =============
  411. if __name__ == "__main__":
  412. import argparse
  413. parser = argparse.ArgumentParser(
  414. description="LLM 有效秩谱收敛验证实验"
  415. )
  416. parser.add_argument(
  417. "--mode",
  418. choices=["mvp", "full", "scale", "glue"],
  419. default="mvp",
  420. help="实验模式:mvp=最小可行实验,full=完整实验,scale=规模效应分析,glue=GLUE 基准实验"
  421. )
  422. parser.add_argument(
  423. "--output",
  424. type=str,
  425. default=None,
  426. help="输出目录(可选)"
  427. )
  428. parser.add_argument(
  429. "--model-family",
  430. type=str,
  431. default="llama",
  432. help="规模效应分析的模型家族(仅 scale 模式)"
  433. )
  434. parser.add_argument(
  435. "--limit",
  436. type=int,
  437. default=5000,
  438. help="GLUE 实验每个领域的样本数限制(仅 glue 模式)"
  439. )
  440. args = parser.parse_args()
  441. if args.mode == "mvp":
  442. run_mvp_experiment(args.output)
  443. elif args.mode == "full":
  444. run_full_experiment(args.output)
  445. elif args.mode == "scale":
  446. run_scale_analysis(args.model_family, args.output or "experiments/output/scale_analysis")