Automatic Speech Recognition
Transformers
qwen3-asr
latent-reasoning
test-time-compute
parameter-efficient
Instructions to use voidful/latentASR with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use voidful/latentASR with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="voidful/latentASR")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("voidful/latentASR", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| """Per-sample paper analysis for LatentASR. | |
| The script runs three paths on one ASR split: | |
| 1. frozen baseline, | |
| 2. LatentASR with the deployed halting threshold, | |
| 3. LatentASR with forced full compute. | |
| It writes per-sample JSON plus a LaTeX snippet containing difficulty bins, | |
| gate-quality diagnostics, qualitative examples, and latent-delta statistics. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import gc | |
| import json | |
| import math | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterable, List, Optional | |
| import numpy as np | |
| import torch | |
| from datasets import Audio, load_dataset | |
| from jiwer import cer, wer | |
| from tqdm import tqdm | |
| REPO_ROOT = Path(__file__).resolve().parents[2] | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| from eval import ( # noqa: E402 | |
| build_base_model_bundle, | |
| build_latent_bundle, | |
| clean_prediction, | |
| configure_text_normalizer, | |
| normalize_text, | |
| ) | |
| MODEL_ID = "Qwen/Qwen3-ASR-0.6B" | |
| LATENT_CKPT = "eval_runs/paper_tbd_retrain_20260518/checkpoints/activation_500/activation_500_epoch10.pth" | |
| MAX_NEW_TOKENS = 128 | |
| DATASET_PRESETS = { | |
| "fleurs_en_us": { | |
| "dataset_name": "google/fleurs", | |
| "config": "en_us", | |
| "split": "test", | |
| "text_columns": ["transcription", "raw_transcription", "sentence", "text"], | |
| "normalizer": "english", | |
| "language_hint": "English", | |
| "label": r"FLEURS (\texttt{en\_us})", | |
| "source_label": "FLEURS Q4", | |
| }, | |
| "voxpopuli_en": { | |
| "dataset_name": "facebook/voxpopuli", | |
| "config": "en", | |
| "split": "test", | |
| "text_columns": ["normalized_text", "raw_text", "text", "sentence"], | |
| "normalizer": "english", | |
| "language_hint": "English", | |
| "label": r"VoxPopuli (\texttt{en})", | |
| "source_label": "VoxPopuli Q4", | |
| }, | |
| } | |
| def choose_device() -> str: | |
| return "cuda" if torch.cuda.is_available() else "cpu" | |
| def choose_dtype(device: str) -> torch.dtype: | |
| return torch.bfloat16 if device == "cuda" and torch.cuda.is_bf16_supported() else torch.float16 | |
| def tensor_to_float(value: Any) -> Optional[float]: | |
| if value is None: | |
| return None | |
| if torch.is_tensor(value): | |
| if value.numel() == 0: | |
| return None | |
| return float(value.detach().flatten()[0].cpu().item()) | |
| try: | |
| return float(value) | |
| except Exception: | |
| return None | |
| def tensor_to_list(value: Any) -> List[float]: | |
| if value is None: | |
| return [] | |
| if torch.is_tensor(value): | |
| return [float(x) for x in value.detach().flatten().cpu().tolist()] | |
| if isinstance(value, (list, tuple)): | |
| return [float(x) for x in value] | |
| return [] | |
| def get_ref(sample: Dict[str, Any], text_columns: List[str]) -> str: | |
| for col in text_columns: | |
| value = sample.get(col) | |
| if isinstance(value, str) and value.strip(): | |
| return value | |
| return "" | |
| def sample_to_features(model: Any, processor: Any, sample: Dict[str, Any]) -> Optional[Dict[str, Any]]: | |
| audio = sample.get("audio") | |
| if not isinstance(audio, dict) or "array" not in audio or "sampling_rate" not in audio: | |
| return None | |
| audio_array = np.array(audio["array"], dtype=np.float64) | |
| target_dtype = model.thinker.dtype if hasattr(model.thinker, "dtype") else torch.float32 | |
| feat_out = processor.feature_extractor( | |
| audio_array, | |
| sampling_rate=audio["sampling_rate"], | |
| return_attention_mask=True, | |
| ) | |
| device = model.base_model.device | |
| feats = torch.tensor(feat_out.input_features[0], dtype=target_dtype, device=device).unsqueeze(0) | |
| n_frames = feats.size(-1) | |
| if getattr(feat_out, "attention_mask", None) is not None: | |
| raw_mask = feat_out.attention_mask[0] | |
| if not isinstance(raw_mask, (list, torch.Tensor)): | |
| raw_mask = list(raw_mask) | |
| if isinstance(raw_mask, torch.Tensor): | |
| raw_mask = raw_mask.long() | |
| else: | |
| raw_mask = torch.tensor(raw_mask, dtype=torch.long) | |
| if raw_mask.size(-1) < n_frames: | |
| raw_mask = torch.cat([raw_mask, torch.zeros(n_frames - raw_mask.size(-1), dtype=torch.long)]) | |
| else: | |
| raw_mask = raw_mask[:n_frames] | |
| feature_attention_mask = raw_mask.to(device=device).unsqueeze(0) | |
| else: | |
| feature_attention_mask = torch.ones((1, n_frames), dtype=torch.long, device=device) | |
| if int(feature_attention_mask.sum().item()) < 10: | |
| return None | |
| return {"feats": feats, "feature_attention_mask": feature_attention_mask} | |
| def transcribe( | |
| model: Any, | |
| processor: Any, | |
| sample: Dict[str, Any], | |
| *, | |
| use_baseline: bool, | |
| theta: float, | |
| language_hint: str, | |
| ) -> Optional[Dict[str, Any]]: | |
| feats = sample_to_features(model, processor, sample) | |
| if feats is None: | |
| return None | |
| prompt_text = f"Transcribe the {language_hint} audio into text." if language_hint else "Transcribe the audio into text." | |
| gen_kwargs = { | |
| "feature_attention_mask": feats["feature_attention_mask"], | |
| "max_new_tokens": MAX_NEW_TOKENS, | |
| "use_baseline": use_baseline, | |
| "return_thoughts": False, | |
| "return_stats": True, | |
| "do_sample": False, | |
| "eos_token_id": [151645, 151643], | |
| "num_beams": 1, | |
| "language_hint": language_hint, | |
| "prompt_text": prompt_text, | |
| "dynamic_halt_threshold": theta, | |
| } | |
| out = model.generate(feats["feats"], **gen_kwargs) | |
| if isinstance(out, tuple): | |
| gen_ids = out[0] | |
| stats = out[1] if len(out) > 1 and isinstance(out[1], dict) else {} | |
| else: | |
| gen_ids = out | |
| stats = {} | |
| ids = gen_ids[0] | |
| eos_id = processor.tokenizer.eos_token_id | |
| if eos_id is not None and (ids == eos_id).any(): | |
| eos_pos = (ids == eos_id).nonzero(as_tuple=True)[0][0] | |
| ids = ids[:eos_pos] | |
| raw = processor.tokenizer.decode(ids, skip_special_tokens=True) | |
| pred = clean_prediction(raw) | |
| deq = tensor_to_float(stats.get("deq_iters")) | |
| return { | |
| "pred": pred, | |
| "pred_norm": normalize_text(pred), | |
| "stats": { | |
| "deq_iters": 0 if deq is None else int(round(deq)), | |
| "skipped": bool(stats.get("skipped", False)) if stats else False, | |
| "v_preds": tensor_to_list(stats.get("v_preds")), | |
| "scaled_norm_mean": tensor_to_list(stats.get("scaled_norm_mean")), | |
| "step_cos": tensor_to_float(stats.get("step_cos")), | |
| "diff_norm": tensor_to_float(stats.get("diff_norm")), | |
| }, | |
| } | |
| def load_rows(path: Path) -> List[Dict[str, Any]]: | |
| if not path.exists(): | |
| return [] | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| def save_rows(path: Path, rows: List[Dict[str, Any]]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(rows, indent=2, ensure_ascii=False), encoding="utf-8") | |
| def iter_dataset(preset: Dict[str, Any], streaming: bool) -> Iterable[Dict[str, Any]]: | |
| ds = load_dataset( | |
| preset["dataset_name"], | |
| preset["config"], | |
| split=preset["split"], | |
| streaming=streaming, | |
| trust_remote_code=True, | |
| ) | |
| if not streaming: | |
| ds = ds.cast_column("audio", Audio(sampling_rate=16000)) | |
| return ds | |
| def collect_refs(preset: Dict[str, Any], streaming: bool) -> List[Dict[str, Any]]: | |
| refs: List[Dict[str, Any]] = [] | |
| for idx, sample in enumerate(tqdm(iter_dataset(preset, streaming), desc="refs")): | |
| ref_raw = get_ref(sample, preset["text_columns"]) | |
| ref_norm = normalize_text(ref_raw) | |
| if ref_norm: | |
| refs.append({"idx": idx, "ref_raw": ref_raw, "ref_norm": ref_norm}) | |
| return refs | |
| def run_baseline(args: argparse.Namespace, preset: Dict[str, Any], refs: List[Dict[str, Any]], rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | |
| done = {r["idx"] for r in rows if "baseline_norm" in r} | |
| if len(done) == len(refs): | |
| return rows | |
| by_idx = {r["idx"]: r for r in rows} | |
| ref_idx = {r["idx"]: r for r in refs} | |
| bundle = build_base_model_bundle(args.model_id, args.device, args.dtype) | |
| for idx, sample in enumerate(tqdm(iter_dataset(preset, args.streaming), desc="baseline")): | |
| if idx not in ref_idx or idx in done: | |
| continue | |
| out = transcribe( | |
| bundle.model, | |
| bundle.processor, | |
| sample, | |
| use_baseline=True, | |
| theta=args.theta, | |
| language_hint=preset["language_hint"], | |
| ) | |
| if out is None: | |
| continue | |
| row = by_idx.setdefault(idx, {"idx": idx, **ref_idx[idx]}) | |
| row["baseline_pred"] = out["pred"] | |
| row["baseline_norm"] = out["pred_norm"] | |
| save_rows(args.out_json, sorted(by_idx.values(), key=lambda x: x["idx"])) | |
| del bundle | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| return sorted(by_idx.values(), key=lambda x: x["idx"]) | |
| def run_latent_path( | |
| args: argparse.Namespace, | |
| preset: Dict[str, Any], | |
| rows: List[Dict[str, Any]], | |
| *, | |
| theta: float, | |
| pred_key: str, | |
| norm_key: str, | |
| stats_key: str, | |
| desc: str, | |
| ) -> List[Dict[str, Any]]: | |
| eligible = {r["idx"] for r in rows if "baseline_norm" in r} | |
| done = {r["idx"] for r in rows if norm_key in r} | |
| if eligible and done == eligible: | |
| return rows | |
| by_idx = {r["idx"]: r for r in rows} | |
| bundle = build_latent_bundle(args.model_id, args.latent_ckpt, args.n_latent, args.device, args.dtype) | |
| for idx, sample in enumerate(tqdm(iter_dataset(preset, args.streaming), desc=desc)): | |
| if idx not in eligible or idx in done: | |
| continue | |
| out = transcribe( | |
| bundle.model, | |
| bundle.processor, | |
| sample, | |
| use_baseline=False, | |
| theta=theta, | |
| language_hint=preset["language_hint"], | |
| ) | |
| if out is None: | |
| continue | |
| row = by_idx[idx] | |
| row[pred_key] = out["pred"] | |
| row[norm_key] = out["pred_norm"] | |
| row[stats_key] = out["stats"] | |
| save_rows(args.out_json, sorted(by_idx.values(), key=lambda x: x["idx"])) | |
| del bundle | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| return sorted(by_idx.values(), key=lambda x: x["idx"]) | |
| def group_wer(group: List[Dict[str, Any]], key: str) -> float: | |
| if not group: | |
| return 0.0 | |
| return 100 * wer([r["ref_norm"] for r in group], [r[key] for r in group]) | |
| def group_cer(group: List[Dict[str, Any]], key: str) -> float: | |
| if not group: | |
| return 0.0 | |
| return 100 * cer([r["ref_norm"] for r in group], [r[key] for r in group]) | |
| def esc_latex(text: str) -> str: | |
| replacements = { | |
| "\\": r"\textbackslash{}", | |
| "&": r"\&", | |
| "%": r"\%", | |
| "$": r"\$", | |
| "#": r"\#", | |
| "_": r"\_", | |
| "{": r"\{", | |
| "}": r"\}", | |
| "~": r"\textasciitilde{}", | |
| "^": r"\textasciicircum{}", | |
| } | |
| return "".join(replacements.get(ch, ch) for ch in text) | |
| def format_delta(value: float, bold_negative: bool = False) -> str: | |
| if bold_negative and value < 0: | |
| return rf"$\boldsymbol{{{value:+.2f}}}$" | |
| return rf"${value:+.2f}$" | |
| def write_latex(args: argparse.Namespace, preset: Dict[str, Any], rows: List[Dict[str, Any]]) -> None: | |
| final_rows = [ | |
| r for r in rows | |
| if all(k in r for k in ("baseline_norm", "latent_norm", "full_norm")) | |
| ] | |
| for r in final_rows: | |
| ref = r["ref_norm"] | |
| r["baseline_wer"] = float(wer(ref, r["baseline_norm"])) | |
| r["latent_wer"] = float(wer(ref, r["latent_norm"])) | |
| r["full_wer"] = float(wer(ref, r["full_norm"])) | |
| r["baseline_cer"] = float(cer(ref, r["baseline_norm"])) | |
| r["latent_cer"] = float(cer(ref, r["latent_norm"])) | |
| r["full_cer"] = float(cer(ref, r["full_norm"])) | |
| base_wer = group_wer(final_rows, "baseline_norm") | |
| lat_wer = group_wer(final_rows, "latent_norm") | |
| full_wer = group_wer(final_rows, "full_norm") | |
| base_cer = group_cer(final_rows, "baseline_norm") | |
| lat_cer = group_cer(final_rows, "latent_norm") | |
| full_cer = group_cer(final_rows, "full_norm") | |
| sorted_rows = sorted(final_rows, key=lambda r: (r["baseline_wer"], r["idx"])) | |
| n = len(sorted_rows) | |
| bins = [] | |
| for q in range(4): | |
| part = sorted_rows[math.floor(q * n / 4): math.floor((q + 1) * n / 4)] | |
| bw = group_wer(part, "baseline_norm") | |
| lw = group_wer(part, "latent_norm") | |
| skip_q = 100 * sum(1 for r in part if r["latent_stats"].get("deq_iters", 0) == 0) / max(1, len(part)) | |
| bins.append((q + 1, len(part), bw, lw, lw - bw, skip_q)) | |
| skipped = [r for r in final_rows if r["latent_stats"].get("deq_iters", 0) == 0] | |
| processed = [r for r in final_rows if r["latent_stats"].get("deq_iters", 0) > 0] | |
| skip_base = group_wer(skipped, "baseline_norm") | |
| skip_full = group_wer(skipped, "full_norm") | |
| proc_base = group_wer(processed, "baseline_norm") | |
| proc_lat = group_wer(processed, "latent_norm") | |
| proc_full = group_wer(processed, "full_norm") | |
| step_counts = {i: 0 for i in range(args.n_latent + 1)} | |
| for r in final_rows: | |
| deq = int(r["latent_stats"].get("deq_iters", 0)) | |
| step_counts[deq] = step_counts.get(deq, 0) + 1 | |
| step_rates = {k: 100 * v / max(1, len(final_rows)) for k, v in step_counts.items()} | |
| avg_steps = sum(k * v for k, v in step_counts.items()) / max(1, len(final_rows)) | |
| processed_full = processed | |
| norms_by_step: List[List[float]] = [[] for _ in range(args.n_latent)] | |
| cos_vals: List[float] = [] | |
| diff_vals: List[float] = [] | |
| for r in processed_full: | |
| norms = r["full_stats"].get("scaled_norm_mean", []) | |
| for i, val in enumerate(norms[:args.n_latent]): | |
| norms_by_step[i].append(float(val)) | |
| if r["full_stats"].get("step_cos") is not None: | |
| cos_vals.append(float(r["full_stats"]["step_cos"])) | |
| if r["full_stats"].get("diff_norm") is not None: | |
| diff_vals.append(float(r["full_stats"]["diff_norm"])) | |
| step_scale_means = [float(np.mean(vals)) if vals else 0.0 for vals in norms_by_step] | |
| step_scale_text = ", ".join(f"${v:.4f}$" for v in step_scale_means) | |
| def dist_stats(vals: List[float]) -> Dict[str, float]: | |
| if not vals: | |
| return { | |
| "mean": 0.0, | |
| "std": 0.0, | |
| "p25": 0.0, | |
| "median": 0.0, | |
| "p75": 0.0, | |
| "min": 0.0, | |
| "max": 0.0, | |
| } | |
| arr = np.asarray(vals, dtype=np.float64) | |
| return { | |
| "mean": float(np.mean(arr)), | |
| "std": float(np.std(arr)), | |
| "p25": float(np.percentile(arr, 25)), | |
| "median": float(np.percentile(arr, 50)), | |
| "p75": float(np.percentile(arr, 75)), | |
| "min": float(np.min(arr)), | |
| "max": float(np.max(arr)), | |
| } | |
| cos_stat = dist_stats(cos_vals) | |
| diff_stat = dist_stats(diff_vals) | |
| examples = [ | |
| r for r in final_rows | |
| if r["baseline_wer"] > r["latent_wer"] | |
| and r["latent_stats"].get("deq_iters", 0) >= 1 | |
| and len(r["ref_norm"].split()) >= 6 | |
| ] | |
| examples.sort(key=lambda r: (r["baseline_wer"] - r["latent_wer"], r["baseline_wer"]), reverse=True) | |
| examples = examples[:4] | |
| lines: List[str] = [] | |
| lines.append("% ================================================================\n") | |
| lines.append(f"% Per-sample analysis generated for {args.dataset_key}\n") | |
| lines.append(f"% Samples used: {len(final_rows)}\n") | |
| lines.append("% ================================================================\n\n") | |
| lines.append("\\subsection{Analysis}\n") | |
| lines.append("\\label{sec:analysis}\n\n") | |
| lines.append("\\textbf{Difficulty-Binned Reductions.}\\quad\n") | |
| lines.append( | |
| f"We partition the {preset['label']} test set into four equal-sized bins by per-utterance " | |
| f"Baseline WER and recompute WER within each bin (Table~\\ref{{tab:difficulty_bins}}). " | |
| f"The aggregate result is small but positive: \\method{{}} reduces WER from " | |
| f"${base_wer:.2f}\\%$ to ${lat_wer:.2f}\\%$ at $\\theta{{=}}{args.theta:.1f}$, " | |
| f"while forced full compute reaches ${full_wer:.2f}\\%$. " | |
| ) | |
| best_bin = min(bins, key=lambda x: x[4]) | |
| lines.append( | |
| f"The largest reduction appears in Q{best_bin[0]}, where $\\Delta$WER is " | |
| f"${best_bin[4]:+.2f}$~pp. This confirms that the average gain should not be read " | |
| "as a uniform per-utterance improvement; latent scaling mainly changes the subset " | |
| "where the frozen baseline leaves residual errors.\n\n" | |
| ) | |
| lines.append("\\begin{table}[ht]\n") | |
| lines.append( | |
| f" \\caption{{Difficulty-binned analysis on {preset['label']} at $\\theta{{=}}{args.theta:.1f}$ " | |
| f"({len(final_rows):,} utterances total). Utterances are partitioned into Baseline-WER quartiles. " | |
| "$\\Delta$WER denotes \\method{} minus Baseline, so negative values indicate improvement.}\n" | |
| ) | |
| lines.append(" \\label{tab:difficulty_bins}\n") | |
| lines.append(" \\centering\n") | |
| lines.append(" \\resizebox{\\columnwidth}{!}{\n") | |
| lines.append(" \\begin{tabular}{l c c c c c}\n") | |
| lines.append(" \\toprule\n") | |
| lines.append(" \\textbf{Bin} & \\textbf{\\#utts} & \\textbf{Baseline WER (\\%)} & \\textbf{\\method{} WER (\\%)} & \\textbf{$\\Delta$WER (pp)} & \\textbf{Skip (\\%)} \\\\\n") | |
| lines.append(" \\midrule\n") | |
| labels = ["Q1 (easiest)", "Q2", "Q3", "Q4 (hardest)"] | |
| for q, count, bw, lw, d, skip_q in bins: | |
| method_cell = rf"\textbf{{{lw:.2f}}}" if d < 0 else f"{lw:.2f}" | |
| lines.append( | |
| f" {labels[q - 1]} & {count:,} & {bw:.2f} & {method_cell} & " | |
| f"{format_delta(d, bold_negative=True)} & {skip_q:.1f} \\\\\n" | |
| ) | |
| lines.append(" \\bottomrule\n") | |
| lines.append(" \\end{tabular}\n") | |
| lines.append(" }\n") | |
| lines.append("\\end{table}\n\n") | |
| lines.append("\\textbf{Value Head Decision Quality.}\\quad\n") | |
| lines.append( | |
| "The step distribution shows how much compute the Value Head allocates: " | |
| f"at $\\theta{{=}}{args.theta:.1f}$, it skips {step_rates.get(0, 0.0):.1f}\\% of utterances " | |
| f"and uses an average of {avg_steps:.2f} latent steps. " | |
| "We further test selectivity by forcing the full $N{=}4$ path on the utterances " | |
| "that the deployed gate skips. Table~\\ref{tab:gate_quality} reports the actual " | |
| "deployed change and this counterfactual full-compute change.\n\n" | |
| ) | |
| lines.append("\\begin{table}[ht]\n") | |
| lines.append( | |
| f" \\caption{{Value Head decision quality on {preset['label']} at $\\theta{{=}}{args.theta:.1f}$. " | |
| "\\textbf{Counterfactual $\\Delta$WER} forces the $N{=}4$ latent path on each subset.}\n" | |
| ) | |
| lines.append(" \\label{tab:gate_quality}\n") | |
| lines.append(" \\centering\n") | |
| lines.append(" \\resizebox{\\columnwidth}{!}{\n") | |
| lines.append(" \\begin{tabular}{l c c c}\n") | |
| lines.append(" \\toprule\n") | |
| lines.append(" \\textbf{Subset (at $\\theta{=}0.0$)} & \\textbf{\\#utts} & \\textbf{Actual $\\Delta$WER (pp)} & \\textbf{Counterfactual $\\Delta$WER (pp)} \\\\\n") | |
| lines.append(" \\midrule\n") | |
| lines.append(f" Skipped ($v_0 < 0$) & {len(skipped):,} & $0.00$ (by construction) & {format_delta(skip_full - skip_base, True)} \\\\\n") | |
| lines.append(f" Processed ($v_0 \\geq 0$) & {len(processed):,} & {format_delta(proc_lat - proc_base, True)} & {format_delta(proc_full - proc_base, True)} \\\\\n") | |
| lines.append(" \\bottomrule\n") | |
| lines.append(" \\end{tabular}\n") | |
| lines.append(" }\n") | |
| lines.append("\\end{table}\n\n") | |
| lines.append("\\textbf{Step Allocation.}\\quad\n") | |
| lines.append( | |
| f"Table~\\ref{{tab:vox_step_dist}} gives the full $N$-step distribution on {preset['label']}. " | |
| "Compared with forced full compute, the deployed policy keeps most examples away from " | |
| "the deepest path while retaining the aggregate WER reduction.\n\n" | |
| ) | |
| lines.append("\\begin{table}[ht]\n") | |
| lines.append( | |
| f" \\caption{{N-step distribution on {preset['label']} at $\\theta{{=}}{args.theta:.1f}$.}}\n" | |
| ) | |
| lines.append(" \\label{tab:vox_step_dist}\n") | |
| lines.append(" \\centering\n") | |
| lines.append(" \\resizebox{0.9\\columnwidth}{!}{\n") | |
| lines.append(" \\begin{tabular}{l c c c c c c}\n") | |
| lines.append(" \\toprule\n") | |
| lines.append(" \\textbf{Dataset} & \\textbf{Avg. steps} & \\textbf{N=0} & \\textbf{N=1} & \\textbf{N=2} & \\textbf{N=3} & \\textbf{N=4} \\\\\n") | |
| lines.append(" \\midrule\n") | |
| lines.append( | |
| f" {preset['label']} & {avg_steps:.2f} & " | |
| f"{step_rates.get(0, 0.0):.1f}\\% & {step_rates.get(1, 0.0):.1f}\\% & " | |
| f"{step_rates.get(2, 0.0):.1f}\\% & {step_rates.get(3, 0.0):.1f}\\% & " | |
| f"{step_rates.get(4, 0.0):.1f}\\% \\\\\n" | |
| ) | |
| lines.append(" \\bottomrule\n") | |
| lines.append(" \\end{tabular}\n") | |
| lines.append(" }\n") | |
| lines.append("\\end{table}\n\n") | |
| if examples: | |
| lines.append("\\textbf{Qualitative Examples.}\\quad\n") | |
| lines.append( | |
| f"Table~\\ref{{tab:qualitative}} shows hard-bin {preset['label']} utterances where " | |
| "the latent loop changes the transcript.\n\n" | |
| ) | |
| lines.append("\\begin{table}[ht]\n") | |
| lines.append( | |
| f" \\caption{{Qualitative examples from {preset['label']} hard bins.}}\n" | |
| ) | |
| lines.append(" \\label{tab:qualitative}\n") | |
| lines.append(" \\centering\n") | |
| lines.append(" \\resizebox{\\columnwidth}{!}{\n") | |
| lines.append(" \\begin{tabular}{p{0.13\\columnwidth} p{0.27\\columnwidth} p{0.27\\columnwidth} p{0.27\\columnwidth}}\n") | |
| lines.append(" \\toprule\n") | |
| lines.append(" \\textbf{Source} & \\textbf{Reference} & \\textbf{Baseline} & \\textbf{\\method{}} \\\\\n") | |
| lines.append(" \\midrule\n") | |
| for r in examples: | |
| lines.append( | |
| f" {preset['source_label']} & {esc_latex(r['ref_raw'])} & " | |
| f"{esc_latex(r['baseline_pred'])} & {esc_latex(r['latent_pred'])} \\\\\n" | |
| ) | |
| lines.append(" \\bottomrule\n") | |
| lines.append(" \\end{tabular}\n") | |
| lines.append(" }\n") | |
| lines.append("\\end{table}\n\n") | |
| lines.append("\\textbf{Refinement-Path Diagnostics.}\\quad\n") | |
| lines.append( | |
| "We recompute the forced-full $N{=}4$ path on the " | |
| f"{preset['label']} processed subset ({len(processed_full):,} utterances with $N{{>}}0$ " | |
| f"under $\\theta{{=}}{args.theta:.1f}$). The scaled delta norms are identical across " | |
| "utterances because each delta is $L_2$-normalized and multiplied by the learned " | |
| "per-step scale; they therefore measure the bounded step-size constraint rather " | |
| f"than dataset-specific refinement behavior. For this run, the per-step scales are {step_scale_text}. " | |
| "To characterize sample-dependent behavior, Table~\\ref{tab:refinement_dynamics} " | |
| "instead reports the distribution of consecutive-delta cosine and consecutive-delta " | |
| "difference norm. " | |
| f"The cosine range is ${cos_stat['min']:.4f}$--${cos_stat['max']:.4f}$ and the " | |
| f"difference-norm range is ${diff_stat['min']:.4f}$--${diff_stat['max']:.4f}$, " | |
| "confirming that the forced refinement path is not a constant copied trajectory " | |
| "while the update magnitudes remain bounded.\n\n" | |
| ) | |
| lines.append("\\begin{table}[ht]\n") | |
| lines.append( | |
| f" \\caption{{Forced-full refinement diagnostics on the {preset['label']} processed subset " | |
| f"({len(processed_full):,} utterances with $N{{>}}0$ under $\\theta{{=}}{args.theta:.1f}$). " | |
| "Scaled delta norms are fixed by the learned step scales; cosine and difference " | |
| "statistics vary across utterances.}\n" | |
| ) | |
| lines.append(" \\label{tab:refinement_dynamics}\n") | |
| lines.append(" \\centering\n") | |
| lines.append(" \\resizebox{\\columnwidth}{!}{\n") | |
| lines.append(" \\begin{tabular}{l c c c c c}\n") | |
| lines.append(" \\toprule\n") | |
| lines.append(r" \textbf{Metric} & \textbf{Mean} & \textbf{Std.} & \textbf{P25} & \textbf{Median} & \textbf{P75} \\" + "\n") | |
| lines.append(" \\midrule\n") | |
| lines.append( | |
| f" Consecutive-delta cosine & {cos_stat['mean']:.4f} & {cos_stat['std']:.4f} & " | |
| f"{cos_stat['p25']:.4f} & {cos_stat['median']:.4f} & {cos_stat['p75']:.4f} " | |
| + r"\\" | |
| + "\n" | |
| ) | |
| lines.append( | |
| f" Consecutive-delta diff. norm & {diff_stat['mean']:.4f} & {diff_stat['std']:.4f} & " | |
| f"{diff_stat['p25']:.4f} & {diff_stat['median']:.4f} & {diff_stat['p75']:.4f} " | |
| + r"\\" | |
| + "\n" | |
| ) | |
| lines.append(" \\bottomrule\n") | |
| lines.append(" \\end{tabular}\n") | |
| lines.append(" }\n") | |
| lines.append("\\end{table}\n\n") | |
| lines.append("% Overall metrics for cross-checking:\n") | |
| lines.append(f"% Baseline WER/CER: {base_wer:.4f}/{base_cer:.4f}\n") | |
| lines.append(f"% Latent theta={args.theta:.1f} WER/CER: {lat_wer:.4f}/{lat_cer:.4f}\n") | |
| lines.append(f"% Forced full WER/CER: {full_wer:.4f}/{full_cer:.4f}\n") | |
| args.out_tex.write_text("".join(lines), encoding="utf-8") | |
| save_rows(args.out_json, final_rows) | |
| print(f"wrote {args.out_json}") | |
| print(f"wrote {args.out_tex}") | |
| print(f"samples={len(final_rows)} baseline_wer={base_wer:.4f} latent_wer={lat_wer:.4f} full_wer={full_wer:.4f}") | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--dataset-key", choices=sorted(DATASET_PRESETS), default="voxpopuli_en") | |
| parser.add_argument("--model-id", default=MODEL_ID) | |
| parser.add_argument("--latent-ckpt", default=LATENT_CKPT) | |
| parser.add_argument("--n-latent", type=int, default=4) | |
| parser.add_argument("--theta", type=float, default=0.0) | |
| parser.add_argument("--full-theta", type=float, default=-2.0) | |
| parser.add_argument("--streaming", action=argparse.BooleanOptionalAction, default=True) | |
| parser.add_argument("--out-dir", type=Path, default=Path("eval_runs/paper_activation500_voxpopuli_analysis")) | |
| args = parser.parse_args() | |
| args.out_dir.mkdir(parents=True, exist_ok=True) | |
| args.out_json = args.out_dir / f"{args.dataset_key}_per_sample.json" | |
| args.out_tex = args.out_dir / f"{args.dataset_key}_analysis_latex.tex" | |
| args.device = choose_device() | |
| args.dtype = choose_dtype(args.device) | |
| return args | |
| def main() -> None: | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| args = parse_args() | |
| preset = DATASET_PRESETS[args.dataset_key] | |
| configure_text_normalizer(preset["normalizer"]) | |
| rows = load_rows(args.out_json) | |
| refs = collect_refs(preset, args.streaming) | |
| existing = {r["idx"]: r for r in rows} | |
| for ref in refs: | |
| existing.setdefault(ref["idx"], {"idx": ref["idx"], **ref}) | |
| rows = sorted(existing.values(), key=lambda x: x["idx"]) | |
| save_rows(args.out_json, rows) | |
| rows = run_baseline(args, preset, refs, rows) | |
| rows = run_latent_path( | |
| args, | |
| preset, | |
| rows, | |
| theta=args.theta, | |
| pred_key="latent_pred", | |
| norm_key="latent_norm", | |
| stats_key="latent_stats", | |
| desc=f"latent_theta{args.theta:g}", | |
| ) | |
| rows = run_latent_path( | |
| args, | |
| preset, | |
| rows, | |
| theta=args.full_theta, | |
| pred_key="full_pred", | |
| norm_key="full_norm", | |
| stats_key="full_stats", | |
| desc="latent_full", | |
| ) | |
| write_latex(args, preset, rows) | |
| if __name__ == "__main__": | |
| main() | |