提交其余项目实验变更

This commit is contained in:
2026-09-25 10:41:58 +08:00
parent 83ec3d1a83
commit 95bd34599b
119 changed files with 5877 additions and 1709 deletions
+298
View File
@@ -0,0 +1,298 @@
from __future__ import annotations
import argparse
import csv
import json
import sys
from pathlib import Path
from typing import Any
import numpy as np
import torch
from .data import RobustStats, apply_robust_stats, load_aligned
from .mofe import EXPERT_NAMES
from .mofe import MixtureOfFusionExperts
from .train_mofe import (
MOFE7_MLP,
SEEDS,
_conditions,
_metric_dict,
_predict,
_sha256,
_write_csv,
)
ROOT = Path(__file__).resolve().parents[1]
REFERENCE_DIR = ROOT / "outputs" / "mofe_7experts"
DEFAULT_DIAGNOSTIC_OUTPUT = ROOT / "outputs" / "followups" / "D0_task_preference"
MODEL_CONFIG: dict[str, Any] = {
"router": "mlp",
"expert_names": EXPERT_NAMES,
"availability_mode": "hard",
}
def _load_model(seed: int, dims: tuple[int, int, int], device: torch.device) -> MixtureOfFusionExperts:
checkpoint = REFERENCE_DIR / "models" / MOFE7_MLP / f"seed_{seed}" / "model_best.pt"
saved = torch.load(checkpoint, map_location=device, weights_only=False)
if saved.get("config") != MODEL_CONFIG or tuple(saved.get("dims", ())) != dims:
raise ValueError(f"checkpoint does not match the selected single-router model: {checkpoint}")
if int(saved.get("seed", -1)) != seed:
raise ValueError(f"checkpoint seed mismatch: expected {seed}, found {saved.get('seed')}")
model = MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG).to(device)
model.load_state_dict(saved["state_dict"])
return model.eval()
def _rank_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[tuple[int, str], list[dict[str, Any]]] = {}
for row in rows:
if row["expert"] == "learned_router":
continue
groups.setdefault((int(row["seed"]), str(row["condition"])), []).append(row)
result: list[dict[str, Any]] = []
for (seed, condition), values in sorted(groups.items()):
by_name = {str(row["expert"]): row for row in values}
ordered = [by_name[name] for name in EXPERT_NAMES]
f1 = [float(row["macro_f1"]) for row in ordered]
mae = [float(row["mae"]) for row in ordered]
pearson = [float(row["pearson"]) for row in ordered]
result.append({
"seed": seed,
"condition": condition,
"spearman_macro_f1_vs_mae": _spearman(f1, mae),
"spearman_macro_f1_vs_pearson": _spearman(f1, pearson),
"best_macro_f1_expert": EXPERT_NAMES[int(np.argmax(f1))],
"best_mae_expert": EXPERT_NAMES[int(np.argmin(mae))],
"best_pearson_expert": EXPERT_NAMES[int(np.argmax(pearson))],
"macro_f1_order_best_to_worst": ">".join(EXPERT_NAMES[i] for i in np.argsort(-np.asarray(f1), kind="stable")),
"mae_order_best_to_worst": ">".join(EXPERT_NAMES[i] for i in np.argsort(np.asarray(mae), kind="stable")),
"pearson_order_best_to_worst": ">".join(EXPERT_NAMES[i] for i in np.argsort(-np.asarray(pearson), kind="stable")),
})
return result
def _spearman(left: list[float], right: list[float]) -> float:
def average_ranks(values: list[float]) -> np.ndarray:
array = np.asarray(values, dtype=np.float64)
order = np.argsort(array, kind="stable")
ranks = np.empty(len(array), dtype=np.float64)
start = 0
while start < len(array):
end = start + 1
while end < len(array) and array[order[end]] == array[order[start]]:
end += 1
ranks[order[start:end]] = (start + 1 + end) / 2
start = end
return ranks
left_ranks = average_ranks(left)
right_ranks = average_ranks(right)
if np.std(left_ranks) == 0 or np.std(right_ranks) == 0:
return 0.0
return float(np.corrcoef(left_ranks, right_ranks)[0, 1])
def _summary_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
result = []
for expert in (*EXPERT_NAMES, "learned_router"):
matching = [row for row in rows if row["expert"] == expert and row["condition"] != "clean"]
per_seed: dict[int, list[dict[str, Any]]] = {}
for row in matching:
per_seed.setdefault(int(row["seed"]), []).append(row)
seed_means = []
for seed, seed_rows in sorted(per_seed.items()):
seed_means.append({
metric: float(np.mean([float(row[metric]) for row in seed_rows]))
for metric in ("macro_f1", "mae", "pearson", "available_position_fraction")
})
if not seed_means:
continue
out: dict[str, Any] = {"expert": expert, "n_seeds": len(seed_means), "conditions_averaged": len(matching) // len(seed_means)}
for metric in ("macro_f1", "mae", "pearson", "available_position_fraction"):
values = [item[metric] for item in seed_means]
out[f"corrupt_{metric}_mean"] = float(np.mean(values))
out[f"corrupt_{metric}_seed_sd"] = float(np.std(values, ddof=1)) if len(values) > 1 else 0.0
result.append(out)
return result
def _write_readout(output: Path, summary: list[dict[str, Any]], ranks: list[dict[str, Any]]) -> None:
rho_f1_mae = np.asarray([float(row["spearman_macro_f1_vs_mae"]) for row in ranks])
rho_f1_pearson = np.asarray([float(row["spearman_macro_f1_vs_pearson"]) for row in ranks])
best_f1 = {name: sum(row["best_macro_f1_expert"] == name for row in ranks) for name in EXPERT_NAMES}
best_mae = {name: sum(row["best_mae_expert"] == name for row in ranks) for name in EXPERT_NAMES}
best_pearson = {name: sum(row["best_pearson_expert"] == name for row in ranks) for name in EXPERT_NAMES}
rank_by_condition: dict[str, list[dict[str, Any]]] = {}
for row in ranks:
rank_by_condition.setdefault(str(row["condition"]), []).append(row)
def winners(condition: str, column: str) -> str:
matching = rank_by_condition[condition]
counts = {name: sum(row[column] == name for row in matching) for name in EXPERT_NAMES}
max_count = max(counts.values())
names = [name for name, count in counts.items() if count == max_count]
return ", ".join(f"{name} ({max_count}/{len(matching)})" for name in names)
by_name = {str(row["expert"]): row for row in summary}
lines = [
"# Single-router MoFE 任务偏好诊断",
"",
"本诊断使用保留的 single-router 检查点和验证集,用于判断是否值得增加第二个 router;它不是测试集估计。",
"",
"## Forced-expert 规则",
"",
"所选模态子集可用的位置强制使用对应 expert;该子集不可用时,由已训练 router 在其他可用 expert 中选择;全模态缺失时沿用 learned missing token。可用率表示所选 expert 能被强制使用的位置比例。",
"",
"## 缺失条件平均指标",
"",
"下表先在每个 seed 内对 15 种连续块缺失条件求平均,再汇总三个 seed;seed 标准差见 CSV。",
"",
"| Expert | Macro-F1 ↑ | MAE ↓ | Pearson ↑ | 可强制使用比例 |",
"| --- | ---: | ---: | ---: | ---: |",
]
for name in (*EXPERT_NAMES, "learned_router"):
row = by_name[name]
lines.append(
f"| {name} | {float(row['corrupt_macro_f1_mean']):.3f} | {float(row['corrupt_mae_mean']):.3f} | "
f"{float(row['corrupt_pearson_mean']):.3f} | {float(row['corrupt_available_position_fraction_mean']):.3f} |"
)
lines.extend([
"",
"## 两个任务的 expert 偏好",
"",
f"在 {len(ranks)} 个 seed—条件组合中,分类 Macro-F1 与回归 MAE 的平均 Spearman ρ 为 **{rho_f1_mae.mean():.3f}**。MAE 越低越好,因此负相关表示两个指标倾向于选中相似的 expert。Macro-F1 与 Pearson 的平均 ρ 为 **{rho_f1_pearson.mean():.3f}**。",
"",
"六个重点条件下的相关性先按三个 seed 求平均;最优 expert 一栏显示三个 seed 中的多数结果:",
"",
"| 条件 | ρ(Macro-F1, MAE) | ρ(Macro-F1, Pearson) | Macro-F1 最优 | MAE 最优 | Pearson 最优 |",
"| --- | ---: | ---: | --- | --- | --- |",
])
key_conditions = (
("clean", "Clean"),
("text_30", "Text 30%"),
("audio_30", "Audio 30%"),
("vision_30", "Vision 30%"),
("audio_vision_30", "Audio+Vision 30%"),
("all_modalities_30", "All-modal 30%"),
)
for condition, label in key_conditions:
condition_rows = rank_by_condition[condition]
rho_mae = float(np.mean([float(row["spearman_macro_f1_vs_mae"]) for row in condition_rows]))
rho_pearson = float(np.mean([float(row["spearman_macro_f1_vs_pearson"]) for row in condition_rows]))
lines.append(
f"| {label} | {rho_mae:.3f} | {rho_pearson:.3f} | "
f"{winners(condition, 'best_macro_f1_expert')} | {winners(condition, 'best_mae_expert')} | "
f"{winners(condition, 'best_pearson_expert')} |"
)
lines.extend([
"",
f"各指标的最优 expert 次数:Macro-F1({_format_counts(best_f1)});MAE({_format_counts(best_mae)});Pearson({_format_counts(best_pearson)})。",
"",
"当前排名没有显示稳定的分类—回归 expert 分工:Macro-F1 较高通常同时对应较低 MAE 和较高 Pearson;文本 expert 在分类与回归指标上都是最常见的赢家。因此,这项诊断**没有提供增加第二个 router 所需的任务特异模态偏好证据**。目前保留 single-router 作为活动参照;这不代表两个任务在任何数据或设置下都不可能需要不同路由。",
"",
"## 结论范围",
"",
"输入是官方提供的 50 个有序 wordpiece 位置。结果只反映这些位置及本次缺失掩码下的任务与 expert 关系,不表示物理时间可靠性。",
"",
"逐条件结果见 `forced_expert_metrics.csv` 和 `rank_concordance.csv`;跨 seed 汇总见 `expert_task_preference_summary.csv`。",
])
(output / "task_preference_diagnostic.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
def _format_counts(counts: dict[str, int]) -> str:
return ", ".join(f"{name}: {count}" for name, count in counts.items())
def run(args: argparse.Namespace) -> None:
output = args.output_dir.resolve()
output.mkdir(parents=True, exist_ok=True)
device = torch.device("cuda" if args.device == "auto" and torch.cuda.is_available() else "cpu") if args.device == "auto" else torch.device(args.device)
torch.set_num_threads(args.threads)
raw = load_aligned()
scaler_path = REFERENCE_DIR / "aligned_robust_stats.npz"
stats = RobustStats.load(scaler_path)
valid = apply_robust_stats(raw["valid"], stats)
dims = tuple(int(x.shape[-1]) for x in valid.x)
metric_rows: list[dict[str, Any]] = []
for seed in args.seeds:
model = _load_model(seed, dims, device)
conditions = _conditions(valid, seed)
for condition, rate, masks in conditions:
predictions = {
"learned_router": _predict(model, valid, masks, device, args.batch_size),
**{
expert: _predict(model, valid, masks, device, args.batch_size, force_expert=expert)
for expert in EXPERT_NAMES
},
}
for expert, prediction in predictions.items():
coverage = 1.0
if expert != "learned_router":
expert_index = EXPERT_NAMES.index(expert)
coverage = float(model._availability(torch.as_tensor(masks, dtype=torch.bool, device=device), EXPERT_NAMES)[..., expert_index].float().mean().item())
else:
coverage = float(prediction["availability"].astype(bool).any(axis=-1).mean())
metric_rows.append({
"method": MOFE7_MLP,
"seed": seed,
"condition": condition,
"missing_rate": rate,
"expert": expert,
"n_valid": valid.n,
"available_position_fraction": coverage,
**_metric_dict(valid.y_cls, valid.y_reg, prediction["logits"], prediction["intensity"]),
})
print(f"forced-expert diagnostic complete for seed={seed} on {device}", flush=True)
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
rank_rows = _rank_rows(metric_rows)
summary = _summary_rows(metric_rows)
_write_csv(output / "forced_expert_metrics.csv", metric_rows)
_write_csv(output / "expert_task_preference_summary.csv", summary)
_write_csv(output / "rank_concordance.csv", rank_rows)
_write_readout(output, summary, rank_rows)
feature_path = ROOT.parents[1] / "E题数据" / "附件2-数据集特征文件" / "aligned_50.pkl"
metadata = {
"diagnostic": "forced-expert task preference for the retained single-router MoFE-7",
"checkpoint_dir": str(REFERENCE_DIR / "models" / MOFE7_MLP),
"checkpoint_seeds": list(args.seeds),
"feature_file": str(feature_path),
"feature_sha256": _sha256(feature_path),
"scaler_file": str(scaler_path),
"scaler_sha256": _sha256(scaler_path),
"device": str(device),
"cuda_device": torch.cuda.get_device_name(0) if device.type == "cuda" else None,
"python_version": sys.version,
"torch_version": torch.__version__,
"numpy_version": np.__version__,
"valid_examples": valid.n,
"conditions": [condition for condition, _, _ in _conditions(valid, args.seeds[0])],
"corruption_seed_protocol": "seed + 13 + pattern_index*101 + int(rate*1000)",
"forced_expert_policy": "use the requested expert where its modality subset is available; fall back to the trained single router at positions where it is unavailable; all-missing positions use the learned missing token",
"scope_note": "50 official ordered wordpiece positions; no claim about physical-time reliability",
"interpretation_note": "Ranking agreement is descriptive on the supplied validation split; it is a motivation diagnostic, not an unbiased test-set estimate.",
}
(output / "run_manifest.json").write_text(json.dumps(metadata, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"saved forced-expert diagnostic to {output}", flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Measure classification/regression preferences across existing MoFE experts.")
parser.add_argument("--seeds", type=int, nargs="+", default=list(SEEDS))
parser.add_argument("--batch-size", type=int, default=128)
parser.add_argument("--threads", type=int, default=4)
parser.add_argument("--device", default="auto")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_DIAGNOSTIC_OUTPUT)
run(parser.parse_args())
if __name__ == "__main__":
main()