File size: 13,229 Bytes
130cbdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a25163e
 
 
 
 
 
 
130cbdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3bcbb
 
 
 
 
 
 
 
 
 
 
 
 
 
130cbdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3bcbb
 
 
 
130cbdd
 
 
 
ef3bcbb
 
 
 
 
130cbdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a25163e
 
 
 
 
 
130cbdd
a25163e
 
130cbdd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3bcbb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# Distilling an operator-video quality stack into a ~2 MB on-device model

This is the design write-up for `video_benchmark.distill` β€” how a heavy, multi-model
video-quality scoring stack is compressed into a **single ~2.5 M-parameter network**
that reproduces it in **one forward pass** and ships at **~2 MB** on device (candle /
Rust and Apple MLX), while staying honest about what it does and does not learn.

---

## 1. The problem

The production pipeline scores first-person / operator video frames (headband
cameras, robotics teleop) on several quality axes. The accurate version runs a stack
of models per frame:

- **Classical OpenCV metrics** β€” brightness, sharpness, blur, exposure anomalies.
- **A 3-paradigm learned-IQA ensemble** (via `pyiqa`): **TOPIQ** (CNN), **MUSIQ**
  (ViT) and **CLIP-IQA+** (CLIP) β€” three different inductive biases voting on
  perceptual quality.
- **A MobileCLIP zero-shot scene classifier** β€” is this a usable operator scene?

That is great for an offline batch, but it is several hundred milliseconds per frame
and many hundreds of MB of weights β€” a non-starter for on-device / real-time use.

**Goal:** one compact model that emits all eight signals at once, fast, small enough
to embed, and faithful enough to trust.

---

## 2. The teacher β†’ student design

```
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ teacher stack (label generator) ───────────────┐
   frame  ─┬───► OpenCV: brightness Β· sharpness Β· blur Β· anomaly                  β”‚
           β”œβ”€β”€β”€β–Ί pyiqa ensemble: TOPIQ(CNN) + MUSIQ(ViT) + CLIP-IQA+(CLIP)        β”‚
           └───► MobileCLIP zero-shot scene usability                            β”‚
                 └──────────────────────────► 8 per-frame targets (0..100) β”€β”€β”€β”€β”€β”€β”˜
                                                          β”‚  (distillation labels)
   frame  ──► frozen tiny backbone ──► embedding ─┐       β–Ό
                                                  β”œβ”€β”€β–Ί fuse ──► residual-MLP trunk ──► 8 MLP heads ──► 8 scores
   frame  ──► 10 classical descriptors β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                   (100Β·Οƒ, bounded 0..100)
```

The **student** is `CompactQualityNet` (`model.py`):

- **Frozen backbone** β€” a `tiny` preset MobileNetV3-Small (β‰ˆ2.45 M params). Frozen so
  the embedding can be **precomputed once** and every training epoch is a cheap matrix
  op on the cache (`data.py`). Presets: `tiny` (default), `micro` (LCNet-050, sub-2 MB
  at every precision), `clip` (the legacy MobileCLIP-S0, max fidelity).
- **Descriptor fusion** (see Β§4) β€” the embedding is concatenated with 10 cheap
  classical descriptors before the trunk.
- **Residual-MLP trunk** β€” pre-norm residual blocks (`x + Wβ‚‚Β·GELU(W₁·LN(x))`).
- **Per-target heads** β€” one small 2-layer MLP per signal, so each stays independently
  calibrated while sharing the trunk.
- **Bounded output** β€” `100Β·sigmoid(logit)`: scores start neutral at 50 and can never
  go negative or saturate the clamp, which keeps them calibrated.

The architecture (trunk dim, blocks, head dim, `extra_dim`) is **persisted in the
checkpoint**, so `infer.py` rebuilds the exact network even if defaults later change.

---

## 3. Training (`train.py`)

A real mini-batch loop, not a single least-squares step:

- **kornia augmentation** (`augment.py`) β€” each sampled frame is expanded into several
  views: a **log-uniform Gaussian-blur** sweep (perceptual blur is multiplicative, so
  log-uniform spreads samples evenly across mild→heavy), a mild **motion blur** (camera
  shake), plus colour/gamma jitter and sensor noise. View 0 is the untouched original.
  This multiplies the data and injects spread into otherwise near-flat signals so their
  distilled correlations become meaningful instead of `n/a`. Empirically, this lifts the
  rank-correlation metrics; *aggressive* degradations (strong motion blur, JPEG
  blocking) were measured to regress fidelity on this small corpus and are left out.
- **SGDR** β€” `CosineAnnealingWarmRestarts`: the LR periodically "resurges" to escape
  plateaus.
- **EMA** β€” an exponential-moving-average shadow of the trainable weights for a
  smoother final model (LayerNorm-only trunk, so no BatchNorm running-stat hazard).
