# Cẩm nang tái lập đường ống Speech DF Arena

Chín cạm bẫy đã vấp phải khi đưa mô hình của mình vào bộ công cụ này. **Phần lớn không báo lỗi** — chúng cho ra một con số trông hoàn toàn hợp lý. Đọc trước khi chạy.

Nguồn: `github.com/Speech-Arena/speech_df_arena`, bài báo arXiv 2509.02859.

---

## 0. Cấu trúc bộ công cụ

```
speech_df_arena/
  Models/            một file .py cho mỗi hệ; ModelFactory dùng importlib
                     -> KHÔNG cần đăng ký, thả file vào là chạy
  protocol_files/    trong repo thì RỖNG, người dùng tự tạo
  utils/
    datamodule.py    librosa.load(sr=16000) -> pad() lặp tới 64600 mẫu
    metrics.py       compute_eer(bonafide, spoof) -> điểm CAO = bonafide
  evaluate.py
```

Hai biến môi trường bắt buộc:

```bash
export DF_ARENA_CHECKPOINTS_DIR=/đường/dẫn/checkpoints
export DF_ARENA_PROTOCOL_FILES_DIR=/đường/dẫn/protocols
```

Protocol là CSV, đường dẫn **tuyệt đối**, nhãn đúng chữ `spoof` / `bonafide`:

```
file_name,label
/abs/path/a.wav,spoof
/abs/path/b.wav,bonafide
```

Một file model cần đúng bốn thứ: lớp kế thừa `pl.LightningModule`, `forward(x)` trả `(hidden, logits)`, `test_step` ghi `"{tên_file} {điểm}\n"`, và hàm `load_model(model_path, out_score_file_name)` ở cấp module.

---

## 1. README ghi sai tên tham số

README viết `--model_name`. Argparse thật dùng **`--models`**.

Ngoài ra `evaluate.py` gọi `os.listdir(PROTOCOL_FILES_DIR)` **ngay khi dựng tham số**, nên thư mục protocol phải tồn tại và có file trước khi chạy, nếu không argparse báo lỗi khó hiểu.

## 2. Checkpoint phải đặt tên bắt đầu bằng tên model

```python
model_path = [os.path.join(ckpt_dir, i) for i in os.listdir(ckpt_dir) if i.startswith(model)][0]
```

Model `audio_jepa` → checkpoint `audio_jepa.pth`. Sai tên thì `IndexError` hoặc tệ hơn là nạp nhầm checkpoint của hệ khác.

## 3. `torch.load` không có `map_location`

`Models/aasist.py` dòng 640 gọi `torch.load(model_path)` trần. Checkpoint gốc của `clovaai/aasist` lưu trên CUDA, nên trên máy CPU sẽ ném `RuntimeError: Attempting to deserialize object on a CUDA device`.

Không cần sửa code họ — lưu lại checkpoint dưới dạng CPU:

```python
sd = torch.load(src, map_location="cpu")
sd = sd.get("state_dict", sd)
sd = {k.replace("module.", ""): v for k, v in sd.items()}
torch.save(sd, f"{CKPT}/aasist.pth")
```

## 4. ⚠ Chiều dấu điểm số — cạm bẫy nguy hiểm nhất

`metrics.py` gọi `compute_eer(bonafide_scores, spoof_scores)`, và `test_step` của các hệ mẫu ghi `out[:, 1]`. Nghĩa là **điểm cao = bonafide**.

Nhưng nếu bạn huấn luyện bằng `softmax(logits)[:, 1]`, đại lượng đó là hàm đơn điệu của **hiệu** `logit₁ − logit₀`, **không** của `logit₁` một mình. Trả logit thô sẽ cho EER lệch mà không có bất kỳ dấu hiệu nào.

Cách đúng:

```python
def forward(self, x):
    logits = self.head(...)
    d = (logits[:, 1] - logits[:, 0]).unsqueeze(1)
    return logits, torch.cat([-d, d], dim=1)     # arena lấy [:,1] = hiệu
```

Cách kiểm: đảo dấu cho ra `100 − EER`. Nếu AASIST ra 52,09 thay vì 47,91 thì chính là lỗi này.

## 5. ⚠ Buffer của `Resample` nằm trong `state_dict`

`torchaudio.transforms.Resample` đăng ký `kernel` là buffer. Nếu bộ nâng mẫu nằm trong lớp khi suy luận nhưng nằm ngoài lớp khi huấn luyện, `load_state_dict` sẽ báo thiếu `_rs.kernel`.

Buffer này được dựng lại xác định từ `(sr_in, sr_out)` trong `__init__`, nên **thiếu là đúng**, không phải hỏng:

```python
assert set(miss) <= {"enc.pos_embed", "_rs.kernel"}, miss[:5]
```

## 6. ⚠ Kho nén chia nhỏ trên Zenodo

Nhiều tập (ADD22, CodecFake) đóng gói kiểu Info-ZIP nhiều phần: `X.z01`…`X.z09` cộng `X.zip`. Phần `.zip` chỉ chứa mục lục ở cuối, dữ liệu nằm rải trong các `.z0*`.

**`zipfile` của Python không đọc được loại này.** Nếu bạn lặp qua từng phần với `try/except` thì mọi phần đều thất bại và bạn nhận được một thư mục rỗng, im lặng.

```bash
apt-get install -y zip unzip p7zip-full
zip -s 0 X.zip --out X_full.zip     # ghép
unzip -q X_full.zip -d ĐÍCH
# dự phòng: 7z x X.zip
```

