Upload folder using huggingface_hub
Browse files- README.md +16 -12
- experiments/README.md +1 -1
- experiments/exp_encoders.py +84 -84
- experiments/exp_ensemble.py +48 -48
- experiments/exp_finetune.py +95 -95
- experiments/exp_heads.py +44 -44
- experiments/exp_layers.py +20 -20
- experiments/exp_pooling.py +36 -36
- experiments/exp_trainsize.py +11 -11
- experiments/exp_utterance.py +50 -50
- experiments/exp_window.py +58 -58
- inference/README.md +1 -10
- inference/config.yaml +3 -3
- inference/phase2_run.err +1 -0
- inference/phase2_run.out +23 -0
- inference/requirements.txt +0 -1
- inference/run.py +6 -3
- requirements.txt +0 -1
README.md
CHANGED
|
@@ -17,6 +17,10 @@ Pick the most **empathetic** spoken response to a human utterance. The candidate
|
|
| 17 |
with essentially the same words — they differ only in **vocal delivery** — so this is a **listening**
|
| 18 |
problem, not a text problem: the answer is in the prosody, never the transcript.
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
## The method
|
| 21 |
|
| 22 |
A **single open-weights model**, run **offline** with **no API** and **no dataset-specific handling**:
|
|
@@ -32,7 +36,7 @@ A **single open-weights model**, run **offline** with **no API** and **no datase
|
|
| 32 |
|
| 33 |
Scored against our expert annotations with the official metric: **FINAL = 0.832** (tone 0.933, context 0.805),
|
| 34 |
**+0.285** over the 0.547 baseline. The method, ablations, environment, and reproduction details are in
|
| 35 |
-
|
| 36 |
|
| 37 |
## Reproduce the submission
|
| 38 |
|
|
@@ -46,16 +50,8 @@ python inference/evaluate.py # official metric vs. inference/annotations.jso
|
|
| 46 |
```
|
| 47 |
|
| 48 |
First run downloads WavLM-large (~1.2 GB) from the Hub; afterwards it runs fully offline (GPU ≈10–15 min for
|
| 49 |
-
530 questions, or CPU).
|
| 50 |
-
|
| 51 |
-
> **Windows / PowerShell:** `.venv\Scripts\activate` can be silently blocked by the default script
|
| 52 |
-
> execution policy, after which `pip` installs into the wrong Python. Safest is to skip activation and
|
| 53 |
-
> call the venv's interpreter directly — `.venv\Scripts\python.exe -m pip install -r inference\requirements.txt`,
|
| 54 |
-
> then `.venv\Scripts\python.exe inference\run.py`. After activating, `pip -V` must show a path inside `.venv`.
|
| 55 |
-
>
|
| 56 |
-
> **GPU:** install the CUDA torch build *before* the requirements (see the note in `requirements.txt`).
|
| 57 |
-
> If a CPU-only torch is already installed, pip will skip the CUDA build unless you force it:
|
| 58 |
-
> `pip install torch --index-url https://download.pytorch.org/whl/cu128 --force-reinstall`
|
| 59 |
|
| 60 |
To retrain the ranker from scratch (the full pipeline that produced `inference/ranker.pkl`):
|
| 61 |
|
|
@@ -75,8 +71,16 @@ scripts/produce_submission.py # full pipeline: source data -> train ranker ->
|
|
| 75 |
experiments/ # one-command reproduction of every table/finding in the report
|
| 76 |
empathyeval/ # release parsing + the 16 kHz-mono audio loader + the official metric
|
| 77 |
configs/phase1.yaml # data paths, audio, cache
|
|
|
|
|
|
|
|
|
|
| 78 |
```
|
| 79 |
|
| 80 |
**For the competition, only `inference/` is needed** — it is fully self-contained (the pre-trained model
|
| 81 |
plus a pure-inference script) and, delivered alone, regenerates the exact result.
|
| 82 |
-
`scripts/produce_submission.py` is the full pipeline that trained the ranker saved as `inference/ranker.pkl`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
with essentially the same words — they differ only in **vocal delivery** — so this is a **listening**
|
| 18 |
problem, not a text problem: the answer is in the prosody, never the transcript.
|
| 19 |
|
| 20 |
+
This repository is the complete submission: the pre-trained model, the self-contained inference and
|
| 21 |
+
evaluation code, the full training pipeline, the reproduction scripts, and the technical report
|
| 22 |
+
(`report/technical_report.pdf`).
|
| 23 |
+
|
| 24 |
## The method
|
| 25 |
|
| 26 |
A **single open-weights model**, run **offline** with **no API** and **no dataset-specific handling**:
|
|
|
|
| 36 |
|
| 37 |
Scored against our expert annotations with the official metric: **FINAL = 0.832** (tone 0.933, context 0.805),
|
| 38 |
**+0.285** over the 0.547 baseline. The method, ablations, environment, and reproduction details are in
|
| 39 |
+
`report/technical_report.pdf`.
|
| 40 |
|
| 41 |
## Reproduce the submission
|
| 42 |
|
|
|
|
| 50 |
```
|
| 51 |
|
| 52 |
First run downloads WavLM-large (~1.2 GB) from the Hub; afterwards it runs fully offline (GPU ≈10–15 min for
|
| 53 |
+
530 questions, or CPU). Full step-by-step instructions — the runtime environment, libraries, and every
|
| 54 |
+
parameter — are in `report/technical_report.pdf` (Appendix A) and `inference/README.md`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
To retrain the ranker from scratch (the full pipeline that produced `inference/ranker.pkl`):
|
| 57 |
|
|
|
|
| 71 |
experiments/ # one-command reproduction of every table/finding in the report
|
| 72 |
empathyeval/ # release parsing + the 16 kHz-mono audio loader + the official metric
|
| 73 |
configs/phase1.yaml # data paths, audio, cache
|
| 74 |
+
submissions/ # the deliverable, <team_id>.jsonl
|
| 75 |
+
report/ # technical report (PDF + LaTeX source + references.bib)
|
| 76 |
+
research/ # ARCHIVE: all experiment code kept for the technical report (reference-only)
|
| 77 |
```
|
| 78 |
|
| 79 |
**For the competition, only `inference/` is needed** — it is fully self-contained (the pre-trained model
|
| 80 |
plus a pure-inference script) and, delivered alone, regenerates the exact result.
|
| 81 |
+
`scripts/produce_submission.py` is the full pipeline that trained the ranker saved as `inference/ranker.pkl`.
|
| 82 |
+
|
| 83 |
+
## Compliance
|
| 84 |
+
|
| 85 |
+
Inference and the submission are produced entirely from **open weights, offline, with no API and no dataset
|
| 86 |
+
labels**. Any API-calling code lives under `research/` and was development-only — never in the submission path.
|
experiments/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Experiments — every number in the technical report, one line each
|
| 2 |
|
| 3 |
-
Reproduces Table 1, the Findings (§5), and Table 3 of
|
| 4 |
score against `inference/annotations.jsonl` with the frozen metric (`common.py`).
|
| 5 |
|
| 6 |
## Setup (once)
|
|
|
|
| 1 |
# Experiments — every number in the technical report, one line each
|
| 2 |
|
| 3 |
+
Reproduces Table 1, the Findings (§5), and Table 3 of `report/technical_report.html`. All experiments share one deterministic feature cache and
|
| 4 |
score against `inference/annotations.jsonl` with the frozen metric (`common.py`).
|
| 5 |
|
| 6 |
## Setup (once)
|
experiments/exp_encoders.py
CHANGED
|
@@ -1,84 +1,84 @@
|
|
| 1 |
-
"""Table 3 (Section 6.2) — alternative encoders: swap WavLM-large for wav2vec2-large, HuBERT-xlarge (1B), or an
|
| 2 |
-
MSP-Podcast emotion-fine-tuned wav2vec2. Each gets its own per-layer mean cache (auto-extracted on first
|
| 3 |
-
run; models auto-download from the HuggingFace Hub) and is granted its BEST layer on the evaluation set —
|
| 4 |
-
an upper bound for the alternative. Mean pooling for all (the comparison predates the pooling upgrades).
|
| 5 |
-
|
| 6 |
-
python experiments/exp_encoders.py (fast if caches exist; else ~20 min per model on GPU)
|
| 7 |
-
"""
|
| 8 |
-
import sys
|
| 9 |
-
import os
|
| 10 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 11 |
-
import hashlib
|
| 12 |
-
import numpy as np
|
| 13 |
-
from experiments import common as C
|
| 14 |
-
from empathyeval.data.audio import cached_load
|
| 15 |
-
from empathyeval.data.release import build_index
|
| 16 |
-
|
| 17 |
-
MODELS = [ # (model id, fp16, audio cap seconds) — fp16/cap12 keeps the 1B model inside 8 GB
|
| 18 |
-
("facebook/wav2vec2-large-lv60", False, 15),
|
| 19 |
-
("facebook/hubert-xlarge-ll60k", True, 12),
|
| 20 |
-
("audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim", False, 15),
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def cache_dir(mid):
|
| 25 |
-
return "cache/" + mid.split("/")[-1].replace("-", "") + "_L"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def extract(mid, fp16, cap):
|
| 29 |
-
import torch
|
| 30 |
-
torch.backends.cudnn.deterministic = True
|
| 31 |
-
torch.backends.cudnn.benchmark = False
|
| 32 |
-
from transformers import AutoFeatureExtractor, AutoModel
|
| 33 |
-
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 34 |
-
fe = AutoFeatureExtractor.from_pretrained(mid)
|
| 35 |
-
kw = {"torch_dtype": torch.float16} if fp16 else {}
|
| 36 |
-
m = AutoModel.from_pretrained(mid, **kw).to(dev).eval()
|
| 37 |
-
print(f" extracting {mid} on {dev}", flush=True)
|
| 38 |
-
cd = cache_dir(mid); os.makedirs(cd, exist_ok=True)
|
| 39 |
-
wavs = [w for it in C.train_items(2500) for w in (it.good_wav, it.bad_wav)]
|
| 40 |
-
wavs += [o.wav for q in C.QS for o in q.options]
|
| 41 |
-
for n, w in enumerate(dict.fromkeys(wavs), 1):
|
| 42 |
-
p = os.path.join(cd, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy")
|
| 43 |
-
if os.path.exists(p):
|
| 44 |
-
continue
|
| 45 |
-
y = cached_load(w, C.cfg)[:16000 * cap]
|
| 46 |
-
inp = fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(dev)
|
| 47 |
-
if fp16:
|
| 48 |
-
inp = inp.half()
|
| 49 |
-
with torch.no_grad():
|
| 50 |
-
hs = m(inp, output_hidden_states=True).hidden_states
|
| 51 |
-
np.save(p, np.stack([h.mean(dim=1).squeeze(0).float().cpu().numpy() for h in hs]).astype(np.float32))
|
| 52 |
-
if n % 500 == 0:
|
| 53 |
-
print(f" {n} clips", flush=True)
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def sweep(mid):
|
| 57 |
-
cd = cache_dir(mid)
|
| 58 |
-
mem = {}
|
| 59 |
-
|
| 60 |
-
def featL(L):
|
| 61 |
-
def f(w):
|
| 62 |
-
a = mem.get(w)
|
| 63 |
-
if a is None:
|
| 64 |
-
a = mem[w] = np.load(os.path.join(cd, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy"))
|
| 65 |
-
return a[L]
|
| 66 |
-
return f
|
| 67 |
-
nL = np.load(os.path.join(cd, os.listdir(cd)[0])).shape[0]
|
| 68 |
-
scores = {L: C.final(C.answers(featL(L))) for L in range(nL)}
|
| 69 |
-
best = max(scores, key=scores.get)
|
| 70 |
-
print(f" {mid}: best layer {best}/{nL - 1} -> Final {scores[best]:.4f} (last layer {scores[nL - 1]:.4f})", flush=True)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
print("WavLM-large reference (mean pooling):")
|
| 74 |
-
C.report("wavlm-large L9 (dev-selected)", C.answers(C.feat([C.R_MEAN()])))
|
| 75 |
-
_scores = {L: C.final(C.answers(C.feat([C.R_MEAN(L)]))) for L in range(25)}
|
| 76 |
-
_b = max(_scores, key=_scores.get)
|
| 77 |
-
print(f" wavlm-large best layer {_b}/24 -> Final {_scores[_b]:.4f} (symmetric post-hoc protocol)", flush=True)
|
| 78 |
-
for mid, fp16, cap in MODELS:
|
| 79 |
-
wavs0 = C.train_items(1)[0].good_wav
|
| 80 |
-
probe = os.path.join(cache_dir(mid), hashlib.md5(wavs0.encode()).hexdigest()[:16] + ".npy")
|
| 81 |
-
if not os.path.exists(probe):
|
| 82 |
-
extract(mid, fp16, cap)
|
| 83 |
-
sweep(mid)
|
| 84 |
-
print("(emotion2vec+: evaluated from its archived ranker predictions — see exp_ensemble.py singles)")
|
|
|
|
| 1 |
+
"""Table 3 (Section 6.2) — alternative encoders: swap WavLM-large for wav2vec2-large, HuBERT-xlarge (1B), or an
|
| 2 |
+
MSP-Podcast emotion-fine-tuned wav2vec2. Each gets its own per-layer mean cache (auto-extracted on first
|
| 3 |
+
run; models auto-download from the HuggingFace Hub) and is granted its BEST layer on the evaluation set —
|
| 4 |
+
an upper bound for the alternative. Mean pooling for all (the comparison predates the pooling upgrades).
|
| 5 |
+
|
| 6 |
+
python experiments/exp_encoders.py (fast if caches exist; else ~20 min per model on GPU)
|
| 7 |
+
"""
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 11 |
+
import hashlib
|
| 12 |
+
import numpy as np
|
| 13 |
+
from experiments import common as C
|
| 14 |
+
from empathyeval.data.audio import cached_load
|
| 15 |
+
from empathyeval.data.release import build_index
|
| 16 |
+
|
| 17 |
+
MODELS = [ # (model id, fp16, audio cap seconds) — fp16/cap12 keeps the 1B model inside 8 GB
|
| 18 |
+
("facebook/wav2vec2-large-lv60", False, 15),
|
| 19 |
+
("facebook/hubert-xlarge-ll60k", True, 12),
|
| 20 |
+
("audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim", False, 15),
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def cache_dir(mid):
|
| 25 |
+
return "cache/" + mid.split("/")[-1].replace("-", "") + "_L"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def extract(mid, fp16, cap):
|
| 29 |
+
import torch
|
| 30 |
+
torch.backends.cudnn.deterministic = True
|
| 31 |
+
torch.backends.cudnn.benchmark = False
|
| 32 |
+
from transformers import AutoFeatureExtractor, AutoModel
|
| 33 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 34 |
+
fe = AutoFeatureExtractor.from_pretrained(mid)
|
| 35 |
+
kw = {"torch_dtype": torch.float16} if fp16 else {}
|
| 36 |
+
m = AutoModel.from_pretrained(mid, **kw).to(dev).eval()
|
| 37 |
+
print(f" extracting {mid} on {dev}", flush=True)
|
| 38 |
+
cd = cache_dir(mid); os.makedirs(cd, exist_ok=True)
|
| 39 |
+
wavs = [w for it in C.train_items(2500) for w in (it.good_wav, it.bad_wav)]
|
| 40 |
+
wavs += [o.wav for q in C.QS for o in q.options]
|
| 41 |
+
for n, w in enumerate(dict.fromkeys(wavs), 1):
|
| 42 |
+
p = os.path.join(cd, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy")
|
| 43 |
+
if os.path.exists(p):
|
| 44 |
+
continue
|
| 45 |
+
y = cached_load(w, C.cfg)[:16000 * cap]
|
| 46 |
+
inp = fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(dev)
|
| 47 |
+
if fp16:
|
| 48 |
+
inp = inp.half()
|
| 49 |
+
with torch.no_grad():
|
| 50 |
+
hs = m(inp, output_hidden_states=True).hidden_states
|
| 51 |
+
np.save(p, np.stack([h.mean(dim=1).squeeze(0).float().cpu().numpy() for h in hs]).astype(np.float32))
|
| 52 |
+
if n % 500 == 0:
|
| 53 |
+
print(f" {n} clips", flush=True)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def sweep(mid):
|
| 57 |
+
cd = cache_dir(mid)
|
| 58 |
+
mem = {}
|
| 59 |
+
|
| 60 |
+
def featL(L):
|
| 61 |
+
def f(w):
|
| 62 |
+
a = mem.get(w)
|
| 63 |
+
if a is None:
|
| 64 |
+
a = mem[w] = np.load(os.path.join(cd, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy"))
|
| 65 |
+
return a[L]
|
| 66 |
+
return f
|
| 67 |
+
nL = np.load(os.path.join(cd, os.listdir(cd)[0])).shape[0]
|
| 68 |
+
scores = {L: C.final(C.answers(featL(L))) for L in range(nL)}
|
| 69 |
+
best = max(scores, key=scores.get)
|
| 70 |
+
print(f" {mid}: best layer {best}/{nL - 1} -> Final {scores[best]:.4f} (last layer {scores[nL - 1]:.4f})", flush=True)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
print("WavLM-large reference (mean pooling):")
|
| 74 |
+
C.report("wavlm-large L9 (dev-selected)", C.answers(C.feat([C.R_MEAN()])))
|
| 75 |
+
_scores = {L: C.final(C.answers(C.feat([C.R_MEAN(L)]))) for L in range(25)}
|
| 76 |
+
_b = max(_scores, key=_scores.get)
|
| 77 |
+
print(f" wavlm-large best layer {_b}/24 -> Final {_scores[_b]:.4f} (symmetric post-hoc protocol)", flush=True)
|
| 78 |
+
for mid, fp16, cap in MODELS:
|
| 79 |
+
wavs0 = C.train_items(1)[0].good_wav
|
| 80 |
+
probe = os.path.join(cache_dir(mid), hashlib.md5(wavs0.encode()).hexdigest()[:16] + ".npy")
|
| 81 |
+
if not os.path.exists(probe):
|
| 82 |
+
extract(mid, fp16, cap)
|
| 83 |
+
sweep(mid)
|
| 84 |
+
print("(emotion2vec+: evaluated from its archived ranker predictions — see exp_ensemble.py singles)")
|
experiments/exp_ensemble.py
CHANGED
|
@@ -1,48 +1,48 @@
|
|
| 1 |
-
"""Table 3 — judge comparison: individual scores of the three archived judges (Qwen3-Omni-30B textualized
|
| 2 |
-
judge, Voxtral-24B omni judge, emotion2vec ranker), plus — for completeness — the uniform majority-vote
|
| 3 |
-
combinations we explored and rejected during development.
|
| 4 |
-
|
| 5 |
-
The judges' per-question predictions are shipped as artifacts (experiments/artifacts/answers_*.json):
|
| 6 |
-
re-running those 24-30B models is expensive and, for the textualized judge, involved development-time API
|
| 7 |
-
access — the archived predictions make the comparison exactly reproducible. The WavLM member is retrained
|
| 8 |
-
live from the unified cache (mean pooling, matching the stage at which the comparison was run).
|
| 9 |
-
Ties break to the strongest member in canonical order wav > cv3 > vox > emb. No dataset-label branching.
|
| 10 |
-
|
| 11 |
-
python experiments/exp_ensemble.py (~2 min)
|
| 12 |
-
"""
|
| 13 |
-
import sys
|
| 14 |
-
import os
|
| 15 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 16 |
-
import json
|
| 17 |
-
from collections import Counter
|
| 18 |
-
from itertools import combinations
|
| 19 |
-
from experiments import common as C
|
| 20 |
-
|
| 21 |
-
ART = os.path.join(os.path.dirname(os.path.abspath(__file__)), "artifacts")
|
| 22 |
-
preds = {"wav": C.answers(C.feat([C.R_MEAN()]))}
|
| 23 |
-
for k, f in [("cv3", "answers_qwen3omni.json"), ("vox", "answers_voxtral.json"), ("emb", "answers_emotion2vec.json")]:
|
| 24 |
-
preds[k] = json.load(open(os.path.join(ART, f), encoding="utf-8"))
|
| 25 |
-
ORDER = ["wav", "cv3", "vox", "emb"]
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def vote(keys):
|
| 29 |
-
out = {}
|
| 30 |
-
for q in C.QS:
|
| 31 |
-
letters = [o.letter for o in q.options]
|
| 32 |
-
counts = Counter(preds[k][q.qid] for k in keys if q.qid in preds[k])
|
| 33 |
-
if not counts:
|
| 34 |
-
out[q.qid] = letters[0]; continue
|
| 35 |
-
top = counts.most_common(1)[0][1]
|
| 36 |
-
tied = {a for a, n in counts.items() if n == top}
|
| 37 |
-
out[q.qid] = next(iter(tied)) if len(tied) == 1 else \
|
| 38 |
-
next((preds[k][q.qid] for k in keys if k in ORDER and preds[k].get(q.qid) in tied), letters[0])
|
| 39 |
-
return out
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
print("== singles ==")
|
| 43 |
-
for k in ORDER:
|
| 44 |
-
C.report(k, preds[k])
|
| 45 |
-
print("== pairs / trios / full (uniform vote, tie -> strongest) ==")
|
| 46 |
-
for r in (2, 3, 4):
|
| 47 |
-
for combo in combinations(ORDER, r):
|
| 48 |
-
C.report("+".join(combo), vote(list(combo)))
|
|
|
|
| 1 |
+
"""Table 3 — judge comparison: individual scores of the three archived judges (Qwen3-Omni-30B textualized
|
| 2 |
+
judge, Voxtral-24B omni judge, emotion2vec ranker), plus — for completeness — the uniform majority-vote
|
| 3 |
+
combinations we explored and rejected during development.
|
| 4 |
+
|
| 5 |
+
The judges' per-question predictions are shipped as artifacts (experiments/artifacts/answers_*.json):
|
| 6 |
+
re-running those 24-30B models is expensive and, for the textualized judge, involved development-time API
|
| 7 |
+
access — the archived predictions make the comparison exactly reproducible. The WavLM member is retrained
|
| 8 |
+
live from the unified cache (mean pooling, matching the stage at which the comparison was run).
|
| 9 |
+
Ties break to the strongest member in canonical order wav > cv3 > vox > emb. No dataset-label branching.
|
| 10 |
+
|
| 11 |
+
python experiments/exp_ensemble.py (~2 min)
|
| 12 |
+
"""
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 16 |
+
import json
|
| 17 |
+
from collections import Counter
|
| 18 |
+
from itertools import combinations
|
| 19 |
+
from experiments import common as C
|
| 20 |
+
|
| 21 |
+
ART = os.path.join(os.path.dirname(os.path.abspath(__file__)), "artifacts")
|
| 22 |
+
preds = {"wav": C.answers(C.feat([C.R_MEAN()]))}
|
| 23 |
+
for k, f in [("cv3", "answers_qwen3omni.json"), ("vox", "answers_voxtral.json"), ("emb", "answers_emotion2vec.json")]:
|
| 24 |
+
preds[k] = json.load(open(os.path.join(ART, f), encoding="utf-8"))
|
| 25 |
+
ORDER = ["wav", "cv3", "vox", "emb"]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def vote(keys):
|
| 29 |
+
out = {}
|
| 30 |
+
for q in C.QS:
|
| 31 |
+
letters = [o.letter for o in q.options]
|
| 32 |
+
counts = Counter(preds[k][q.qid] for k in keys if q.qid in preds[k])
|
| 33 |
+
if not counts:
|
| 34 |
+
out[q.qid] = letters[0]; continue
|
| 35 |
+
top = counts.most_common(1)[0][1]
|
| 36 |
+
tied = {a for a, n in counts.items() if n == top}
|
| 37 |
+
out[q.qid] = next(iter(tied)) if len(tied) == 1 else \
|
| 38 |
+
next((preds[k][q.qid] for k in keys if k in ORDER and preds[k].get(q.qid) in tied), letters[0])
|
| 39 |
+
return out
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
print("== singles ==")
|
| 43 |
+
for k in ORDER:
|
| 44 |
+
C.report(k, preds[k])
|
| 45 |
+
print("== pairs / trios / full (uniform vote, tie -> strongest) ==")
|
| 46 |
+
for r in (2, 3, 4):
|
| 47 |
+
for combo in combinations(ORDER, r):
|
| 48 |
+
C.report("+".join(combo), vote(list(combo)))
|
experiments/exp_finetune.py
CHANGED
|
@@ -1,95 +1,95 @@
|
|
| 1 |
-
"""Table 1 fine-tuning row / Finding 3 — fine-tuning WavLM on the pairwise task (a negative result: never beats frozen features).
|
| 2 |
-
Truncates WavLM to 9 layers, unfreezes the top 3 + a linear scoring head, and trains the pairwise
|
| 3 |
-
good>bad objective directly on audio. LONG-RUNNING (hours on an 8 GB GPU).
|
| 4 |
-
|
| 5 |
-
python experiments/exp_finetune.py warm # head warm-started from the frozen-feature solution
|
| 6 |
-
python experiments/exp_finetune.py random # randomly initialized head
|
| 7 |
-
python experiments/exp_finetune.py gentle # warm start + 10x lower learning rate
|
| 8 |
-
"""
|
| 9 |
-
import sys
|
| 10 |
-
import os
|
| 11 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 12 |
-
import numpy as np
|
| 13 |
-
import torch
|
| 14 |
-
import torch.nn as nn
|
| 15 |
-
from experiments import common as C
|
| 16 |
-
from empathyeval.data.audio import cached_load
|
| 17 |
-
|
| 18 |
-
MODE = sys.argv[1] if len(sys.argv) > 1 else "warm"
|
| 19 |
-
LR = 1e-6 if MODE == "gentle" else 1e-5
|
| 20 |
-
STEPS, EVAL_EVERY, KEEP, UNFREEZE = 3000, 300, 9, 3
|
| 21 |
-
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class Ranker(nn.Module):
|
| 25 |
-
def __init__(self):
|
| 26 |
-
super().__init__()
|
| 27 |
-
from transformers import WavLMModel
|
| 28 |
-
m = WavLMModel.from_pretrained(C.MID)
|
| 29 |
-
m.encoder.layers = m.encoder.layers[:KEEP] # last_hidden_state == layer-9 output
|
| 30 |
-
m.gradient_checkpointing_enable()
|
| 31 |
-
for p in m.parameters():
|
| 32 |
-
p.requires_grad_(False)
|
| 33 |
-
for lyr in m.encoder.layers[KEEP - UNFREEZE:]:
|
| 34 |
-
for p in lyr.parameters():
|
| 35 |
-
p.requires_grad_(True)
|
| 36 |
-
self.wavlm, self.head = m, nn.Linear(1024, 1)
|
| 37 |
-
|
| 38 |
-
def score(self, iv):
|
| 39 |
-
return self.head(self.wavlm(iv).last_hidden_state.mean(dim=1)).squeeze(-1)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def warm_start(model):
|
| 43 |
-
"""Init the head from the frozen layer-9 mean-pool logistic, so training starts AT the frozen baseline."""
|
| 44 |
-
sc, clf = C.train_ranker(C.feat([C.R_MEAN()]))
|
| 45 |
-
w, b, mu, sd = clf.coef_[0], clf.intercept_[0], sc.mean_, sc.scale_
|
| 46 |
-
with torch.no_grad():
|
| 47 |
-
model.head.weight.copy_(torch.tensor((w / sd)[None, :], dtype=model.head.weight.dtype))
|
| 48 |
-
model.head.bias.copy_(torch.tensor(b - float((w * mu / sd).sum()), dtype=model.head.bias.dtype))
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
from transformers import AutoFeatureExtractor # noqa: E402
|
| 52 |
-
fe = AutoFeatureExtractor.from_pretrained(C.MID)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def inp(w):
|
| 56 |
-
y = cached_load(w, C.cfg)[:16000 * C.CAP_S]
|
| 57 |
-
return fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(DEV)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def evaluate(model):
|
| 61 |
-
model.eval()
|
| 62 |
-
ans = {}
|
| 63 |
-
with torch.no_grad():
|
| 64 |
-
for q in C.QS:
|
| 65 |
-
s = [model.score(inp(o.wav)).item() for o in q.options]
|
| 66 |
-
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(s))]
|
| 67 |
-
model.train()
|
| 68 |
-
return ans
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
model = Ranker().to(DEV).train()
|
| 72 |
-
if MODE in ("warm", "gentle"):
|
| 73 |
-
warm_start(model)
|
| 74 |
-
opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=LR)
|
| 75 |
-
scaler = torch.cuda.amp.GradScaler(enabled=DEV == "cuda")
|
| 76 |
-
bce = nn.BCEWithLogitsLoss()
|
| 77 |
-
items = C.train_items(2500)
|
| 78 |
-
print(f"mode={MODE} lr={LR} frozen baseline (mean pooling): {C.final(C.answers(C.feat([C.R_MEAN()]))):.4f}")
|
| 79 |
-
best = -1.0
|
| 80 |
-
for step in range(1, STEPS + 1):
|
| 81 |
-
it = items[(step - 1) % len(items)]
|
| 82 |
-
try:
|
| 83 |
-
gi, bi = inp(it.good_wav), inp(it.bad_wav)
|
| 84 |
-
except Exception:
|
| 85 |
-
continue
|
| 86 |
-
with torch.autocast(DEV, enabled=DEV == "cuda"):
|
| 87 |
-
loss = bce(model.score(gi) - model.score(bi), torch.ones(1, device=DEV))
|
| 88 |
-
opt.zero_grad(); scaler.scale(loss).backward(); scaler.step(opt); scaler.update()
|
| 89 |
-
if step % 50 == 0:
|
| 90 |
-
print(f"step {step}/{STEPS} loss {loss.item():.3f}", flush=True)
|
| 91 |
-
if step % EVAL_EVERY == 0:
|
| 92 |
-
f = C.final(evaluate(model))
|
| 93 |
-
best = max(best, f)
|
| 94 |
-
print(f" [eval] step {step}: Final={f:.4f} (best {best:.4f})", flush=True)
|
| 95 |
-
print(f"DONE mode={MODE}: best fine-tuned Final {best:.4f}")
|
|
|
|
| 1 |
+
"""Table 1 fine-tuning row / Finding 3 — fine-tuning WavLM on the pairwise task (a negative result: never beats frozen features).
|
| 2 |
+
Truncates WavLM to 9 layers, unfreezes the top 3 + a linear scoring head, and trains the pairwise
|
| 3 |
+
good>bad objective directly on audio. LONG-RUNNING (hours on an 8 GB GPU).
|
| 4 |
+
|
| 5 |
+
python experiments/exp_finetune.py warm # head warm-started from the frozen-feature solution
|
| 6 |
+
python experiments/exp_finetune.py random # randomly initialized head
|
| 7 |
+
python experiments/exp_finetune.py gentle # warm start + 10x lower learning rate
|
| 8 |
+
"""
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
from experiments import common as C
|
| 16 |
+
from empathyeval.data.audio import cached_load
|
| 17 |
+
|
| 18 |
+
MODE = sys.argv[1] if len(sys.argv) > 1 else "warm"
|
| 19 |
+
LR = 1e-6 if MODE == "gentle" else 1e-5
|
| 20 |
+
STEPS, EVAL_EVERY, KEEP, UNFREEZE = 3000, 300, 9, 3
|
| 21 |
+
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Ranker(nn.Module):
|
| 25 |
+
def __init__(self):
|
| 26 |
+
super().__init__()
|
| 27 |
+
from transformers import WavLMModel
|
| 28 |
+
m = WavLMModel.from_pretrained(C.MID)
|
| 29 |
+
m.encoder.layers = m.encoder.layers[:KEEP] # last_hidden_state == layer-9 output
|
| 30 |
+
m.gradient_checkpointing_enable()
|
| 31 |
+
for p in m.parameters():
|
| 32 |
+
p.requires_grad_(False)
|
| 33 |
+
for lyr in m.encoder.layers[KEEP - UNFREEZE:]:
|
| 34 |
+
for p in lyr.parameters():
|
| 35 |
+
p.requires_grad_(True)
|
| 36 |
+
self.wavlm, self.head = m, nn.Linear(1024, 1)
|
| 37 |
+
|
| 38 |
+
def score(self, iv):
|
| 39 |
+
return self.head(self.wavlm(iv).last_hidden_state.mean(dim=1)).squeeze(-1)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def warm_start(model):
|
| 43 |
+
"""Init the head from the frozen layer-9 mean-pool logistic, so training starts AT the frozen baseline."""
|
| 44 |
+
sc, clf = C.train_ranker(C.feat([C.R_MEAN()]))
|
| 45 |
+
w, b, mu, sd = clf.coef_[0], clf.intercept_[0], sc.mean_, sc.scale_
|
| 46 |
+
with torch.no_grad():
|
| 47 |
+
model.head.weight.copy_(torch.tensor((w / sd)[None, :], dtype=model.head.weight.dtype))
|
| 48 |
+
model.head.bias.copy_(torch.tensor(b - float((w * mu / sd).sum()), dtype=model.head.bias.dtype))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
from transformers import AutoFeatureExtractor # noqa: E402
|
| 52 |
+
fe = AutoFeatureExtractor.from_pretrained(C.MID)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def inp(w):
|
| 56 |
+
y = cached_load(w, C.cfg)[:16000 * C.CAP_S]
|
| 57 |
+
return fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(DEV)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def evaluate(model):
|
| 61 |
+
model.eval()
|
| 62 |
+
ans = {}
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
for q in C.QS:
|
| 65 |
+
s = [model.score(inp(o.wav)).item() for o in q.options]
|
| 66 |
+
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(s))]
|
| 67 |
+
model.train()
|
| 68 |
+
return ans
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
model = Ranker().to(DEV).train()
|
| 72 |
+
if MODE in ("warm", "gentle"):
|
| 73 |
+
warm_start(model)
|
| 74 |
+
opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=LR)
|
| 75 |
+
scaler = torch.cuda.amp.GradScaler(enabled=DEV == "cuda")
|
| 76 |
+
bce = nn.BCEWithLogitsLoss()
|
| 77 |
+
items = C.train_items(2500)
|
| 78 |
+
print(f"mode={MODE} lr={LR} frozen baseline (mean pooling): {C.final(C.answers(C.feat([C.R_MEAN()]))):.4f}")
|
| 79 |
+
best = -1.0
|
| 80 |
+
for step in range(1, STEPS + 1):
|
| 81 |
+
it = items[(step - 1) % len(items)]
|
| 82 |
+
try:
|
| 83 |
+
gi, bi = inp(it.good_wav), inp(it.bad_wav)
|
| 84 |
+
except Exception:
|
| 85 |
+
continue
|
| 86 |
+
with torch.autocast(DEV, enabled=DEV == "cuda"):
|
| 87 |
+
loss = bce(model.score(gi) - model.score(bi), torch.ones(1, device=DEV))
|
| 88 |
+
opt.zero_grad(); scaler.scale(loss).backward(); scaler.step(opt); scaler.update()
|
| 89 |
+
if step % 50 == 0:
|
| 90 |
+
print(f"step {step}/{STEPS} loss {loss.item():.3f}", flush=True)
|
| 91 |
+
if step % EVAL_EVERY == 0:
|
| 92 |
+
f = C.final(evaluate(model))
|
| 93 |
+
best = max(best, f)
|
| 94 |
+
print(f" [eval] step {step}: Final={f:.4f} (best {best:.4f})", flush=True)
|
| 95 |
+
print(f"DONE mode={MODE}: best fine-tuned Final {best:.4f}")
|
experiments/exp_heads.py
CHANGED
|
@@ -1,44 +1,44 @@
|
|
| 1 |
-
"""Table 1 ranker-head row / Finding 5 — ranker heads on the final pooling features: logistic C sweep, an MLP, and an RBF-SVM.
|
| 2 |
-
|
| 3 |
-
python experiments/exp_heads.py (~20 min; the RBF-SVM on 5120-d features dominates)
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
-
import os
|
| 7 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
-
import numpy as np
|
| 9 |
-
from sklearn.linear_model import LogisticRegression
|
| 10 |
-
from sklearn.neural_network import MLPClassifier
|
| 11 |
-
from sklearn.preprocessing import StandardScaler
|
| 12 |
-
from sklearn.svm import SVC
|
| 13 |
-
from experiments import common as C
|
| 14 |
-
|
| 15 |
-
ff = C.feat(C.POOL_FINAL)
|
| 16 |
-
X, y = [], []
|
| 17 |
-
for it in C.train_items(2500):
|
| 18 |
-
try:
|
| 19 |
-
g, b = ff(it.good_wav), ff(it.bad_wav)
|
| 20 |
-
except Exception:
|
| 21 |
-
continue
|
| 22 |
-
X.append(g - b); y.append(1)
|
| 23 |
-
X.append(b - g); y.append(0)
|
| 24 |
-
X = np.array(X)
|
| 25 |
-
sc = StandardScaler().fit(X)
|
| 26 |
-
Xs = sc.transform(X)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def run(name, clf, proba=False):
|
| 30 |
-
clf.fit(Xs, y)
|
| 31 |
-
fn = (lambda z: clf.predict_proba(z)[:, 1]) if proba else clf.decision_function
|
| 32 |
-
ans = {}
|
| 33 |
-
for q in C.QS:
|
| 34 |
-
fv = sc.transform([ff(o.wav) for o in q.options])
|
| 35 |
-
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(fn(fv)))]
|
| 36 |
-
C.report(name, ans)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
for Creg in [0.1, 0.25, 0.5, 1.0, 2.0]:
|
| 40 |
-
run(f"LogReg C={Creg}", LogisticRegression(max_iter=3000, C=Creg))
|
| 41 |
-
run("MLP(64)", MLPClassifier(hidden_layer_sizes=(64,), alpha=1e-2, max_iter=500,
|
| 42 |
-
early_stopping=True, random_state=0), proba=True)
|
| 43 |
-
print(" (RBF-SVM is slow on 5120-d features ...)", flush=True)
|
| 44 |
-
run("RBF-SVM C=1", SVC(C=1.0, kernel="rbf", gamma="scale"))
|
|
|
|
| 1 |
+
"""Table 1 ranker-head row / Finding 5 — ranker heads on the final pooling features: logistic C sweep, an MLP, and an RBF-SVM.
|
| 2 |
+
|
| 3 |
+
python experiments/exp_heads.py (~20 min; the RBF-SVM on 5120-d features dominates)
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.linear_model import LogisticRegression
|
| 10 |
+
from sklearn.neural_network import MLPClassifier
|
| 11 |
+
from sklearn.preprocessing import StandardScaler
|
| 12 |
+
from sklearn.svm import SVC
|
| 13 |
+
from experiments import common as C
|
| 14 |
+
|
| 15 |
+
ff = C.feat(C.POOL_FINAL)
|
| 16 |
+
X, y = [], []
|
| 17 |
+
for it in C.train_items(2500):
|
| 18 |
+
try:
|
| 19 |
+
g, b = ff(it.good_wav), ff(it.bad_wav)
|
| 20 |
+
except Exception:
|
| 21 |
+
continue
|
| 22 |
+
X.append(g - b); y.append(1)
|
| 23 |
+
X.append(b - g); y.append(0)
|
| 24 |
+
X = np.array(X)
|
| 25 |
+
sc = StandardScaler().fit(X)
|
| 26 |
+
Xs = sc.transform(X)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def run(name, clf, proba=False):
|
| 30 |
+
clf.fit(Xs, y)
|
| 31 |
+
fn = (lambda z: clf.predict_proba(z)[:, 1]) if proba else clf.decision_function
|
| 32 |
+
ans = {}
|
| 33 |
+
for q in C.QS:
|
| 34 |
+
fv = sc.transform([ff(o.wav) for o in q.options])
|
| 35 |
+
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(fn(fv)))]
|
| 36 |
+
C.report(name, ans)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
for Creg in [0.1, 0.25, 0.5, 1.0, 2.0]:
|
| 40 |
+
run(f"LogReg C={Creg}", LogisticRegression(max_iter=3000, C=Creg))
|
| 41 |
+
run("MLP(64)", MLPClassifier(hidden_layer_sizes=(64,), alpha=1e-2, max_iter=500,
|
| 42 |
+
early_stopping=True, random_state=0), proba=True)
|
| 43 |
+
print(" (RBF-SVM is slow on 5120-d features ...)", flush=True)
|
| 44 |
+
run("RBF-SVM C=1", SVC(C=1.0, kernel="rbf", gamma="scale"))
|
experiments/exp_layers.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
"""Table 1 layer row / Finding 2 — layer sweep: Final per WavLM hidden layer, under mean pooling and mean+std pooling.
|
| 2 |
-
|
| 3 |
-
python experiments/exp_layers.py (~5 min: trains 50 rankers)
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
-
import os
|
| 7 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
-
from experiments import common as C
|
| 9 |
-
|
| 10 |
-
print("layer mean-Final mean+std-Final")
|
| 11 |
-
best = {}
|
| 12 |
-
for L in range(25):
|
| 13 |
-
fm = C.final(C.answers(C.feat([C.R_MEAN(L)])))
|
| 14 |
-
fs = C.final(C.answers(C.feat([C.R_MEAN(L), C.R_STD(L)])))
|
| 15 |
-
best[L] = (fm, fs)
|
| 16 |
-
print(f" {L:2d} {fm:.4f} {fs:.4f}", flush=True)
|
| 17 |
-
bm = max(best, key=lambda L: best[L][0])
|
| 18 |
-
bs = max(best, key=lambda L: best[L][1])
|
| 19 |
-
print(f"best layer (mean): {bm} ({best[bm][0]:.4f}) last layer: {best[24][0]:.4f}")
|
| 20 |
-
print(f"best layer (mean+std): {bs} ({best[bs][1]:.4f})")
|
|
|
|
| 1 |
+
"""Table 1 layer row / Finding 2 — layer sweep: Final per WavLM hidden layer, under mean pooling and mean+std pooling.
|
| 2 |
+
|
| 3 |
+
python experiments/exp_layers.py (~5 min: trains 50 rankers)
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
+
from experiments import common as C
|
| 9 |
+
|
| 10 |
+
print("layer mean-Final mean+std-Final")
|
| 11 |
+
best = {}
|
| 12 |
+
for L in range(25):
|
| 13 |
+
fm = C.final(C.answers(C.feat([C.R_MEAN(L)])))
|
| 14 |
+
fs = C.final(C.answers(C.feat([C.R_MEAN(L), C.R_STD(L)])))
|
| 15 |
+
best[L] = (fm, fs)
|
| 16 |
+
print(f" {L:2d} {fm:.4f} {fs:.4f}", flush=True)
|
| 17 |
+
bm = max(best, key=lambda L: best[L][0])
|
| 18 |
+
bs = max(best, key=lambda L: best[L][1])
|
| 19 |
+
print(f"best layer (mean): {bm} ({best[bm][0]:.4f}) last layer: {best[24][0]:.4f}")
|
| 20 |
+
print(f"best layer (mean+std): {bs} ({best[bs][1]:.4f})")
|
experiments/exp_pooling.py
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
"""Table 1 pooling & K rows / Finding 1 — pooling ablation: mean -> +std -> +3 segment means (with paired bootstraps), plus the
|
| 2 |
-
rejected alternatives (max / velocity / delta-std / L2 norm / multi-layer / speech-segments / K sweep).
|
| 3 |
-
|
| 4 |
-
python experiments/exp_pooling.py (~10 min; bootstraps dominate)
|
| 5 |
-
"""
|
| 6 |
-
import sys
|
| 7 |
-
import os
|
| 8 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 9 |
-
import numpy as np
|
| 10 |
-
from experiments import common as C
|
| 11 |
-
|
| 12 |
-
MEAN = [C.R_MEAN()]
|
| 13 |
-
MS = [C.R_MEAN(), C.R_STD()]
|
| 14 |
-
|
| 15 |
-
print("== Table 2: pooling ablation (WavLM layer 9) ==")
|
| 16 |
-
a_mean = C.answers(C.feat(MEAN)); C.report("mean", a_mean)
|
| 17 |
-
a_ms = C.answers(C.feat(MS)); C.report("mean+std", a_ms)
|
| 18 |
-
a_fin = C.answers(C.feat(C.POOL_FINAL)); C.report("mean+std+3seg (final)", a_fin)
|
| 19 |
-
b1 = C.paired_bootstrap(a_mean, a_ms)
|
| 20 |
-
b2 = C.paired_bootstrap(a_ms, a_fin)
|
| 21 |
-
print(f" bootstrap mean->mean+std : median {b1['median']:+.4f} 95% CI [{b1['lo']:+.4f},{b1['hi']:+.4f}] P(better)={b1['p_better']:.1%}")
|
| 22 |
-
print(f" bootstrap +std->final : median {b2['median']:+.4f} 95% CI [{b2['lo']:+.4f},{b2['hi']:+.4f}] P(better)={b2['p_better']:.1%}")
|
| 23 |
-
|
| 24 |
-
print("== rejected alternatives ==")
|
| 25 |
-
C.report("mean+std+max", C.answers(C.feat(MS + [C.R_MAX])))
|
| 26 |
-
C.report("mean+std+velocity", C.answers(C.feat(MS + [C.R_DVEL])))
|
| 27 |
-
C.report("mean+std+delta-std", C.answers(C.feat(MS + [C.R_DSTD])))
|
| 28 |
-
_ff = C.feat(C.POOL_FINAL)
|
| 29 |
-
C.report("final, L2-normalized", C.answers(lambda w: (lambda v: v / (np.linalg.norm(v) + 1e-8))(_ff(w))))
|
| 30 |
-
C.report("multi-layer L8+9+10 mean+std", C.answers(C.feat(
|
| 31 |
-
[C.R_MEAN(8), C.R_STD(8), C.R_MEAN(9), C.R_STD(9), C.R_MEAN(10), C.R_STD(10)])))
|
| 32 |
-
C.report("mean+std+3 speech-seg (VAD)", C.answers(C.feat(MS + [C.R_SPEECH0, C.R_SPEECH0 + 1, C.R_SPEECH0 + 2])))
|
| 33 |
-
|
| 34 |
-
print("== segment-count sweep (mean+std + K derived equal-time segments) ==")
|
| 35 |
-
for K in [2, 3, 4, 6, 12]:
|
| 36 |
-
C.report(f"K={K}", C.answers(C.feat_fineK(K)))
|
|
|
|
| 1 |
+
"""Table 1 pooling & K rows / Finding 1 — pooling ablation: mean -> +std -> +3 segment means (with paired bootstraps), plus the
|
| 2 |
+
rejected alternatives (max / velocity / delta-std / L2 norm / multi-layer / speech-segments / K sweep).
|
| 3 |
+
|
| 4 |
+
python experiments/exp_pooling.py (~10 min; bootstraps dominate)
|
| 5 |
+
"""
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 9 |
+
import numpy as np
|
| 10 |
+
from experiments import common as C
|
| 11 |
+
|
| 12 |
+
MEAN = [C.R_MEAN()]
|
| 13 |
+
MS = [C.R_MEAN(), C.R_STD()]
|
| 14 |
+
|
| 15 |
+
print("== Table 2: pooling ablation (WavLM layer 9) ==")
|
| 16 |
+
a_mean = C.answers(C.feat(MEAN)); C.report("mean", a_mean)
|
| 17 |
+
a_ms = C.answers(C.feat(MS)); C.report("mean+std", a_ms)
|
| 18 |
+
a_fin = C.answers(C.feat(C.POOL_FINAL)); C.report("mean+std+3seg (final)", a_fin)
|
| 19 |
+
b1 = C.paired_bootstrap(a_mean, a_ms)
|
| 20 |
+
b2 = C.paired_bootstrap(a_ms, a_fin)
|
| 21 |
+
print(f" bootstrap mean->mean+std : median {b1['median']:+.4f} 95% CI [{b1['lo']:+.4f},{b1['hi']:+.4f}] P(better)={b1['p_better']:.1%}")
|
| 22 |
+
print(f" bootstrap +std->final : median {b2['median']:+.4f} 95% CI [{b2['lo']:+.4f},{b2['hi']:+.4f}] P(better)={b2['p_better']:.1%}")
|
| 23 |
+
|
| 24 |
+
print("== rejected alternatives ==")
|
| 25 |
+
C.report("mean+std+max", C.answers(C.feat(MS + [C.R_MAX])))
|
| 26 |
+
C.report("mean+std+velocity", C.answers(C.feat(MS + [C.R_DVEL])))
|
| 27 |
+
C.report("mean+std+delta-std", C.answers(C.feat(MS + [C.R_DSTD])))
|
| 28 |
+
_ff = C.feat(C.POOL_FINAL)
|
| 29 |
+
C.report("final, L2-normalized", C.answers(lambda w: (lambda v: v / (np.linalg.norm(v) + 1e-8))(_ff(w))))
|
| 30 |
+
C.report("multi-layer L8+9+10 mean+std", C.answers(C.feat(
|
| 31 |
+
[C.R_MEAN(8), C.R_STD(8), C.R_MEAN(9), C.R_STD(9), C.R_MEAN(10), C.R_STD(10)])))
|
| 32 |
+
C.report("mean+std+3 speech-seg (VAD)", C.answers(C.feat(MS + [C.R_SPEECH0, C.R_SPEECH0 + 1, C.R_SPEECH0 + 2])))
|
| 33 |
+
|
| 34 |
+
print("== segment-count sweep (mean+std + K derived equal-time segments) ==")
|
| 35 |
+
for K in [2, 3, 4, 6, 12]:
|
| 36 |
+
C.report(f"K={K}", C.answers(C.feat_fineK(K)))
|
experiments/exp_trainsize.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
"""Table 1 training-size row / Finding 3 — training-set size: Final vs. number of training triples (final pooling, seed-0 order).
|
| 2 |
-
|
| 3 |
-
python experiments/exp_trainsize.py (~3 min)
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
-
import os
|
| 7 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
-
from experiments import common as C
|
| 9 |
-
|
| 10 |
-
for n in [500, 1000, 1500, 2000, 2500, 3000, 4000, 4892]:
|
| 11 |
-
C.report(f"N={n}", C.answers(C.feat(C.POOL_FINAL), n_items=n))
|
|
|
|
| 1 |
+
"""Table 1 training-size row / Finding 3 — training-set size: Final vs. number of training triples (final pooling, seed-0 order).
|
| 2 |
+
|
| 3 |
+
python experiments/exp_trainsize.py (~3 min)
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 8 |
+
from experiments import common as C
|
| 9 |
+
|
| 10 |
+
for n in [500, 1000, 1500, 2000, 2500, 3000, 4000, 4892]:
|
| 11 |
+
C.report(f"N={n}", C.answers(C.feat(C.POOL_FINAL), n_items=n))
|
experiments/exp_utterance.py
CHANGED
|
@@ -1,50 +1,50 @@
|
|
| 1 |
-
"""Table 1 utterance-conditioning row / Finding 4 — utterance conditioning: inject the human utterance's acoustics via interaction features
|
| 2 |
-
(cosine delivery-match / element-wise products). Plain concatenation cancels in the pairwise difference,
|
| 3 |
-
so interactions are the only way the utterance can enter a linear pairwise ranker.
|
| 4 |
-
|
| 5 |
-
python experiments/exp_utterance.py (~3 min)
|
| 6 |
-
"""
|
| 7 |
-
import sys
|
| 8 |
-
import os
|
| 9 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 10 |
-
import numpy as np
|
| 11 |
-
from sklearn.linear_model import LogisticRegression
|
| 12 |
-
from sklearn.preprocessing import StandardScaler
|
| 13 |
-
from experiments import common as C
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def nrm(v):
|
| 17 |
-
return v / (np.linalg.norm(v) + 1e-8)
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def feature(resp, utt, mode):
|
| 21 |
-
a = C.arr(resp)
|
| 22 |
-
base = np.concatenate([a[r] for r in C.POOL_FINAL])
|
| 23 |
-
if mode == "base":
|
| 24 |
-
return base
|
| 25 |
-
um = C.arr(utt)[C.R_MEAN()]
|
| 26 |
-
resp_parts = [a[C.R_MEAN()], a[C.R_SEG3], a[C.R_SEG3 + 1], a[C.R_SEG3 + 2]]
|
| 27 |
-
if mode == "+cos":
|
| 28 |
-
return np.concatenate([base, np.array([float(nrm(p) @ nrm(um)) for p in resp_parts], dtype=np.float32)])
|
| 29 |
-
if mode == "+prod":
|
| 30 |
-
return np.concatenate([base, a[C.R_MEAN()] * um])
|
| 31 |
-
raise ValueError(mode)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
for mode in ["base", "+cos", "+prod"]:
|
| 35 |
-
X, y = [], []
|
| 36 |
-
for it in C.train_items(2500):
|
| 37 |
-
try:
|
| 38 |
-
g, b = feature(it.good_wav, it.utterance_wav, mode), feature(it.bad_wav, it.utterance_wav, mode)
|
| 39 |
-
except Exception:
|
| 40 |
-
continue
|
| 41 |
-
X.append(g - b); y.append(1)
|
| 42 |
-
X.append(b - g); y.append(0)
|
| 43 |
-
X = np.array(X)
|
| 44 |
-
sc = StandardScaler().fit(X)
|
| 45 |
-
clf = LogisticRegression(max_iter=3000, C=0.5).fit(sc.transform(X), y)
|
| 46 |
-
ans = {}
|
| 47 |
-
for q in C.QS:
|
| 48 |
-
fv = sc.transform([feature(o.wav, q.utterance_wav, mode) for o in q.options])
|
| 49 |
-
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(clf.decision_function(fv)))]
|
| 50 |
-
C.report(mode, ans)
|
|
|
|
| 1 |
+
"""Table 1 utterance-conditioning row / Finding 4 — utterance conditioning: inject the human utterance's acoustics via interaction features
|
| 2 |
+
(cosine delivery-match / element-wise products). Plain concatenation cancels in the pairwise difference,
|
| 3 |
+
so interactions are the only way the utterance can enter a linear pairwise ranker.
|
| 4 |
+
|
| 5 |
+
python experiments/exp_utterance.py (~3 min)
|
| 6 |
+
"""
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 10 |
+
import numpy as np
|
| 11 |
+
from sklearn.linear_model import LogisticRegression
|
| 12 |
+
from sklearn.preprocessing import StandardScaler
|
| 13 |
+
from experiments import common as C
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def nrm(v):
|
| 17 |
+
return v / (np.linalg.norm(v) + 1e-8)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def feature(resp, utt, mode):
|
| 21 |
+
a = C.arr(resp)
|
| 22 |
+
base = np.concatenate([a[r] for r in C.POOL_FINAL])
|
| 23 |
+
if mode == "base":
|
| 24 |
+
return base
|
| 25 |
+
um = C.arr(utt)[C.R_MEAN()]
|
| 26 |
+
resp_parts = [a[C.R_MEAN()], a[C.R_SEG3], a[C.R_SEG3 + 1], a[C.R_SEG3 + 2]]
|
| 27 |
+
if mode == "+cos":
|
| 28 |
+
return np.concatenate([base, np.array([float(nrm(p) @ nrm(um)) for p in resp_parts], dtype=np.float32)])
|
| 29 |
+
if mode == "+prod":
|
| 30 |
+
return np.concatenate([base, a[C.R_MEAN()] * um])
|
| 31 |
+
raise ValueError(mode)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
for mode in ["base", "+cos", "+prod"]:
|
| 35 |
+
X, y = [], []
|
| 36 |
+
for it in C.train_items(2500):
|
| 37 |
+
try:
|
| 38 |
+
g, b = feature(it.good_wav, it.utterance_wav, mode), feature(it.bad_wav, it.utterance_wav, mode)
|
| 39 |
+
except Exception:
|
| 40 |
+
continue
|
| 41 |
+
X.append(g - b); y.append(1)
|
| 42 |
+
X.append(b - g); y.append(0)
|
| 43 |
+
X = np.array(X)
|
| 44 |
+
sc = StandardScaler().fit(X)
|
| 45 |
+
clf = LogisticRegression(max_iter=3000, C=0.5).fit(sc.transform(X), y)
|
| 46 |
+
ans = {}
|
| 47 |
+
for q in C.QS:
|
| 48 |
+
fv = sc.transform([feature(o.wav, q.utterance_wav, mode) for o in q.options])
|
| 49 |
+
ans[q.qid] = [o.letter for o in q.options][int(np.argmax(clf.decision_function(fv)))]
|
| 50 |
+
C.report(mode, ans)
|
experiments/exp_window.py
CHANGED
|
@@ -1,58 +1,58 @@
|
|
| 1 |
-
"""Table 1 context-window row / Finding 4 — context-window length: mean-pooled layer-9 performance with 15/30/45/60 s of audio.
|
| 2 |
-
Uses a 9-layer-truncated WavLM (last_hidden_state == layer-9 output) so 60 s fits an 8 GB GPU, and one
|
| 3 |
-
60 s forward per clip whose prefix means give every shorter cap. Own cache (~35 min extraction on GPU).
|
| 4 |
-
|
| 5 |
-
python experiments/exp_window.py
|
| 6 |
-
"""
|
| 7 |
-
import sys
|
| 8 |
-
import os
|
| 9 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 10 |
-
import hashlib
|
| 11 |
-
import numpy as np
|
| 12 |
-
from experiments import common as C
|
| 13 |
-
from empathyeval.data.audio import cached_load
|
| 14 |
-
|
| 15 |
-
CAPS = [15, 30, 45, 60]
|
| 16 |
-
CD = "cache/exp_window"
|
| 17 |
-
os.makedirs(CD, exist_ok=True)
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def cp(w):
|
| 21 |
-
return os.path.join(CD, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def extract():
|
| 25 |
-
import torch
|
| 26 |
-
torch.backends.cudnn.deterministic = True
|
| 27 |
-
torch.backends.cudnn.benchmark = False
|
| 28 |
-
from transformers import AutoFeatureExtractor, WavLMModel
|
| 29 |
-
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 30 |
-
fe = AutoFeatureExtractor.from_pretrained(C.MID)
|
| 31 |
-
m = WavLMModel.from_pretrained(C.MID)
|
| 32 |
-
m.encoder.layers = m.encoder.layers[:C.LAYER] # truncate -> output == layer-9
|
| 33 |
-
m = m.to(dev).eval()
|
| 34 |
-
print(f"extracting 60 s layer-9 features on {dev}", flush=True)
|
| 35 |
-
wavs = [w for it in C.train_items(2500) for w in (it.good_wav, it.bad_wav)]
|
| 36 |
-
wavs += [o.wav for q in C.QS for o in q.options]
|
| 37 |
-
for n, w in enumerate(dict.fromkeys(wavs), 1):
|
| 38 |
-
if os.path.exists(cp(w)):
|
| 39 |
-
continue
|
| 40 |
-
y = cached_load(w, C.cfg)[:16000 * CAPS[-1]]
|
| 41 |
-
inp = fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(dev)
|
| 42 |
-
with torch.no_grad():
|
| 43 |
-
h = m(inp).last_hidden_state[0] # [T,1024]
|
| 44 |
-
np.save(cp(w), np.stack([h[:50 * s].mean(0).cpu().numpy() for s in CAPS]).astype(np.float32))
|
| 45 |
-
if n % 500 == 0:
|
| 46 |
-
print(f" {n} clips", flush=True)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
if not os.path.exists(cp(C.train_items(1)[0].good_wav)):
|
| 50 |
-
extract()
|
| 51 |
-
mem = {}
|
| 52 |
-
for i, s in enumerate(CAPS):
|
| 53 |
-
def f(w, i=i):
|
| 54 |
-
a = mem.get(w)
|
| 55 |
-
if a is None:
|
| 56 |
-
a = mem[w] = np.load(cp(w))
|
| 57 |
-
return a[i]
|
| 58 |
-
C.report(f"cap {s:2d}s (mean pooling)", C.answers(f))
|
|
|
|
| 1 |
+
"""Table 1 context-window row / Finding 4 — context-window length: mean-pooled layer-9 performance with 15/30/45/60 s of audio.
|
| 2 |
+
Uses a 9-layer-truncated WavLM (last_hidden_state == layer-9 output) so 60 s fits an 8 GB GPU, and one
|
| 3 |
+
60 s forward per clip whose prefix means give every shorter cap. Own cache (~35 min extraction on GPU).
|
| 4 |
+
|
| 5 |
+
python experiments/exp_window.py
|
| 6 |
+
"""
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
| 10 |
+
import hashlib
|
| 11 |
+
import numpy as np
|
| 12 |
+
from experiments import common as C
|
| 13 |
+
from empathyeval.data.audio import cached_load
|
| 14 |
+
|
| 15 |
+
CAPS = [15, 30, 45, 60]
|
| 16 |
+
CD = "cache/exp_window"
|
| 17 |
+
os.makedirs(CD, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def cp(w):
|
| 21 |
+
return os.path.join(CD, hashlib.md5(w.encode()).hexdigest()[:16] + ".npy")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def extract():
|
| 25 |
+
import torch
|
| 26 |
+
torch.backends.cudnn.deterministic = True
|
| 27 |
+
torch.backends.cudnn.benchmark = False
|
| 28 |
+
from transformers import AutoFeatureExtractor, WavLMModel
|
| 29 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 30 |
+
fe = AutoFeatureExtractor.from_pretrained(C.MID)
|
| 31 |
+
m = WavLMModel.from_pretrained(C.MID)
|
| 32 |
+
m.encoder.layers = m.encoder.layers[:C.LAYER] # truncate -> output == layer-9
|
| 33 |
+
m = m.to(dev).eval()
|
| 34 |
+
print(f"extracting 60 s layer-9 features on {dev}", flush=True)
|
| 35 |
+
wavs = [w for it in C.train_items(2500) for w in (it.good_wav, it.bad_wav)]
|
| 36 |
+
wavs += [o.wav for q in C.QS for o in q.options]
|
| 37 |
+
for n, w in enumerate(dict.fromkeys(wavs), 1):
|
| 38 |
+
if os.path.exists(cp(w)):
|
| 39 |
+
continue
|
| 40 |
+
y = cached_load(w, C.cfg)[:16000 * CAPS[-1]]
|
| 41 |
+
inp = fe(y, sampling_rate=16000, return_tensors="pt").input_values.to(dev)
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
h = m(inp).last_hidden_state[0] # [T,1024]
|
| 44 |
+
np.save(cp(w), np.stack([h[:50 * s].mean(0).cpu().numpy() for s in CAPS]).astype(np.float32))
|
| 45 |
+
if n % 500 == 0:
|
| 46 |
+
print(f" {n} clips", flush=True)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if not os.path.exists(cp(C.train_items(1)[0].good_wav)):
|
| 50 |
+
extract()
|
| 51 |
+
mem = {}
|
| 52 |
+
for i, s in enumerate(CAPS):
|
| 53 |
+
def f(w, i=i):
|
| 54 |
+
a = mem.get(w)
|
| 55 |
+
if a is None:
|
| 56 |
+
a = mem[w] = np.load(cp(w))
|
| 57 |
+
return a[i]
|
| 58 |
+
C.report(f"cap {s:2d}s (mean pooling)", C.answers(f))
|
inference/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Self-contained. `run.py` reads the test release JSON file(s) and writes **`outpu
|
|
| 20 |
## Step-by-step
|
| 21 |
|
| 22 |
### 1. Prerequisites
|
| 23 |
-
- **Python 3.
|
| 24 |
- **~1.5 GB free disk** for the WavLM model (downloaded automatically on first run).
|
| 25 |
- A CUDA **GPU** is used automatically if present; otherwise it runs on **CPU** (slower, still fine).
|
| 26 |
|
|
@@ -28,12 +28,6 @@ Self-contained. `run.py` reads the test release JSON file(s) and writes **`outpu
|
|
| 28 |
```bash
|
| 29 |
pip install -r requirements.txt
|
| 30 |
```
|
| 31 |
-
For GPU, install the CUDA torch build **first** (a plain install pulls the CPU-only wheel; if the CPU
|
| 32 |
-
wheel is already installed, add `--force-reinstall`):
|
| 33 |
-
```bash
|
| 34 |
-
pip install torch --index-url https://download.pytorch.org/whl/cu128
|
| 35 |
-
pip install -r requirements.txt
|
| 36 |
-
```
|
| 37 |
|
| 38 |
### 3. Point it at your test data
|
| 39 |
Open `config.yaml` and list your test release JSON file(s):
|
|
@@ -50,9 +44,6 @@ that JSON's own folder (absolute paths also work), so keep each release file nex
|
|
| 50 |
```bash
|
| 51 |
python run.py
|
| 52 |
```
|
| 53 |
-
Any **relative** paths under `releases:` resolve against your *current working directory* — with the
|
| 54 |
-
absolute paths of step 3 you can run from anywhere; with relative paths, run from the folder they are
|
| 55 |
-
relative to.
|
| 56 |
**On the very first run** it downloads `microsoft/wavlm-large` (~1.2 GB, MIT, open weights) from Hugging Face —
|
| 57 |
this needs **internet once**. It is cached under `~/.cache/huggingface`, so **every later run is fully offline.**
|
| 58 |
You'll see progress like:
|
|
|
|
| 20 |
## Step-by-step
|
| 21 |
|
| 22 |
### 1. Prerequisites
|
| 23 |
+
- **Python 3.9+**
|
| 24 |
- **~1.5 GB free disk** for the WavLM model (downloaded automatically on first run).
|
| 25 |
- A CUDA **GPU** is used automatically if present; otherwise it runs on **CPU** (slower, still fine).
|
| 26 |
|
|
|
|
| 28 |
```bash
|
| 29 |
pip install -r requirements.txt
|
| 30 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
### 3. Point it at your test data
|
| 33 |
Open `config.yaml` and list your test release JSON file(s):
|
|
|
|
| 44 |
```bash
|
| 45 |
python run.py
|
| 46 |
```
|
|
|
|
|
|
|
|
|
|
| 47 |
**On the very first run** it downloads `microsoft/wavlm-large` (~1.2 GB, MIT, open weights) from Hugging Face —
|
| 48 |
this needs **internet once**. It is cached under `~/.cache/huggingface`, so **every later run is fully offline.**
|
| 49 |
You'll see progress like:
|
inference/config.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
releases:
|
| 2 |
-
- "
|
| 3 |
-
- "
|
| 4 |
-
- "
|
|
|
|
| 1 |
releases:
|
| 2 |
+
- "C:/Users/andy/Documents/Claude/Projects/HumOmni Track 1/humomni-empathyeval/data/phase2-test_multi-context_gigaspeech/phase2-test_gigaspeech_release.json"
|
| 3 |
+
- "C:/Users/andy/Documents/Claude/Projects/HumOmni Track 1/humomni-empathyeval/data/phase2-test_multi-context_meld/phase2-test_meld_release.json"
|
| 4 |
+
- "C:/Users/andy/Documents/Claude/Projects/HumOmni Track 1/humomni-empathyeval/data/phase2-test_multi-emotion_emovdb/phase2-test_emovdb_release.json"
|
inference/phase2_run.err
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
inference/phase2_run.out
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
==================================================================
|
| 2 |
+
HumOmni 2026 Track 1 — EmpathyEval | inference (open weights, offline)
|
| 3 |
+
==================================================================
|
| 4 |
+
loading microsoft/wavlm-large on cuda (first run downloads ~1.2 GB from Hugging Face)...
|
| 5 |
+
model ready on cuda.
|
| 6 |
+
3 release file(s), 542 questions to answer.
|
| 7 |
+
|
| 8 |
+
scoring responses with WavLM segment-pooling...
|
| 9 |
+
50/542 answered
|
| 10 |
+
100/542 answered
|
| 11 |
+
150/542 answered
|
| 12 |
+
200/542 answered
|
| 13 |
+
250/542 answered
|
| 14 |
+
300/542 answered
|
| 15 |
+
350/542 answered
|
| 16 |
+
400/542 answered
|
| 17 |
+
450/542 answered
|
| 18 |
+
500/542 answered
|
| 19 |
+
542/542 answered
|
| 20 |
+
==================================================================
|
| 21 |
+
DONE — wrote 542 answers to:
|
| 22 |
+
C:\Users\andy\Documents\Claude\Projects\HumOmni Track 1\humomni-empathyeval\inference\output.jsonl
|
| 23 |
+
==================================================================
|
inference/requirements.txt
CHANGED
|
@@ -6,7 +6,6 @@
|
|
| 6 |
# (PyPI default). For GPU, install the CUDA build FIRST, then install the rest, e.g.:
|
| 7 |
# pip install torch --index-url https://download.pytorch.org/whl/cu128
|
| 8 |
# pip install -r inference/requirements.txt
|
| 9 |
-
# (If a CPU-only torch is ALREADY installed, pip will skip the CUDA build — add --force-reinstall.)
|
| 10 |
# The code auto-selects the GPU when a CUDA torch is present; CPU works too, just slower.
|
| 11 |
torch>=2.2 # tested: 2.11.0+cu128 (CUDA); CPU wheel also works
|
| 12 |
transformers>=4.40 # tested: 5.12.1 (microsoft/wavlm-large, downloaded once, open weights)
|
|
|
|
| 6 |
# (PyPI default). For GPU, install the CUDA build FIRST, then install the rest, e.g.:
|
| 7 |
# pip install torch --index-url https://download.pytorch.org/whl/cu128
|
| 8 |
# pip install -r inference/requirements.txt
|
|
|
|
| 9 |
# The code auto-selects the GPU when a CUDA torch is present; CPU works too, just slower.
|
| 10 |
torch>=2.2 # tested: 2.11.0+cu128 (CUDA); CPU wheel also works
|
| 11 |
transformers>=4.40 # tested: 5.12.1 (microsoft/wavlm-large, downloaded once, open weights)
|
inference/run.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""INFERENCE — read the test release + the pre-trained ranker, write output.jsonl. Nothing else.
|
| 2 |
|
| 3 |
-
python
|
| 4 |
|
| 5 |
Self-contained: this file + ranker.pkl + config.yaml are all the code/model you need. It does NOT train
|
| 6 |
and does NOT evaluate. Open weights, offline, no API, and the SAME decision for every question (it never
|
|
@@ -73,9 +73,12 @@ def read_questions(releases):
|
|
| 73 |
for r in rows:
|
| 74 |
try:
|
| 75 |
opts = [(k[-1], os.path.join(base, v)) for k, v in sorted(r["options"].items())]
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
-
fail(f"malformed entry in {path} (expected 'question_id' + 'options'): {e}")
|
| 79 |
if not qs:
|
| 80 |
fail("no questions found in the release file(s).")
|
| 81 |
return qs
|
|
|
|
| 1 |
"""INFERENCE — read the test release + the pre-trained ranker, write output.jsonl. Nothing else.
|
| 2 |
|
| 3 |
+
python run.py
|
| 4 |
|
| 5 |
Self-contained: this file + ranker.pkl + config.yaml are all the code/model you need. It does NOT train
|
| 6 |
and does NOT evaluate. Open weights, offline, no API, and the SAME decision for every question (it never
|
|
|
|
| 73 |
for r in rows:
|
| 74 |
try:
|
| 75 |
opts = [(k[-1], os.path.join(base, v)) for k, v in sorted(r["options"].items())]
|
| 76 |
+
qid = r.get("question_id") or r.get("data_index") # Phase 2 renamed the field to data_index
|
| 77 |
+
if not qid:
|
| 78 |
+
raise KeyError("question_id / data_index")
|
| 79 |
+
qs.append((qid, opts))
|
| 80 |
except Exception as e:
|
| 81 |
+
fail(f"malformed entry in {path} (expected 'question_id' or 'data_index' + 'options'): {e}")
|
| 82 |
if not qs:
|
| 83 |
fail("no questions found in the release file(s).")
|
| 84 |
return qs
|
requirements.txt
CHANGED
|
@@ -5,7 +5,6 @@
|
|
| 5 |
# For GPU, install the CUDA build FIRST, then the rest, e.g.:
|
| 6 |
# pip install torch --index-url https://download.pytorch.org/whl/cu128
|
| 7 |
# pip install -r requirements.txt
|
| 8 |
-
# (If a CPU-only torch is ALREADY installed, pip will skip the CUDA build — add --force-reinstall.)
|
| 9 |
# The code auto-selects the GPU when a CUDA torch is present; CPU works too, just slower.
|
| 10 |
torch>=2.2 # tested: 2.11.0+cu128 (CUDA); CPU wheel also works
|
| 11 |
transformers>=4.40 # tested: 5.12.1 (microsoft/wavlm-large)
|
|
|
|
| 5 |
# For GPU, install the CUDA build FIRST, then the rest, e.g.:
|
| 6 |
# pip install torch --index-url https://download.pytorch.org/whl/cu128
|
| 7 |
# pip install -r requirements.txt
|
|
|
|
| 8 |
# The code auto-selects the GPU when a CUDA torch is present; CPU works too, just slower.
|
| 9 |
torch>=2.2 # tested: 2.11.0+cu128 (CUDA); CPU wheel also works
|
| 10 |
transformers>=4.40 # tested: 5.12.1 (microsoft/wavlm-large)
|