Multimodal Training Data › Code Implementation: Processing Multimodal Datasets

Michael BrenndoerferFebruary 13, 202611 min read
Language AI HandbookAll chapters
Part 50 of 67Multimodal Models
Chapter 387Multimodal Training Data › Code Implementation: Processing Multimodal Datasets
Reading section 6 of 9 · Code Implementation: Processing Multimodal Datasets · 11 minSection 6 of 9 · 11 min

Book contents

Section 6 of 9 · 11 min read

Multimodal Training Data › Code Implementation: Processing Multimodal DatasetsOne section · Sections in this chapter6 / 9 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us examine how to load and filter before batching multimodal training data using modern tools. We will use the HuggingFace datasets library for handling large-scale image-text collections and show preprocessing pipelines needed for training vision-language models.

In[9]:
Code
import subprocess
import sys

subprocess.run(
    [
        "uv",
        "pip",
        "install",
        "--python",
        sys.executable,
        "datasets",
        "transformers",
        "torch",
        "requests",
        "Pillow",
        "matplotlib",
        "numpy",
        "-q",
    ],
    check=True,
)

We begin by loading a small subset of the Conceptual Captions dataset, which gives image URLs and captions. In production, you would use streaming mode for billion-scale datasets, but we use a local subset for demonstration.

In[11]:
Code
# Load a small sample of Conceptual Captions 12M
# In practice, use streaming=True for the full dataset
try:
    dataset = load_dataset(
        "conceptual_captions", "unlabeled", split="train", streaming=True
    )
    # Take first 100 examples for demonstration
    sample_data = []
    for i, example in enumerate(dataset):
        if i >= 100:
            break
        sample_data.append(example)
except Exception:
    # Create mock data for demonstration when dataset is unavailable
    captions = [
        "A red apple on a wooden table",
        "A golden retriever playing in a park",
        "A vintage bicycle leaning against a brick wall",
        "A steaming cup of coffee on a rainy morning",
        "Colorful hot air balloons floating over mountains",
        "An ancient stone archway in a medieval town",
        "A modern glass skyscraper which shows the sunset",
        "Fresh strawberries in a ceramic bowl",
    ]
    sample_data = [
        {
            "image_url": f"http://example.com/{i}.jpg",
            "caption": captions[i % len(captions)],
        }
        for i in range(20)
    ]

Multimodal datasets often store images as URLs rather than binary data to reduce storage costs. We need to download and decode these, handling failures gracefully since web links rot.

In[12]:
Code
import hashlib
from io import BytesIO

import requests
from PIL import Image


def load_image_from_url(url, timeout=5):
    """Download and convert image URL to PIL Image."""
    try:
        response = requests.get(url, timeout=timeout)
        response.raise_for_status()
        image = Image.open(BytesIO(response.content)).convert("RGB")
        return image
    except Exception:
        # Generate a synthetic image as fallback when URL fails
        fallback_seed = int.from_bytes(
            hashlib.sha256(url.encode("utf-8")).digest()[:4], "little"
        )
        fallback_rng = np.random.default_rng(fallback_seed)
        img_array = fallback_rng.integers(0, 255, (224, 224, 3), dtype=np.uint8)
        return Image.fromarray(img_array)


# Attempt to load images for our sample
loaded_examples = []
for ex in sample_data[:20]:  # Try first 20 to account for broken links
    img = load_image_from_url(ex["image_url"])
    if img is not None:
        loaded_examples.append(
            {"image": img, "caption": ex["caption"], "url": ex["image_url"]}
        )
Out[13]:
Console
Successfully loaded 20 images from 20 URLs
Sample caption: 'A red apple on a wooden table'
Image size: (224, 224)

The number of successfully loaded images indicates the reliability of the source URLs. A low success rate suggests high link rot, which is common in web-scraped datasets and necessitates reliable error handling in production pipelines. When working with billions of URLs, even a 50% success rate leaves hundreds of millions of valid examples, but the failed requests consume significant time and bandwidth during preprocessing. Production pipelines therefore pre-validate URLs in a separate stage, caching the successfully loaded images to avoid repeated download failures during training.

Now we show text-image alignment scoring using CLIP. This filters out pairs where the caption does not match the image content, a important quality control step that removes noise from web-scraped data.

In[14]:
Code
import torch
from transformers import CLIPModel, CLIPProcessor

# Load a small CLIP model for similarity scoring
model_name = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(model_name)
model = CLIPModel.from_pretrained(model_name)


