[object Object]

← back to Dw Photo Capture

Augmentation re-fine-tune: warm-start from dw_clip_ft.pt + heavy handheld-photo augmentation (rotate/crop/blur/glare/JPEG) on the image side → teach degraded→clean matching (targets the 28% real-photo top-1)

54e085f5bd38dae656f792424dd4d261889cb84a · 2026-07-07 12:57:37 -0700 · Steve Abrams

Files touched

Diff

commit 54e085f5bd38dae656f792424dd4d261889cb84a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 12:57:37 2026 -0700

    Augmentation re-fine-tune: warm-start from dw_clip_ft.pt + heavy handheld-photo augmentation (rotate/crop/blur/glare/JPEG) on the image side → teach degraded→clean matching (targets the 28% real-photo top-1)
---
 visual-search/train/finetune_aug.py | 87 +++++++++++++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/visual-search/train/finetune_aug.py b/visual-search/train/finetune_aug.py
new file mode 100644
index 0000000..e768bdf
--- /dev/null
+++ b/visual-search/train/finetune_aug.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+# AUGMENTATION re-fine-tune: warm-start from dw_clip_ft.pt and continue training with heavy
+# handheld-photo augmentation (rotate/crop/blur/glare/JPEG) on the IMAGE side, so the model learns
+# that a degraded phone shot maps to the same catalog identity — the gap robustness_test.py exposed
+# (28% top-1 on degraded photos). Caption stays clean; only the image is degraded during training.
+#   PYTORCH_ENABLE_MPS_FALLBACK=1 ./venv/bin/python finetune_aug.py --epochs 3 --batch 96
+import os, sys, io, time, random, argparse
+os.environ.setdefault('PYTORCH_ENABLE_MPS_FALLBACK', '1')
+import torch, torch.nn.functional as F
+from torch.utils.data import Dataset, DataLoader
+from PIL import Image, ImageFilter, ImageEnhance
+import open_clip
+import finetune as T   # reuse caption() + load_rows()
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+CACHE = os.path.join(HERE, 'cache'); CKPT = os.path.join(HERE, 'ckpt')
+Image.MAX_IMAGE_PIXELS = None
+
+def augment(im):
+    # simulate a handheld iPhone shot of a physical sample — varied strength, each applied ~probabilistically
+    w, h = im.size
+    if random.random() < 0.7: im = im.rotate(random.uniform(-8, 8), expand=False, fillcolor=(238,238,238))
+    if random.random() < 0.7:
+        cx, cy = random.uniform(0.02, 0.10), random.uniform(0.02, 0.10)
+        im = im.crop((int(w*cx), int(h*cy), int(w*(1-cx)), int(h*(1-cy))))
+    if random.random() < 0.6: im = im.filter(ImageFilter.GaussianBlur(random.uniform(0.4, 1.6)))
+    if random.random() < 0.7: im = ImageEnhance.Brightness(im).enhance(random.uniform(0.85, 1.30))   # glare / shadow
+    if random.random() < 0.5: im = ImageEnhance.Contrast(im).enhance(random.uniform(0.80, 1.15))
+    if random.random() < 0.6:
+        b = io.BytesIO(); im.save(b, 'JPEG', quality=random.randint(45, 80)); b.seek(0); im = Image.open(b).convert('RGB')
+    return im
+
+class AugSet(Dataset):
+    def __init__(self, rows, preprocess, tokenizer):
+        self.rows = rows; self.pp = preprocess; self.tok = tokenizer
+    def __len__(self): return len(self.rows)
+    def __getitem__(self, i):
+        idv, pat, col, ven, typ = self.rows[i]
+        im = Image.open(os.path.join(CACHE, idv + '.jpg')).convert('RGB')
+        img = self.pp(augment(im))                       # <-- degrade before CLIP preprocess
+        txt = self.tok([T.caption(pat, col, ven, typ)])[0]
+        return img, txt
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--epochs', type=int, default=3)
+    ap.add_argument('--batch', type=int, default=96)
+    ap.add_argument('--lr', type=float, default=2e-6)      # gentle — warm-starting an already-good model
+    ap.add_argument('--max_hours', type=float, default=3.0)
+    a = ap.parse_args()
+
+    dev = 'mps' if torch.backends.mps.is_available() else 'cpu'
+    model, _, preprocess = open_clip.create_model_and_transforms('ViT-B-32', pretrained='laion2b_s34b_b79k')
+    tokenizer = open_clip.get_tokenizer('ViT-B-32')
+    warm = os.path.join(CKPT, 'dw_clip_ft.pt')             # WARM-START from the current fine-tuned model
+    model.load_state_dict(torch.load(warm, map_location='cpu'), strict=False)
+    print('warm-started from dw_clip_ft.pt · device', dev, flush=True)
+    model = model.to(dev).float(); model.train()
+
+    rows = T.load_rows()
+    print(f'training pairs (cached): {len(rows)}', flush=True)
+    dl = DataLoader(AugSet(rows, preprocess, tokenizer), batch_size=a.batch, shuffle=True,
+                    num_workers=5, drop_last=True, persistent_workers=True)
+    opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0.1)
+    t0 = time.time(); step = 0
+    for ep in range(a.epochs):
+        for imgs, txts in dl:
+            imgs = imgs.to(dev); txts = txts.to(dev)
+            imf = F.normalize(model.encode_image(imgs), dim=-1)
+            txf = F.normalize(model.encode_text(txts), dim=-1)
+            scale = model.logit_scale.exp().clamp(max=100)
+            logits = scale * imf @ txf.t()
+            labels = torch.arange(logits.size(0), device=dev)
+            loss = (F.cross_entropy(logits, labels) + F.cross_entropy(logits.t(), labels)) / 2
+            opt.zero_grad(); loss.backward(); opt.step()
+            with torch.no_grad(): model.logit_scale.clamp_(0, 4.6052)
+            step += 1
+            if step % 20 == 0: print(f'ep{ep} step{step} loss={loss.item():.4f} elapsed={(time.time()-t0)/3600:.2f}h', flush=True)
+            if (time.time()-t0)/3600 > a.max_hours:
+                torch.save(model.state_dict(), os.path.join(CKPT, 'dw_clip_ft_aug.pt')); print('max_hours — saved', flush=True); return
+        torch.save(model.state_dict(), os.path.join(CKPT, f'dw_clip_ft_aug_ep{ep}.pt'))
+        print(f'saved ep{ep}', flush=True)
+    torch.save(model.state_dict(), os.path.join(CKPT, 'dw_clip_ft_aug.pt'))
+    print(f'DONE — aug weights → ckpt/dw_clip_ft_aug.pt ({(time.time()-t0)/3600:.2f}h)', flush=True)
+
+if __name__ == '__main__':
+    main()

← 6a137d9 5x contrarian gate: honest re-scope — FT visual-ID validated  ·  back to Dw Photo Capture  ·  eval_aug: A/B degraded→clean image retrieval (base vs curren 33641ca →