SigLIP-2 models show lower zero-shot accuracy than reported

I’m seeking help in reproducing the zero-shot ImageNet-1k results for SigLIP-2 models.

While my evaluation setup reproduces SigLIP v1 results almost perfectly, it consistently fails for SigLIP-2 models with a significant accuracy drop.

Summary of Results

Here is a summary of the performance I’m observing compared to the reported numbers in the paper:

Model Version Model Size Paper Accuracy My Accuracy Difference
google/siglip-base-patch16-256 Base 76.7% 76.6% -0.1%
google/siglip-large-patch16-256 Large 80.5% 80.4% -0.1%
google/siglip2-base-patch16-256 Base 79.1% 70.6% -8.5%
google/siglip2-large-patch16-256 Large 82.5% 73.2% -9.3%

As the table shows, the v1 models are working as expected. However, both v2 models suffer from a consistent and large drop of ~9%. This suggests the issue is not with my general framework but with a v2-specific detail I might be missing.

This problem has also been reported by other users: *(Link: <[ google/siglip2-base-patch16-224 · The accuracy on the ImageNet dataset is low ]>)*

My Evaluation Logic

My code follows a standard zero-shot approach with 80-template ensembling.

  1. Load model : I use AutoModel.from_pretrained(model_path)
  2. Image Processing : For all image transformations, I use processor.image_processor from the loaded AutoProcessor.
  3. Text classifier generation: Here is the function I use to generate the text classifier weights (slightly modified from open_clip’s zero-shot evaluation code):
python
import torch
from tqdm.auto import tqdm
from functools import partial
from itertools import batched

# Assuming OPENAI_IMAGENET_TEMPLATES and IMAGENET_CLASSNAMES are predefined
def build_text_classifier(model, tokenizer, device, num_classes_per_batch=10):    
   num_templates = len(OPENAI_IMAGENET_TEMPLATES)    
   num_classes = len(IMAGENET_CLASSNAMES)    
   num_iter = (num_classes - 1) // num_classes_per_batch + 1    
   iter_wrap = partial(tqdm, total=num_iter, unit_scale=num_classes_per_batch, desc=“Building text classifier”)    

   def _process_batch(batch_classnames):        
      num_batch_classes = len(batch_classnames)        
      texts = [template.format(c) for c in batch_classnames for template in OPENAI_IMAGENET_TEMPLATES]        
      inputs = tokenizer(texts, padding=“max_length”, max_length=64, return_tensors=“pt”).to(device)        
      class_embeddings = model.get_text_features(**inputs)        
      class_embeddings = class_embeddings.reshape(num_batch_classes, num_templates, -1).mean(dim=1)        
      class_embeddings = class_embeddings / class_embeddings.norm(dim=1, keepdim=True)

      return class_embeddings.T # Returns (embed_dim, num_batch_classes)  
  
   with torch.no_grad():        
      batched_embeds = [_process_batch(batch) for batch in iter_wrap(batched(IMAGENET_CLASSNAMES, num_classes_per_batch))]        
      zeroshot_weights = torch.cat(batched_embeds, dim=1) # Final shape: (embed_dim, num_classes)  
   return zeroshot_weights 

Image features are also L2-normalized. I computed the image features by using

vision_output = model.vision_model(images)

image_features = vision_output.pooler_output

image_features = image_features / image_features.norm(p=2, dim=-1, keepdim=True)

The final logits are computed via logits = image_features @ zeroshot_weights.

My Question

Given the consistent ~9% performance drop only for v2 models, I suspect I’m handling something incorrectly. Could you please help me identify if I’m missing a step required for SigLIP-2?

Specifically, I’m wondering about:

  1. logit_scale / logit_bias: Is there a specific way these must be applied for v2 models during inference, which is different from v1?
  2. Model Architecture: Does the v2 architecture, despite inheriting from v1, require passing through an extra projection layer or a different method to extract the final embeddings?
  3. My Code: Is there a subtle bug in my build_text_classifier function that would affect only v2 models?

Any guidance on the correct procedure to evaluate SigLIP-2, or any pointers on what I might be doing wrong, would be greatly appreciated.

Thank you!