def compute_clip_similarity(image, text):
    """Compute CLIP cosine similarity between image and text."""
    inputs = processor(
        text=[text], images=image, return_tensors="pt", padding=True
    )

    with torch.no_grad():
        outputs = model(**inputs)
        image_embeds = outputs.image_embeds
        text_embeds = outputs.text_embeds

        # Normalize and compute cosine similarity
        image_embeds = image_embeds / image_embeds.norm(dim=-1, keepdim=True)
        text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)
        similarity = (image_embeds @ text_embeds.T).item()

    return similarity
In[15]:
Code
# Compute similarities for the six examples displayed below
similarities = []
for i, ex in enumerate(loaded_examples[:6]):
    sim = compute_clip_similarity(ex["image"], ex["caption"])
    similarities.append(sim)
Out[16]:
Console
Example 1: CLIP similarity = 0.187
  Caption: A red apple on a wooden table...

Example 2: CLIP similarity = 0.172
  Caption: A golden retriever playing in a park...

Example 3: CLIP similarity = 0.148
  Caption: A vintage bicycle leaning against a brick wall...

Example 4: CLIP similarity = 0.181
  Caption: A steaming cup of coffee on a rainy morning...

Example 5: CLIP similarity = 0.197
  Caption: Colorful hot air balloons floating over mountains...

Example 6: CLIP similarity = 0.134
  Caption: An ancient stone archway in a medieval town...

CLIP similarity scores above 0.25 generally indicate reasonable alignment between image and text, while scores below 0.15 often signal misaligned or noisy pairs that should be filtered out during dataset curation. Scores between 0.15 and 0.25 stand for a gray area where human review might be necessary to determine if the caption is sufficiently descriptive.

Notice that our synthetic images (generated as random pixel arrays when real URLs fail) will score very low on CLIP similarity with their original captions. This is expected and shows how the filter works: random pixel patterns have no semantic content, so CLIP correctly assigns them near-zero similarity with any real caption. In practice, you would pre-filter to remove failed image downloads before running CLIP scoring.

Next, we construct an interleaved document format. While real interleaved data comes from HTML scraping, we simulate the structure by creating sequences with multiple images and text segments to show how such data is processed.

In[17]:
Code
def create_interleaved_sequence(examples, max_images=3):
    """
    Simulate an interleaved document by combining multiple image-text pairs
    into a sequence with special tokens.
    """
    selected = examples[:max_images]

    sequence = []
    for i, ex in enumerate(selected):
        # Add text context
        sequence.append(f"<text>Figure {i + 1} shows: {ex['caption']}</text>")
        # Add image placeholder
        sequence.append("<image>")

    return {
        "images": [ex["image"] for ex in selected],
        "text_sequence": " ".join(sequence),
        "num_images": len(selected),
    }


interleaved = create_interleaved_sequence(loaded_examples, max_images=3)
Out[18]:
Console
Created interleaved sequence with 3 images
Text sequence: <text>Figure 1 shows: A red apple on a wooden table</text> <image> <text>Figure 2 shows: A golden retriever playing in a park</text> <image> <text>Figure 3 shows: A vintage bicycle leaning against a b...

The interleaved sequence output shows how multimodal documents structure content with special tokens marking image boundaries. This format lets models to process variable numbers of images within a single text context, maintaining the natural flow of documents where visual elements interrupt prose mid-sentence. During training, the model learns to predict the text following an image token based on both the preceding text and the visual features extracted from the image.

Out[19]:
Visualization
First runtime sample image displayed with its computed CLIP similarity and full caption text.
First runtime image-text sample with its computed CLIP similarity. If the source URL was unavailable, the loader's fallback noise image appears and should receive a weak semantic match.
Second runtime sample image displayed with its computed CLIP similarity and full caption text.
Second runtime image-text sample with its computed CLIP similarity, used to inspect whether the loaded pixels correspond to the accompanying caption.
Third runtime sample image displayed with its computed CLIP similarity and full caption text.
Third runtime image-text sample with its computed CLIP similarity. Higher values indicate stronger semantic correspondence for the displayed pair.
Out[20]:
Visualization
Fourth runtime sample image displayed with its computed CLIP similarity and full caption text.
Fourth runtime image-text sample with its computed CLIP similarity, preserving both the loaded image and its full caption for direct inspection.
Fifth runtime sample image displayed with its computed CLIP similarity and full caption text.
Fifth runtime image-text sample with its computed CLIP similarity. Fallback noise, when present, makes failed downloads visually explicit rather than masquerading as valid data.
Sixth runtime sample image displayed with its computed CLIP similarity and full caption text.
Sixth runtime image-text sample with its computed CLIP similarity, completing the small diagnostic set used to demonstrate alignment scoring.

The variation in CLIP scores reveals the noise inherent in web-scraped data. Pairs with scores below 0.2 typically indicate completely mismatched content (for example, a stock photo with a generic filename used as caption), while scores above 0.3 suggest real correspondence between visual and textual content. This distribution informs where to set filtering thresholds during dataset construction.

Finally, we show batching strategies for training. Multimodal batches must handle variable numbers of images per sample and differing image resolutions, requiring custom collation functions.

In[21]:
Code
def collate_multimodal_batch(examples, proc, max_length=128):
    """
    Collate function for multimodal training batches.
    Handles variable image counts and text lengths.
    """
    images = []
    texts = []

    for ex in examples:
        # Each example may have multiple images
        imgs = ex.get("images", [ex.get("image")])
        images.extend(imgs)

        # Create text with image tokens
        text = ex.get("text_sequence", ex.get("caption"))
        # Replace <image> with special tokens or process separately
        texts.append(text)

    # Process all images together
    if images:
        pixel_values = proc(images=images, return_tensors="pt")["pixel_values"]
    else:
        pixel_values = None

    # Process text
    text_inputs = proc(
        text=texts,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=max_length,
    )

    return {
        "pixel_values": pixel_values,
        "input_ids": text_inputs["input_ids"],
        "attention_mask": text_inputs["attention_mask"],
    }
Out[22]:
Console
Batch pixel values shape: torch.Size([2, 3, 224, 224])
Batch input ids shape: torch.Size([2, 9])

The output shapes reveal the data structure that enters the model during training. The pixel values tensor has shape [num_images, channels, height, width], grouping all images from the batch together. The input ids tensor has shape [batch_size, sequence_length], padded to uniform length. Real training pipelines must track which images belong to which batch element, since multiple images from one sample should not be treated as images from different samples.

Now let us examine how CLIP similarity filters affect dataset composition by analyzing what fraction of pairs survive different threshold settings.

In[23]:
Code
def analyze_filtering_thresholds(
    examples, thresholds=[0.1, 0.15, 0.2, 0.25, 0.3, 0.35]
):
    """
    Compute retention rates at different CLIP similarity thresholds.
    """
    # Compute all similarities first
    all_sims = []
    for ex in examples:
        sim = compute_clip_similarity(ex["image"], ex["caption"])
        all_sims.append(sim)

    all_sims = np.array(all_sims)
    results = {}
    for thresh in thresholds:
        retained = np.sum(all_sims >= thresh)
        results[thresh] = {
            "retained": int(retained),
            "total": len(all_sims),
            "fraction": retained / len(all_sims),
            "mean_sim_retained": float(all_sims[all_sims >= thresh].mean())
            if retained > 0
            else 0.0,
        }
    return results, all_sims


# Analyze our sample (may be synthetic images with low CLIP scores)
filter_results, all_similarities = analyze_filtering_thresholds(
    loaded_examples[:10]
)
Out[24]:
Console
Threshold | Retained | Fraction | Mean Sim (retained)
-------------------------------------------------------
  0.10    |    10/ 10  |  1.00    |  0.168
  0.15    |     7/ 10  |  0.70    |  0.179
  0.20    |     0/ 10  |  0.00    |  0.000
  0.25    |     0/ 10  |  0.00    |  0.000
  0.30    |     0/ 10  |  0.00    |  0.000
  0.35    |     0/ 10  |  0.00    |  0.000
Out[25]:
Visualization
model.safetensors: reconstructing file:   0%|          |  0.00B /  605MB            
model.safetensors: downloading bytes:           |  0.00B            
Dual-axis line chart showing retention fraction decreasing and mean similarity increasing as the CLIP threshold rises.
Retention rate and mean similarity of retained pairs as a function of the CLIP filtering threshold. The left axis shows the fraction of data that survives, while the right axis shows the average similarity of retained pairs. The marked 0.25 threshold illustrates a commonly used compromise, but the appropriate operating point must be validated for the target dataset and downstream tasks.

The filtering curve reveals the basic quality-quantity trade-off in multimodal dataset curation. At low thresholds (0.10), nearly all data is retained but quality is low. At high thresholds (0.40), quality is high but most data is discarded. The optimal threshold balances these competing goals, and its location depends on the specific capabilities being optimized. Models trained on larger, noisier datasets sometimes generalize better to rare visual domains, while models trained on smaller, cleaner datasets perform better on benchmarks that reflect the clean distribution.