Scaling Contrastive Learning: Understanding SigLIP
Scaling Contrastive Learning: Understanding SigLIP
In the world of Vision-Language Pre-training (VLP), CLIP (Contrastive Language-Image Pre-training) set the gold standard. However, CLIP comes with a hidden cost: a massive dependency on batch size and a memory-intensive softmax operation.
Enter SigLIP. By rethinking the fundamental loss function of contrastive learning, SigLIP decouples the relationship between batch size and memory, enabling more efficient scaling and better performance on limited hardware.
In this post, we will dive deep into the intuition, the mathematics, and a production-ready PyTorch implementation of the Sigmoid Loss for Language-Image Pre-training.
The Core Intuition: From Softmax to Sigmoid
To understand SigLIP, we first have to understand the "bottleneck" in standard CLIP.
The CLIP Approach (Global Softmax)
CLIP treats contrastive learning as a multi-class classification problem. For a given image, the model asks: "Which of these $N$ text descriptions in the current batch is the correct one?"
To answer this, CLIP uses a global softmax across the entire batch. This requires calculating a probability distribution, meaning every image embedding must be compared against every text embedding in a single, monolithic operation. As you scale the batch size ($B$), the memory requirements for the similarity matrix grow quadratically ($B^2$), creating a massive memory bottleneck.
The SigLIP Approach (Pairwise Sigmoid)
SigLIP shifts the paradigm. Instead of one multi-class problem, it treats contrastive learning as a series of independent binary classification tasks.
For every possible image-text pair $(i, j)$, SigLIP asks: "Do these two belong together? Yes or No?"
By using a sigmoid loss, the model evaluates each pair independently. This decoupling is a game-changer: it allows for "chunked" implementation. Devices can swap small subsets of embeddings to compute the loss without ever needing to materialize the full $B \times B$ matrix in memory.
The Technical Architecture
The Mathematical Framework
The transition from softmax to sigmoid is captured in the loss function. Instead of normalizing across a row or column, SigLIP optimizes the following:
$$\mathcal{L} = -\sum_{i,j} \log \sigma(z_{ij} \cdot (t \cdot \text{sim}(I_i, T_j) + b))$$
Where:
- $z_{ij}$ is the label: $1$ if $i=j$ (positive pair), and $-1$ if $i \neq j$ (negative pair).
- $\text{sim}(I_i, T_j)$ is the cosine similarity between image and text embeddings.
- $t$ and $b$ are learnable parameters (temperature and bias) that allow the model to adapt the decision boundary.
The Pipeline Flow
The following diagram illustrates how data flows from raw input to the final weight update:
Implementation in PyTorch
Below is a complete implementation. I have included a SigLIPLoss module and a toy DualEncoder to demonstrate how the loss aligns random vectors in a shared embedding space.
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
class SigLIPLoss(nn.Module):
"""
Implementation of the Sigmoid Loss for Language-Image Pre-training (SigLIP).
Replaces global softmax with pairwise binary classification.
"""
def __init__(self, t_prime=0.0, b=0.0):
super().__init__()
# t_prime and b are learnable parameters
# t = exp(t_prime) ensures temperature remains positive
self.t_prime = nn.Parameter(torch.tensor(t_prime))
self.b = nn.Parameter(torch.tensor(b))
def forward(self, img_emb, txt_emb):
# 1. L2 Normalize embeddings to project them onto a unit hypersphere
z_img = F.normalize(img_emb, p=2, dim=-1)
z_txt = F.normalize(txt_emb, p=2, dim=-1)
# 2. Compute pairwise similarity matrix
# logits[i, j] = dot(z_img[i], z_txt[j]) * exp(t_prime) + b
t = torch.exp(self.t_prime)
logits = torch.matmul(z_img, z_txt.T) * t + self.b
# 3. Create labels: 1 for diagonal (positive), -1 for off-diagonal (negative)
n = img_emb.shape[0]
labels = 2 * torch.eye(n, device=img_emb.device) - 1
# 4. Compute Sigmoid Loss
# We use logsigmoid for numerical stability.
# labels * logits flips the sign for negatives, treating it as binary classification.
loss = -F.logsigmoid(labels * logits).sum() / n
return loss
class SimpleDualEncoder(nn.Module):
def __init__(self, input_dim, embed_dim):
super().__init__()
self.image_encoder = nn.Linear(input_dim, embed_dim)
self.text_encoder = nn.Linear(input_dim, embed_dim)
def forward(self, img_data, txt_data):
return self.image_encoder(img_data), self.text_encoder(txt_data)
class MockImageTextDataset(Dataset):
def __init__(self, num_samples=1000, dim=128):
self.num_samples = num_samples
self.dim = dim
self.base_vectors = torch.randn(num_samples, dim)
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
# Create pairs by adding slight noise to the same base vector
img = self.base_vectors[idx] + torch.randn(self.dim) * 0.1
txt = self.base_vectors[idx] + torch.randn(self.dim) * 0.1
return img, txt
# --- Execution ---
if __name__ == '__main__':
# Hyperparameters
BATCH_SIZE, EMBED_DIM, INPUT_DIM, EPOCHS = 64, 128, 128, 20
dataset = MockImageTextDataset(num_samples=2000, dim=INPUT_DIM)
dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
model = SimpleDualEncoder(INPUT_DIM, EMBED_DIM)
criterion = SigLIPLoss()
optimizer = torch.optim.Adam(list(model.parameters()) + list(criterion.parameters()), lr=1e-3)
model.train()
for epoch in range(EPOCHS):
total_loss = 0
for img_batch, txt_batch in dataloader:
optimizer.zero_grad()
img_emb, txt_emb = model(img_batch, txt_batch)
loss = criterion(img_emb, txt_emb)
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 5 == 0:
print(f"Epoch [{epoch+1}/{EPOCHS}] | Avg Loss: {total_loss/len(dataloader):.4f}")
# Evaluation
model.eval()
with torch.no_grad():
test_img, test_txt = dataset[0]
img_emb, txt_emb = model(test_img.unsqueeze(0), test_txt.unsqueeze(0))
similarity = torch.matmul(F.normalize(img_emb, dim=-1), F.normalize(txt_emb, dim=-1).T).item()
print(f"\nFinal Test Pair Cosine Similarity: {similarity:.4f}")
Key Takeaways for Engineers
- Memory Efficiency: By moving from a global softmax to a pairwise sigmoid, SigLIP removes the need to normalize across the batch, enabling the use of "chunked" gradients.
- Scaling: This architecture is designed for extreme batch sizes, which is critical for the performance of foundation models.
- Learnable Parameters: Don't forget the $t$ (temperature) and $b$ (bias). These are not hyperparameters to be tuned by hand but are learned by the model to optimize the sigmoid boundary.
- Numerical Stability: When implementing this, always use
logsigmoidorBCEWithLogitsLossrather than applying a sigmoid followed by a log to avoid underflow/overflow.