cyanwingsbird commited on
Commit
e8db028
·
verified ·
1 Parent(s): 6c919f8

Upload folder using huggingface_hub

Browse files
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
- the technical report (submitted separately, not part of this repository).
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 the technical report (submitted separately). All experiments share one deterministic feature cache and
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.10+** (the pinned `scikit-learn==1.7.2` that loads `ranker.pkl` requires ≥3.10; tested on 3.10.8)
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
- - "../data/phase1-test_multi-context_gigaspeech/phase1-test_gigaspeech_release.json"
3
- - "../data/phase1-test_multi-context_meld/phase1-test_meld_release.json"
4
- - "../data/phase1-test_multi-emotion_emovdb/phase1-test_emovdb_release.json"
 
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 inference/run.py # from the repo root; relative paths in config.yaml resolve against the cwd
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
- qs.append((r["question_id"], opts))
 
 
 
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)