napoli-pizza-style

A three-class photo classifier that answers one question about a photo scraped from a restaurant listing: is this a pizza, and if so, is it Neapolitan?

architecture resnet18, fine-tuned from ImageNet weights
classes not_pizza, napoletana, other_pizza
input 224ร—224, resize 256 โ†’ center crop, ImageNet mean/std
checkpoint md5 01642752b6757d5ea04a367c1192876e
file size 44.8 MB
training labels 1273 human labels
split sizes train 904 / val 173 / test 196

Results

Headline is macro-F1 0.8188 on a shared test split re-scored by just model-eval.

class precision recall F1 support
not_pizza 0.9407 0.9407 0.9407 118
napoletana 0.8611 0.7750 0.8158 40
other_pizza 0.6667 0.7368 0.7000 38

Accuracy on the same split was 0.8673, and that number should be ignored. Roughly 70% of the photos in this corpus are not_pizza, so a model that answers not_pizza every single time scores 0.70 accuracy and is worth nothing. Macro-F1 is the selection metric here, always. A smoke run during development scored accuracy 1.00 and macro-F1 0.33 โ€” that is the whole argument in one line.

Intended use

Bulk-labelling photos for a map of pizzerias: every place gets a verdict from a vote over its photos rather than from any single prediction, and predictions below 0.80 confidence are discarded before the vote. It is a filter for a map legend, not a judgement about a restaurant.

Limitations and biases

  • Trained on Google Maps listing photos, which are lit, framed and selected the way restaurant photos are. Home-cooked or professionally-plated pizza is out of distribution.
  • other_pizza is the weak class โ€” it is the residual category, and it is where the confusion concentrates. not_pizza is easy; the pizza/pizza boundary is not.
  • "Neapolitan" here means what the labellers called Neapolitan, from a photo alone. Leopard-spotted cornicione and a wet centre are visual cues, not a certification, and no photo can show you a dough hydration or an oven.
  • Training data is Europe-heavy, Seoul aside. It has not been tested against US or Latin American pizza styles.
  • The corpus was compacted to 512px WebP after this model was trained, so a retrain sees lossy inputs where this run saw the originals.

Usage

import torch
from torchvision import models, transforms
from PIL import Image

ckpt = torch.load('best.pt', map_location='cpu')
net = models.resnet18()
net.fc = torch.nn.Linear(net.fc.in_features, len(ckpt['classes']))
net.load_state_dict(ckpt['state_dict'])
net.eval()

tf = transforms.Compose([
    transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
with torch.no_grad():
    p = net(tf(Image.open('photo.jpg').convert('RGB'))[None]).softmax(-1)[0]
print(dict(zip(ckpt['classes'], p.tolist())))

Training notes

  • Split by PLACE, not by image. One restaurant's photos are near-duplicates of each other; a random image-level split leaks a twin into validation and inflates the score by roughly 15 points.
  • Weak labels describe the restaurant, not the photo. A chain's storefront picture is a building, and training on weak labels alone teaches the model what a chain's photography looks like.
  • Promotion is gated: a new checkpoint replaces the live one only after both are scored on the same split built from today's labels, and it is refused if it scores worse. Two runs' own reported numbers are two different exams.

metrics.json in this repo is the full record for these bytes.

License

MIT.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support