Nhớ xoá các phần zip sau khi giải nén — ADD22 lấy lại được ~20 GB.

## 7. ⚠ `assert` theo tỉ lệ không bắt được tập rỗng

Lỗi này để lọt một protocol rỗng và sau đó mọi thứ vẫn in ra số:

```python
assert len(mg) >= 0.99 * len(paths)     # paths rỗng -> 0 >= 0.0 -> LỌT
```

Kết quả: protocol 0 dòng, mẫu con rỗng, EER in ra 50,00 % trông rất hợp lý.

Luôn kiểm bằng **số tuyệt đối đã biết trước**:

```python
assert len(mg) >= 0.99 * 109_199
assert counts["bonafide"] >= 0.99 * 31_334
assert counts["spoof"]    >= 0.99 * 77_865
```

*Một phép kiểm tra không thể thất bại thì không phải phép kiểm tra.*

## 8. Tràn RAM khi gọi `evaluate.py` từ notebook

Colab có 12,7 GB. Ba thứ cộng dồn: notebook mẹ giữ mô hình và context CUDA; `subprocess.run(capture_output=True)` đệm **toàn bộ** stdout của thanh tiến trình trong bộ nhớ; tiến trình con nạp thêm một bản PyTorch nữa cùng các worker.

```python
with open(f"/content/run_{tag}.log", "w") as fh:      # ghi ra đĩa
    subprocess.run([...], stdout=fh, stderr=subprocess.STDOUT)
```

Thêm: `--num_workers 1`, chạy từng hệ một, và nếu vẫn sập thì **Restart session** (giữ `/content`) chứ **không** Change runtime type (xoá `/content`).

## 9. ⚠ Chia validation ngẫu nhiên trên ASVspoof train là vô nghĩa

ASVspoof2019 LA train chỉ có 20 người nói và 6 attack. Chia ngẫu nhiên theo utterance khiến cả hai bên dùng chung người nói, attack và bản ghi gốc. Kết quả: val EER = 0,00 % từ epoch 2, không có tín hiệu nào để chọn epoch.

Chia đúng là giữ nguyên một **attack** ra ngoài (ví dụ A05) hoặc một nhóm **người nói**. Lưu ý A05 đã được chứng minh là nghịch chiều với khả năng chuyển miền sang codec (Spearman −1,00), nên dùng để chọn epoch thì được, dùng để chọn learning rate thì không.

---

## Ba cổng kiểm chứng bắt buộc

Chạy theo thứ tự. Không tin số của mô hình mình trước khi cả ba qua.

**Cổng 1 — metric và ánh xạ nhãn.** Tải gói điểm số của arena (367 MB, Google Drive trong README), ghép với nhãn gốc, tính lại EER bằng hàm chép nguyên từ `utils/metrics.py`. Kết quả đã đạt: **7/8 hệ khớp Bảng 2 đến 0,00–0,01**. Không tốn một byte audio nào.

**Cổng 2 — suy luận và tiền xử lý.** Chạy một hệ đã biết đáp án qua chính bộ công cụ. Kết quả đã đạt: **AASIST trên ADD22-T1 = 47,9161 %** so với 47,91 % công bố.

**Cổng 3 — lớp bọc của mình.** Chấm cùng một waveform hai đường: qua `Models/của_bạn.py` và trực tiếp trong notebook. Phải trùng đến 1e-3. Lệch nghĩa là lớp bọc không tái lập đúng mô hình đã huấn luyện.

---

## Giá trị đối chiếu (Bảng 2, arXiv 2509.02859)

| hệ | ADD22-T1 | ITW | ASVspoof19 | CodecFake |
|---|---|---|---|---|
| aasist | 47,91 | 43,00 | 0,82 | 51,05 |
| rawnet_2 | 50,35 | 49,00 | 4,59 | 49,75 |
| rawgat_st | 42,90 | 52,53 | 1,06 | 50,00 |
| wavlm_ecapa | 44,16 | 34,64 | 0,76 | 46,18 |
| hubert_ecapa | 47,66 | 38,65 | 1,05 | 46,22 |
| wav2vec2_ecapa | 46,43 | 30,69 | 29,69 | 40,97 |
| whisper_mesonet | 38,38 | 26,72 | 5,83 | 34,66 |
| xlsr_sls | 33,95 | 7,45 | 0,23 | 33,43 |
| tcm_add | 37,40 | 7,79 | 0,18 | 36,00 |
| xlsr_mamba | 34,22 | 6,70 | 0,42 | 35,26 |
| nes2net_x | 34,47 | 7,75 | 0,12 | 39,34 |
| wav2vec2_aasist | 31,04 | 11,19 | 0,22 | 43,36 |

Gói điểm số công bố chỉ có **11 hệ**; thiếu `nes2net_x` và `wav2vec2_aasist`. Hai dòng đó phải trích từ bảng, ghi rõ là trích chứ không tự tính.

## Số lượng utterance từng tập (đọc từ file điểm)

| tập | utterance | ghi chú |
|---|---|---|
| ADD22-T1 | 109.199 | 31.334 bonafide / 77.865 spoof |
| ASVspoof19 LA eval | 71.237 | 7.355 bonafide |
| In-the-Wild | 31.779 | |
| CodecFake | 304.241 | C1–C6 mỗi tập 26.456 + **C7 145.505** |

CodecFake của arena **có C7**, nằm ở một bản Zenodo khác (record 11125029). Tổng khoảng 52 GB qua hai bản. Không có A1–A3 (ALM test).
