Flatten submit package structure

This commit is contained in:
2026-09-26 16:47:17 +08:00
parent 411f0f97e5
commit c116ce60aa
173 changed files with 1242 additions and 1243 deletions
+1
View File
@@ -0,0 +1 @@
"""Q2 training, comparison, and inference pipelines."""
+1
View File
@@ -0,0 +1 @@
"""Maintained Q2 deep-learning schemes."""
+1
View File
@@ -0,0 +1 @@
"""Q2 multimodal emotion-recognition experiments."""
+214
View File
@@ -0,0 +1,214 @@
from __future__ import annotations
import pickle
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
from data_paths import ATTACHMENT2, PROJECT_ROOT
ROOT = PROJECT_ROOT
MODALITIES = ("text", "audio", "vision")
@dataclass
class Split:
x: tuple[np.ndarray, np.ndarray, np.ndarray]
mask: np.ndarray # N x T x 3
y_cls: np.ndarray
y_reg: np.ndarray
ids: list[str]
@property
def n(self) -> int:
return len(self.y_cls)
@property
def steps(self) -> int:
return int(self.x[0].shape[1])
@dataclass
class RobustStats:
center: tuple[np.ndarray, np.ndarray, np.ndarray]
scale: tuple[np.ndarray, np.ndarray, np.ndarray]
def save(self, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(
path,
text_center=self.center[0], text_scale=self.scale[0],
audio_center=self.center[1], audio_scale=self.scale[1],
vision_center=self.center[2], vision_scale=self.scale[2],
)
@classmethod
def load(cls, path: Path) -> "RobustStats":
with np.load(path) as data:
return cls(
tuple(data[f"{m}_center"].astype(np.float32) for m in MODALITIES),
tuple(data[f"{m}_scale"].astype(np.float32) for m in MODALITIES),
)
def _unpickle(path: Path) -> dict[str, Any]:
with path.open("rb") as stream:
return pickle.load(stream, encoding="latin1")
def _ids_and_targets(part: dict[str, Any]) -> tuple[list[str], np.ndarray, np.ndarray]:
ids = [str(x) for x in part["id"]]
y_cls = np.asarray(part["classification_labels"], dtype=np.int64).reshape(-1)
y_reg = np.asarray(part["regression_labels"], dtype=np.float32).reshape(-1)
return ids, y_cls, y_reg
def _text_mask(part: dict[str, Any]) -> np.ndarray:
tokens = np.asarray(part["text_bert"])
if tokens.ndim != 3 or tokens.shape[1] < 2:
raise ValueError(f"unexpected text_bert shape: {tokens.shape}")
# MOSEI text_bert rows are input_ids, input_mask, segment_ids.
return tokens[:, 1, :].astype(bool)
def load_aligned(path: Path | None = None) -> dict[str, Split]:
path = path or ATTACHMENT2 / "aligned_50.pkl"
raw = _unpickle(path)
result: dict[str, Split] = {}
for name in ("train", "valid"):
part = raw[name]
xs = tuple(np.asarray(part[m], dtype=np.float32) for m in MODALITIES)
masks = [
_text_mask(part),
np.any(np.isfinite(xs[1]) & (xs[1] != 0), axis=-1),
np.any(np.isfinite(xs[2]) & (xs[2] != 0), axis=-1),
]
mask = np.stack(masks, axis=-1)
ids, y_cls, y_reg = _ids_and_targets(part)
if any(x.shape[1] != 50 for x in xs):
raise ValueError(f"{name} aligned feature tensors must have 50 slots")
result[name] = Split(xs, mask, y_cls, y_reg, ids)
train_videos = {x.split("$_$", 1)[0] for x in result["train"].ids}
valid_videos = {x.split("$_$", 1)[0] for x in result["valid"].ids}
overlap = train_videos & valid_videos
if overlap:
raise ValueError(f"official train/valid split leaks {len(overlap)} source video ids")
return result
def _resample_rows_to_50(values: np.ndarray, lengths: list[int] | np.ndarray) -> tuple[np.ndarray, np.ndarray]:
n, source_steps, dim = values.shape
output = np.zeros((n, 50, dim), dtype=np.float32)
mask = np.zeros((n, 50), dtype=bool)
lengths_arr = np.asarray(lengths, dtype=np.int64).reshape(-1)
for i in range(n):
length = int(np.clip(lengths_arr[i], 0, source_steps))
if length == 0:
continue
source = np.nan_to_num(values[i, :length], nan=0.0, posinf=0.0, neginf=0.0)
observed = np.any(source != 0, axis=-1)
for j in range(50):
left = int(np.floor(j * length / 50))
right = max(left + 1, int(np.ceil((j + 1) * length / 50)))
right = min(right, length)
use = observed[left:right]
if use.any():
output[i, j] = source[left:right][use].mean(axis=0)
mask[i, j] = True
return output, mask
def load_fixed_window(path: Path | None = None) -> dict[str, Split]:
"""Build a matched 50-slot equal-window control from the unaligned file."""
path = path or ATTACHMENT2 / "unaligned_50.pkl"
raw = _unpickle(path)
result: dict[str, Split] = {}
for name in ("train", "valid"):
part = raw[name]
text = np.asarray(part["text"], dtype=np.float32)
audio, audio_mask = _resample_rows_to_50(part["audio"], part["audio_lengths"])
vision, vision_mask = _resample_rows_to_50(part["vision"], part["vision_lengths"])
text_mask = _text_mask(part)
xs = (text, audio, vision)
mask = np.stack((text_mask, audio_mask, vision_mask), axis=-1)
ids, y_cls, y_reg = _ids_and_targets(part)
result[name] = Split(xs, mask, y_cls, y_reg, ids)
return result
def fit_robust_stats(split: Split) -> RobustStats:
centers: list[np.ndarray] = []
scales: list[np.ndarray] = []
for modality in range(3):
observed = split.mask[:, :, modality].reshape(-1)
values = split.x[modality].reshape(-1, split.x[modality].shape[-1])[observed]
if not len(values):
raise ValueError(f"no observed values for {MODALITIES[modality]}")
values = np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0)
center = np.median(values, axis=0)
mad = np.median(np.abs(values - center), axis=0)
scale = 1.4826 * mad
std = np.std(values, axis=0)
scale = np.where(scale > 1e-6, scale, std)
scale = np.where(scale > 1e-6, scale, 1.0)
centers.append(center.astype(np.float32))
scales.append(scale.astype(np.float32))
return RobustStats(tuple(centers), tuple(scales))
def apply_robust_stats(split: Split, stats: RobustStats) -> Split:
xs: list[np.ndarray] = []
for modality in range(3):
values = (split.x[modality] - stats.center[modality]) / stats.scale[modality]
values = np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0)
values *= split.mask[:, :, modality, None]
xs.append(values.astype(np.float32, copy=False))
return Split(tuple(xs), split.mask.copy(), split.y_cls, split.y_reg, split.ids)
def corrupt_masks(
base: np.ndarray,
ratio: float,
modalities: tuple[int, ...],
seed: int,
) -> np.ndarray:
result = base.copy()
rng = np.random.default_rng(seed)
n, steps, _ = result.shape
width = max(1, min(steps, int(round(ratio * steps))))
starts = rng.integers(0, steps - width + 1, size=n)
for row, start in enumerate(starts.tolist()):
result[row, start:start + width, list(modalities)] = False
return result
def augment_masks(base: np.ndarray, rng: np.random.Generator) -> np.ndarray:
result = base.copy()
n, steps, _ = result.shape
for row in range(n):
if rng.random() >= 0.85:
continue
count = int(rng.integers(1, 4))
modalities = rng.choice(3, size=count, replace=False)
ratio = float(rng.choice((0.10, 0.20, 0.30)))
width = max(1, int(round(ratio * steps)))
start = int(rng.integers(0, steps - width + 1))
result[row, start:start + width, modalities] = False
return result
def shift_audio_vision(split: Split, seed: int, max_shift: int = 10) -> Split:
rng = np.random.default_rng(seed)
xs = [x.copy() for x in split.x]
masks = split.mask.copy()
for row in range(split.n):
for modality in (1, 2):
shift = int(rng.integers(1, max_shift + 1))
if rng.random() < 0.5:
shift = -shift
xs[modality][row] = np.roll(xs[modality][row], shift, axis=0)
masks[row, :, modality] = np.roll(masks[row, :, modality], shift)
return Split(tuple(xs), masks, split.y_cls, split.y_reg, split.ids)
@@ -0,0 +1,614 @@
"""Score the frozen EarlyConcat and MoFE checkpoints using the math-Q2 protocol.
This script performs no training and selects no models. It evaluates the saved
three-seed checkpoints on the official labeled test split once, and reuses the
fixed 42-scenario validation-mask audit as the controlled-missingness protocol.
This source is retained for protocol helpers used by the standalone training runner.
"""
from __future__ import annotations
import csv
import argparse
import hashlib
import json
import math
import statistics
import time
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
import torch
from sklearn.metrics import accuracy_score, f1_score, mean_absolute_error, mean_squared_error
from torch import nn
from .data import (
ATTACHMENT2,
MODALITIES,
RobustStats,
Split,
_ids_and_targets,
_text_mask,
_unpickle,
apply_robust_stats,
fit_robust_stats,
load_aligned,
)
from .models import AlignedFusionModel
from .mofe import MixtureOfFusionExperts
from .train_mofe import MODEL_CONFIG, _predict, _device_for, EARLYCONCAT, MOFE7_MLP
Q2_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = Q2_ROOT.parents[1]
REFERENCE_DIR = Q2_ROOT / "outputs" / "followups" / "R01_selected_model_reevaluation"
OUTPUT_DIR = Q2_ROOT / "outputs" / "followups" / "R02_math_protocol_evaluation"
SEEDS = (42, 3407, 2026)
BOOTSTRAP_REPS = 1000
TEST_BOOTSTRAP_SEED = 20260925
AURC_BOOTSTRAP_SEED = 20260926
SCENARIO_SEED = 20261833
METHODS = (EARLYCONCAT, MOFE7_MLP)
CURVE_MODES = ("single", "sync", "partial", "async")
CURVE_RATES = (0.0, 0.1, 0.3, 0.5, 0.7)
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
if not rows:
return
path.parent.mkdir(parents=True, exist_ok=True)
fields = list(dict.fromkeys(key for row in rows for key in row))
with path.open("w", newline="", encoding="utf-8-sig") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def split_from_part(part: dict[str, Any]) -> Split:
xs = tuple(np.asarray(part[name], dtype=np.float32) for name in MODALITIES)
masks = [
_text_mask(part),
np.any(np.isfinite(xs[1]) & (xs[1] != 0), axis=-1),
np.any(np.isfinite(xs[2]) & (xs[2] != 0), axis=-1),
]
ids, y_cls, y_reg = _ids_and_targets(part)
return Split(xs, np.stack(masks, axis=-1), y_cls, y_reg, ids)
def load_splits(feature_path: Path) -> dict[str, Split]:
raw = _unpickle(feature_path)
usual = load_aligned(feature_path)
splits = {"train": usual["train"], "valid": usual["valid"], "test": split_from_part(raw["test"])}
groups = {
name: {sample_id.split("$_$", 1)[0] for sample_id in split.ids}
for name, split in splits.items()
}
for first, second in (("train", "valid"), ("train", "test"), ("valid", "test")):
overlap = groups[first] & groups[second]
if overlap:
raise ValueError(f"official {first}/{second} source-video groups overlap: {len(overlap)}")
for name, split in splits.items():
expected = np.where(split.y_reg < 0, 0, np.where(split.y_reg == 0, 1, 2))
if not np.array_equal(expected, split.y_cls):
raise ValueError(f"{name}: classification labels disagree with strict sign of regression labels")
return splits
def metrics(split: Split, logits: np.ndarray, intensity: np.ndarray, indices: np.ndarray | None = None) -> dict[str, float]:
if indices is None:
indices = np.arange(split.n)
y_cls = split.y_cls[indices]
y_reg = split.y_reg[indices]
pred_cls = np.asarray(logits)[indices].argmax(axis=-1)
pred_reg = np.clip(np.asarray(intensity).reshape(-1)[indices], -3.0, 3.0)
return {
"accuracy": float(accuracy_score(y_cls, pred_cls)),
"macro_f1": float(f1_score(y_cls, pred_cls, labels=[0, 1, 2], average="macro", zero_division=0)),
"mae": float(mean_absolute_error(y_reg, pred_reg)),
"rmse": float(math.sqrt(mean_squared_error(y_reg, pred_reg))),
"pearson": float(np.corrcoef(y_reg, pred_reg)[0, 1]) if np.std(y_reg) > 0 and np.std(pred_reg) > 0 else float("nan"),
}
def load_model(method: str, seed: int, dims: tuple[int, int, int], device: torch.device) -> nn.Module:
if method == EARLYCONCAT:
checkpoint = REFERENCE_DIR / "models" / "baselines" / "concat" / f"seed_{seed}" / "model_best.pt"
model: nn.Module = AlignedFusionModel("concat", dims=dims).to(device)
state = torch.load(checkpoint, map_location=device, weights_only=False)
if state.get("kind") != "concat" or int(state.get("seed", -1)) != seed:
raise ValueError(f"unexpected EarlyConcat checkpoint: {checkpoint}")
elif method == MOFE7_MLP:
checkpoint = REFERENCE_DIR / "models" / MOFE7_MLP / f"seed_{seed}" / "model_best.pt"
model = MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG).to(device)
state = torch.load(checkpoint, map_location=device, weights_only=False)
if state.get("config") != MODEL_CONFIG or int(state.get("seed", -1)) != seed:
raise ValueError(f"unexpected MoFE checkpoint: {checkpoint}")
else:
raise ValueError(f"unknown model {method}")
if tuple(state.get("dims", ())) != dims:
raise ValueError(f"feature dimensions do not match checkpoint: {checkpoint}")
model.load_state_dict(state["state_dict"])
model.eval()
return model
def best_interval(visible: np.ndarray, wanted: int, cap: int, location: str, rng: np.random.Generator) -> tuple[int, int] | None:
steps = len(visible)
candidates: list[tuple[int, int, int, int]] = []
for left in range(steps):
hits = 0
for right in range(left, steps):
hits += int(visible[right])
count = min(hits, cap)
if count:
candidates.append((abs(count - wanted), right - left + 1, left, right))
if not candidates:
return None
best = min((error, span) for error, span, _, _ in candidates)
tied = [(left, right) for error, span, left, right in candidates if (error, span) == best]
if location == "start":
return min(tied, key=lambda pair: (pair[0], pair[1]))
if location == "end":
return max(tied, key=lambda pair: (pair[1], pair[0]))
if location == "middle":
center = (steps - 1) / 2
return min(tied, key=lambda pair: (abs((pair[0] + pair[1]) / 2 - center), pair[0]))
if location != "random":
raise ValueError(f"unknown interval location: {location}")
return tied[int(rng.integers(0, len(tied)))]
def spread_short_spans(visible: np.ndarray, wanted: int, cap: int) -> np.ndarray:
positions = np.flatnonzero(visible)
count = min(int(wanted), int(cap), len(positions))
chosen = np.zeros(len(visible), dtype=bool)
if count <= 0:
return chosen
n_spans = min(3, count)
chunks = np.array_split(positions, n_spans)
allocations = [count // n_spans + int(i < count % n_spans) for i in range(n_spans)]
for chunk, amount in zip(chunks, allocations):
if amount <= 0 or len(chunk) == 0:
continue
amount = min(amount, len(chunk))
start = max(0, (len(chunk) - amount) // 2)
chosen[chunk[start:start + amount]] = True
return chosen
def continuous_mask(
original: np.ndarray,
rate: float,
mode: str,
rng: np.random.Generator,
*,
modalities: tuple[int, ...] | None = None,
location: str = "random",
span_structure: str = "long",
) -> np.ndarray:
"""Reproduce math/Q2 continuous masking on this model's observed positions."""
observed = np.asarray(original, dtype=bool)
result = observed.copy()
if rate <= 0 or mode == "none":
return result
steps, modality_count = observed.shape
present = [m for m in range(modality_count) if observed[:, m].any()]
if not present:
return result
if modalities is not None:
selected = [int(m) for m in modalities if int(m) in present]
if not selected:
return result
elif mode == "single":
selected = [int(rng.choice(present))]
elif mode in {"sync", "partial", "async"}:
if len(present) == 1:
selected = present
else:
count = int(rng.integers(2, min(3, len(present)) + 1))
selected = sorted(int(v) for v in rng.choice(present, size=count, replace=False))
else:
raise ValueError(f"unknown mask mode: {mode}")
def max_hide(modality: int) -> int:
count = int(observed[:, modality].sum())
keep = max(1, int(math.ceil(0.2 * count)))
return max(0, count - keep)
target = {m: min(max_hide(m), int(round(rate * int(observed[:, m].sum())))) for m in selected}
if mode == "sync":
span = max(1, int(round(rate * steps)))
if location == "start":
left = 0
elif location == "end":
left = steps - span
elif location == "middle":
left = (steps - span) // 2
elif location == "random":
left = int(rng.integers(0, max(1, steps - span + 1)))
else:
raise ValueError(f"unknown interval location: {location}")
right = min(steps - 1, left + span - 1)
for m in selected:
candidates = np.flatnonzero(observed[left:right + 1, m]) + left
amount = min(len(candidates), max_hide(m), target[m])
if amount:
offset = 0 if location != "end" else len(candidates) - amount
result[candidates[max(0, offset):max(0, offset) + amount], m] = False
else:
common_span = max(1, int(round(rate * steps)))
for rank, m in enumerate(selected):
wanted = target[m]
if wanted <= 0:
continue
cap = max_hide(m)
if span_structure == "multi_short":
hide = spread_short_spans(observed[:, m], wanted, cap)
elif span_structure != "long":
raise ValueError(f"unknown span structure: {span_structure}")
elif mode == "single" and location != "random":
# Place a contiguous block at the requested relative location
# among observed positions, while keeping the selected-source
# missing amount fixed. This avoids treating padding as time.
interval = best_interval(observed[:, m], wanted, cap, location, rng)
hide = np.zeros(steps, dtype=bool)
if interval is not None:
left, right = interval
candidates = np.flatnonzero(observed[left:right + 1, m]) + left
amount = min(len(candidates), wanted, cap)
if amount:
offset = 0 if location != "end" else len(candidates) - amount
hide[candidates[max(0, offset):max(0, offset) + amount]] = True
elif mode in {"partial", "async"}:
if mode == "partial":
base_left = int(rng.integers(0, max(1, steps - common_span + 1))) if location == "random" else (
0 if location == "start" else steps - common_span if location == "end" else (steps - common_span) // 2
)
offset = int(round(rank * common_span * 0.5))
else:
base_left = 0 if location == "random" else (
0 if location == "start" else steps - common_span if location == "end" else (steps - common_span) // 2
)
available = max(1, steps - common_span + 1)
offsets = np.rint(np.linspace(0, max(0, available - 1), len(selected))).astype(int)
if location == "random":
rng.shuffle(offsets)
offset = int(offsets[rank])
left = min(max(0, base_left + offset), max(0, steps - common_span))
right = min(steps - 1, left + common_span - 1)
hide = np.zeros(steps, dtype=bool)
candidates = np.flatnonzero(observed[left:right + 1, m]) + left
amount = min(len(candidates), wanted, cap)
if amount:
hide[candidates[:amount]] = True
else:
interval = best_interval(observed[:, m], wanted, cap, location, rng)
hide = np.zeros(steps, dtype=bool)
if interval is not None:
left, right = interval
candidates = np.flatnonzero(observed[left:right + 1, m]) + left
amount = min(len(candidates), wanted, cap)
if amount:
offset = 0 if location != "end" else len(candidates) - amount
hide[candidates[max(0, offset):max(0, offset) + amount]] = True
result[hide, m] = False
return result
def scenario_seed(seed: int, sample_id: str, key: str) -> int:
return int.from_bytes(hashlib.sha256(f"{seed}:{sample_id}:{key}".encode()).digest()[:8], "little")
def make_scenarios(valid: Split, seed: int = SCENARIO_SEED) -> dict[str, np.ndarray]:
scenarios = {"0.0/none": valid.mask.copy()}
for rate in CURVE_RATES[1:]:
for mode in CURVE_MODES:
key = f"{rate:.1f}/{mode}"
scenarios[key] = np.stack([
continuous_mask(mask, rate, mode, np.random.default_rng(scenario_seed(seed, sample_id, key)))
for sample_id, mask in zip(valid.ids, valid.mask)
])
modality_sets = (((0,), "T"), ((1,), "A"), ((2,), "V"), ((0, 1), "TA"), ((0, 2), "TV"), ((1, 2), "AV"), ((0, 1, 2), "TAV"))
for selected, label in modality_sets:
key = f"0.3/modality_{label}"
scenarios[key] = np.stack([
continuous_mask(mask, 0.3, "sync", np.random.default_rng(scenario_seed(seed, sample_id, key)), modalities=selected)
for sample_id, mask in zip(valid.ids, valid.mask)
])
for modality_index, label in enumerate(("T", "A", "V")):
for location in ("start", "middle", "end"):
key = f"0.3/location_{location}_{label}"
scenarios[key] = np.stack([
continuous_mask(mask, 0.3, "single", np.random.default_rng(scenario_seed(seed, sample_id, key)), modalities=(modality_index,), location=location)
for sample_id, mask in zip(valid.ids, valid.mask)
])
for structure in ("long", "multi_short"):
key = f"0.3/span_{structure}_{label}"
scenarios[key] = np.stack([
continuous_mask(mask, 0.3, "single", np.random.default_rng(scenario_seed(seed, sample_id, key)), modalities=(modality_index,), span_structure=structure)
for sample_id, mask in zip(valid.ids, valid.mask)
])
for mode in ("sync", "partial", "async"):
key = f"0.3/synchrony_{mode}"
scenarios[key] = np.stack([
continuous_mask(mask, 0.3, mode, np.random.default_rng(scenario_seed(seed, sample_id, key)), modalities=(0, 1, 2))
for sample_id, mask in zip(valid.ids, valid.mask)
])
return scenarios
def actual_additional_rates(base: np.ndarray, scenarios: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
result = {}
observed = base.sum(axis=1)
for scenario, current in scenarios.items():
newly_hidden = base & ~current
hidden_count = newly_hidden.sum(axis=1)
by_modality = np.divide(
hidden_count,
observed,
out=np.full(hidden_count.shape, np.nan, dtype=np.float64),
where=observed > 0,
)
result[scenario] = np.nanmean(by_modality, axis=1)
return result
def aurc_from_curve(rates: list[float], maes: list[float]) -> float:
order = np.argsort(np.asarray(rates), kind="stable")
x = np.asarray(rates, dtype=np.float64)[order]
y = np.asarray(maes, dtype=np.float64)[order]
unique_x, inverse = np.unique(x, return_inverse=True)
unique_y = np.asarray([y[inverse == i].mean() for i in range(len(unique_x))])
if len(unique_x) <= 1 or unique_x[-1] <= 0:
return float(maes[0])
return float(np.trapezoid(unique_y, unique_x) / unique_x[-1])
def curve_scenarios(mode: str) -> list[str]:
return ["0.0/none"] + [f"{rate:.1f}/{mode}" for rate in CURVE_RATES[1:]]
def group_indices(ids: list[str]) -> tuple[list[str], dict[str, np.ndarray]]:
groups = sorted({sample_id.split("$_$", 1)[0] for sample_id in ids})
mapping = {group: np.flatnonzero(np.asarray([x.split("$_$", 1)[0] == group for x in ids])) for group in groups}
return groups, mapping
def bootstrap_clean_test(
split: Split,
preds: dict[tuple[str, int], dict[str, np.ndarray]],
) -> list[dict[str, Any]]:
groups, mapping = group_indices(split.ids)
rng = np.random.default_rng(TEST_BOOTSTRAP_SEED)
draws: dict[str, list[float]] = defaultdict(list)
for _ in range(BOOTSTRAP_REPS):
chosen = rng.choice(groups, size=len(groups), replace=True)
indices = np.concatenate([mapping[group] for group in chosen])
per_method = {}
for method in METHODS:
per_seed = [metrics(split, preds[(method, seed)]["logits"], preds[(method, seed)]["intensity"], indices) for seed in SEEDS]
per_method[method] = {key: float(np.mean([row[key] for row in per_seed])) for key in per_seed[0]}
for metric in per_method[EARLYCONCAT]:
draws[metric].append(per_method[MOFE7_MLP][metric] - per_method[EARLYCONCAT][metric])
rows = []
for metric, values in draws.items():
rows.append({
"comparison": "MoFE-7 + MLP Router minus EarlyConcat + BiGRU",
"metric": metric,
"delta_mean_over_seeds": float(np.mean([r[metric] for r in [
metrics(split, preds[(MOFE7_MLP, seed)]["logits"], preds[(MOFE7_MLP, seed)]["intensity"])
for seed in SEEDS
]]) - np.mean([r[metric] for r in [
metrics(split, preds[(EARLYCONCAT, seed)]["logits"], preds[(EARLYCONCAT, seed)]["intensity"])
for seed in SEEDS
]])),
"bootstrap_ci_2p5": float(np.quantile(values, 0.025)),
"bootstrap_ci_97p5": float(np.quantile(values, 0.975)),
"bootstrap_probability_delta_gt_0": float(np.mean(np.asarray(values) > 0)),
"replicates": BOOTSTRAP_REPS,
"resampling_unit": "source video id",
"paired": True,
"seed": TEST_BOOTSTRAP_SEED,
})
return rows
def bootstrap_aurc(
valid: Split,
predictions: dict[tuple[str, int, str], dict[str, np.ndarray]],
scenarios: dict[str, np.ndarray],
rates_by_sample: dict[str, np.ndarray],
) -> list[dict[str, Any]]:
groups, mapping = group_indices(valid.ids)
rng = np.random.default_rng(AURC_BOOTSTRAP_SEED)
delta_by_mode: dict[str, list[float]] = {mode: [] for mode in CURVE_MODES}
for _ in range(BOOTSTRAP_REPS):
chosen = rng.choice(groups, size=len(groups), replace=True)
indices = np.concatenate([mapping[group] for group in chosen])
for mode in CURVE_MODES:
keys = curve_scenarios(mode)
model_aucs: dict[str, list[float]] = {method: [] for method in METHODS}
for method in METHODS:
for seed in SEEDS:
xs = [float(np.nanmean(rates_by_sample[key][indices])) for key in keys]
ys = [float(np.abs(valid.y_reg[indices] - predictions[(method, seed, key)]["intensity"][indices]).mean()) for key in keys]
model_aucs[method].append(aurc_from_curve(xs, ys))
delta_by_mode[mode].append(float(np.mean(model_aucs[MOFE7_MLP]) - np.mean(model_aucs[EARLYCONCAT])))
point = {}
for mode in CURVE_MODES:
model_aucs = {}
for method in METHODS:
model_aucs[method] = []
for seed in SEEDS:
keys = curve_scenarios(mode)
xs = [float(np.nanmean(rates_by_sample[key])) for key in keys]
ys = [float(np.abs(valid.y_reg - predictions[(method, seed, key)]["intensity"]).mean()) for key in keys]
model_aucs[method].append(aurc_from_curve(xs, ys))
point[mode] = float(np.mean(model_aucs[MOFE7_MLP]) - np.mean(model_aucs[EARLYCONCAT]))
rows = []
for mode, values in delta_by_mode.items():
rows.append({
"mode": mode,
"delta_aurc_mae_mofe_minus_earlyconcat": point[mode],
"bootstrap_ci_2p5": float(np.quantile(values, 0.025)),
"bootstrap_ci_97p5": float(np.quantile(values, 0.975)),
"bootstrap_probability_delta_lt_0": float(np.mean(np.asarray(values) < 0)),
"replicates": BOOTSTRAP_REPS,
"resampling_unit": "source video id",
"paired": True,
"seed": AURC_BOOTSTRAP_SEED,
})
return rows
def run(device_name: str = "auto", batch_size: int = 64, masks_only: bool = False) -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
device = _device_for(device_name)
torch.set_num_threads(4)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
feature_path = ATTACHMENT2 / "aligned_50.pkl"
with (REFERENCE_DIR / "run_manifest.json").open("r", encoding="utf-8") as stream:
reference_manifest = json.load(stream)
if sha256(feature_path) != reference_manifest["feature_sha256"]:
raise ValueError("current official feature file hash differs from the checkpoint evaluation manifest")
raw_splits = load_splits(feature_path)
train = raw_splits["train"]
valid = raw_splits["valid"]
test = raw_splits["test"]
scaler_path = REFERENCE_DIR / "aligned_robust_stats.npz"
stats = RobustStats.load(scaler_path)
computed = fit_robust_stats(train)
scaler_diff = max(
max(float(np.max(np.abs(a - b))) for a, b in zip(computed.center, stats.center)),
max(float(np.max(np.abs(a - b))) for a, b in zip(computed.scale, stats.scale)),
)
if scaler_diff > 1e-6:
raise ValueError(f"checkpoint scaler is not the train-only scaler (max difference {scaler_diff})")
valid = apply_robust_stats(valid, stats)
test = apply_robust_stats(test, stats)
dims = tuple(x.shape[-1] for x in train.x)
if not masks_only:
# Final, clean official-test evaluation; no retraining or selection occurs here.
test_predictions: dict[tuple[str, int], dict[str, np.ndarray]] = {}
test_rows: list[dict[str, Any]] = []
for method in METHODS:
for seed in SEEDS:
model = load_model(method, seed, dims, device)
prediction = _predict(model, test, test.mask, device, batch_size)
test_predictions[(method, seed)] = prediction
test_rows.append({"method": method, "seed": seed, "n_test": test.n, **metrics(test, prediction["logits"], prediction["intensity"])})
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
summary_rows = []
for method in METHODS:
subset = [row for row in test_rows if row["method"] == method]
for metric in ("accuracy", "macro_f1", "mae", "rmse", "pearson"):
values = [float(row[metric]) for row in subset]
summary_rows.append({"method": method, "metric": metric, "mean": float(np.mean(values)), "sd_across_seeds": float(np.std(values, ddof=1))})
write_csv(OUTPUT_DIR / "official_test_metrics_by_seed.csv", test_rows)
write_csv(OUTPUT_DIR / "official_test_summary.csv", summary_rows)
write_csv(OUTPUT_DIR / "official_test_paired_bootstrap.csv", bootstrap_clean_test(test, test_predictions))
# Reproduce the math-Q2 42-scenario design with a per-sample stable seed,
# while applying it to the observation masks used to train these models.
scenario_masks = make_scenarios(valid)
rates_by_sample = actual_additional_rates(valid.mask, scenario_masks)
condition_predictions: dict[tuple[str, int, str], dict[str, np.ndarray]] = {}
condition_rows: list[dict[str, Any]] = []
for method in METHODS:
for seed in SEEDS:
model = load_model(method, seed, dims, device)
for scenario, masks in scenario_masks.items():
prediction = _predict(model, valid, masks, device, batch_size)
condition_predictions[(method, seed, scenario)] = prediction
values = metrics(valid, prediction["logits"], prediction["intensity"])
condition_rows.append({
"method": method,
"seed": seed,
"scenario": scenario,
"realized_additional_global_rate": float(np.nanmean(rates_by_sample[scenario])),
"n_valid": valid.n,
**values,
})
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
write_csv(OUTPUT_DIR / "controlled_metrics_by_scenario.csv", condition_rows)
auc_rows: list[dict[str, Any]] = []
for method in METHODS:
for seed in SEEDS:
for mode in CURVE_MODES:
keys = curve_scenarios(mode)
xs = [float(np.nanmean(rates_by_sample[key])) for key in keys]
ys = [float(np.abs(valid.y_reg - condition_predictions[(method, seed, key)]["intensity"]).mean()) for key in keys]
auc_rows.append({"method": method, "seed": seed, "mask_mode": mode, "aurc_mae": aurc_from_curve(xs, ys), "rates_realized": json.dumps(xs)})
write_csv(OUTPUT_DIR / "aurc_mae_by_mode_seed.csv", auc_rows)
auc_summary = []
for method in METHODS:
for mode in CURVE_MODES:
values = [row["aurc_mae"] for row in auc_rows if row["method"] == method and row["mask_mode"] == mode]
auc_summary.append({"method": method, "mask_mode": mode, "mean": float(np.mean(values)), "sd_across_seeds": float(np.std(values, ddof=1))})
write_csv(OUTPUT_DIR / "aurc_mae_summary.csv", auc_summary)
write_csv(OUTPUT_DIR / "aurc_mae_paired_bootstrap.csv", bootstrap_aurc(valid, condition_predictions, scenario_masks, rates_by_sample))
manifest = {
"experiment": "Frozen EarlyConcat vs MoFE-7 evaluation under math/Q2 test protocol",
"created_unix": time.time(),
"device": str(device),
"cuda_device": torch.cuda.get_device_name(0) if device.type == "cuda" else None,
"feature_file": str(feature_path),
"feature_sha256": sha256(feature_path),
"representation": "official aligned_50 ordered positions; not physical-time bins",
"train_valid_test_counts": {name: split.n for name, split in raw_splits.items()},
"source_video_groups": {name: len({sample_id.split("$_$", 1)[0] for sample_id in split.ids}) for name, split in raw_splits.items()},
"official_group_splits_disjoint": True,
"test_evaluation": (
"one final clean evaluation on official labeled test split; no training/model selection/calibration"
if not masks_only else "test outputs preserved from the earlier single evaluation; no test prediction was rerun"
),
"test_prediction_performed_this_invocation": not masks_only,
"seeds": list(SEEDS),
"checkpoint_source": str(REFERENCE_DIR / "models"),
"train_only_scaler": str(scaler_path),
"scaler_max_abs_difference_from_train_refit": scaler_diff,
"test_labels_used_for_training_or_selection": False,
"controlled_missingness": {
"scenario_seed": SCENARIO_SEED,
"scenario_design": "math/Q2 42-scenario design regenerated on the Q2 models' BERT attention-mask base",
"scenarios": len(scenario_masks),
"AURC": "normalized trapezoidal area of MAE over realized equal-modality-weighted added missing rate, at 0/.1/.3/.5/.7 for single/sync/partial/async",
},
"bootstrap": {
"replicates": BOOTSTRAP_REPS,
"test_seed": TEST_BOOTSTRAP_SEED,
"aurc_seed": AURC_BOOTSTRAP_SEED,
"unit": "source video id",
"paired": True,
},
}
(OUTPUT_DIR / "run_manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(f"wrote math-protocol comparison to {OUTPUT_DIR}")
print(f"n_test={test.n}; n_valid={valid.n}; device={device}; scenarios={len(scenario_masks)}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--masks-only", action="store_true", help="Recompute validation mask scenarios without rerunning official-test inference")
args = parser.parse_args()
run(masks_only=args.masks_only)
+4
View File
@@ -0,0 +1,4 @@
"""Compatibility import for the maintained model registry."""
from model.early_concat import AlignedFusionModel
__all__ = ["AlignedFusionModel"]
+4
View File
@@ -0,0 +1,4 @@
"""Compatibility import for the maintained model registry."""
from model.mofe import EXPERT_NAMES, SUBSETS, MixtureOfFusionExperts
__all__ = ["EXPERT_NAMES", "SUBSETS", "MixtureOfFusionExperts"]
+490
View File
@@ -0,0 +1,490 @@
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import math
import random
import shutil
import time
from collections import Counter
from pathlib import Path
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn.functional as F
from sklearn.metrics import accuracy_score, f1_score, mean_absolute_error
from torch import nn
from .data import (
ATTACHMENT2,
ROOT,
MODALITIES,
RobustStats,
Split,
apply_robust_stats,
augment_masks,
corrupt_masks,
fit_robust_stats,
load_aligned,
load_fixed_window,
shift_audio_vision,
)
from .models import AlignedFusionModel
PATTERNS = {
"text": (0,),
"audio": (1,),
"vision": (2,),
"audio_vision": (1, 2),
"all_modalities": (0, 1, 2),
}
KINDS = ("concat",)
def seed_everything(seed: int) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def _tensor_split(split: Split, device: torch.device) -> tuple[tuple[torch.Tensor, ...], torch.Tensor, torch.Tensor, torch.Tensor]:
xs = tuple(torch.as_tensor(x, dtype=torch.float32, device=device) for x in split.x)
mask = torch.as_tensor(split.mask, dtype=torch.bool, device=device)
y_cls = torch.as_tensor(split.y_cls, dtype=torch.long, device=device)
y_reg = torch.as_tensor(split.y_reg, dtype=torch.float32, device=device)
return xs, mask, y_cls, y_reg
def _loss(output: dict[str, torch.Tensor], y_cls: torch.Tensor, y_reg: torch.Tensor) -> torch.Tensor:
class_loss = F.cross_entropy(output["logits"], y_cls)
intensity_loss = F.smooth_l1_loss(output["intensity"] / 3.0, y_reg / 3.0)
return class_loss + 0.5 * intensity_loss
@torch.inference_mode()
def _score_arrays(
model: AlignedFusionModel,
split: Split,
mask: np.ndarray,
device: torch.device,
batch_size: int = 128,
) -> tuple[dict[str, float], dict[str, np.ndarray]]:
model.eval()
predictions: dict[str, list[np.ndarray]] = {"logits": [], "intensity": []}
xs = split.x
for start in range(0, split.n, batch_size):
end = min(start + batch_size, split.n)
xb = tuple(torch.as_tensor(x[start:end], dtype=torch.float32, device=device) for x in xs)
mb = torch.as_tensor(mask[start:end], dtype=torch.bool, device=device)
output = model(xb, mb)
predictions["logits"].append(output["logits"].float().cpu().numpy())
predictions["intensity"].append(output["intensity"].float().cpu().numpy())
logits = np.concatenate(predictions["logits"], axis=0)
intensity = np.clip(np.concatenate(predictions["intensity"], axis=0), -3.0, 3.0)
pred_cls = logits.argmax(axis=-1)
pearson = _pearson(split.y_reg, intensity)
metrics = {
"accuracy": float(accuracy_score(split.y_cls, pred_cls)),
"macro_f1": float(f1_score(split.y_cls, pred_cls, labels=[0, 1, 2], average="macro", zero_division=0)),
"mae": float(mean_absolute_error(split.y_reg, intensity)),
"pearson": pearson,
}
return metrics, {"logits": logits, "intensity": intensity, "class": pred_cls}
def _pearson(y: np.ndarray, pred: np.ndarray) -> float:
a = np.asarray(y, dtype=np.float64)
b = np.asarray(pred, dtype=np.float64)
if a.std() < 1e-12 or b.std() < 1e-12:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
def _validation_loss(model: AlignedFusionModel, valid: Split, device: torch.device, batch_size: int) -> float:
model.eval()
xs, masks, y_cls, y_reg = _tensor_split(valid, device)
losses: list[float] = []
with torch.inference_mode():
for start in range(0, valid.n, batch_size):
idx = slice(start, min(start + batch_size, valid.n))
output = model(tuple(x[idx] for x in xs), masks[idx])
losses.append(float(_loss(output, y_cls[idx], y_reg[idx]).item()))
return float(np.average(losses, weights=[min(batch_size, valid.n - i) for i in range(0, valid.n, batch_size)]))
def _write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not rows:
return
fields = list(dict.fromkeys(key for row in rows for key in row))
with path.open("w", newline="", encoding="utf-8-sig") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
def _train_one(
kind: str,
train: Split,
valid: Split,
output_dir: Path,
device: torch.device,
seed: int,
epochs: int,
patience: int,
batch_size: int,
) -> tuple[AlignedFusionModel, int, list[dict[str, float]]]:
seed_everything(seed)
dims = tuple(int(x.shape[-1]) for x in train.x)
model = AlignedFusionModel(kind, dims=dims).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1.5e-4, weight_decay=1e-4)
train_tensors = _tensor_split(train, device)
xs, base_masks, y_cls, y_reg = train_tensors
rng = np.random.default_rng(seed + 809)
best_loss = math.inf
best_epoch = 0
stale_epochs = 0
history: list[dict[str, float]] = []
checkpoint_path = output_dir / "model_best.pt"
output_dir.mkdir(parents=True, exist_ok=True)
for epoch in range(1, epochs + 1):
model.train()
order = rng.permutation(train.n)
batch_losses: list[float] = []
for start in range(0, train.n, batch_size):
ids_np = order[start:start + batch_size]
ids = torch.as_tensor(ids_np, dtype=torch.long, device=device)
masks_np = augment_masks(train.mask[ids_np], rng)
masks = torch.as_tensor(masks_np, dtype=torch.bool, device=device)
output = model(tuple(x.index_select(0, ids) for x in xs), masks)
loss = _loss(output, y_cls.index_select(0, ids), y_reg.index_select(0, ids))
optimizer.zero_grad(set_to_none=True)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
batch_losses.append(float(loss.detach().item()))
valid_loss = _validation_loss(model, valid, device, batch_size)
row = {"epoch": float(epoch), "train_loss": float(np.mean(batch_losses)), "valid_clean_loss": valid_loss}
history.append(row)
print(f"[{kind}] epoch={epoch:02d} train={row['train_loss']:.4f} valid={valid_loss:.4f}", flush=True)
if valid_loss < best_loss - 1e-4:
best_loss = valid_loss
best_epoch = epoch
stale_epochs = 0
torch.save({"kind": kind, "dims": dims, "state_dict": model.state_dict(), "seed": seed, "best_epoch": epoch}, checkpoint_path)
else:
stale_epochs += 1
if stale_epochs >= patience:
break
saved = torch.load(checkpoint_path, map_location=device, weights_only=False)
model.load_state_dict(saved["state_dict"])
model.eval()
_write_csv(output_dir / "training_history.csv", history)
return model, best_epoch, history
def _conditions(valid: Split, seed: int) -> list[tuple[str, float, np.ndarray]]:
result = [("clean", 0.0, valid.mask.copy())]
for rate in (0.10, 0.20, 0.30):
for pattern_id, (pattern, mods) in enumerate(PATTERNS.items()):
result.append((pattern, rate, corrupt_masks(valid.mask, rate, mods, seed + pattern_id * 101 + int(rate * 1000))))
return result
def _eval_conditions(
model: AlignedFusionModel,
valid: Split,
device: torch.device,
seed: int,
seed_run: int,
method: str,
representation: str,
) -> list[dict[str, Any]]:
rows = []
for condition, rate, masks in _conditions(valid, seed):
metrics, _ = _score_arrays(model, valid, masks, device)
rows.append({"method": method, "representation": representation, "seed": seed_run, "condition": condition,
"missing_rate": rate, "n_valid": valid.n, **metrics})
print(f"[{method}/{representation}] {condition:14s} rate={rate:.1f} "
f"F1={metrics['macro_f1']:.3f} MAE={metrics['mae']:.3f} "
f"P={metrics['pearson']:.3f}", flush=True)
return rows
def _summary(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups = list(dict.fromkeys((row["method"], row["representation"]) for row in rows))
summary: list[dict[str, Any]] = []
for method, representation in groups:
matching = [r for r in rows if r["method"] == method and r["representation"] == representation]
local = [r for r in matching if r["condition"] != "clean" and r["missing_rate"] > 0]
clean = [r for r in matching if r["condition"] == "clean"]
seeds = sorted({int(r.get("seed", 0)) for r in matching})
def per_seed_mean(selected: list[dict[str, Any]], metric: str) -> list[float]:
return [float(np.mean([r[metric] for r in selected if int(r.get("seed", 0)) == seed]))
for seed in seeds if any(int(r.get("seed", 0)) == seed for r in selected)]
clean_f1 = per_seed_mean(clean, "macro_f1")
clean_accuracy = per_seed_mean(clean, "accuracy")
clean_mae = per_seed_mean(clean, "mae")
clean_pearson = per_seed_mean(clean, "pearson")
corrupt_f1 = per_seed_mean(local, "macro_f1")
corrupt_accuracy = per_seed_mean(local, "accuracy")
corrupt_mae = per_seed_mean(local, "mae")
corrupt_pearson = per_seed_mean(local, "pearson")
row: dict[str, Any] = {
"method": method,
"representation": representation,
"n_seeds": len(seeds),
"clean_accuracy": float(np.mean(clean_accuracy)),
"clean_accuracy_sd": float(np.std(clean_accuracy, ddof=1)) if len(clean_accuracy) > 1 else 0.0,
"clean_macro_f1": float(np.mean(clean_f1)),
"clean_macro_f1_sd": float(np.std(clean_f1, ddof=1)) if len(clean_f1) > 1 else 0.0,
"clean_mae": float(np.mean(clean_mae)),
"clean_mae_sd": float(np.std(clean_mae, ddof=1)) if len(clean_mae) > 1 else 0.0,
"clean_pearson": float(np.mean(clean_pearson)),
"clean_pearson_sd": float(np.std(clean_pearson, ddof=1)) if len(clean_pearson) > 1 else 0.0,
"corrupt_accuracy_mean": float(np.mean(corrupt_accuracy)),
"corrupt_accuracy_sd": float(np.std(corrupt_accuracy, ddof=1)) if len(corrupt_accuracy) > 1 else 0.0,
"corrupt_macro_f1_mean": float(np.mean(corrupt_f1)),
"corrupt_macro_f1_sd": float(np.std(corrupt_f1, ddof=1)) if len(corrupt_f1) > 1 else 0.0,
"corrupt_macro_f1_worst": float(np.min([r["macro_f1"] for r in local])),
"corrupt_mae_mean": float(np.mean(corrupt_mae)),
"corrupt_mae_sd": float(np.std(corrupt_mae, ddof=1)) if len(corrupt_mae) > 1 else 0.0,
"corrupt_pearson_mean": float(np.mean(corrupt_pearson)),
"corrupt_pearson_sd": float(np.std(corrupt_pearson, ddof=1)) if len(corrupt_pearson) > 1 else 0.0,
}
for rate in (0.10, 0.20, 0.30):
at_rate = [r for r in local if r["missing_rate"] == rate]
f1_by_seed = per_seed_mean(at_rate, "macro_f1")
accuracy_by_seed = per_seed_mean(at_rate, "accuracy")
mae_by_seed = per_seed_mean(at_rate, "mae")
row[f"f1_rate_{int(rate * 100)}"] = float(np.mean(f1_by_seed))
row[f"accuracy_rate_{int(rate * 100)}"] = float(np.mean(accuracy_by_seed))
row[f"mae_rate_{int(rate * 100)}"] = float(np.mean(mae_by_seed))
summary.append(row)
for row in summary:
row["pareto_nondominated"] = not any(
other is not row and other["representation"] == row["representation"]
and other["corrupt_macro_f1_mean"] >= row["corrupt_macro_f1_mean"]
and other["corrupt_mae_mean"] <= row["corrupt_mae_mean"]
and other["corrupt_pearson_mean"] >= row["corrupt_pearson_mean"]
and (
other["corrupt_macro_f1_mean"] > row["corrupt_macro_f1_mean"]
or other["corrupt_mae_mean"] < row["corrupt_mae_mean"]
or other["corrupt_pearson_mean"] > row["corrupt_pearson_mean"]
)
for other in summary
)
return summary
def _plot(summary: list[dict[str, Any]], rows: list[dict[str, Any]], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
colors = {"concat": "#4e79a7"}
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4), constrained_layout=True)
for row in summary:
kind = row["method"]
y_f1 = [row["clean_macro_f1"]] + [row[f"f1_rate_{r}"] for r in (10, 20, 30)]
y_mae = [row["clean_mae"]] + [row[f"mae_rate_{r}"] for r in (10, 20, 30)]
axes[0].plot([0, 10, 20, 30], y_f1, marker="o", label=kind, color=colors.get(kind))
axes[1].plot([0, 10, 20, 30], y_mae, marker="o", label=kind, color=colors.get(kind))
axes[0].set(title="Polarity under contiguous local missingness", xlabel="masked slots (%)", ylabel="Macro-F1 (higher is better)")
axes[1].set(title="Intensity under contiguous local missingness", xlabel="masked slots (%)", ylabel="MAE (lower is better)")
for ax in axes:
ax.grid(alpha=0.25)
ax.legend(frameon=False)
fig.savefig(path, dpi=180)
plt.close(fig)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _run(args: argparse.Namespace) -> None:
seed_everything(args.seeds[0])
if args.device == "auto":
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
else:
device = torch.device(args.device)
torch.set_num_threads(args.threads)
output = Path(args.output_dir)
output.mkdir(parents=True, exist_ok=True)
aligned_raw = load_aligned()
stats = fit_robust_stats(aligned_raw["train"])
stats.save(output / "aligned_robust_stats.npz")
aligned = {k: apply_robust_stats(v, stats) for k, v in aligned_raw.items()}
audit = {
"source": str(ATTACHMENT2 / "aligned_50.pkl"),
"train_samples": aligned["train"].n,
"valid_samples": aligned["valid"].n,
"train_classes": np.bincount(aligned["train"].y_cls, minlength=3).tolist(),
"valid_classes": np.bincount(aligned["valid"].y_cls, minlength=3).tolist(),
"mean_observed_slots": {
MODALITIES[m]: float(aligned["train"].mask[:, :, m].sum(axis=1).mean()) for m in range(3)
},
"train_valid_video_overlap": 0,
}
with (output / "data_audit.json").open("w", encoding="utf-8") as stream:
json.dump(audit, stream, ensure_ascii=False, indent=2)
print(f"device={device}; train={audit['train_samples']}; valid={audit['valid_samples']}; audit={audit}", flush=True)
metric_rows: list[dict[str, Any]] = []
best_epochs: dict[str, int] = {}
for kind in KINDS:
for seed in args.seeds:
seed_dir = output / "models" / "aligned" / kind / f"seed_{seed}"
model, best_epoch, _ = _train_one(
kind, aligned["train"], aligned["valid"], seed_dir,
device, seed, args.epochs, args.patience, args.batch_size,
)
best_epochs[f"{kind}_seed_{seed}"] = best_epoch
metric_rows.extend(_eval_conditions(model, aligned["valid"], device, seed + 13, seed, kind, "provided_word_aligned_50"))
if seed == args.seeds[0]:
shutil.copy2(seed_dir / "model_best.pt", output / "models" / "aligned" / kind / "model_best.pt")
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
summary = _summary(metric_rows)
selected = sorted(summary, key=lambda r: (-r["corrupt_macro_f1_mean"], r["corrupt_mae_mean"], r["method"]))[0]["method"]
(output / "selected_method.txt").write_text(
f"Macro-F1-first validation selection: {selected}. See summary.csv for the full multi-metric tradeoff.\n",
encoding="utf-8",
)
# Matched audio/vision temporal-shift control for the selected architecture and every seed.
for seed in args.seeds:
aligned_payload = torch.load(output / "models" / "aligned" / selected / f"seed_{seed}" / "model_best.pt",
map_location=device, weights_only=False)
aligned_model = AlignedFusionModel(selected, tuple(aligned_payload["dims"])).to(device)
aligned_model.load_state_dict(aligned_payload["state_dict"])
shifted = shift_audio_vision(aligned["valid"], seed=seed + 2026, max_shift=10)
shift_metrics, _ = _score_arrays(aligned_model, shifted, shifted.mask, device)
metric_rows.append({"method": selected, "representation": "provided_word_aligned_50", "seed": seed,
"condition": "audio_vision_shifted_1_to_10_slots", "missing_rate": 0.0,
"n_valid": shifted.n, **shift_metrics})
del aligned_model
if torch.cuda.is_available():
torch.cuda.empty_cache()
# Same selected fusion architecture, but equal-window audio/vision pooling of the unaligned source.
print(f"selected_by_corrupt_macro_f1={selected}; starting fixed-window alignment control", flush=True)
fixed_raw = load_fixed_window()
fixed_stats = fit_robust_stats(fixed_raw["train"])
fixed_stats.save(output / "fixed_window_robust_stats.npz")
fixed = {k: apply_robust_stats(v, fixed_stats) for k, v in fixed_raw.items()}
for seed in args.seeds:
fixed_model, fixed_epoch, _ = _train_one(
selected, fixed["train"], fixed["valid"], output / "models" / "fixed_window" / selected / f"seed_{seed}",
device, seed, args.epochs, args.patience, args.batch_size,
)
best_epochs[f"fixed_window_{selected}_seed_{seed}"] = fixed_epoch
metric_rows.extend(_eval_conditions(fixed_model, fixed["valid"], device, seed + 13, seed, selected,
"equal_window_resampled_unaligned"))
fixed_shifted = shift_audio_vision(fixed["valid"], seed=seed + 2026, max_shift=10)
fixed_shift_metrics, _ = _score_arrays(fixed_model, fixed_shifted, fixed_shifted.mask, device)
metric_rows.append({"method": selected, "representation": "equal_window_resampled_unaligned", "seed": seed,
"condition": "audio_vision_shifted_1_to_10_slots", "missing_rate": 0.0,
"n_valid": fixed_shifted.n, **fixed_shift_metrics})
del fixed_model
if torch.cuda.is_available():
torch.cuda.empty_cache()
all_summary = _summary(metric_rows)
_write_csv(output / "validation_metrics_by_condition.csv", metric_rows)
_write_csv(output / "summary.csv", all_summary)
aligned_summary = [r for r in all_summary if r["representation"] == "provided_word_aligned_50"]
_plot(aligned_summary, metric_rows, output / "missing_rate_comparison.png")
alignment_rows = []
for rep in ("provided_word_aligned_50", "equal_window_resampled_unaligned"):
for condition in ("clean", "audio_vision_shifted_1_to_10_slots"):
match = [r for r in metric_rows if r["method"] == selected and r["representation"] == rep
and r["condition"] == condition]
if match:
row = {"method": selected, "representation": rep, "condition": condition,
"n_valid": aligned["valid"].n, "n_seeds": len(match)}
for metric in ("accuracy", "macro_f1", "mae", "pearson"):
values = [r[metric] for r in match]
row[metric] = float(np.mean(values))
row[f"{metric}_sd"] = float(np.std(values, ddof=1)) if len(values) > 1 else 0.0
alignment_rows.append(row)
corrupt = [r for r in metric_rows if r["method"] == selected and r["representation"] == rep
and r["condition"] != "clean" and r["missing_rate"] > 0]
if corrupt:
per_seed = []
for seed in args.seeds:
local = [r for r in corrupt if int(r["seed"]) == seed]
if local:
per_seed.append({metric: float(np.mean([r[metric] for r in local])) for metric in
("accuracy", "macro_f1", "mae", "pearson")})
alignment_rows.append({
"method": selected, "representation": rep, "condition": "all_local_corruption_mean",
"missing_rate": float(np.mean([r["missing_rate"] for r in corrupt])),
"n_valid": aligned["valid"].n, "n_seeds": len(per_seed),
**{metric: float(np.mean([r[metric] for r in per_seed])) for metric in ("accuracy", "macro_f1", "mae", "pearson")},
**{f"{metric}_sd": float(np.std([r[metric] for r in per_seed], ddof=1)) if len(per_seed) > 1 else 0.0
for metric in ("accuracy", "macro_f1", "mae", "pearson")},
})
_write_csv(output / "alignment_transfer_ablation.csv", alignment_rows)
source_path = ATTACHMENT2 / "aligned_50.pkl"
manifest = {
"source_feature": str(source_path),
"source_sha256": _sha256(source_path),
"device": str(device),
"cuda_name": torch.cuda.get_device_name(0) if device.type == "cuda" else None,
"seeds": args.seeds,
"epochs_max": args.epochs,
"patience": args.patience,
"batch_size": args.batch_size,
"best_epochs": best_epochs,
"selected_macro_f1_first": selected,
"selection_policy": "report Macro-F1, MAE, and Pearson separately; selected model maximizes mean validation Macro-F1 across 15 contiguous corruption conditions, then uses MAE and lexical model name only as tie-breaks",
"models": list(KINDS),
"corruption_rates": [0.10, 0.20, 0.30],
"corruption_patterns": list(PATTERNS),
"feature_scaling": "training split median/MAD; fallback to standard deviation for zero-MAD dimensions",
"test_labels_used": False,
"alignment_transfer_limit": "The official aligned_50 data use a 50-slot wordpiece sequence with no per-slot seconds or stored Q1 B1 time_bounds. The fixed-window comparison is a downstream alignment control, not a re-run of Q1 B1 on the full dataset.",
"python": __import__("sys").version,
"torch": torch.__version__,
"numpy": np.__version__,
"created_unix": time.time(),
}
with (output / "run_manifest.json").open("w", encoding="utf-8") as stream:
json.dump(manifest, stream, ensure_ascii=False, indent=2)
print(f"saved selection artifacts to {output}; selected={selected}; seeds={args.seeds}", flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Train the EarlyConcat baseline and its alignment-transfer control")
parser.add_argument("--seeds", type=int, nargs="+", default=[42, 3407, 2026])
parser.add_argument("--epochs", type=int, default=32)
parser.add_argument("--patience", type=int, default=6)
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--threads", type=int, default=4)
parser.add_argument("--device", default="auto")
parser.add_argument("--output-dir", default=str(Path(__file__).resolve().parents[1] / "outputs" / "followups" / "earlyconcat_standalone"))
args = parser.parse_args()
_run(args)
if __name__ == "__main__":
main()
+787
View File
@@ -0,0 +1,787 @@
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import math
import random
import sys
import time
from pathlib import Path
from typing import Any
import numpy as np
import torch
from sklearn.metrics import accuracy_score, f1_score, mean_absolute_error
from torch import nn
from .data import (
ATTACHMENT2,
MODALITIES,
RobustStats,
Split,
apply_robust_stats,
augment_masks,
corrupt_masks,
fit_robust_stats,
load_aligned,
)
from .models import AlignedFusionModel
from .mofe import EXPERT_NAMES, SUBSETS, MixtureOfFusionExperts
from .train_compare import PATTERNS, _loss, _pearson, _train_one, seed_everything
ROOT = Path(__file__).resolve().parents[1]
REFERENCE_OUTPUT = ROOT / "outputs" / "mofe_7experts"
DEFAULT_OUTPUT = ROOT / "outputs" / "mofe_7experts"
EARLYCONCAT = "B0_early_concat"
MOFE7_MLP = "B5_mofe_mlp"
SEEDS = (42, 3407, 2026)
RATES = (0.10, 0.20, 0.30)
HIDDEN = 128
LATENT_DIM = 64
MODEL_CONFIG: dict[str, Any] = {
"router": "mlp",
"expert_names": EXPERT_NAMES,
"availability_mode": "hard",
}
SUMMARY_METRICS = (
"corrupt_macro_f1",
"worst_condition_macro_f1",
"text_30_macro_f1",
"corrupt_mae",
"corrupt_pearson",
)
def _write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not rows:
return
fields = list(dict.fromkeys(key for row in rows for key in row))
with path.open("w", newline="", encoding="utf-8-sig") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
def _read_csv(path: Path) -> list[dict[str, str]]:
if not path.exists():
return []
with path.open("r", newline="", encoding="utf-8-sig") as stream:
return list(csv.DictReader(stream))
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _device_for(name: str) -> torch.device:
if name == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(name)
def _conditions(valid: Split, seed: int) -> list[tuple[str, float, np.ndarray]]:
rows = [("clean", 0.0, valid.mask.copy())]
for rate in RATES:
for pattern_idx, (pattern, modalities) in enumerate(PATTERNS.items()):
masks = corrupt_masks(
valid.mask,
rate,
modalities,
seed + 13 + pattern_idx * 101 + int(rate * 1000),
)
rows.append((f"{pattern}_{int(rate * 100)}", rate, masks))
return rows
def _metric_dict(
y_cls: np.ndarray,
y_reg: np.ndarray,
logits: np.ndarray,
intensity: np.ndarray,
) -> dict[str, float]:
predicted_class = np.asarray(logits).argmax(axis=-1)
predicted_intensity = np.clip(np.asarray(intensity).reshape(-1), -3.0, 3.0)
return {
"accuracy": float(accuracy_score(y_cls, predicted_class)),
"macro_f1": float(f1_score(y_cls, predicted_class, labels=[0, 1, 2], average="macro", zero_division=0)),
"mae": float(mean_absolute_error(y_reg, predicted_intensity)),
"pearson": _pearson(y_reg, predicted_intensity),
}
def _validation_loss(model: nn.Module, valid: Split, device: torch.device, batch_size: int) -> float:
model.eval()
values: list[float] = []
weights: list[int] = []
with torch.inference_mode():
for start in range(0, valid.n, batch_size):
end = min(start + batch_size, valid.n)
xs = tuple(torch.as_tensor(x[start:end], dtype=torch.float32, device=device) for x in valid.x)
masks = torch.as_tensor(valid.mask[start:end], dtype=torch.bool, device=device)
y_cls = torch.as_tensor(valid.y_cls[start:end], dtype=torch.long, device=device)
y_reg = torch.as_tensor(valid.y_reg[start:end], dtype=torch.float32, device=device)
values.append(float(_loss(model(xs, masks), y_cls, y_reg).item()))
weights.append(end - start)
return float(np.average(values, weights=weights))
def _train_mofe(
train: Split,
valid: Split,
output_dir: Path,
device: torch.device,
seed: int,
epochs: int,
patience: int,
batch_size: int,
reuse_checkpoint: bool,
) -> tuple[MixtureOfFusionExperts, int, list[dict[str, Any]]]:
dims = tuple(int(x.shape[-1]) for x in train.x)
checkpoint_path = output_dir / "model_best.pt"
history_path = output_dir / "training_history.csv"
if reuse_checkpoint and checkpoint_path.exists():
saved = torch.load(checkpoint_path, map_location=device, weights_only=False)
if saved.get("config") != MODEL_CONFIG or tuple(saved.get("dims", ())) != dims or int(saved.get("seed", -1)) != seed:
raise ValueError(f"cached MoFE checkpoint does not match the selected configuration: {checkpoint_path}")
model = MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG).to(device)
model.load_state_dict(saved["state_dict"])
history = [
{"method": MOFE7_MLP, "seed": seed, **{key: float(value) for key, value in row.items() if key in {"epoch", "train_loss", "valid_clean_loss"}}}
for row in _read_csv(history_path)
]
return model.eval(), int(saved.get("best_epoch", 0)), history
output_dir.mkdir(parents=True, exist_ok=True)
seed_everything(seed)
model = MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1.5e-4, weight_decay=1e-4)
xs = tuple(torch.as_tensor(x, dtype=torch.float32, device=device) for x in train.x)
base_masks = train.mask
y_cls = torch.as_tensor(train.y_cls, dtype=torch.long, device=device)
y_reg = torch.as_tensor(train.y_reg, dtype=torch.float32, device=device)
rng = np.random.default_rng(seed + 809)
best_loss = math.inf
best_epoch = 0
stale_epochs = 0
history: list[dict[str, Any]] = []
for epoch in range(1, epochs + 1):
model.train()
order = rng.permutation(train.n)
batch_losses: list[float] = []
for start in range(0, train.n, batch_size):
ids_np = order[start:start + batch_size]
ids = torch.as_tensor(ids_np, dtype=torch.long, device=device)
masks_np = augment_masks(base_masks[ids_np], rng)
masks = torch.as_tensor(masks_np, dtype=torch.bool, device=device)
output = model(tuple(x.index_select(0, ids) for x in xs), masks)
loss = _loss(output, y_cls.index_select(0, ids), y_reg.index_select(0, ids))
optimizer.zero_grad(set_to_none=True)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
batch_losses.append(float(loss.detach().item()))
valid_loss = _validation_loss(model, valid, device, batch_size)
row = {
"method": MOFE7_MLP,
"seed": seed,
"epoch": epoch,
"train_loss": float(np.mean(batch_losses)),
"valid_clean_loss": valid_loss,
}
history.append(row)
print(f"[MoFE-7 MLP] seed={seed} epoch={epoch:02d} train={row['train_loss']:.4f} valid={valid_loss:.4f}", flush=True)
if valid_loss < best_loss - 1e-4:
best_loss = valid_loss
best_epoch = epoch
stale_epochs = 0
torch.save({
"method": MOFE7_MLP,
"config": MODEL_CONFIG,
"dims": dims,
"state_dict": model.state_dict(),
"seed": seed,
"best_epoch": epoch,
}, checkpoint_path)
else:
stale_epochs += 1
if stale_epochs >= patience:
break
saved = torch.load(checkpoint_path, map_location=device, weights_only=False)
model.load_state_dict(saved["state_dict"])
model.eval()
_write_csv(history_path, history)
return model, best_epoch, history
def _load_or_train_concat(
train: Split,
valid: Split,
output_dir: Path,
device: torch.device,
seed: int,
epochs: int,
patience: int,
batch_size: int,
reuse_checkpoint: bool,
) -> tuple[AlignedFusionModel, int, list[dict[str, Any]]]:
checkpoint_path = output_dir / "model_best.pt"
dims = tuple(int(x.shape[-1]) for x in train.x)
if reuse_checkpoint and checkpoint_path.exists():
saved = torch.load(checkpoint_path, map_location=device, weights_only=False)
if saved.get("kind") != "concat" or tuple(saved.get("dims", ())) != dims or int(saved.get("seed", -1)) != seed:
raise ValueError(f"cached EarlyConcat checkpoint does not match: {checkpoint_path}")
model = AlignedFusionModel("concat", dims=dims).to(device)
model.load_state_dict(saved["state_dict"])
history = [
{"method": EARLYCONCAT, "seed": seed, **{key: float(value) for key, value in row.items() if key in {"epoch", "train_loss", "valid_clean_loss"}}}
for row in _read_csv(output_dir / "training_history.csv")
]
return model.eval(), int(saved.get("best_epoch", 0)), history
model, best_epoch, history = _train_one(
"concat", train, valid, output_dir, device, seed, epochs, patience, batch_size
)
rows = [{"method": EARLYCONCAT, "seed": seed, **row} for row in history]
return model.eval(), best_epoch, rows
@torch.inference_mode()
def _predict(
model: nn.Module,
split: Split,
masks: np.ndarray,
device: torch.device,
batch_size: int,
force_expert: str | None = None,
) -> dict[str, np.ndarray]:
fields = ["logits", "intensity"]
if isinstance(model, MixtureOfFusionExperts):
fields.extend(("alpha", "utility", "availability", "fallback"))
chunks: dict[str, list[np.ndarray]] = {name: [] for name in fields}
for start in range(0, split.n, batch_size):
end = min(start + batch_size, split.n)
xs = tuple(torch.as_tensor(x[start:end], dtype=torch.float32, device=device) for x in split.x)
mask_batch = torch.as_tensor(masks[start:end], dtype=torch.bool, device=device)
output = model(xs, mask_batch, force_expert=force_expert) if isinstance(model, MixtureOfFusionExperts) else model(xs, mask_batch)
for name in fields:
value = output[name]
chunks[name].append(value.float().cpu().numpy())
result = {name: np.concatenate(values, axis=0) for name, values in chunks.items()}
result["intensity"] = np.clip(result["intensity"].reshape(-1), -3.0, 3.0)
return result
def _condition_row(
method: str,
seed: int,
condition: str,
rate: float,
split: Split,
prediction: dict[str, np.ndarray],
) -> dict[str, Any]:
return {
"method": method,
"seed": seed,
"condition": condition,
"missing_rate": rate,
"n_valid": split.n,
**_metric_dict(split.y_cls, split.y_reg, prediction["logits"], prediction["intensity"]),
}
def _diagnostics(
seed: int,
condition: str,
masks: np.ndarray,
prediction: dict[str, np.ndarray],
) -> tuple[dict[str, Any], dict[str, Any]]:
alpha = prediction["alpha"]
availability = prediction["availability"].astype(bool)
active = availability.any(axis=-1)
active_alpha = alpha[active]
if active_alpha.size:
means = active_alpha.mean(axis=0)
entropy = -(active_alpha * np.log(np.maximum(active_alpha, 1e-12))).sum(axis=-1) / np.log(len(EXPERT_NAMES))
high_weight = (active_alpha.max(axis=-1) > 0.8).mean()
else:
means = np.zeros(len(EXPERT_NAMES), dtype=np.float64)
entropy = np.zeros(0, dtype=np.float64)
high_weight = 0.0
route_row: dict[str, Any] = {
"method": MOFE7_MLP,
"seed": seed,
"condition": condition,
"active_position_fraction": float(active.mean()),
"fallback_position_fraction": float((~active).mean()),
"normalized_router_entropy": float(entropy.mean()) if entropy.size else 0.0,
"fraction_active_positions_max_weight_over_0p8": float(high_weight),
}
for index, name in enumerate(EXPERT_NAMES):
route_row[f"alpha_{name}_mean"] = float(means[index])
utility = prediction["utility"]
utility_row: dict[str, Any] = {"method": MOFE7_MLP, "seed": seed, "condition": condition}
for modality, name in enumerate(MODALITIES):
observed = masks[..., modality]
utility_row[f"utility_{name}_mean"] = float(utility[..., modality][observed].mean()) if observed.any() else 0.0
return route_row, utility_row
def _summary_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
summaries: list[dict[str, Any]] = []
for method in (EARLYCONCAT, MOFE7_MLP):
matching = [row for row in rows if row["method"] == method]
seeds = sorted({int(row["seed"]) for row in matching})
conditions = list(dict.fromkeys(row["condition"] for row in matching))
by_seed_condition = {(int(row["seed"]), row["condition"]): row for row in matching}
clean = [by_seed_condition[(seed, "clean")] for seed in seeds]
corrupt_conditions = [condition for condition in conditions if condition != "clean"]
corrupt_by_seed = {
seed: [by_seed_condition[(seed, condition)] for condition in corrupt_conditions]
for seed in seeds
}
condition_f1 = {
condition: float(np.mean([by_seed_condition[(seed, condition)]["macro_f1"] for seed in seeds]))
for condition in corrupt_conditions
}
worst_condition = min(condition_f1, key=condition_f1.get)
row: dict[str, Any] = {"method": method, "n_seeds": len(seeds), "worst_condition": worst_condition}
for metric in ("accuracy", "macro_f1", "mae", "pearson"):
clean_values = [float(item[metric]) for item in clean]
corrupt_values = [float(np.mean([item[metric] for item in corrupt_by_seed[seed]])) for seed in seeds]
row[f"clean_{metric}"] = float(np.mean(clean_values))
row[f"clean_{metric}_sd"] = float(np.std(clean_values, ddof=1)) if len(clean_values) > 1 else 0.0
row[f"corrupt_{metric}_mean"] = float(np.mean(corrupt_values))
row[f"corrupt_{metric}_sd"] = float(np.std(corrupt_values, ddof=1)) if len(corrupt_values) > 1 else 0.0
row["worst_condition_macro_f1"] = condition_f1[worst_condition]
row["worst_single_run_macro_f1"] = min(
item["macro_f1"] for seed in seeds for item in corrupt_by_seed[seed]
)
text_30 = [by_seed_condition[(seed, "text_30")] for seed in seeds]
row["text_30_macro_f1"] = float(np.mean([item["macro_f1"] for item in text_30]))
row["text_30_macro_f1_sd"] = float(np.std([item["macro_f1"] for item in text_30], ddof=1)) if len(text_30) > 1 else 0.0
for condition in ("audio_30", "vision_30", "audio_vision_30", "all_modalities_30"):
values = [by_seed_condition[(seed, condition)]["macro_f1"] for seed in seeds]
row[f"{condition}_macro_f1"] = float(np.mean(values))
row[f"{condition}_macro_f1_sd"] = float(np.std(values, ddof=1)) if len(values) > 1 else 0.0
row["corrupt_macro_f1"] = row["corrupt_macro_f1_mean"]
row["corrupt_mae"] = row["corrupt_mae_mean"]
row["corrupt_pearson"] = row["corrupt_pearson_mean"]
summaries.append(row)
return summaries
def _bootstrap_distributions(
method: str,
predictions: dict[tuple[str, int, str], dict[str, np.ndarray]],
valid: Split,
seeds: list[int],
conditions: list[str],
group_counts: np.ndarray,
) -> dict[str, np.ndarray]:
group_names = sorted({sample_id.split("$_$", 1)[0] for sample_id in valid.ids})
group_index = {name: index for index, name in enumerate(group_names)}
row_group = np.asarray([group_index[sample_id.split("$_$", 1)[0]] for sample_id in valid.ids], dtype=np.int64)
n_groups = len(group_names)
n_slots = len(seeds) * len(conditions)
confusion_by_group = np.zeros((n_groups, n_slots, 9), dtype=np.float64)
regression_by_group = np.zeros((n_groups, n_slots, 7), dtype=np.float64)
for seed_index, seed in enumerate(seeds):
for condition_index, condition in enumerate(conditions):
slot = seed_index * len(conditions) + condition_index
pred = predictions[(method, seed, condition)]
predicted_class = pred["logits"].argmax(axis=-1)
code = valid.y_cls * 3 + predicted_class
np.add.at(confusion_by_group[:, slot, :], (row_group, code), 1.0)
intensity = np.clip(pred["intensity"].reshape(-1), -3.0, 3.0)
values = np.stack((
np.ones(valid.n),
np.abs(valid.y_reg - intensity),
valid.y_reg,
valid.y_reg ** 2,
intensity,
intensity ** 2,
valid.y_reg * intensity,
), axis=-1)
for statistic in range(values.shape[-1]):
np.add.at(regression_by_group[:, slot, statistic], row_group, values[:, statistic])
weighted_confusion = np.einsum("rg,gsk->rsk", group_counts, confusion_by_group, optimize=True)
cm = weighted_confusion.reshape(len(group_counts), len(seeds), len(conditions), 3, 3)
true_count = cm.sum(axis=-1)
predicted_count = cm.sum(axis=-2)
true_positive = np.diagonal(cm, axis1=-2, axis2=-1)
denominator = true_count + predicted_count
class_f1 = np.divide(2.0 * true_positive, denominator, out=np.zeros_like(true_positive), where=denominator > 0)
macro_f1 = class_f1.mean(axis=-1)
weighted_regression = np.einsum("rg,gsk->rsk", group_counts, regression_by_group, optimize=True)
regression = weighted_regression.reshape(len(group_counts), len(seeds), len(conditions), 7)
count = np.maximum(regression[..., 0], 1.0)
mae = regression[..., 1] / count
sum_y, sum_y2, sum_pred, sum_pred2, sum_yp = (regression[..., index] for index in range(2, 7))
covariance = sum_yp - sum_y * sum_pred / count
variance_y = np.maximum(sum_y2 - sum_y ** 2 / count, 0.0)
variance_pred = np.maximum(sum_pred2 - sum_pred ** 2 / count, 0.0)
denominator_corr = np.sqrt(variance_y * variance_pred)
pearson = np.divide(covariance, denominator_corr, out=np.zeros_like(covariance), where=denominator_corr > 1e-12)
text_30_index = conditions.index("text_30")
return {
"corrupt_macro_f1": macro_f1[:, :, 1:].mean(axis=(1, 2)),
"worst_condition_macro_f1": macro_f1[:, :, 1:].mean(axis=1).min(axis=1),
"text_30_macro_f1": macro_f1[:, :, text_30_index].mean(axis=1),
"corrupt_mae": mae[:, :, 1:].mean(axis=(1, 2)),
"corrupt_pearson": pearson[:, :, 1:].mean(axis=(1, 2)),
}
def _paired_bootstrap(
predictions: dict[tuple[str, int, str], dict[str, np.ndarray]],
valid: Split,
seeds: list[int],
conditions: list[str],
reps: int,
bootstrap_seed: int,
summaries: list[dict[str, Any]],
) -> list[dict[str, Any]]:
groups = sorted({sample_id.split("$_$", 1)[0] for sample_id in valid.ids})
rng = np.random.default_rng(bootstrap_seed)
draws = rng.integers(0, len(groups), size=(reps, len(groups)))
group_counts = np.zeros((reps, len(groups)), dtype=np.float64)
for rep in range(reps):
group_counts[rep] = np.bincount(draws[rep], minlength=len(groups))
candidate = _bootstrap_distributions(MOFE7_MLP, predictions, valid, seeds, conditions, group_counts)
reference = _bootstrap_distributions(EARLYCONCAT, predictions, valid, seeds, conditions, group_counts)
summary_map = {row["method"]: row for row in summaries}
point_keys = {
"corrupt_macro_f1": "corrupt_macro_f1",
"worst_condition_macro_f1": "worst_condition_macro_f1",
"text_30_macro_f1": "text_30_macro_f1",
"corrupt_mae": "corrupt_mae",
"corrupt_pearson": "corrupt_pearson",
}
rows = []
for metric in SUMMARY_METRICS:
delta = candidate[metric] - reference[metric]
key = point_keys[metric]
rows.append({
"comparison": "MoFE-7 MLP vs EarlyConcat",
"candidate": MOFE7_MLP,
"reference": EARLYCONCAT,
"metric": metric,
"delta_candidate_minus_reference": float(summary_map[MOFE7_MLP][key] - summary_map[EARLYCONCAT][key]),
"bootstrap_ci_2p5": float(np.quantile(delta, 0.025)),
"bootstrap_ci_97p5": float(np.quantile(delta, 0.975)),
"bootstrap_probability_delta_gt_0": float(np.mean(delta > 0.0)),
"bootstrap_replicates": reps,
"resampling_unit": "source video id",
"paired": True,
"seed": bootstrap_seed,
})
return rows
def _plot_summary(output: Path, summaries: list[dict[str, Any]]) -> None:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
labels = ["EarlyConcat + BiGRU", "MoFE-7 + MLP Router"]
by_method = {row["method"]: row for row in summaries}
methods = (EARLYCONCAT, MOFE7_MLP)
metrics = ("clean_macro_f1", "corrupt_macro_f1", "worst_condition_macro_f1")
names = ("Clean", "Mean corrupted", "Worst condition")
x = np.arange(len(names))
width = 0.34
fig, ax = plt.subplots(figsize=(8.6, 4.8), constrained_layout=True)
for offset, method, label, color in (
(-width / 2, methods[0], labels[0], "#4e79a7"),
(width / 2, methods[1], labels[1], "#f28e2b"),
):
values = [by_method[method][metric] for metric in metrics]
ax.bar(x + offset, values, width, label=label, color=color)
ax.set_xticks(x, names)
ax.set_ylabel("Macro-F1")
ax.set_ylim(0, 1)
ax.set_title("Q2 selected-model validation comparison")
ax.legend(frameon=False)
output.mkdir(parents=True, exist_ok=True)
fig.savefig(output / "comparison_earlyconcat_mofe7.png", dpi=180)
plt.close(fig)
def _parameter_rows(dims: tuple[int, int, int], device: torch.device) -> list[dict[str, Any]]:
models: dict[str, nn.Module] = {
EARLYCONCAT: AlignedFusionModel("concat", dims=dims),
MOFE7_MLP: MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG),
}
baseline_count = sum(parameter.numel() for parameter in models[EARLYCONCAT].parameters() if parameter.requires_grad)
rows = []
for name, model in models.items():
count = sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad)
rows.append({
"method": name,
"trainable_parameters": count,
"ratio_to_earlyconcat": count / baseline_count,
"within_2x_earlyconcat": bool(count <= 2 * baseline_count),
})
return rows
def _smoke_test(train: Split, output: Path, device: torch.device, seed: int) -> dict[str, Any]:
seed_everything(seed)
dims = tuple(int(x.shape[-1]) for x in train.x)
count = min(4, train.n)
xs = tuple(torch.as_tensor(x[:count], dtype=torch.float32, device=device) for x in train.x)
masks = torch.as_tensor(train.mask[:count].copy(), dtype=torch.bool, device=device)
masks[0] = True
if count > 1:
masks[1, 5:12, 0] = False
if count > 2:
masks[2, 18:23, :] = False
target_class = torch.as_tensor(train.y_cls[:count], dtype=torch.long, device=device)
target_intensity = torch.as_tensor(train.y_reg[:count], dtype=torch.float32, device=device)
reports: dict[str, Any] = {}
models: dict[str, nn.Module] = {
EARLYCONCAT: AlignedFusionModel("concat", dims=dims).to(device),
MOFE7_MLP: MixtureOfFusionExperts(dims=dims, **MODEL_CONFIG).to(device),
}
for name, model in models.items():
model.train()
result = model(xs, masks)
loss = _loss(result, target_class, target_intensity)
loss.backward()
gradient = sum(float(p.grad.detach().abs().sum().cpu()) for p in model.parameters() if p.grad is not None)
reports[name] = {
"logits_shape": list(result["logits"].shape),
"intensity_shape": list(result["intensity"].shape),
"finite_loss": bool(torch.isfinite(loss).item()),
"gradient_l1": gradient,
}
mofe_model = models[MOFE7_MLP]
mo = mofe_model(xs, masks)
active = mo["availability"].any(dim=-1)
alpha_sums = mo["alpha"].sum(dim=-1)
alpha_error = float((alpha_sums[active] - 1).abs().max().cpu()) if active.any() else 0.0
unavailable_weights = float(mo["alpha"].masked_select(~mo["availability"]).abs().max().cpu()) if (~mo["availability"]).any() else 0.0
expert_gradients = {
name: sum(float(parameter.grad.detach().abs().sum().cpu()) for parameter in expert.parameters() if parameter.grad is not None)
for name, expert in mofe_model.experts.items()
}
router_gradient = sum(float(parameter.grad.detach().abs().sum().cpu()) for parameter in mofe_model.router.parameters() if parameter.grad is not None)
if alpha_error > 1e-6 or unavailable_weights > 1e-8:
raise RuntimeError(f"MoFE routing mask invariant failed: sum_error={alpha_error}, unavailable={unavailable_weights}")
if not all(value > 0 for value in expert_gradients.values()) or router_gradient <= 0:
raise RuntimeError(f"MoFE expert/router gradients are incomplete: {expert_gradients}; router={router_gradient}")
report = {
"passed": all(item["finite_loss"] and item["gradient_l1"] > 0 for item in reports.values()),
"seed": seed,
"device": str(device),
"cuda_device": torch.cuda.get_device_name(0) if device.type == "cuda" else None,
"batch_size_checked": count,
"steps": train.steps,
"models": reports,
"mofe_experts": list(EXPERT_NAMES),
"mofe_alpha_shape": list(mo["alpha"].shape),
"mofe_max_weight_sum_error": alpha_error,
"mofe_max_weight_on_unavailable_experts": unavailable_weights,
"mofe_expert_gradient_l1": expert_gradients,
"mofe_router_gradient_l1": router_gradient,
"parameter_count": {row["method"]: row["trainable_parameters"] for row in _parameter_rows(dims, device)},
}
output.mkdir(parents=True, exist_ok=True)
(output / "smoke_test.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
return report
def _run(args: argparse.Namespace) -> None:
output = args.output_dir.resolve()
output.mkdir(parents=True, exist_ok=True)
device = _device_for(args.device)
torch.set_num_threads(args.threads)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
raw = load_aligned()
computed_stats = fit_robust_stats(raw["train"])
reference_stats_path = REFERENCE_OUTPUT / "aligned_robust_stats.npz"
if reference_stats_path.exists():
stats = RobustStats.load(reference_stats_path)
scaler_diff = max(
max(float(np.max(np.abs(a - b))) for a, b in zip(computed_stats.center, stats.center)),
max(float(np.max(np.abs(a - b))) for a, b in zip(computed_stats.scale, stats.scale)),
)
else:
stats = computed_stats
scaler_diff = 0.0
train = apply_robust_stats(raw["train"], stats)
valid = apply_robust_stats(raw["valid"], stats)
stats.save(output / "aligned_robust_stats.npz")
dims = tuple(int(x.shape[-1]) for x in train.x)
feature_path = ATTACHMENT2 / "aligned_50.pkl"
if not feature_path.exists():
raise FileNotFoundError(f"official aligned feature file not found: {feature_path}")
if args.phase == "smoke":
report = _smoke_test(train, output, device, args.seeds[0])
report["scaler_max_abs_difference_from_reference"] = scaler_diff
(output / "smoke_test.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"selected-model smoke: passed={report['passed']} device={device}", flush=True)
return
seeds = list(args.seeds)
metrics_rows: list[dict[str, Any]] = []
predictions: dict[tuple[str, int, str], dict[str, np.ndarray]] = {}
router_rows: list[dict[str, Any]] = []
utility_rows: list[dict[str, Any]] = []
expert_rows: list[dict[str, Any]] = []
history_rows: list[dict[str, Any]] = []
best_epochs: dict[str, int] = {}
condition_names: list[str] = []
for seed in seeds:
baseline_dir = output / "models" / "baselines" / "concat" / f"seed_{seed}"
baseline, baseline_epoch, baseline_history = _load_or_train_concat(
train, valid, baseline_dir, device, seed, args.epochs, args.patience,
args.batch_size, args.reuse_checkpoints and not args.force_retrain,
)
mofe_dir = output / "models" / MOFE7_MLP / f"seed_{seed}"
mofe, mofe_epoch, mofe_history = _train_mofe(
train, valid, mofe_dir, device, seed, args.epochs, args.patience,
args.batch_size, args.reuse_checkpoints and not args.force_retrain,
)
best_epochs[f"{EARLYCONCAT}_seed_{seed}"] = baseline_epoch
best_epochs[f"{MOFE7_MLP}_seed_{seed}"] = mofe_epoch
history_rows.extend(baseline_history)
history_rows.extend(mofe_history)
conditions = _conditions(valid, seed)
names = [condition for condition, _, _ in conditions]
if condition_names and names != condition_names:
raise RuntimeError("validation condition ordering changed between seeds")
condition_names = names
for method, model in ((EARLYCONCAT, baseline), (MOFE7_MLP, mofe)):
for condition, rate, masks in conditions:
prediction = _predict(model, valid, masks, device, args.batch_size)
predictions[(method, seed, condition)] = prediction
metrics_rows.append(_condition_row(method, seed, condition, rate, valid, prediction))
if method == MOFE7_MLP:
route_row, utility_row = _diagnostics(seed, condition, masks, prediction)
router_rows.append(route_row)
utility_rows.append(utility_row)
for expert in EXPERT_NAMES:
forced = _predict(model, valid, masks, device, args.batch_size, force_expert=expert)
observed = masks[..., list(SUBSETS[expert])].all(axis=-1)
expert_rows.append({
"method": MOFE7_MLP,
"seed": seed,
"condition": condition,
"expert": expert,
"available_position_fraction": float(observed.mean()),
**_metric_dict(valid.y_cls, valid.y_reg, forced["logits"], forced["intensity"]),
})
print(f"evaluated {method}/seed{seed}", flush=True)
del baseline, mofe
if torch.cuda.is_available():
torch.cuda.empty_cache()
summaries = _summary_rows(metrics_rows)
paired = _paired_bootstrap(
predictions, valid, seeds, condition_names, args.bootstrap_reps,
args.bootstrap_seed, summaries,
) if args.bootstrap_reps > 0 else []
parameter_rows = _parameter_rows(dims, device)
output_rows = {
"metrics_by_condition.csv": metrics_rows,
"summary.csv": summaries,
"paired_bootstrap.csv": paired,
"parameter_count.csv": parameter_rows,
"router_weights_by_condition.csv": router_rows,
"routing_entropy.csv": router_rows,
"modality_utility_by_condition.csv": utility_rows,
"expert_condition_matrix.csv": expert_rows,
"training_history.csv": history_rows,
}
for filename, rows in output_rows.items():
_write_csv(output / filename, rows)
_plot_summary(output / "figures", summaries)
manifest = {
"experiment": "Q2 selected models: EarlyConcat + BiGRU and MoFE-7 + MLP Router",
"created_unix": time.time(),
"python_version": sys.version,
"torch_version": torch.__version__,
"numpy_version": np.__version__,
"device": str(device),
"cuda_device": torch.cuda.get_device_name(0) if device.type == "cuda" else None,
"feature_file": str(feature_path),
"feature_sha256": _sha256(feature_path),
"feature_dimensions": dict(zip(MODALITIES, dims)),
"sequence_length": train.steps,
"representation_note": "official ordered 50-wordpiece positions; not 50 physical-time bins",
"train_examples": train.n,
"valid_examples": valid.n,
"train_source_video_groups": len({sample_id.split("$_$", 1)[0] for sample_id in train.ids}),
"valid_source_video_groups": len({sample_id.split("$_$", 1)[0] for sample_id in valid.ids}),
"train_only_scaler": str(output / "aligned_robust_stats.npz"),
"scaler_max_abs_difference_from_reference": scaler_diff,
"test_labels_used": False,
"seeds": seeds,
"epochs_max": args.epochs,
"patience": args.patience,
"batch_size": args.batch_size,
"optimizer": "AdamW(lr=1.5e-4, weight_decay=1e-4), gradient clip 1.0",
"training_mask_augmentation": "same contiguous-block augment_masks protocol for both models",
"validation_conditions": condition_names,
"validation_corruption_seed": "seed + 13 + pattern_index*101 + int(rate*1000)",
"loss": "cross_entropy + 0.5*SmoothL1(intensity/3, regression_label/3)",
"models": {
EARLYCONCAT: "project modalities independently, concatenate features and masks, then BiGRU",
MOFE7_MLP: {
"experts": list(EXPERT_NAMES),
"router": "MLP over per-position observed values and local observation statistics",
"availability": "hard mask; unavailable expert weights are zero",
"shared_temporal_backbone": "one BiGRU after position-wise expert mixture",
},
},
"best_epochs": best_epochs,
"paired_bootstrap": {
"replicates": args.bootstrap_reps,
"seed": args.bootstrap_seed,
"resampling_unit": "source video id",
"paired": True,
},
}
(output / "run_manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(f"selected-model results saved to {output}", flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Train and compare the two retained Q2 models.")
parser.add_argument("--phase", choices=("smoke", "full"), default="full")
parser.add_argument("--epochs", type=int, default=32)
parser.add_argument("--patience", type=int, default=6)
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--threads", type=int, default=4)
parser.add_argument("--device", default="auto")
parser.add_argument("--seeds", type=int, nargs="+", default=list(SEEDS))
parser.add_argument("--bootstrap-reps", type=int, default=1000)
parser.add_argument("--bootstrap-seed", type=int, default=20260924)
parser.add_argument("--reuse-checkpoints", action="store_true")
parser.add_argument("--force-retrain", action="store_true")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
_run(parser.parse_args())
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
"""Mathematical Q2 model family and training driver."""
+213
View File
@@ -0,0 +1,213 @@
"""Restricted readers and split preparation for the official Q2 inputs."""
from __future__ import annotations
import pickle
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
from data_paths import ATTACHMENT2, ATTACHMENT3, DATA_ROOT, PROJECT_ROOT
ROOT = PROJECT_ROOT
ATTACHMENT2_DIR = ATTACHMENT2
ALIGNED_PATH = ATTACHMENT2 / "aligned_50.pkl"
ATTACHMENT3_ALIGNED = ATTACHMENT3 / "对齐版本"
ATTACHMENT3_UNALIGNED = ATTACHMENT3 / "未对齐版本"
MODALITIES = ("text", "audio", "vision")
EXPECTED_DIMS = {"text": 768, "audio": 74, "vision": 35}
class RestrictedUnpickler(pickle.Unpickler):
"""Allow only primitive containers and NumPy reconstruction primitives."""
_allowed = {
("builtins", name): getattr(__import__("builtins"), name)
for name in ("set", "frozenset", "slice", "complex", "bytearray")
}
_allowed.update({
("collections", "OrderedDict"): __import__("collections").OrderedDict,
("numpy", "ndarray"): np.ndarray,
("numpy", "dtype"): np.dtype,
("numpy", "asarray"): np.asarray,
("numpy.core.multiarray", "_reconstruct"): np.core.multiarray._reconstruct,
("numpy.core.multiarray", "scalar"): np.core.multiarray.scalar,
("numpy._core.multiarray", "_reconstruct"): np.core.multiarray._reconstruct,
("numpy._core.multiarray", "scalar"): np.core.multiarray.scalar,
})
if hasattr(np.core.numeric, "_frombuffer"):
_allowed[("numpy.core.numeric", "_frombuffer")] = np.core.numeric._frombuffer
_allowed[("numpy._core.numeric", "_frombuffer")] = np.core.numeric._frombuffer
def find_class(self, module: str, name: str) -> Any:
try:
return self._allowed[(module, name)]
except KeyError as exc:
raise pickle.UnpicklingError(f"blocked pickle global: {module}.{name}") from exc
def restricted_load(path: Path) -> Any:
with path.open("rb") as stream:
return RestrictedUnpickler(stream).load()
def _decode(value: Any) -> str:
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
if isinstance(value, np.bytes_):
return bytes(value).decode("utf-8", errors="replace")
if isinstance(value, np.ndarray) and value.shape == ():
return _decode(value.item())
return str(value)
def _one_dim(value: Any, dtype: Any | None = None) -> np.ndarray:
out = np.asarray(value)
if out.ndim > 1 and out.shape[-1] == 1:
out = out.reshape(-1)
elif out.ndim > 1 and out.shape[0] == 1:
out = out.reshape(-1)
else:
out = out.reshape(-1)
return out.astype(dtype) if dtype is not None else out
@dataclass
class SplitData:
name: str
x: dict[str, np.ndarray]
mask: np.ndarray
class_y: np.ndarray | None
regression_y: np.ndarray | None
ids: list[str]
groups: np.ndarray
alignment_audit: dict[str, Any] | None = None
@property
def n(self) -> int:
return len(self.ids)
def _extract_split(
name: str, obj: dict[str, Any], with_labels: bool,
mask_override: np.ndarray | None = None,
alignment_audit: dict[str, Any] | None = None,
) -> SplitData:
raw: dict[str, np.ndarray] = {}
masks = []
for modality in MODALITIES:
arr = np.asarray(obj[modality])
if arr.ndim != 3 or arr.shape[1] != 50 or arr.shape[2] != EXPECTED_DIMS[modality]:
raise ValueError(f"{name}.{modality}: unexpected feature shape {arr.shape}")
arr = arr.astype(np.float32)
if not np.isfinite(arr).all():
raise ValueError(f"{name}.{modality}: non-finite feature values; refusing to reinterpret them as missing")
# The dataset documentation defines all-zero aligned rows as missing.
observed = np.any(arr != 0.0, axis=-1)
raw[modality] = arr
masks.append(observed)
mask = np.stack(masks, axis=-1)
if mask_override is not None:
override = np.asarray(mask_override, bool)
if override.shape != mask.shape:
raise ValueError(f"{name}: projected mask shape {override.shape} differs from {mask.shape}")
mask = override
ids = [_decode(v) for v in _one_dim(obj["id"])]
if len(ids) != len(mask):
raise ValueError(f"{name}: id count differs from feature count")
if len(set(ids)) != len(ids):
raise ValueError(f"{name}: duplicate video$_$clip primary keys")
malformed = [sample_id for sample_id in ids if "$_$" not in sample_id or not all(sample_id.split("$_$", 1))]
if malformed:
raise ValueError(f"{name}: malformed video$_$clip keys: {malformed[:5]}")
groups = np.asarray([sample_group(v) for v in ids], dtype=str)
if with_labels:
class_y = _one_dim(obj["classification_labels"], np.int64)
regression_y = _one_dim(obj["regression_labels"], np.float32)
if len(class_y) != len(ids) or len(regression_y) != len(ids):
raise ValueError(f"{name}: label count differs from feature count")
if not np.isfinite(regression_y).all() or np.any(np.abs(regression_y) > 3.0):
raise ValueError(f"{name}: regression labels must be finite and within [-3,3]")
if not np.isin(class_y, [0, 1, 2]).all():
raise ValueError(f"{name}: expected class labels in 0,1,2")
expected_class = np.where(regression_y < 0.0, 0, np.where(regression_y == 0.0, 1, 2))
mismatch = np.flatnonzero(class_y != expected_class)
if len(mismatch):
examples = [(ids[int(i)], int(class_y[i]), float(regression_y[i])) for i in mismatch[:5]]
raise ValueError(f"{name}: polarity/regression label mismatch (sample, class, score): {examples}")
else:
class_y = regression_y = None
return SplitData(name, raw, mask, class_y, regression_y, ids, groups, alignment_audit)
def sample_group(sample_id: str) -> str:
"""Official ids are video$_$clip; group on the source video only."""
return sample_id.split("$_$", 1)[0]
def load_official_splits(path: Path = ALIGNED_PATH, *, version: str = "aligned_50") -> dict[str, SplitData]:
if version not in {"aligned_50", "unaligned_50"}:
raise ValueError(f"unsupported feature version: {version}")
obj = restricted_load(path)
required = {"train", "valid", "test"}
if not isinstance(obj, dict) or not required.issubset(obj):
raise ValueError(f"{path.name} must contain train, valid, and test dictionaries")
if version == "unaligned_50":
from adapter import adapt_official_split
splits = {}
for name in ("train", "valid", "test"):
projected, mask, audit = adapt_official_split(obj[name])
fields = {**obj[name], **projected}
splits[name] = _extract_split(name, fields, with_labels=True,
mask_override=mask, alignment_audit=audit)
else:
splits = {name: _extract_split(name, obj[name], with_labels=True) for name in ("train", "valid", "test")}
del obj
return splits
def load_attachment3_case(path: Path) -> dict[str, np.ndarray]:
obj = restricted_load(path)
case = obj.get("test", obj)
text_bert = np.asarray(case["text_bert"])
audio = np.asarray(case["audio"])
vision = np.asarray(case["vision"])
if text_bert.ndim == 3 and text_bert.shape[0] == 1:
text_bert = text_bert[0]
if text_bert.shape != (3, 50):
raise ValueError(f"{path.name}: expected text_bert (1,3,50), got {np.asarray(case['text_bert']).shape}")
result = {"input_ids": text_bert[0].astype(np.int64), "attention_mask": text_bert[1].astype(bool), "token_type_ids": text_bert[2].astype(np.int64)}
for name, arr, dim in (("audio", audio, 74), ("vision", vision, 35)):
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.shape != (50, dim):
raise ValueError(f"{path.name}: expected {name} (1,50,{dim}), got {np.asarray(case[name]).shape}")
arr = arr.astype(np.float32)
if not np.isfinite(arr).all():
raise ValueError(f"{path.name}: {name} contains non-finite features")
result[name] = arr
return result
def fit_preprocessor(train: SplitData) -> dict[str, dict[str, np.ndarray]]:
"""Fit per-dimension mean/std on observed training rows only."""
fitted: dict[str, dict[str, np.ndarray]] = {}
for j, name in enumerate(MODALITIES):
rows = train.x[name][train.mask[:, :, j]]
mean = rows.mean(axis=0, dtype=np.float64).astype(np.float32)
std = rows.std(axis=0, dtype=np.float64).astype(np.float32)
std[std < 1e-5] = 1.0
fitted[name] = {"mean": mean, "std": std}
return fitted
def transform_split(split: SplitData, fitted: dict[str, dict[str, np.ndarray]]) -> dict[str, np.ndarray]:
output = {}
for j, name in enumerate(MODALITIES):
arr = (split.x[name] - fitted[name]["mean"]) / fitted[name]["std"]
arr = np.clip(arr, -10.0, 10.0)
arr[~split.mask[:, :, j]] = 0.0
output[name] = arr.astype(np.float32)
return output
+99
View File
@@ -0,0 +1,99 @@
"""Run a saved Q2 model on the unlabeled Attachment 3 cases."""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
import numpy as np
import torch
from data_paths import PROJECT_ROOT
from model.crg import INPUT_DIMS, MODALITIES, StructuredGaussianImputer
from .train import (
_make_variant,
infer_attachment3,
reencode_attachment3,
validate_attachment3_predictions,
write_csv,
)
DEFAULT_RESULTS_DIR = PROJECT_ROOT / "experiments" / "q2" / "unaligned_math_all_b128"
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "output" / "q2"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input-version", choices=("aligned_50", "unaligned_50"), default="unaligned_50")
parser.add_argument("--results-dir", type=Path, default=DEFAULT_RESULTS_DIR,
help="saved Q2 checkpoint and calibration directory")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto")
args = parser.parse_args()
if args.device == "cuda" and not torch.cuda.is_available():
parser.error("CUDA was requested but is not available")
device_name = "cuda" if args.device == "auto" and torch.cuda.is_available() else args.device
if device_name == "auto":
device_name = "cpu"
device = torch.device(device_name)
results_dir = args.results_dir.expanduser().resolve()
output_dir = args.output_dir.expanduser().resolve()
manifest_path = results_dir / "run_manifest.json"
calibration_path = results_dir / "validation_metrics.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
calibration = json.loads(calibration_path.read_text(encoding="utf-8"))
selected = calibration.get("selected_model", manifest.get("selected_model"))
if not selected:
raise ValueError(f"no selected_model recorded in {calibration_path}")
imputer = StructuredGaussianImputer(INPUT_DIMS).to(device)
imputer.load_state_dict(torch.load(results_dir / "structured_imputer.pt", map_location=device, weights_only=True))
model = _make_variant(selected, imputer).to(device)
model.load_state_dict(torch.load(results_dir / "crg_student.pt", map_location=device, weights_only=True))
with np.load(results_dir / "preprocessor.npz", allow_pickle=False) as archive:
fitted = {m: {k: archive[f"{m}_{k}"].copy() for k in ("mean", "std")} for m in MODALITIES}
priors = manifest["attachment3_low_information_priors"]
temperature = float(calibration["temperature"])
class_prior = np.asarray(priors["class_probability_values"], dtype=np.float64)
magnitude_priors = np.asarray((priors["negative_beta"], priors["positive_beta"]), dtype=np.float32)
cases, source_audit = reencode_attachment3(device, input_version=args.input_version)
predictions, inference_audit = infer_attachment3(
model, cases, fitted, device, temperature, class_prior, magnitude_priors,
)
validate_attachment3_predictions([case["case_id"] for case in cases], predictions)
output_dir.mkdir(parents=True, exist_ok=True)
inference_by_id = {row["case_id"]: row for row in inference_audit}
predictions_path = output_dir / "attachment3_predictions.csv"
audit_path = output_dir / "attachment3_audit.csv"
manifest_out_path = output_dir / "attachment3_prediction_manifest.json"
write_csv(predictions_path, predictions)
write_csv(audit_path, [{**source, **inference_by_id[source["case_id"]]} for source in source_audit])
try:
results_reference = results_dir.relative_to(PROJECT_ROOT).as_posix()
except ValueError:
results_reference = "external checkpoint directory"
prediction_manifest = {
"task": "unlabeled Attachment 3 inference",
"input_version": args.input_version,
"selected_model": selected,
"checkpoint_run": results_reference,
"prediction_count": len(predictions),
"temperature": temperature,
"labels_available": False,
"prediction_file": predictions_path.name,
"audit_file": audit_path.name,
"completed_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
manifest_out_path.write_text(json.dumps(prediction_manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Wrote {len(predictions)} unlabeled Attachment 3 predictions to {output_dir}", flush=True)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff