vmaf_tiny_v3 — wider/deeper VMAF feature-fusion estimator¶
vmaf_tiny_v3 is a tiny multi-layer perceptron that predicts a VMAF score from the same six classic libvmaf features as vmaf_tiny_v2 (adm2, vif_scale0..3, motion2 — the canonical-6 set used by vmaf_v0.6.1). It carries roughly 3x the hidden capacity of v2 (mlp_medium = 6 → 32 → 16 → 1, ~769 params vs v2's 257) to test whether extra capacity buys headroom over v2's PLCC = 0.9978 ± 0.0021 baseline on Netflix LOSO.
Production default stays
vmaf_tiny_v2. v3 is shipped alongside v2 as a higher-PLCC option, not a replacement: the measured LOSO win is small (+0.0008 PLCC mean, lower variance), the parameter count triples, and the on-disk ONNX size grows from 2 446 to 4 496 bytes. Pick v3 when the extra PLCC is worth the extra capacity; pick v2 when you want the smallest possible bundle.
What the output means¶
A single scalar per frame on the same 0–100 VMAF scale as the classic SVM regressor and as v2. Identical interpretation table:
| Value | Interpretation |
|---|---|
| 100 | Perceptually identical to the reference |
| 80–95 | High-quality encode |
| 60–80 | Visible compression artifacts |
| < 60 | Heavy degradation |
Shipped checkpoint¶
| Field | Value |
|---|---|
| Model name | vmaf_tiny_v3 |
| Location | model/tiny/vmaf_tiny_v3.onnx |
| Architecture | mlp_medium — Linear(6, 32) → ReLU → Linear(32, 16) → ReLU → Linear(16, 1), 769 params |
| Input | features — float32 [N, 6], dynamic batch |
| Feature order | adm2, vif_scale0, vif_scale1, vif_scale2, vif_scale3, motion2 |
| Output | vmaf — float32 [N] |
| ONNX opset | 17 |
| Quantisation | fp32 + dynamic-PTQ int8 sidecar (ADR-0275) |
| License | BSD-3-Clause-Plus-Patent |
| Registry entry | vmaf_tiny_v3 in model/tiny/registry.json |
| Sidecar | model/tiny/vmaf_tiny_v3.json |
| Exporter | ai/scripts/export_vmaf_tiny_v3.py |
| Trainer | ai/scripts/train_vmaf_tiny_v3.py |
The graph bakes the StandardScaler (mean, std) from the training set as Constant nodes that run before the MLP — exactly the same runtime contract as v2. There is no out-of-band scaler file to ship.
Fresh exports add a run_provenance block to the sidecar. It records the exporter entrypoint, CLI arguments, checkpoint input, and ONNX / sidecar output paths so refreshed model artifacts can be traced without reading local shell history.
Effective topology:
features [N, 6]
|
Sub <- mean ([6] constant)
|
Div <- std ([6] constant)
|
Linear(6,32) → ReLU → Linear(32,16) → ReLU → Linear(16,1)
|
Squeeze(-1) -> vmaf [N]
Training data¶
Identical to v2: runs/full_features_4corpus.parquet (Netflix Public
- KoNViD-1k 5-fold + BVI-DVC A + B + C + D, 330 499 frame-rows × 22 FULL_FEATURES +
vmafteacher score fromvmaf_v0.6.1). The StandardScaler is fit on the 4-corpus union and baked into the exported ONNX as Constant nodes.
Validation¶
| Methodology | v2 (mlp_small, 257 params) | v3 (mlp_medium, 769 params) | Δ |
|---|---|---|---|
| Netflix LOSO (9 folds, seed=0) mean PLCC | 0.9978 ± 0.0021 | 0.9986 ± 0.0015 | +0.0008 |
| Netflix LOSO mean SROCC | 0.9959 ± 0.0027 | 0.9977 ± 0.0017 | +0.0018 |
| Netflix LOSO mean RMSE | — | 1.256 ± 0.604 | — |
| 5000-row Netflix smoke PLCC | 0.9998 | 1.0000 | +0.0002 |
| Train-set RMSE (4-corpus) | 0.153 | 0.112 | -0.041 |
| Parameter count | 257 | 769 | ×3.0 |
| ONNX file size | 2 446 B | 4 496 B | +2 050 B (×1.84) |
LOSO methodology: for each of the 9 Netflix sources, the model is trained from scratch on the union of the other 8 sources (with StandardScaler fit on those 8) and evaluated on the held-out source. Recipe held identical between v2 and v3 — only the architecture function differs. Per-fold metrics are pinned in runs/vmaf_tiny_v3_loso_metrics.json.
Fresh LOSO reports from ai/scripts/eval_loso_vmaf_tiny_v3.py include a run_provenance block with the evaluation entrypoint, argv, parsed hyperparameters, feature parquet input, and report target path. This lets refreshed v3 numbers be compared against older runs without guessing which local feature table produced them.
The PLCC delta is small in absolute terms but the variance shrinks ~30 % — v3 is a more consistent estimator across hold-out clips. SROCC also improves by 0.0018 mean, suggesting the ranking signal is slightly cleaner with the extra capacity.
The min-PLCC = 0.97 ship gate runs in ai/scripts/validate_vmaf_tiny_v3.py against runs/full_features_netflix.parquet; refuses to exit-0 below the gate. Pass --out-json when preserving promotion evidence; the JSON report includes ADR-0661 run_provenance for the ONNX, parquet, optional v2 comparison model, parsed gate arguments, and report path.
Usage — CLI¶
# Use vmaf_tiny_v3 instead of the default v2 / classic SVM.
vmaf -r ref.yuv -d dis.yuv -w 1920 -h 1080 -p 420 -b 8 \
--tiny-model model/tiny/vmaf_tiny_v3.onnx \
--tiny-device auto
--tiny-device auto walks cuda → openvino → rocm → cpu. As with v2 the model is small enough (~4 KB) that dispatch overhead dominates; CPU is usually the fastest path.
Usage — Python (ONNX Runtime)¶
import numpy as np
import onnxruntime as ort
import pandas as pd
sess = ort.InferenceSession("model/tiny/vmaf_tiny_v3.onnx",
providers=["CPUExecutionProvider"])
df = pd.read_parquet("runs/full_features_netflix.parquet").head(100)
features = df[
["adm2", "vif_scale0", "vif_scale1",
"vif_scale2", "vif_scale3", "motion2"]
].to_numpy(dtype=np.float32)
(vmaf,) = sess.run(None, {"features": features})
print(vmaf[:5]) # -> per-frame VMAF estimates
Reproducer¶
# 1. Train on the 4-corpus parquet (~30 s wall on a 16-thread CPU).
python3 ai/scripts/train_vmaf_tiny_v3.py \
--parquet runs/full_features_4corpus.parquet \
--out-ckpt /tmp/vmaf_tiny_v3.pt \
--out-stats /tmp/vmaf_tiny_v3_stats.json
# 2. Export to ONNX with bundled scaler stats.
python3 ai/scripts/export_vmaf_tiny_v3.py \
--ckpt /tmp/vmaf_tiny_v3.pt \
--out-onnx model/tiny/vmaf_tiny_v3.onnx \
--out-sidecar model/tiny/vmaf_tiny_v3.json
# 3. Validate (PLCC must be >= 0.97 on the Netflix slice).
python3 ai/scripts/validate_vmaf_tiny_v3.py \
--onnx model/tiny/vmaf_tiny_v3.onnx \
--parquet runs/full_features_netflix.parquet \
--rows 5000 --min-plcc 0.97 \
--v2-onnx model/tiny/vmaf_tiny_v2.onnx \
--out-json runs/vmaf_tiny_v3_validate.json
# 4. LOSO comparison vs v2.
python3 ai/scripts/eval_loso_vmaf_tiny_v3.py \
--parquet runs/full_features_netflix.parquet \
--out-json runs/vmaf_tiny_v3_loso_metrics.json
The training stats JSON includes ADR-0661 run_provenance with the trainer entrypoint, argv, parsed hyperparameters, parquet input, checkpoint target, and stats target. Keep that block with any refreshed stats used for export.
Choosing between v2 and v3¶
- Default to v2. Smaller bundle (2 446 B vs 4 496 B), validated Phase-3 chain, +0.005–0.018 PLCC over the upstream SVM. v2 is the baseline for 99 % of users.
- Use v3 when: you need the lowest-variance VMAF estimator across diverse content (the LOSO std shrinks 30 %), or when the upstream pipeline is already paying ONNX-Runtime dispatch cost and the extra +0.0008 PLCC mean is worth the +2 KB.
- Don't use v3 when: disk / network footprint matters (e.g. embedded deploys, very-many-model bundles), or when the measurement target is upstream-comparable metrics — v2 is the cited baseline in the Phase-3 chain.
Quantisation (dynamic-PTQ int8 sidecar — ADR-0275)¶
A dynamic-PTQ int8 sibling lives at model/tiny/vmaf_tiny_v3.int8.onnx, produced by ai/scripts/ptq_dynamic.py. The runtime redirect in vmaf_dnn_session_open (ADR-0174) loads the int8 sidecar when the registry entry's quant_mode != "fp32"; the fp32 file stays on disk as the regression baseline.
| Field | Value |
|---|---|
| Quant mode | dynamic (weights-only int8; activations quantised at runtime) |
| Sidecar file | model/tiny/vmaf_tiny_v3.int8.onnx |
| sha256 (int8) | b4bdbb353b1b395adb22b77e890117228de1cfd12b94cb46fd0173c2fd12a343 |
| File size | 4 267 B int8 vs 4 496 B fp32 (×0.95) — tiny MLP, weights are already a small fraction of the graph |
| PLCC drop (int8 vs fp32) | 0.000120 (Netflix features parquet, ~11k rows) |
| Budget | 0.01 (per ADR-0174 / ADR-0173 default) |
| CI gate | ai-quant-accuracy step in Tiny AI job |
The 4-KB MLP graph is dominated by op metadata and Constant scaler nodes; only the three Linear weight tensors quantise. The size delta is therefore small but the PLCC drop is still well inside budget. v4's mlp_large carries enough weight mass that its int8 sidecar shrinks ~45% — see vmaf_tiny_v4.
# Reproduce
python ai/scripts/ptq_dynamic.py model/tiny/vmaf_tiny_v3.onnx
python ai/scripts/measure_quant_drop.py model/tiny/vmaf_tiny_v3.onnx
# -> [PASS] vmaf_tiny_v3 mode=dynamic PLCC=0.999880 drop=0.000120 budget=0.0100
Limitations¶
- The model fuses six already-extracted features — it is not a pixel-input quality model. To use it from raw YUV, the feature extraction stage runs first (the regular libvmaf path).
- Trained on SDR content. HDR coverage is out of scope until the upstream HDR feature extractors land.
- The 4-corpus parquet uses
vmaf_v0.6.1as the teacher score; v3 cannot exceedvmaf_v0.6.1in absolute correctness — it approximates the SVM with a slightly larger MLP. - Bit-exactness across CPU/GPU execution providers is not guaranteed (ADR-0042 / ADR-0119 — places=4 tolerance applies to tiny-AI models too).
- Single-seed LOSO. v2's published 0.9978 was averaged over 5 seeds; v3's 0.9986 is the seed=0 number. A multi-seed v3 sweep is follow-up scope.