| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- #!/usr/bin/env python
- """
- 跨模型谱收敛实验流水线 - 方向 9 核心实验
- 功能:
- - 提取多个模型在同一文本集上的隐层表示
- - 计算跨模型谱稳定性指标 (RS_cross)
- - 计算谱 Platonic 距离矩阵 (SPD)
- - 逐层谱收敛轮廓分析 (LCP)
- 用法:
- # 运行完整 E1 实验
- python experiments/cross_model_convergence.py --experiment E1 --output-dir output/e1
- # 运行逐层分析
- python experiments/cross_model_convergence.py --experiment LCP --layers 10
- """
- import os
- import json
- import torch
- import numpy as np
- from pathlib import Path
- from typing import Dict, List, Tuple, Optional, Any
- from dataclasses import dataclass, asdict
- from datetime import datetime
- # 设置环境变量防止显存碎片化
- os.environ["PYTORCH_ALLOC_CONF"] = "expandable_segments:True"
- os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
- from transformers import AutoTokenizer, AutoModelForCausalLM
- from tqdm import tqdm
- # 导入谱计算模块
- import sys
- sys.path.insert(0, str(Path(__file__).parent.parent))
- from model.spectrum import compute_gram_spectrum, compute_spectrum_array, wasserstein1_distance
- from model.extractor import save_H, load_H, RepresentationLoader
- @dataclass
- class ExperimentConfig:
- """实验配置"""
- models: List[str]
- model_paths: Dict[str, str]
- datasets: List[str]
- output_dir: str
- batch_size: int = 16
- max_length: int = 512
- pooling: str = "mean"
- layer: int = -1 # -1 表示最后一层
- device: str = "cuda"
- @dataclass
- class ExperimentResult:
- """实验结果"""
- timestamp: str
- config: dict
- r_effs: Dict[str, Dict[str, float]] # {model: {dataset: r_eff}}
- rs_cross: Dict[str, float] # {dataset: RS_cross}
- spd_matrices: Dict[str, List[List[float]]] # {dataset: SPD_matrix}
- spectra: Dict[str, Dict[str, List[float]]] # {model: {dataset: spectrum}}
- class CrossModelConvergence:
- """跨模型谱收敛分析器"""
- def __init__(self, config: ExperimentConfig):
- self.config = config
- self.output_dir = Path(config.output_dir)
- self.output_dir.mkdir(parents=True, exist_ok=True)
- # 缓存加载的模型
- self.loaded_models: Dict[str, Tuple[AutoTokenizer, AutoModelForCausalLM]] = {}
- # 文本缓存
- self.texts_cache: Dict[str, List[str]] = {}
- def unload_all_models(self):
- """卸载所有模型并清理 GPU 缓存"""
- import gc
- self.loaded_models.clear()
- gc.collect()
- if torch.cuda.is_available():
- torch.cuda.empty_cache()
- def load_texts(self, dataset_name: str, limit: int = 500) -> List[str]:
- """加载数据集文本"""
- if dataset_name in self.texts_cache:
- return self.texts_cache[dataset_name][:limit]
- from database.corpus import CorpusLoader
- loader = CorpusLoader("database/corpus")
- try:
- texts = loader.load_texts(dataset_name, limit=limit)
- self.texts_cache[dataset_name] = texts
- print(f"加载 {dataset_name}: {len(texts)} 条文本")
- return texts
- except FileNotFoundError:
- print(f"警告:数据集 {dataset_name} 不存在")
- return []
- def load_model(self, model_name: str):
- """加载模型(带缓存)"""
- if model_name in self.loaded_models:
- return self.loaded_models[model_name]
- model_path = self.config.model_paths.get(
- model_name,
- f"model/weights/{model_name}"
- )
- print(f"加载模型:{model_name} (from {model_path})")
- tokenizer = AutoTokenizer.from_pretrained(
- model_path,
- trust_remote_code=True
- )
- # 设置 pad_token
- if tokenizer.pad_token is None:
- tokenizer.pad_token = tokenizer.eos_token
- if tokenizer.eos_token_id is None:
- tokenizer.eos_token_id = 50256 # 默认 EOS
- model = AutoModelForCausalLM.from_pretrained(
- model_path,
- torch_dtype=torch.float16,
- device_map="auto",
- output_hidden_states=True,
- trust_remote_code=True
- )
- model.eval()
- self.loaded_models[model_name] = (tokenizer, model)
- return tokenizer, model
- @torch.no_grad()
- def extract_hidden_states(
- self,
- model_name: str,
- texts: List[str],
- layer: int = -1
- ) -> torch.Tensor:
- """
- 提取隐层表示
- Returns:
- H: 表示矩阵 (d, N)
- """
- # 先清理缓存再加载新模型
- self.unload_all_models()
- tokenizer, model = self.load_model(model_name)
- all_features = []
- for i in range(0, len(texts), self.config.batch_size):
- batch = texts[i:i + self.config.batch_size]
- inputs = tokenizer(
- batch,
- return_tensors="pt",
- padding=True,
- truncation=True,
- max_length=self.config.max_length
- ).to(model.device)
- outputs = model(**inputs)
- hidden = outputs.hidden_states[layer] # (B, seq_len, d)
- attention_mask = inputs["attention_mask"]
- if self.config.pooling == "mean":
- # 对有效 token 取均值
- mask_expanded = attention_mask.unsqueeze(-1).float()
- feat = (hidden * mask_expanded).sum(1) / (mask_expanded.sum(1) + 1e-10)
- elif self.config.pooling == "last_token":
- # 取最后一个有效 token
- lengths = attention_mask.sum(1) - 1
- feat = hidden[torch.arange(len(batch)), lengths]
- else:
- # CLS token
- feat = hidden[:, 0, :]
- all_features.append(feat.cpu().float())
- # 转置为 (d, N)
- H = torch.cat(all_features, dim=0).T
- return H
- def compute_rs_cross(
- self,
- dataset_name: str,
- model_names: List[str],
- texts: Optional[List[str]] = None
- ) -> Dict[str, Any]:
- """
- 计算跨模型谱稳定性 RS_cross
- Returns:
- {
- "rs_cross": float,
- "r_effs": {model: r_eff},
- "std": float,
- "range": float
- }
- """
- if texts is None:
- texts = self.load_texts(dataset_name)
- if not texts:
- return {"rs_cross": 0, "r_effs": {}, "std": 0, "range": 0}
- r_effs = {}
- spectra = {}
- for model_name in tqdm(model_names, desc=f"RS_cross [{dataset_name}]"):
- # 提取或加载表示
- rep_path = f"representations/{model_name}_{dataset_name}.pt"
- rep_file = self.output_dir.parent / rep_path
- if rep_file.exists():
- H = load_H(model_name, dataset_name,
- representations_dir=str(rep_file.parent))
- else:
- H = self.extract_hidden_states(model_name, texts)
- save_H(H, model_name, dataset_name,
- representations_dir=str(rep_file.parent))
- # 计算有效秩
- H_tensor = torch.tensor(H.numpy() if hasattr(H, "cpu") else H) if not isinstance(H, torch.Tensor) else H
- r_eff = compute_gram_spectrum(H_tensor)
- r_effs[model_name] = r_eff
- # 计算谱(用于 SPD)
- _, prob = compute_spectrum_array(H)
- spectra[model_name] = prob
- # 计算 RS_cross = 方差
- values = list(r_effs.values())
- rs_cross = float(np.var(values))
- std = float(np.std(values))
- range_val = float(max(values) - min(values)) if values else 0
- return {
- "rs_cross": rs_cross,
- "r_effs": r_effs,
- "std": std,
- "range": range_val,
- "spectra": spectra
- }
- def compute_spd_matrix(
- self,
- spectra_dict: Dict[str, np.ndarray]
- ) -> np.ndarray:
- """
- 计算谱 Platonic 距离矩阵
- Returns:
- SPD 矩阵 (n, n)
- """
- model_names = list(spectra_dict.keys())
- n = len(model_names)
- spd_matrix = np.zeros((n, n))
- for i in range(n):
- for j in range(i + 1, n):
- p_i = spectra_dict[model_names[i]]
- p_j = spectra_dict[model_names[j]]
- # 对齐长度
- max_len = max(len(p_i), len(p_j))
- p_i_pad = np.pad(p_i, (0, max_len - len(p_i)))
- p_j_pad = np.pad(p_j, (0, max_len - len(p_j)))
- d = wasserstein1_distance(p_i_pad, p_j_pad)
- spd_matrix[i, j] = d
- spd_matrix[j, i] = d
- return spd_matrix, model_names
- def compute_layer_convergence_profile(
- self,
- dataset_name: str,
- model_names: List[str],
- n_layers: int,
- sample_layers: int = 10
- ) -> np.ndarray:
- """
- 逐层谱收敛轮廓 (LCP)
- Returns:
- RS_cross 值数组 (sample_layers,)
- """
- texts = self.load_texts(dataset_name)
- layer_indices = np.linspace(0, n_layers - 1, sample_layers, dtype=int)
- lcp = []
- for layer_idx in tqdm(layer_indices, desc="LCP"):
- result = self.compute_rs_cross(
- dataset_name, model_names, texts,
- layer=int(layer_idx)
- )
- lcp.append(result["rs_cross"])
- return np.array(lcp), layer_indices
- def run_experiment_e1(self) -> ExperimentResult:
- """运行 E1 基础谱收敛实验"""
- print("=" * 60)
- print("运行 E1: 基础谱收敛验证")
- print("=" * 60)
- def run_experiment_e2(self) -> Dict[str, Any]:
- """
- 运行 E2: 跨域谱分层验证
- 目的:验证不同领域的谱收敛程度不同
- 假设:形式化领域(代码/数学)收敛更好,开放领域(指令/创作)收敛更差
- """
- print("=" * 60)
- print("运行 E2: 跨域谱分层验证")
- print("=" * 60)
- # 领域分类(按形式化程度)
- domain_categories = {
- "highly_formal": ["humaneval", "code"], # 高度形式化:代码
- "formal": ["gsm8k", "math"], # 形式化:数学/推理
- "semi_formal": ["flores200", "translation"], # 半形式化:翻译
- "open": ["alpaca", "instruction"], # 开放:指令跟随
- }
- # 加载 E1 结果(如果已运行)
- e1_result_file = self.output_dir / "e1_result.json"
- if e1_result_file.exists():
- print(f"从 E1 结果加载数据:{e1_result_file}")
- with open(e1_result_file) as f:
- e1_result = json.load(f)
- rs_cross_all = e1_result.get("rs_cross", {})
- r_effs_all = e1_result.get("r_effs", {})
- else:
- print("未找到 E1 结果,重新计算...")
- # 需要先运行 E1
- self.run_experiment_e1()
- return self.run_experiment_e2()
- # 按类别聚合 RS_cross
- category_rs = {}
- category_domains = {}
- for category, domains in domain_categories.items():
- matching_domains = [d for d in domains if d in rs_cross_all]
- if matching_domains:
- rs_values = [rs_cross_all[d] for d in matching_domains]
- category_rs[category] = {
- "mean": float(np.mean(rs_values)),
- "std": float(np.std(rs_values)) if len(rs_values) > 1 else 0,
- "values": {d: rs_cross_all[d] for d in matching_domains},
- "domains": matching_domains
- }
- category_domains[category] = matching_domains
- # 排序:按 RS_cross 从小到大(收敛程度从高到低)
- sorted_categories = sorted(
- category_rs.items(),
- key=lambda x: x[1]["mean"]
- )
- # 计算领域间差异
- formal_domains = category_rs.get("highly_formal", {}).get("domains", []) + \
- category_rs.get("formal", {}).get("domains", [])
- open_domains = category_rs.get("open", {}).get("domains", [])
- formal_rs = [rs_cross_all[d] for d in formal_domains if d in rs_cross_all]
- open_rs = [rs_cross_all[d] for d in open_domains if d in rs_cross_all]
- comparison = {
- "formal_mean": float(np.mean(formal_rs)) if formal_rs else None,
- "open_mean": float(np.mean(open_rs)) if open_rs else None,
- "difference": None,
- "ratio": None
- }
- if comparison["formal_mean"] and comparison["open_mean"]:
- comparison["difference"] = comparison["open_mean"] - comparison["formal_mean"]
- comparison["ratio"] = comparison["open_mean"] / comparison["formal_mean"]
- result = {
- "timestamp": datetime.now().isoformat(),
- "category_rs_cross": category_rs,
- "sorted_by_convergence": [
- {"category": cat, "mean_rs_cross": data["mean"], "domains": data["domains"]}
- for cat, data in sorted_categories
- ],
- "formal_vs_open_comparison": comparison,
- "hypothesis_support": self._evaluate_hypothesis(comparison),
- "raw_rs_cross": rs_cross_all
- }
- # 保存结果
- self._save_e2_result(result)
- return result
- def _evaluate_hypothesis(self, comparison: Dict) -> Dict[str, Any]:
- """评估假设是否得到支持"""
- if not comparison["formal_mean"] or not comparison["open_mean"]:
- return {"supported": False, "reason": "数据不足"}
- # 假设:形式化领域 RS_cross < 开放领域 RS_cross
- supported = comparison["formal_mean"] < comparison["open_mean"]
- effect_size = comparison["ratio"]
- # 效应量判断
- if effect_size is None:
- strength = "unknown"
- elif effect_size > 10:
- strength = "very_strong"
- elif effect_size > 5:
- strength = "strong"
- elif effect_size > 2:
- strength = "moderate"
- else:
- strength = "weak"
- return {
- "supported": supported,
- "formal_mean": comparison["formal_mean"],
- "open_mean": comparison["open_mean"],
- "ratio_open_to_formal": effect_size,
- "effect_strength": strength,
- "interpretation": "形式化领域谱收敛显著优于开放领域" if supported else "假设未获支持"
- }
- def _save_e2_result(self, result: Dict):
- """保存 E2 结果"""
- result_file = self.output_dir / "e2_result.json"
- with open(result_file, "w") as f:
- json.dump(result, f, indent=2)
- print(f"\nE2 结果已保存到:{result_file}")
- # 生成摘要报告
- self._generate_e2_summary(result)
- def _generate_e2_summary(self, result: Dict):
- """生成 E2 摘要报告"""
- summary = [
- "# E2 实验摘要:跨域谱分层验证",
- "",
- f"实验时间:{result['timestamp']}",
- "",
- "## 假设",
- "形式化领域(代码/数学)的谱收敛程度优于开放领域(指令/创作)",
- "",
- "## 按类别 RS_cross(越小越收敛)",
- ""
- ]
- summary.append("| 类别 | 平均 RS_cross | 包含领域 |")
- summary.append("|------|--------------|----------|")
- for cat, data in result["category_rs_cross"].items():
- domains = ", ".join(data["domains"])
- summary.append(f"| {cat} | {data['mean']:.4f} | {domains} |")
- summary.append("")
- summary.append("## 形式化 vs 开放领域对比")
- comp = result["formal_vs_open_comparison"]
- if comp["formal_mean"]:
- summary.append(f"- 形式化领域平均 RS_cross: {comp['formal_mean']:.4f}")
- summary.append(f"- 开放领域平均 RS_cross: {comp['open_mean']:.4f}")
- summary.append(f"- 差异:{comp['difference']:.4f}")
- summary.append(f"- 比率(开放/形式化):{comp['ratio']:.2f}x")
- summary.append("")
- summary.append("## 假设验证")
- hyp = result["hypothesis_support"]
- status = "✅ 支持" if hyp["supported"] else "❌ 不支持"
- summary.append(f"状态:{status}")
- summary.append(f"效应强度:{hyp.get('effect_strength', 'unknown')}")
- summary.append(f"解释:{hyp['interpretation']}")
- summary.append("")
- summary.append("## 原始数据")
- summary.append("| 领域 | RS_cross |")
- summary.append("|------|----------|")
- for domain, rs in sorted(result["raw_rs_cross"].items(), key=lambda x: x[1]):
- summary.append(f"| {domain} | {rs:.4f} |")
- summary_file = self.output_dir / "e2_summary.md"
- with open(summary_file, "w") as f:
- f.write("\n".join(summary))
- print(f"摘要报告已保存到:{summary_file}")
- config_dict = asdict(self.config)
- result = ExperimentResult(
- timestamp=datetime.now().isoformat(),
- config=config_dict,
- r_effs={},
- rs_cross={},
- spd_matrices={},
- spectra={}
- )
- # 对每个数据集计算 RS_cross
- for dataset_name in self.config.datasets:
- print(f"\n[数据集] {dataset_name}")
- rs_result = self.compute_rs_cross(
- dataset_name,
- self.config.models
- )
- result.r_effs[dataset_name] = rs_result["r_effs"]
- result.rs_cross[dataset_name] = rs_result["rs_cross"]
- # 计算 SPD 矩阵
- spd_matrix, model_names = self.compute_spd_matrix(
- rs_result["spectra"]
- )
- result.spd_matrices[dataset_name] = spd_matrix.tolist()
- result.spectra[dataset_name] = {
- m: s.tolist() for m, s in
- zip(model_names, rs_result["spectra"].values())
- }
- print(f" RS_cross = {rs_result['rs_cross']:.4f}")
- print(f" 模型 r_eff 范围:[{min(rs_result['r_effs'].values()):.2f}, "
- f"{max(rs_result['r_effs'].values()):.2f}]")
- # 保存结果
- self._save_result(result)
- return result
- def run_experiment_lcp(
- self,
- dataset_name: str,
- n_layers: int,
- sample_layers: int = 10
- ) -> Dict[str, Any]:
- """运行逐层谱收敛轮廓实验"""
- print("=" * 60)
- print(f"运行 LCP: 逐层谱收敛分析 [{dataset_name}]")
- print("=" * 60)
- lcp, layer_indices = self.compute_layer_convergence_profile(
- dataset_name,
- self.config.models,
- n_layers,
- sample_layers
- )
- result = {
- "timestamp": datetime.now().isoformat(),
- "dataset": dataset_name,
- "models": self.config.models,
- "layer_indices": layer_indices.tolist(),
- "lcp_values": lcp.tolist(),
- "n_layers": n_layers
- }
- # 保存结果
- result_file = self.output_dir / f"lcp_{dataset_name}.json"
- with open(result_file, "w") as f:
- json.dump(result, f, indent=2)
- print(f"\nLCP 结果已保存到:{result_file}")
- print(f"RS_cross 从 {lcp[0]:.4f} (浅层) 到 {lcp[-1]:.4f} (深层)")
- return result
- def _save_result(self, result: ExperimentResult):
- """保存实验结果"""
- result_dict = {
- "timestamp": result.timestamp,
- "config": result.config,
- "r_effs": result.r_effs,
- "rs_cross": result.rs_cross,
- "spd_matrices": result.spd_matrices,
- }
- result_file = self.output_dir / "e1_result.json"
- with open(result_file, "w") as f:
- json.dump(result_dict, f, indent=2)
- print(f"\nE1 结果已保存到:{result_file}")
- # 生成摘要报告
- self._generate_summary(result_dict)
- def _generate_summary(self, result: Dict):
- """生成摘要报告"""
- summary = ["# E1 实验摘要", ""]
- summary.append("## 实验配置")
- summary.append(f"- 时间:{result['timestamp']}")
- summary.append(f"- 模型数:{len(result['config']['models'])}")
- summary.append(f"- 数据集数:{len(result['config']['datasets'])}")
- summary.append("")
- summary.append("## RS_cross 结果")
- summary.append("| 数据集 | RS_cross |")
- summary.append("|--------|----------|")
- for ds, rs in result["rs_cross"].items():
- summary.append(f"| {ds} | {rs:.4f} |")
- summary.append("")
- summary.append("## 各模型 r_eff")
- for ds, r_effs in result["r_effs"].items():
- summary.append(f"### {ds}")
- for model, r_eff in r_effs.items():
- summary.append(f"- {model}: {r_eff:.2f}")
- summary.append("")
- summary_file = self.output_dir / "e1_summary.md"
- with open(summary_file, "w") as f:
- f.write("\n".join(summary))
- print(f"摘要报告已保存到:{summary_file}")
- def main():
- import argparse
- parser = argparse.ArgumentParser(description="跨模型谱收敛实验")
- parser.add_argument("--experiment", type=str,
- choices=["E1", "E2", "LCP"], default="E1",
- help="实验类型")
- parser.add_argument("--output-dir", type=str,
- default="experiments/output/cross_model",
- help="输出目录")
- parser.add_argument("--dataset", type=str,
- default="gsm8k",
- help="数据集名称 (LCP 实验用)")
- parser.add_argument("--layers", type=int, default=32,
- help="模型层数 (LCP 实验用)")
- parser.add_argument("--sample-layers", type=int, default=10,
- help="采样层数 (LCP 实验用)")
- args = parser.parse_args()
- # 实验配置
- config = ExperimentConfig(
- models=[
- "llama3.2-3b-instruct",
- "Mistral-7B-v0.3",
- "llama3-8b",
- "Qwen2.5-7B-Instruct",
- "gemma-2-9b-it"
- ],
- model_paths={
- "llama3.2-3b-instruct": "model/weights/llama3.2-3b-instruct",
- "Mistral-7B-v0.3": "model/weights/Mistral-7B-v0.3",
- "llama3-8b": "model/weights/llama3-8b",
- "Qwen2.5-7B-Instruct": "model/weights/Qwen2.5-7B-Instruct",
- "gemma-2-9b-it": "model/weights/gemma-2-9b-it"
- },
- datasets=["gsm8k", "math", "humaneval", "alpaca"],
- output_dir=args.output_dir,
- batch_size=8,
- max_length=512,
- pooling="mean"
- )
- analyzer = CrossModelConvergence(config)
- if args.experiment == "E1":
- analyzer.run_experiment_e1()
- elif args.experiment == "E2":
- analyzer.run_experiment_e2()
- elif args.experiment == "LCP":
- analyzer.run_experiment_lcp(
- args.dataset,
- args.layers,
- args.sample_layers
- )
- if __name__ == "__main__":
- main()
|