- **Loss** β€” `SmoothL1(Ξ²=0.1)` (Ξ²=1.0 degenerates to plain MSE on [0,1] targets) **plus
  a differentiable Pearson-correlation term**, so we optimise the exact fidelity
  metric (PLCC) the evaluation reports. Per-target weighting counts the deep signals
  double.
- **Ship-by-deep-PLCC** β€” on tiny data the EMA is not guaranteed to win, so we keep the
  best EMA and the best raw weights and ship whichever scores higher held-out
  deep-PLCC.
- **`accelerate`** β€” the same loop is correct on CPU / Apple MPS / CUDA.

---

## 4. The key idea: classical-descriptor fusion (`descriptors.py`)

A frozen ImageNet backbone is *trained to be invariant* to exactly what some quality
signals measure: it normalises away absolute exposure, global contrast and colour
cast. So `brightness` and exposure `anomaly` are nearly unrecoverable from its
embedding β€” their distilled correlation collapsed to ~0.

**Fix:** a hybrid hand-crafted + deep representation. We concatenate a fixed 14-D
vector of normalised classical statistics to the embedding:

- **Photometric / focus (10):** luma mean/std, dark/bright fraction, RMS contrast,
  colourfulness (Hasler–SΓΌsstrunk), saturation, Laplacian-variance sharpness, Canny
  edge density, Tenengrad.
- **BRISQUE natural-scene statistics (4):** on the MSCN field
  `I = (luma βˆ’ ΞΌ_local) / (Οƒ_local + 1)`, a pristine image's coefficients are
  ~unit-Gaussian; blur/noise/compression push the **variance, excess kurtosis** and the
  **horizontal/vertical neighbour-product means** off their natural values β€” a strong,
  classic no-reference quality cue (Mittal et al., 2012).

All are computed **identically** at cache-build and inference time with fixed scales
(no data-dependent statistics), so the model stays self-contained and reproducible.

Two design choices make this strictly beneficial (`model._FusedInput`):

1. **Split projection.** The embedding keeps its own projection; the descriptors enter
   through a **separate** projection that is added on. This keeps the big embedding
   matrix's row length **block-aligned**, so it int4/int8 block-quantises in GGUF/MLX
   (a single fat `Linear` over the concatenation would have an unaligned row and fall
   back to fp16 β€” ~4Γ— larger; this is exactly the regression that pushed an early build
   to 2.38 MB).
2. **Zero-initialised.** The descriptor projection starts at zero, so fusion is an
   exact **no-op at init** and can only *help* β€” descriptors are used only insofar as
   they reduce the loss, never diluting the embedding the deep signals rely on.

Result: best composite scores of any build, `clipiqa`/`anomaly`/`brightness` lifted,
deep-signal PLCC held β€” at negligible size cost.

---

## 5. Evaluation (`evaluate.py`)

IQA-grade reporting, with rigor appropriate to a small held-out set:

- **The standard quad per signal** β€” PLCC (Pearson), SRCC (Spearman), **KRCC
  (Kendall)**, plus **MAE and RMSE** (0–100 units).
- **VQEG logistic-fitted PLCC / RMSE** β€” the IQA convention (ITU-T P.1401 / VQEG) maps
  predictions through a monotonic **5-parameter logistic** before correlating, so a
  model isn't penalised for an arbitrary nonlinear score scale when its ranking is
  right. Reported next to the raw numbers.
- **Composite** β€” agreement on the overall quality verdict (mean over signals that
  genuinely vary), the bottom-line number.
- **Deep-signal** aggregates β€” mean over the signals a learned model actually earns
  (`iqa, musiq, clipiqa, scene`).
- **BCa bootstrap 95 % CI** on the composite PLCC β€” a single estimate on ~190 frames
  hides real uncertainty. We use the **bias-corrected-and-accelerated** bootstrap
  (Efron, 1987), which corrects the percentile interval for the median-bias and skew a
  correlation's sampling distribution has near Β±1; it degrades to a percentile
  bootstrap if BCa can't be formed.
- **Robustness sweep** β€” blur frames at increasing Οƒ and check the student *tracks* the
  teacher's degradation (not just degrades arbitrarily), reported as a tracking MAE.
- **Fidelity vs Οƒ** β€” deep-PLCC bucketed by blur severity: a clean frame is easy, heavy
  blur is the real test.
- **Honesty guards** β€” a near-flat teacher signal (std < 5 on 0–100) is reported as
  `n/a`, never a fake correlation. The train/val split is **leakage-free by clip**.

All of this is rendered to `report.md` / `report.txt` and published to the Hub in the
modern **`.eval_results/`** format (PLCC/SRCC/KRCC/MAE/RMSE per signal + composites),
keyed to a companion benchmark dataset.

---

## 6. Results (latest run)

Final `tiny` + fusion run on the operator-video corpus (see `report.md` for the full
per-signal table with KRCC/RMSE and the blur sweep):

