Views
No views yet
bear produces a vector that is nearest to the bear
text embedding — without any classification head.| Metric | Value |
|---|---|
| I2T Recall@1 | 77.77% |
| Val cosine similarity | 0.3927 |
| Random baseline (1/100) | 1.00% |
| Best epoch | 47 / 200 |
| Component | Detail |
|---|---|
| Backbone | MobileNetV3-Small (ImageNet pretrained, nn.Sequential(*list(base.children())[:-1])) |
| Fine-tuning | Last 3 of 13 inverted residual blocks unfrozen; rest frozen |
| Backbone LR | 10× lower than projection head (lr × 0.1) |
| Projection head | 4-layer MLP: 576 → 1024 → 512 → 256 → 128 (BN + ReLU + Dropout each layer) |
| Dropout | 0.20 / 0.15 / 0.10 across layers 1–3 |
| Output | 128-dim vector (L2-normalised outside the model) |
| Trainable parameters | 1,928,096 |
| Training objective | Symmetric InfoNCE with label smoothing ε=0.1, τ=0.07 |
| Optimizer | AdamW (backbone: lr=2.5e-5, projection: lr=2.5e-4, weight_decay=0.007) |
| LR schedule | 15-epoch linear warmup → cosine annealing (η_min=1e-7) |
| Batch size | 256 × 2 gradient accumulation steps = effective 512 |
| Data augmentation | RandAugment(2, 6) + RandomErasing(p=0.2) + RandomResizedCrop(scale=0.7–1.0) |
| Mixed precision | FP16 (AMP) |
| Early stopping | patience=40 on val cosine similarity |
ImageEncoder. The forward() method returns
(backbone_features, projected_embedding) — use the second output, then L2-normalise externally.1import torch
2import torch.nn as nn
3import torch.nn.functional as F
4import torchvision.models as models
5import torchvision.transforms as transforms
6import numpy as np
7from huggingface_hub import hf_hub_download
8from PIL import Image
9
10class ImageEncoder(nn.Module):
11 def __init__(self, proj_dim=128, device="cpu"):
12 super().__init__()
13 self.device = device
14 base = models.mobilenet_v3_small(
15 weights=models.MobileNet_V3_Small_Weights.DEFAULT
16 )
17 self.backbone = nn.Sequential(*list(base.children())[:-1]).to(device)
18
19 for p in self.backbone.parameters():
20 p.requires_grad = False
21
22 total_layers = len(list(self.backbone[0].children()))
23 for i, layer in enumerate(self.backbone[0].children()):
24 if i >= total_layers - 3:
25 for p in layer.parameters():
26 p.requires_grad = True
27
28 self.projection = nn.Sequential(
29 nn.Linear(576, 1024), nn.BatchNorm1d(1024), nn.ReLU(inplace=True), nn.Dropout(0.20),
30 nn.Linear(1024, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True), nn.Dropout(0.15),
31 nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(inplace=True), nn.Dropout(0.10),
32 nn.Linear(256, proj_dim),
33 ).to(device)
34
35 def forward(self, x):
36 feats = self.backbone(x).flatten(1) # [B, 576]
37 out = self.projection(feats) # [B, proj_dim]
38 return feats, out # use 'out', normalise externally
39
40# Download and load
41path = hf_hub_download(repo_id="haripra1112001/clip-cifar100-mobilenet",
42 filename="best_cifar100_projection.pth")
43ckpt = torch.load(path, map_location="cpu", weights_only=False)
44
45model = ImageEncoder(proj_dim=128, device="cpu")
46model.load_state_dict(ckpt["model_state_dict"])
47model.eval()
48
49text_emb = ckpt["text_embeddings"].numpy() # (100, 128)
50class_words = ckpt["class_words"] # list of 100 CIFAR-100 class names
51text_emb_norm = text_emb / np.linalg.norm(text_emb, axis=1, keepdims=True)1preprocess = transforms.Compose([
2 transforms.Resize(256),
3 transforms.CenterCrop(224),
4 transforms.ToTensor(),
5 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
6])
7
8def predict(image_path, top_k=5):
9 img = preprocess(Image.open(image_path).convert("RGB")).unsqueeze(0)
10 with torch.no_grad():
11 _, proj = model(img) # use second output
12 img_emb = F.normalize(proj, p=2, dim=1).numpy() # (1, 128)
13 sims = (text_emb_norm @ img_emb.T).flatten() # (100,)
14 top_idx = np.argsort(-sims)[:top_k]
15 return [(class_words[i], round(float(sims[i]), 3)) for i in top_idx]
16
17print(predict("my_image.jpg"))
18# e.g. [('bear', 0.42), ('leopard', 0.31), ('wolf', 0.28), ...]1def create_tta_transforms():
2 """8 deterministic TTA transforms: center crop, flips, and multiple scales."""
3 normalize = transforms.Normalize(
4 mean=[0.485, 0.456, 0.406],
5 std=[0.229, 0.224, 0.225]
6 )
7 return [
8 # View 1: center crop baseline
9 transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224),
10 transforms.ToTensor(), normalize]),
11 # View 2: center crop + horizontal flip
12 transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224),
13 transforms.RandomHorizontalFlip(p=1.0),
14 transforms.ToTensor(), normalize]),
15 # Views 3-5: multiple scales
16 transforms.Compose([transforms.Resize(232), transforms.CenterCrop(224),
17 transforms.ToTensor(), normalize]),
18 transforms.Compose([transforms.Resize(240), transforms.CenterCrop(224),
19 transforms.ToTensor(), normalize]),
20 transforms.Compose([transforms.Resize(248), transforms.CenterCrop(224),
21 transforms.ToTensor(), normalize]),
22 # Views 6-8: scales + flip
23 transforms.Compose([transforms.Resize(232), transforms.CenterCrop(224),
24 transforms.RandomHorizontalFlip(p=1.0),
25 transforms.ToTensor(), normalize]),
26 transforms.Compose([transforms.Resize(240), transforms.CenterCrop(224),
27 transforms.RandomHorizontalFlip(p=1.0),
28 transforms.ToTensor(), normalize]),
29 transforms.Compose([transforms.Resize(248), transforms.CenterCrop(224),
30 transforms.RandomHorizontalFlip(p=1.0),
31 transforms.ToTensor(), normalize]),
32 ]
33
34
35def apply_tta_to_image(image_tensor, vision_model, device="cpu"):
36 """
37 Apply 8-view TTA to a single image tensor and return a normalised embedding.
38
39 Args:
40 image_tensor: torch.Tensor (C, H, W) or (H, W, C), range [0,1] or [0,255]
41 vision_model: loaded ImageEncoder in eval mode
42 device: 'cpu' or 'cuda'
43
44 Returns:
45 avg_embedding: np.ndarray of shape (1, proj_dim), L2-normalised
46 """
47 # Convert tensor → PIL Image
48 if len(image_tensor.shape) == 3:
49 if image_tensor.shape[0] == 3: # (C, H, W)
50 img_np = image_tensor.cpu().permute(1, 2, 0).numpy()
51 else: # (H, W, C)
52 img_np = image_tensor.cpu().numpy()
53 else:
54 raise ValueError(f"Expected 3D tensor, got shape {image_tensor.shape}")
55
56 if img_np.max() > 1.0:
57 img_np = img_np / 255.0
58
59 pil_image = Image.fromarray((img_np * 255).astype(np.uint8))
60
61 # Apply all 8 transforms and stack into a batch
62 tta_batch = torch.stack([t(pil_image) for t in create_tta_transforms()]).to(device)
63
64 # Get embeddings for all 8 views
65 with torch.no_grad():
66 model_output = vision_model(tta_batch)
67 visual_proj = model_output[1] if isinstance(model_output, tuple) else model_output
68
69 # Average UNNORMALISED embeddings, then normalise once
70 avg_embedding = F.normalize(visual_proj.mean(dim=0, keepdim=True), p=2, dim=1)
71 return avg_embedding.cpu().numpy() # (1, proj_dim)
72
73
74def predict_tta(image_path, top_k=5):
75 img_tensor = transforms.ToTensor()(Image.open(image_path).convert("RGB"))
76 img_emb = apply_tta_to_image(img_tensor, model, device="cpu") # (1, 128)
77 sims = (text_emb_norm @ img_emb.T).flatten() # (100,)
78 top_idx = np.argsort(-sims)[:top_k]
79 return [(class_words[i], round(float(sims[i]), 3)) for i in top_idx]
80
81print(predict_tta("my_image.jpg"))
82# e.g. [('bear', 0.44), ('leopard', 0.32), ('wolf', 0.27), ...]| File | Description |
|---|---|
best_cifar100_projection.pth | Model weights + text embeddings + config |
report_image_model.md | Full technical report — architecture, training dynamics, ablation |
1@misc{prajapati2026clip,
2 title = {CLIP-Style Visual Encoder for CIFAR-100: Contrastive Alignment
3 with Visually-Grounded Skip-Gram Embeddings},
4 author = {Prajapati, Harishkumar Kishorkumar},
5 year = {2026}
6}