visualization.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. """
  2. 实验可视化模块
  3. 功能:
  4. - 绘制谱距离热力图
  5. - 绘制有效秩柱状图
  6. - 绘制谱分布对比图
  7. """
  8. import os
  9. import numpy as np
  10. from pathlib import Path
  11. from typing import Dict, List, Optional, Tuple, Any
  12. import json
  13. # 延迟导入 matplotlib,避免不必要的依赖
  14. _mpl_available = False
  15. try:
  16. import matplotlib
  17. matplotlib.use('Agg') # 非交互式后端
  18. import matplotlib.pyplot as plt
  19. import seaborn as sns
  20. _mpl_available = True
  21. except ImportError:
  22. pass
  23. def check_mpl_available():
  24. """检查 matplotlib 是否可用"""
  25. if not _mpl_available:
  26. raise ImportError(
  27. "matplotlib 或 seaborn 未安装。请运行:pip install matplotlib seaborn"
  28. )
  29. def plot_heatmap(
  30. matrix: np.ndarray,
  31. labels: List[str],
  32. title: str = "Spectrum Distance Matrix",
  33. cmap: str = "viridis",
  34. output_path: Optional[str] = None,
  35. figsize: Tuple[int, int] = (8, 6),
  36. ) -> Optional[bytes]:
  37. """
  38. 绘制热力图
  39. Args:
  40. matrix: n×n 距离矩阵
  41. labels: 模型/领域标签
  42. title: 图标题
  43. cmap: 颜色映射
  44. output_path: 输出路径(None 则返回 bytes)
  45. figsize: 图像大小
  46. Returns:
  47. 图像 bytes 或 None(如果保存到文件)
  48. """
  49. check_mpl_available()
  50. fig, ax = plt.subplots(figsize=figsize)
  51. sns.heatmap(
  52. matrix,
  53. annot=True,
  54. fmt=".3f",
  55. cmap=cmap,
  56. xticklabels=labels,
  57. yticklabels=labels,
  58. ax=ax,
  59. cbar_kws={"label": "Wasserstein-1 Distance"},
  60. )
  61. ax.set_title(title)
  62. ax.set_xlabel("Model")
  63. ax.set_ylabel("Model")
  64. plt.tight_layout()
  65. if output_path:
  66. Path(output_path).parent.mkdir(parents=True, exist_ok=True)
  67. plt.savefig(output_path, dpi=150, bbox_inches="tight")
  68. plt.close(fig)
  69. return None
  70. else:
  71. from io import BytesIO
  72. buf = BytesIO()
  73. plt.savefig(buf, format="png", dpi=150, bbox_inches="tight")
  74. plt.close(fig)
  75. buf.seek(0)
  76. return buf.read()
  77. def plot_bar_chart(
  78. data: Dict[str, float],
  79. title: str = "Effective Rank Comparison",
  80. xlabel: str = "Model",
  81. ylabel: str = "Effective Rank",
  82. output_path: Optional[str] = None,
  83. figsize: Tuple[int, int] = (10, 6),
  84. color: str = "steelblue",
  85. errorBars: Optional[Dict[str, float]] = None,
  86. ) -> Optional[bytes]:
  87. """
  88. 绘制柱状图
  89. Args:
  90. data: {label: value, ...}
  91. title: 图标题
  92. xlabel: X 轴标签
  93. ylabel: Y 轴标签
  94. output_path: 输出路径
  95. figsize: 图像大小
  96. color: 柱子颜色
  97. errorBars: {label: std, ...} 误差线
  98. Returns:
  99. 图像 bytes 或 None
  100. """
  101. check_mpl_available()
  102. labels = list(data.keys())
  103. values = list(data.values())
  104. fig, ax = plt.subplots(figsize=figsize)
  105. x_pos = range(len(labels))
  106. if errorBars:
  107. errors = [errorBars.get(l, 0) for l in labels]
  108. ax.bar(x_pos, values, yerr=errors, capsize=5, color=color, alpha=0.8)
  109. else:
  110. ax.bar(x_pos, values, color=color, alpha=0.8)
  111. ax.set_xticks(x_pos)
  112. ax.set_xticklabels(labels, rotation=45, ha="right")
  113. ax.set_title(title)
  114. ax.set_xlabel(xlabel)
  115. ax.set_ylabel(ylabel)
  116. plt.tight_layout()
  117. if output_path:
  118. Path(output_path).parent.mkdir(parents=True, exist_ok=True)
  119. plt.savefig(output_path, dpi=150, bbox_inches="tight")
  120. plt.close(fig)
  121. return None
  122. else:
  123. from io import BytesIO
  124. buf = BytesIO()
  125. plt.savefig(buf, format="png", dpi=150, bbox_inches="tight")
  126. plt.close(fig)
  127. buf.seek(0)
  128. return buf.read()
  129. def plot_spectrum_comparison(
  130. prob_dict: Dict[str, np.ndarray],
  131. title: str = "Spectrum Distribution Comparison",
  132. output_path: Optional[str] = None,
  133. figsize: Tuple[int, int] = (10, 6),
  134. log_scale: bool = True,
  135. ) -> Optional[bytes]:
  136. """
  137. 绘制多个模型的谱分布对比图
  138. Args:
  139. prob_dict: {model_name: prob_distribution, ...}
  140. title: 图标题
  141. output_path: 输出路径
  142. figsize: 图像大小
  143. log_scale: Y 轴是否使用对数刻度
  144. Returns:
  145. 图像 bytes 或 None
  146. """
  147. check_mpl_available()
  148. fig, ax = plt.subplots(figsize=figsize)
  149. colors = plt.cm.tab10.colors
  150. markers = ["o", "s", "^", "D", "v", "<", ">", "p"]
  151. for i, (model_name, prob) in enumerate(prob_dict.items()):
  152. color = colors[i % len(colors)]
  153. marker = markers[i % len(markers)]
  154. x = range(len(prob))
  155. ax.plot(
  156. x, prob,
  157. marker=marker,
  158. markersize=4,
  159. linestyle="-",
  160. label=model_name,
  161. color=color,
  162. alpha=0.8,
  163. )
  164. ax.set_title(title)
  165. ax.set_xlabel("Eigenvalue Index")
  166. ax.set_ylabel("Normalized Probability")
  167. if log_scale:
  168. ax.set_yscale("log")
  169. ax.legend(loc="upper right", fontsize=8)
  170. ax.grid(True, alpha=0.3)
  171. plt.tight_layout()
  172. if output_path:
  173. Path(output_path).parent.mkdir(parents=True, exist_ok=True)
  174. plt.savefig(output_path, dpi=150, bbox_inches="tight")
  175. plt.close(fig)
  176. return None
  177. else:
  178. from io import BytesIO
  179. buf = BytesIO()
  180. plt.savefig(buf, format="png", dpi=150, bbox_inches="tight")
  181. plt.close(fig)
  182. buf.seek(0)
  183. return buf.read()
  184. def plot_convergence_analysis(
  185. result_dict: Dict[str, Any],
  186. output_path: str,
  187. ) -> None:
  188. """
  189. 绘制收敛分析综合图
  190. Args:
  191. result_dict: 包含以下键:
  192. - r_eff_by_model: {model: r_eff}
  193. - distance_matrix: n×n 矩阵
  194. - model_names: 标签列表
  195. output_path: 输出路径
  196. """
  197. check_mpl_available()
  198. fig, axes = plt.subplots(1, 2, figsize=(16, 6))
  199. # 左图:有效秩对比
  200. r_eff_data = result_dict.get("r_eff_by_model", {})
  201. if r_eff_data:
  202. labels = list(r_eff_data.keys())
  203. values = [r_eff_data[l] for l in labels]
  204. axes[0].bar(range(len(labels)), values, color="steelblue", alpha=0.8)
  205. axes[0].set_xticks(range(len(labels)))
  206. axes[0].set_xticklabels(labels, rotation=45, ha="right")
  207. axes[0].set_title("Effective Rank by Model")
  208. axes[0].set_ylabel("Effective Rank")
  209. axes[0].grid(True, alpha=0.3)
  210. # 右图:距离热力图
  211. dist_matrix = result_dict.get("distance_matrix")
  212. model_names = result_dict.get("model_names", [])
  213. if dist_matrix is not None and len(model_names) > 0:
  214. sns.heatmap(
  215. dist_matrix,
  216. annot=True,
  217. fmt=".3f",
  218. cmap="viridis",
  219. xticklabels=model_names,
  220. yticklabels=model_names,
  221. ax=axes[1],
  222. cbar_kws={"label": "W1 Distance"},
  223. )
  224. axes[1].set_title("Cross-Model Spectrum Distance")
  225. plt.tight_layout()
  226. Path(output_path).parent.mkdir(parents=True, exist_ok=True)
  227. plt.savefig(output_path, dpi=150, bbox_inches="tight")
  228. plt.close(fig)
  229. def generate_all_visualizations(
  230. result_dir: str,
  231. output_dir: str,
  232. ) -> Dict[str, str]:
  233. """
  234. 从实验结果生成所有可视化图
  235. Args:
  236. result_dir: 实验结果目录
  237. output_dir: 输出目录
  238. Returns:
  239. {plot_name: output_path, ...}
  240. """
  241. check_mpl_available()
  242. # 加载实验结果
  243. with open(Path(result_dir) / "spectrum_results.json", "r") as f:
  244. spectrum_results = json.load(f)
  245. with open(Path(result_dir) / "config.json", "r") as f:
  246. config = json.load(f)
  247. # 加载距离矩阵
  248. dist_dir = Path(result_dir) / "distance_matrices"
  249. distance_matrices = {}
  250. for path in dist_dir.glob("*.npy"):
  251. domain = path.stem.replace("_w1_matrix", "")
  252. distance_matrices[domain] = np.load(path)
  253. output_path = Path(output_dir)
  254. output_path.mkdir(parents=True, exist_ok=True)
  255. generated = {}
  256. # 1. 每个领域的距离热力图
  257. for domain, matrix in distance_matrices.items():
  258. plot_path = str(output_path / f"heatmap_{domain}.png")
  259. plot_heatmap(matrix, config["model_names"], title=f"{domain} - Spectrum Distance", output_path=plot_path)
  260. generated[f"heatmap_{domain}"] = plot_path
  261. # 2. 有效秩柱状图(按领域分组)
  262. for domain in config["domains"]:
  263. r_eff_data = {}
  264. for key, data in spectrum_results.items():
  265. if data["domain"] == domain:
  266. r_eff_data[data["model_name"]] = data["r_eff"]
  267. if r_eff_data:
  268. plot_path = str(output_path / f"bar_r_eff_{domain}.png")
  269. plot_bar_chart(r_eff_data, title=f"Effective Rank - {domain}", output_path=plot_path)
  270. generated[f"bar_r_eff_{domain}"] = plot_path
  271. # 3. 谱分布对比图(每个领域)
  272. for domain in config["domains"]:
  273. prob_dict = {}
  274. for key, data in spectrum_results.items():
  275. if data["domain"] == domain:
  276. prob_dict[data["model_name"]] = data["prob"]
  277. if prob_dict:
  278. plot_path = str(output_path / f"spectrum_{domain}.png")
  279. plot_spectrum_comparison(prob_dict, title=f"Spectrum Distribution - {domain}", output_path=plot_path)
  280. generated[f"spectrum_{domain}"] = plot_path
  281. # 4. 综合收敛分析图
  282. for domain, matrix in distance_matrices.items():
  283. r_eff_data = {}
  284. for key, data in spectrum_results.items():
  285. if data["domain"] == domain:
  286. r_eff_data[data["model_name"]] = data["r_eff"]
  287. result_dict = {
  288. "r_eff_by_model": r_eff_data,
  289. "distance_matrix": matrix,
  290. "model_names": config["model_names"],
  291. }
  292. plot_path = str(output_path / f"convergence_{domain}.png")
  293. plot_convergence_analysis(result_dict, plot_path)
  294. generated[f"convergence_{domain}"] = plot_path
  295. return generated
  296. # 命令行入口
  297. if __name__ == "__main__":
  298. import argparse
  299. parser = argparse.ArgumentParser(description="生成实验可视化图")
  300. parser.add_argument("--result-dir", type=str, required=True, help="实验结果目录")
  301. parser.add_argument("--output-dir", type=str, default="experiments/output/figures", help="输出目录")
  302. args = parser.parse_args()
  303. generated = generate_all_visualizations(args.result_dir, args.output_dir)
  304. print(f"已生成 {len(generated)} 个可视化图:")
  305. for name, path in generated.items():
  306. print(f" - {name}: {path}")