| metric | value |
|---|---|
| **Composite PLCC** | **0.899** (95 % BCa CI [0.854, 0.925]) |
| Composite PLCC β€” VQEG logistic-fitted | 0.902 |
| Composite SRCC / KRCC | 0.896 / 0.714 |
| Deep-signal PLCC | 0.69 |
| `iqa` / `musiq` PLCC | 0.88 / 0.84 |
| `sharpness` / `blur` PLCC | 0.90 / 0.86 |
| Throughput | ~120Γ— the teacher stack |
| Params / int4 weights | 2.45 M / ~1.9 MB |
| int4 GGUF / MLX file | 2.00 MB / 2.40 MB |

---

## 7. Deployment (`quantize.py`, `export_hf.py`)

Sub-fp16 packing with **real, scale-aware size accounting** (it models the fp16
fallback for tensors GGUF can't block-quantise, so the reported int4/int8 numbers
match what the files actually weigh β€” not an optimistic `params Γ— bytes` guess):

- **GGUF** (candle / Rust) β€” ggml Q8_0 / Q4_0 + fp16 fallback. Arch + target order +
  descriptor names ride along as metadata so a candle program rebuilds the forward
  pass. int4 GGUF β‰ˆ 2.0 MB.
- **MLX** (Apple Silicon) β€” grouped affine quant, int8 + int4, at the empirically
  smallest group size per bit-width (smaller groups qualify more matrices and cut fp16
  fallbacks). Verified with an MLX-native quantise→dequantise round-trip self-test.
- **safetensors / `.pt`** β€” the canonical fp16 weights `infer.py` loads.
- **LICENSE / NOTICE** β€” backbone-aware: MIT for the trained trunk + heads; the timm
  MobileNetV3 backbone is Apache-2.0 (apple-amlr only when a MobileCLIP tower is
  actually bundled); teacher models (BSD pyiqa, apple-amlr MobileCLIP) are used only to
  generate labels and are **not** redistributed.

---

## 8. Reproduce

```bash
uv sync --group distill                 # gguf + mlx export backends (optional)
uv run python -m video_benchmark.distill --videos videos --epochs 300 \
    --preset tiny --quantize int4 --gguf --mlx --fusion
uv run python -m video_benchmark.distill.infer frame.jpg   # run the distilled model
uv run pytest tests/test_distill.py tests/test_distill_model.py
```

---

## 9. Honest limitations

- **`scene` is flat** on this corpus (only a handful of clips, all valid operator
  scenes), so its distilled correlation is `n/a` β€” a *data* limitation, not a model
  one. More diverse footage would give it real spread.
- **`brightness` / `anomaly`** are exact OpenCV stats in production; the distilled head
  is only a unified read-out for them. Fusion lifts them but they are better computed
  directly.
- **The Hub benchmark** uploads correctly, but HF only renders a *live* leaderboard
  once it allow-lists the `eval.yaml` β€” and no `evaluation_framework` enum value fits a
  custom distillation-fidelity eval, so that field is a documented placeholder.

---

## 10. References

The design borrows established methods rather than inventing them:

- **Knowledge distillation** β€” Hinton, Vinyals & Dean, *Distilling the Knowledge in a
  Neural Network*, 2015.
- **BRISQUE / MSCN natural-scene statistics** β€” Mittal, Moorthy & Bovik, *No-Reference
  Image Quality Assessment in the Spatial Domain*, IEEE TIP 2012.
- **VQEG logistic fit for IQA** β€” VQEG, *Final Report on the Validation of Objective
  Models of Video Quality Assessment*, 2003; ITU-T Rec. P.1401.
- **BCa bootstrap** β€” Efron, *Better Bootstrap Confidence Intervals*, JASA 1987.
- **SGDR warm restarts** β€” Loshchilov & Hutter, *SGDR: Stochastic Gradient Descent with
  Warm Restarts*, ICLR 2017.
- **Weight averaging (EMA / Polyak)** β€” Polyak & Juditsky, 1992; Izmailov et al., *SWA*,
  2018.
- **Backbone & teachers** β€” MobileNetV3 (Howard et al., 2019); MobileCLIP (Vasu et al.,
  CVPR 2024); the learned-IQA ensemble TOPIQ (Chen et al., 2024), MUSIQ (Ke et al.,
  ICCV 2021), CLIP-IQA (Wang et al., AAAI 2023), via `pyiqa`.
- **Colourfulness** β€” Hasler & SΓΌsstrunk, *Measuring Colourfulness in Natural Images*,
  2003. **Tenengrad focus** β€” Krotkov, 1987.
- **kornia** β€” Riba et al., *Kornia: an Open Source Differentiable Computer Vision
  Library for PyTorch*, WACV 2020.
- **GGUF / ggml k-quants** (llama.cpp) and **MLX** (Apple) for the on-device exports.