feat: fine-tune the identity head, and measure what it buys

Part 2 ended on the number that matters: enrolling a real shelf from itself leaves
off-the-shelf DINOv2 at ~25% top-1, because real milk cartons differ by small text
and a colour band. This trains a small projection head on frozen features - 384 -> 128,
cosine classifier, angular margin - with augmentation standing in for the second visit
we do not have, and measures it on 72 held-out facings over 8 products.

  raw DINOv2 cosine               top-1  2/72 (2%)   balanced  1%
  raw, rejecting below 0.55       top-1  2/72 (2%)   balanced  1%   (5% of answered right)
  fine-tuned + margin             top-1 28/72 (38%)  balanced 73%
  fine-tuned, rejecting below 0.4 top-1 17/72 (23%)  balanced 58%  (94% of answered right)

So fine-tuning is the difference between working and not (balanced 1% -> 73%), and
with a rejection threshold the model becomes precise - it answers 18 of 72 facings and
is right on 94% of them, which is the operating point a store wants. The raw model
cannot do that at any threshold.

Where it fails is where my labels are coarse: MEGMILK, which the contact sheet shows
is probably several products under one brand, scores 1/32 because the class is
internally inconsistent and the model correctly refuses most of it.

Honest limit, stated in the doc: enrolment and held-out facings come from one
photograph, so these are upper bounds; only a recorded second visit settles the real
number. What is settled is the mechanism - a head trained on labels the pipeline can
produce itself moves fine-grained identity from useless to usable.

175 tests pass.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kiyo9w 2026-09-25 03:15:31 +09:00
parent 7d01de8062
commit 4fd5059ef0
2 changed files with 303 additions and 0 deletions

View file

@ -180,3 +180,55 @@ So the honest design is: **reject by default**, name the product from the tag wh
is readable, from the packaging text when it is legible, from a fine-tuned embedding
otherwise - and hand the remainder to the operator, whose answers become the next
enrollment.
---
# Part 3: fine-tuning the embedding, and what it buys
Part 2 ended on the number that matters: enrolling a real shelf from itself leaves
off-the-shelf DINOv2 at ~25 % top-1, because real milk cartons differ by small text
and a colour band. The literature's answer is fine-tuning, so this measures it.
`code/finetune_identity.py` trains a small projection head (384 -> 128, cosine
classifier, angular margin) on frozen DINOv2 features, with augmentation standing in
for the second visit we do not have. Two enrolment views per product, twelve
augmented copies each, 72 held-out facings, 8 products. Balanced accuracy is reported
because the shelf is unbalanced (MEGMILK has 34 facings, 酪農3.6 has two - a system
that always answered "MEGMILK" would look respectable on the raw rate).
| | top-1 | balanced | answered | of those right |
| --- | --- | --- | --- | --- |
| raw DINOv2 cosine | 2/72 (2 %) | 1 % | 72 | 3 % |
| raw, rejecting below 0.55 | 2/72 (2 %) | 1 % | 44 | 5 % |
| **fine-tuned + margin** | 28/72 (38 %) | **73 %** | 72 | 39 % |
| **fine-tuned, rejecting below 0.4** | 17/72 (23 %) | 58 % | 18 | **94 %** |
Three things follow.
1. **Fine-tuning is not a refinement, it is the difference between working and not.**
Balanced accuracy goes from **1 % to 73 %** on the same held-out facings. Off-the-
shelf features cannot separate these products; a head trained on sixteen crops can.
2. **With a rejection threshold it becomes precise**: answering only 18 of 72 facings
and being right on 94 % of them is exactly the operating point a store wants -
name what you are sure of, and hand the rest to the operator. The raw model cannot
do this at any threshold (5 % of answered).
3. **Where it fails is where the labels are coarse.** Per class, with rejection:
明治ラブ 2/2, おいしい無調整豆乳 2/2, 低脂肪 1/1, 特濃4.5 1/2, 北海道牛乳 8/21,
明治 おいしい牛乳 2/12, and **MEGMILK 1/32**. MEGMILK is the class I labelled most
coarsely - the contact sheet shows what are probably several different products
under one brand - so the class is internally inconsistent and the model correctly
refuses most of it. Better labels, not a better model, is what that needs.
## The honest limit
Enrolment and held-out facings come from **one photograph**. Augmentation stands in
for a second visit, which is not the same thing: a real second visit changes the
light, the angle and the stock. So 73 % balanced and 94 % precision are **upper
bounds**, and the only way to know the real number is to record a second visit
(`docs/DATA.md`).
What is settled is the mechanism: **a head trained on labels the pipeline can produce
itself** - tag-named facings, plus operator confirmations - moves fine-grained
identity from useless to usable, and a rejection threshold turns it into something a
store can act on.