Environment:

  • transformers: 4.54.1
  • torch: 2.7.0+cu128

3

I’m not sure if this will have any effect, but there seems to be a quirk with the arguments that should be passed to processor.

Thank you for your reply, but that part is already applied in my code… I will try imagenet zero shot classification task with different codebase, such as weights from openclip and compare the results.

image_features = vision_output.pooler_output

The only other part of the code that felt a little off was this section. Perhaps it would be better to reference it via a function?

I’m afraid not. Because get_image_features() function is equivalent to vision_outputs = self.vision_model(images) and return vision_outputs.pooler_output.

I just tried with the weights from open_clip (timm). Replace the functions with the equivalent ones. And I was able to reproduce similar results for both SigLIP and SigLIP-2 (Base and Large). If I have time, I’ll dig into this a bit more, but for now, I’m going to try using the weights from open_clip.

This seems to improve the accuracy somewhat…

# pip install -U transformers>=4.53
import io, urllib.request, torch, torch.nn.functional as F
from PIL import Image, ImageDraw
from torchvision import transforms as T
from transformers import AutoModel, AutoProcessor, AutoTokenizer

CKPT = "google/siglip2-base-patch16-224"
LABELS = ["tabby cat", "lion", "cheeseburger"]
IMG_URL = "https://commons.wikimedia.org/wiki/Special:FilePath/Cat_August_2010-4.jpg?width=512"

def fetch(url):
    try:
        with urllib.request.urlopen(url, timeout=15) as r:
            return Image.open(io.BytesIO(r.read())).convert("RGB")
    except Exception:
        img = Image.new("RGB", (256, 256), "white")
        d = ImageDraw.Draw(img); d.rectangle([32,32,224,224], outline="black", width=6)
        return img

def wrong_zeroshot(img, labels, ckpt, model):
    bad = T.Compose([T.Resize(256), T.CenterCrop(224), T.ToTensor(),
                     T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])])
    pixel_values = bad(img).unsqueeze(0)
    tok = AutoTokenizer.from_pretrained(ckpt)
    text = tok(labels, return_tensors="pt", padding=True, truncation=True)

    with torch.no_grad():
        vi = model.vision_model(pixel_values=pixel_values).pooler_output
        tx = model.text_model(**text).pooler_output
        vi = F.normalize(vi, dim=-1); tx = F.normalize(tx, dim=-1)
        sims = vi @ tx.T
        probs = sims.softmax(-1)
    return probs

def right_zeroshot(img, labels, ckpt, model):
    labels = [l.lower() for l in labels]
    texts = [f"This is a photo of {l}." for l in labels]  # template + lowercase
    proc = AutoProcessor.from_pretrained(ckpt)
    kwargs = dict(images=img, text=texts, padding="max_length", max_length=64, return_tensors="pt")
    inputs = proc(**kwargs)
    with torch.no_grad():
        probs = model(**inputs).logits_per_image.softmax(-1)
    return probs

def topk(probs, labels, k=5):
    v, i = probs[0].topk(min(k, len(labels))); return [(float(v[j]), labels[i[j]]) for j in range(len(v))]

img = fetch(IMG_URL)
model = AutoModel.from_pretrained(CKPT).eval()
w = wrong_zeroshot(img, LABELS, CKPT, model=model)
r = right_zeroshot(img, LABELS, CKPT, model=model)
print("\nWRONG path:");  [print(f"{p:.3f}  {l}") for p,l in topk(w, LABELS)]
print("\nRIGHT path:");  [print(f"{p:.3f}  {l}") for p,l in topk(r, LABELS)]
print("\nTop-1:", topk(w, LABELS, 1)[0][1], "vs", topk(r, LABELS, 1)[0][1])

"""
WRONG path:
0.347  tabby cat
0.328  cheeseburger
0.326  lion

RIGHT path:
1.000  tabby cat
0.000  lion
0.000  cheeseburger

Top-1: tabby cat vs tabby cat
"""

I guess that’s because the image inputs for SigLIP models are normalize with [0.5,0.5,0.5], not with imagenet stats ([0.485,0.456,0.406],[0.229,0.224,0.225]).

Siglip2 seems very sensitive to Uppercase and lowercase.