Instructions to use HopitAI/moda-pro-lite-plus with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- OpenCLIP
How to use HopitAI/moda-pro-lite-plus with OpenCLIP:
import open_clip model, preprocess_train, preprocess_val = open_clip.create_model_and_transforms('hf-hub:HopitAI/moda-pro-lite-plus') tokenizer = open_clip.get_tokenizer('hf-hub:HopitAI/moda-pro-lite-plus') - Notebooks
- Google Colab
- Kaggle
MODA Pro Lite+: moda-pro-lite with its calibrated serving recipe
Browse files- README.md +118 -0
- config.json +39 -0
- serving_ann.py +173 -0
README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
tags:
|
| 4 |
+
- fashion
|
| 5 |
+
- retrieval
|
| 6 |
+
- text-to-image
|
| 7 |
+
- open_clip
|
| 8 |
+
- siglip2
|
| 9 |
+
pipeline_tag: feature-extraction
|
| 10 |
+
library_name: open_clip
|
| 11 |
+
base_model: HopitAI/moda-pro-lite
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# MODA Pro Lite+
|
| 15 |
+
|
| 16 |
+
**The strongest open system at ≤250M parameters on catalogue and title search.**
|
| 17 |
+
|
| 18 |
+
MODA Pro Lite+ is [MODA Pro Lite](https://huggingface.co/HopitAI/moda-pro-lite) — a 213M
|
| 19 |
+
fashion retrieval encoder — served with a calibrated multi-view recipe. This repository holds
|
| 20 |
+
the recipe, as runnable code; the weights are pulled from `moda-pro-lite` at load time.
|
| 21 |
+
|
| 22 |
+
Zero added parameters. One stored vector per item. The uplift is paid once at index time and
|
| 23 |
+
costs nothing at query time.
|
| 24 |
+
|
| 25 |
+
## Results
|
| 26 |
+
|
| 27 |
+
MAP@10, full corpus, all ground-truth queries, one evaluator (`pytrec_eval map_cut.10`).
|
| 28 |
+
`MODA` is FashionSigLIP with its own serving recipe, shown for reference.
|
| 29 |
+
|
| 30 |
+
| benchmark | MODA | Pro Lite (bare) | **Pro Lite+** (with recipe) |
|
| 31 |
+
|---|---:|---:|---:|
|
| 32 |
+
| KAGL | 0.2887 | 0.3055 | **0.3201** |
|
| 33 |
+
| Polyvore | 0.3726 | 0.3952 | **0.4049** |
|
| 34 |
+
| Atlas | 0.1862 | 0.1814 | **0.1904** |
|
| 35 |
+
| Fashion200K | **0.1946** | 0.1758 | 0.1846 |
|
| 36 |
+
| DeepFashion In-Shop | **0.1642** | 0.0930 | 0.1026 |
|
| 37 |
+
| DeepFashion Multimodal | **0.0147** | 0.0118 | 0.0133 |
|
| 38 |
+
|
| 39 |
+
**Pro Lite+ leads the ≤250M class on KAGL, Polyvore and Atlas** — +10.9% over MODA on KAGL,
|
| 40 |
+
+8.7% on Polyvore, both significant under a paired bootstrap (10,000 resamples).
|
| 41 |
+
|
| 42 |
+
The recipe is worth +2.5% to +12.8% over the bare encoder on every benchmark, and costs
|
| 43 |
+
nothing at query time: the views are fused into a single vector before indexing.
|
| 44 |
+
|
| 45 |
+
**Where this model is weak, stated plainly.** Pro Lite is tuned for short catalogue titles.
|
| 46 |
+
On long natural-language descriptions it trails FashionSigLIP substantially — DeepFashion
|
| 47 |
+
In-Shop queries average 75 words, and Pro Lite+ scores 0.1026 there against MODA's 0.1642.
|
| 48 |
+
If your queries are descriptions rather than titles, use
|
| 49 |
+
[MODA Duo](https://huggingface.co/HopitAI/moda-duo), which routes per query.
|
| 50 |
+
|
| 51 |
+
## Serving cost
|
| 52 |
+
|
| 53 |
+
```
|
| 54 |
+
stored vectors per item : 1
|
| 55 |
+
ANN queries per search : 1
|
| 56 |
+
image forwards at index : 3x offline, paid once
|
| 57 |
+
text forwards per query : 2x negligible beside the ANN probe
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
The recipe is a rule for *what you encode*, not a model change. Views are combined into one
|
| 61 |
+
unit vector before indexing, so nearest-neighbour search costs exactly what the bare encoder
|
| 62 |
+
costs — same index, same probe, no extra routes and no re-ranking.
|
| 63 |
+
|
| 64 |
+
## Use
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
pip install open_clip_torch pillow numpy hnswlib
|
| 68 |
+
python serving_ann.py --demo
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
```python
|
| 72 |
+
from serving_ann import load, encode_images, encode_queries, build_index, search
|
| 73 |
+
|
| 74 |
+
enc = load() # open_clip, this repo's weights
|
| 75 |
+
docs = encode_images(catalogue, enc) # (n, 768) float32, one vector per item
|
| 76 |
+
index = build_index(docs) # hnswlib, cosine via inner product
|
| 77 |
+
qry = encode_queries(["black leather ankle boots"], enc)
|
| 78 |
+
ids, scores = search(index, qry, k=10)
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Bare encoder, if you would rather not use the recipe:
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
import open_clip, torch
|
| 85 |
+
|
| 86 |
+
model, _, preprocess = open_clip.create_model_and_transforms("hf-hub:HopitAI/moda-pro-lite")
|
| 87 |
+
tokenizer = open_clip.get_tokenizer("hf-hub:HopitAI/moda-pro-lite")
|
| 88 |
+
model.eval()
|
| 89 |
+
|
| 90 |
+
with torch.no_grad():
|
| 91 |
+
image = torch.nn.functional.normalize(model.encode_image(preprocess(img).unsqueeze(0)), dim=-1)
|
| 92 |
+
text = torch.nn.functional.normalize(model.encode_text(tokenizer(["black leather ankle boots"])), dim=-1)
|
| 93 |
+
score = (text @ image.T).item()
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
768-d embeddings, cosine similarity, one vector per item. Index them in any vector database.
|
| 97 |
+
|
| 98 |
+
## The recipe
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
document = normalize(official + 0.25 * square_pad + 0.25 * foreground_pad)
|
| 102 |
+
query = normalize(raw + 0.25 * "a photo of {query}")
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
`serving_ann.py` implements it. Zero added parameters, one stored vector.
|
| 106 |
+
|
| 107 |
+
## Evaluation
|
| 108 |
+
|
| 109 |
+
All figures are full corpus, all ground-truth queries, MAP@10 under one evaluator
|
| 110 |
+
(`pytrec_eval map_cut.10`), float32. Per-query results and confidence intervals are in the
|
| 111 |
+
[repository](https://github.com/hopit-ai/Moda).
|
| 112 |
+
|
| 113 |
+
## Related
|
| 114 |
+
|
| 115 |
+
- [MODA Pro Lite](https://huggingface.co/HopitAI/moda-pro-lite) — the bare encoder these weights come from.
|
| 116 |
+
- [MODA Duo](https://huggingface.co/HopitAI/moda-duo) — routes each query to Pro Lite+ or MODA by its shape; beats both on a mixed workload.
|
| 117 |
+
- [MODA](https://huggingface.co/HopitAI/moda-fashionsiglip-multiview-203m) — FashionSigLIP with a serving recipe. Stronger on long descriptions.
|
| 118 |
+
- [MODA-SigLIP-Distilled](https://huggingface.co/HopitAI/moda-fashion-distilled) — image-to-image retrieval.
|
config.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "MODA Pro Lite+",
|
| 3 |
+
"artifact_type": "retrieval_system",
|
| 4 |
+
"base_model": "HopitAI/moda-pro-lite",
|
| 5 |
+
"base_model_parameters": 213159938,
|
| 6 |
+
"additional_learned_parameters": 0,
|
| 7 |
+
"embedding_dimension": 768,
|
| 8 |
+
"gallery": {
|
| 9 |
+
"views": {
|
| 10 |
+
"official": 1.0,
|
| 11 |
+
"pad": 0.25,
|
| 12 |
+
"foreground_pad": 0.25
|
| 13 |
+
},
|
| 14 |
+
"normalize": true,
|
| 15 |
+
"stored_vectors_per_item": 1
|
| 16 |
+
},
|
| 17 |
+
"query": {
|
| 18 |
+
"prompts": {
|
| 19 |
+
"raw": 1.0,
|
| 20 |
+
"photo": 0.25
|
| 21 |
+
},
|
| 22 |
+
"normalize": true
|
| 23 |
+
},
|
| 24 |
+
"prompt_templates": {
|
| 25 |
+
"raw": "{query}",
|
| 26 |
+
"photo": "a photo of {query}"
|
| 27 |
+
},
|
| 28 |
+
"serving": {
|
| 29 |
+
"stored_vectors_per_item": 1,
|
| 30 |
+
"ann_queries_per_search": 1,
|
| 31 |
+
"image_forwards_per_item_at_index_time": 3,
|
| 32 |
+
"text_forwards_per_query": 2,
|
| 33 |
+
"rerank": false
|
| 34 |
+
},
|
| 35 |
+
"evaluation": {
|
| 36 |
+
"protocol": "full corpus, all ground-truth queries, MAP@10, pytrec_eval map_cut.10, paired bootstrap 10000",
|
| 37 |
+
"receipt": "results/multiview_target_eval_fp32/"
|
| 38 |
+
}
|
| 39 |
+
}
|
serving_ann.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MODA Pro Lite+ (moda-pro-lite with its calibrated serving harness) -- retrieval with ANN, end to end.
|
| 2 |
+
|
| 3 |
+
The harness below was selected on held-out development data only (OpenVTON + GLAMI); no target benchmark was touched during selection.
|
| 4 |
+
It adds ZERO parameters and stores ONE vector per item.
|
| 5 |
+
|
| 6 |
+
Serving cost
|
| 7 |
+
------------
|
| 8 |
+
stored vectors per item : 1
|
| 9 |
+
ANN queries per search : 1
|
| 10 |
+
image forwards at index : 3x (paid once, offline)
|
| 11 |
+
text forwards per query : 2x (cheap next to the ANN probe)
|
| 12 |
+
|
| 13 |
+
The harness is a recipe for WHAT YOU ENCODE, not a model change: the views are
|
| 14 |
+
combined into a single unit vector before indexing, so nearest-neighbour search
|
| 15 |
+
costs exactly what it costs for the bare model. No extra routes, no re-ranking.
|
| 16 |
+
|
| 17 |
+
pip install open_clip_torch pillow numpy hnswlib
|
| 18 |
+
|
| 19 |
+
Example
|
| 20 |
+
-------
|
| 21 |
+
python serving_ann.py --demo
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import argparse
|
| 26 |
+
import math
|
| 27 |
+
from typing import Sequence
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
from PIL import Image
|
| 33 |
+
|
| 34 |
+
MODEL = "hf-hub:HopitAI/moda-pro-lite"
|
| 35 |
+
IMAGE_MIX = {'official': 1.0, 'pad': 0.25, 'foreground_pad': 0.25} # view -> weight, combined then L2-normalised
|
| 36 |
+
PROMPT_MIX = {'raw': 1.0, 'photo': 0.25} # prompt -> weight, combined then L2-normalised
|
| 37 |
+
PROMPT_TEMPLATES = {
|
| 38 |
+
"raw": "{query}",
|
| 39 |
+
"photo": "a photo of {query}",
|
| 40 |
+
"product": "a fashion product photo of {query}",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------- views ----
|
| 45 |
+
def square_pad(image: Image.Image, fill: int = 128) -> Image.Image:
|
| 46 |
+
image = image.convert("RGB")
|
| 47 |
+
side = max(image.size)
|
| 48 |
+
canvas = Image.new("RGB", (side, side), (fill, fill, fill))
|
| 49 |
+
canvas.paste(image, ((side - image.width) // 2, (side - image.height) // 2))
|
| 50 |
+
return canvas
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def center_square(image: Image.Image) -> Image.Image:
|
| 54 |
+
side = min(image.size)
|
| 55 |
+
left, top = (image.width - side) // 2, (image.height - side) // 2
|
| 56 |
+
return image.convert("RGB").crop((left, top, left + side, top + side))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def foreground_square(image: Image.Image) -> Image.Image:
|
| 60 |
+
"""Crop a near-white catalog border, then pad without distorting aspect."""
|
| 61 |
+
image = image.convert("RGB")
|
| 62 |
+
preview = image.copy()
|
| 63 |
+
preview.thumbnail((256, 256), Image.Resampling.BILINEAR)
|
| 64 |
+
mask = np.any(np.asarray(preview, dtype=np.uint8) < 242, axis=-1)
|
| 65 |
+
if float(mask.mean()) < 0.01:
|
| 66 |
+
return square_pad(image)
|
| 67 |
+
ys, xs = np.nonzero(mask)
|
| 68 |
+
sx, sy = image.width / preview.width, image.height / preview.height
|
| 69 |
+
left = max(0, math.floor(float(xs.min()) * sx))
|
| 70 |
+
right = min(image.width, math.ceil(float(xs.max() + 1) * sx))
|
| 71 |
+
top = max(0, math.floor(float(ys.min()) * sy))
|
| 72 |
+
bottom = min(image.height, math.ceil(float(ys.max() + 1) * sy))
|
| 73 |
+
mx, my = max(1, round((right - left) * 0.05)), max(1, round((bottom - top) * 0.05))
|
| 74 |
+
return square_pad(image.crop((max(0, left - mx), max(0, top - my),
|
| 75 |
+
min(image.width, right + mx), min(image.height, bottom + my))))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
VIEWS = {
|
| 79 |
+
"official": lambda im: im.convert("RGB"),
|
| 80 |
+
"pad": square_pad,
|
| 81 |
+
"pad_white": lambda im: square_pad(im, fill=255),
|
| 82 |
+
"center_crop": center_square,
|
| 83 |
+
"foreground_pad": foreground_square,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# -------------------------------------------------------------- encoding ----
|
| 88 |
+
def load(device: str = "cpu"):
|
| 89 |
+
import open_clip
|
| 90 |
+
model, _, preprocess = open_clip.create_model_and_transforms(MODEL)
|
| 91 |
+
model.eval().to(device)
|
| 92 |
+
for p in model.parameters():
|
| 93 |
+
p.requires_grad = False
|
| 94 |
+
return model, preprocess, open_clip.get_tokenizer(MODEL), device
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@torch.inference_mode()
|
| 98 |
+
def encode_images(images: Sequence[Image.Image], enc, batch_size: int = 32) -> np.ndarray:
|
| 99 |
+
"""-> (n, 768) float32, unit norm. ONE vector per item."""
|
| 100 |
+
model, preprocess, _, device = enc
|
| 101 |
+
parts = {v: [] for v in IMAGE_MIX}
|
| 102 |
+
for s in range(0, len(images), batch_size):
|
| 103 |
+
chunk = images[s:s + batch_size]
|
| 104 |
+
for view in IMAGE_MIX:
|
| 105 |
+
px = torch.stack([preprocess(VIEWS[view](im)) for im in chunk]).to(device)
|
| 106 |
+
parts[view].append(F.normalize(model.encode_image(px).float(), dim=-1).cpu())
|
| 107 |
+
fused = sum(w * torch.cat(parts[v]) for v, w in IMAGE_MIX.items())
|
| 108 |
+
return F.normalize(fused, dim=-1).numpy().astype("float32")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@torch.inference_mode()
|
| 112 |
+
def encode_queries(texts: Sequence[str], enc, batch_size: int = 128) -> np.ndarray:
|
| 113 |
+
"""-> (n, 768) float32, unit norm. ONE vector per query."""
|
| 114 |
+
model, _, tokenizer, device = enc
|
| 115 |
+
parts = {p: [] for p in PROMPT_MIX}
|
| 116 |
+
for s in range(0, len(texts), batch_size):
|
| 117 |
+
chunk = texts[s:s + batch_size]
|
| 118 |
+
for prompt in PROMPT_MIX:
|
| 119 |
+
rendered = [PROMPT_TEMPLATES[prompt].format(query=t) for t in chunk]
|
| 120 |
+
tok = tokenizer(rendered).to(device)
|
| 121 |
+
parts[prompt].append(F.normalize(model.encode_text(tok).float(), dim=-1).cpu())
|
| 122 |
+
fused = sum(w * torch.cat(parts[p]) for p, w in PROMPT_MIX.items())
|
| 123 |
+
return F.normalize(fused, dim=-1).numpy().astype("float32")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ------------------------------------------------------------------ ANN ----
|
| 127 |
+
def build_index(vectors: np.ndarray, m: int = 32, ef_construction: int = 200):
|
| 128 |
+
"""Cosine similarity over unit vectors is inner product, so use space='ip'."""
|
| 129 |
+
import hnswlib
|
| 130 |
+
index = hnswlib.Index(space="ip", dim=vectors.shape[1])
|
| 131 |
+
index.init_index(max_elements=len(vectors), ef_construction=ef_construction, M=m)
|
| 132 |
+
index.add_items(vectors, np.arange(len(vectors)))
|
| 133 |
+
return index
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def search(index, queries: np.ndarray, k: int = 10, ef: int = 64):
|
| 137 |
+
index.set_ef(max(ef, k))
|
| 138 |
+
ids, distances = index.knn_query(queries, k=k)
|
| 139 |
+
return ids, 1.0 - distances # ip distance -> cosine similarity
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def search_exact(vectors: np.ndarray, queries: np.ndarray, k: int = 10):
|
| 143 |
+
"""Ground truth, for checking ANN recall on your own corpus."""
|
| 144 |
+
sims = queries @ vectors.T
|
| 145 |
+
ids = np.argsort(-sims, axis=1)[:, :k]
|
| 146 |
+
return ids, np.take_along_axis(sims, ids, axis=1)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _demo() -> None:
|
| 150 |
+
enc = load()
|
| 151 |
+
corpus = [Image.new("RGB", (w, h), c) for w, h, c in
|
| 152 |
+
[(224, 300, "white"), (300, 224, "black"), (256, 256, "navy"),
|
| 153 |
+
(400, 200, "beige"), (200, 400, "maroon")]]
|
| 154 |
+
doc = encode_images(corpus, enc)
|
| 155 |
+
qry = encode_queries(["black leather ankle boots", "navy wool coat"], enc)
|
| 156 |
+
print(f"documents {doc.shape} queries {qry.shape} (one vector each)")
|
| 157 |
+
index = build_index(doc)
|
| 158 |
+
ann_ids, ann_scores = search(index, qry, k=3)
|
| 159 |
+
ex_ids, _ = search_exact(doc, qry, k=3)
|
| 160 |
+
agree = float(np.mean([len(set(a) & set(b)) / len(b) for a, b in zip(ann_ids, ex_ids)]))
|
| 161 |
+
print(f"ANN top-3 ids {ann_ids.tolist()}")
|
| 162 |
+
print(f"exact top-3 ids {ex_ids.tolist()}")
|
| 163 |
+
print(f"ANN/exact overlap@3: {agree:.3f} (1.000 expected on a corpus this small)")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
ap = argparse.ArgumentParser(description=__doc__)
|
| 168 |
+
ap.add_argument("--demo", action="store_true")
|
| 169 |
+
args = ap.parse_args()
|
| 170 |
+
if args.demo:
|
| 171 |
+
_demo()
|
| 172 |
+
else:
|
| 173 |
+
ap.print_help()
|