Files
hypertower/v4/scripts/analysis/inspect_embeddings.py
T

113 lines
4.5 KiB
Python

"""inspect_embeddings — per-dim statistics on a run's saved feature embeddings.
Loads features.h5 from a run folder (must have been produced with
``save_features: true`` in the config), computes per-dimension variance,
sparsity (fraction of |z| < threshold), and useful summary stats.
Helpful for diagnosing whether a fusion stage is collapsing dimensions to
near-zero — which would silently null out information when the bridge uses
a Hadamard product.
Usage:
python -m v4.scripts.analysis.inspect_embeddings <run_folder> [--phase NAME]
[--threshold 0.05]
Examples:
python -m v4.scripts.analysis.inspect_embeddings \
v4/results/experiments/tri_v1/baseline_tri/rep00 --phase nt
"""
from __future__ import annotations
import argparse
from pathlib import Path
import h5py
import numpy as np
def load_phase(features_path: Path, phase: str | None):
with h5py.File(features_path, "r") as f:
phases = list(f.keys())
if phase is None:
phase = phases[-1]
if phase not in phases:
raise SystemExit(
f"Phase {phase!r} not in {features_path} (available: {phases})"
)
g = f[phase]
emb_keys = [k for k in g.keys() if k.endswith("_embedding")]
if not emb_keys:
raise SystemExit(
f"No '*_embedding' dataset in phase {phase!r} of {features_path}"
)
# Prefer the embedding named after the phase if present, else first match.
emb_key = f"{phase}_embedding" if f"{phase}_embedding" in emb_keys else emb_keys[0]
z_raw = g[emb_key][:]
# Stored shape is (n_folds, n_epochs, n_samples, n_dim). Use last epoch.
if z_raw.ndim == 4:
z = z_raw[:, -1, :, :]
else:
z = z_raw
split = g["split"][:].astype(str)
y_true = g["y_true"][:]
return phase, z, split, y_true, phases
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("run_dir", type=Path, help="Folder containing features.h5 (under repNN)")
ap.add_argument("--phase", default=None,
help="Bridge phase name (e.g. nt, hb, cd_fuse). Default: last in file.")
ap.add_argument("--threshold", type=float, default=0.05,
help="|z| threshold for the sparsity count (default: 0.05)")
args = ap.parse_args()
feat = next(iter(args.run_dir.rglob("features.h5")), None)
if feat is None:
raise SystemExit(f"No features.h5 found under {args.run_dir}")
phase, z, split, _, all_phases = load_phase(feat, args.phase)
val_mask = (split == "val")
z_val = z[val_mask]
z_train = z[(split == "train")]
n_dim = z_val.shape[-1] if z_val.size else 0
if n_dim == 0:
raise SystemExit(f"No val embeddings found for phase {phase!r}")
print(f"Run: {args.run_dir}")
print(f"File: {feat}")
print(f"Phases: {all_phases}")
print(f"Phase: {phase} (z shape: {z.shape})")
print(f"Split sizes: train={len(z_train)} val={len(z_val)}")
print()
abs_z = np.abs(z_val)
per_dim_var = z_val.var(axis=0)
per_dim_abs = abs_z.mean(axis=0)
per_dim_max = abs_z.max(axis=0)
sparsity = (abs_z < args.threshold).mean(axis=0)
print(f"Aggregate stats over val embeddings (|z| < {args.threshold} = 'near-zero'):")
print(f" global mean(|z|): {abs_z.mean():.4f}")
print(f" global var(z): {z_val.var():.4f}")
print(f" fraction near-zero (global): {(abs_z < args.threshold).mean():.4f}")
print()
print(f"Per-dimension summary ({n_dim} dims):")
print(f" variance: min={per_dim_var.min():.4f} med={np.median(per_dim_var):.4f} max={per_dim_var.max():.4f}")
print(f" mean |z|: min={per_dim_abs.min():.4f} med={np.median(per_dim_abs):.4f} max={per_dim_abs.max():.4f}")
print(f" near-zero rate: min={sparsity.min():.4f} med={np.median(sparsity):.4f} max={sparsity.max():.4f}")
# Dead dimensions: high near-zero rate
dead = np.where(sparsity > 0.9)[0]
print(f" 'dead' dims (>90% near-zero): {len(dead)}/{n_dim} "
f"{('idx: ' + str(dead.tolist())) if 0 < len(dead) <= 20 else ''}")
weak = np.where(per_dim_var < 1e-4)[0]
print(f" 'weak' dims (var < 1e-4): {len(weak)}/{n_dim} "
f"{('idx: ' + str(weak.tolist())) if 0 < len(weak) <= 20 else ''}")
if __name__ == "__main__":
main()