Multimodal Evaluation › Code Implementation: Loading and Evaluating Multimodal Data

Michael BrenndoerferFebruary 14, 202610 min read
Language AI HandbookAll chapters
Part 50 of 67Multimodal Models
Chapter 388Multimodal Evaluation › Code Implementation: Loading and Evaluating Multimodal Data
Reading section 6 of 10 · Code Implementation: Loading and Evaluating Multimodal Data · 10 minSection 6 of 10 · 10 min

Book contents

Section 6 of 10 · 10 min read

Multimodal Evaluation › Code Implementation: Loading and Evaluating Multimodal DataOne section · Sections in this chapter6 / 10 ↓
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's implement a pipeline to evaluate multimodal predictions using standard libraries. We'll load VQA-style data, compute metrics, and visualize results that reveal capability gaps.

Simulating VQA Data

Since VQA datasets require large image files, we'll simulate a subset with the expected structure. The simulated data covers the three main VQA question types: yes/no, number, or other (open-ended). This structure mirrors the real VQA v2 annotation format exactly.

In[13]:
Code
# Simulate VQA v2 style annotations
vqa_data = [
    {
        "question_id": 1,
        "question": "What color is the fire hydrant?",
        "image_id": 101,
        "answers": [
            {"answer": "red", "count": 7},
            {"answer": "crimson", "count": 1},
            {"answer": "scarlet", "count": 1},
            {"answer": "orange-red", "count": 1},
        ],
        "answer_type": "other",
        "question_type": "what color",
    },
    {
        "question_id": 2,
        "question": "How many dogs are in the picture?",
        "image_id": 102,
        "answers": [{"answer": "2", "count": 8}, {"answer": "two", "count": 2}],
        "answer_type": "number",
        "question_type": "how many",
    },
    {
        "question_id": 3,
        "question": "Is this a stop sign?",
        "image_id": 103,
        "answers": [{"answer": "yes", "count": 10}],
        "answer_type": "yes/no",
        "question_type": "is this",
    },
]

# Simulate model predictions - intentionally wrong on counting and yes/no
model_predictions = {
    1: "red",  # Correct and consensus
    2: "3",  # Wrong number (model overcounts)
    3: "no",  # Wrong boolean (model fails to recognize stop sign)
}

Computing VQA Metrics

Now let's implement the standard VQA evaluation metric. The implementation reconstructs the full answer list with repetitions (matching the original VQA format) and applies the consensus scoring formula.

In[14]:
Code
from collections import defaultdict
from typing import Dict, List


def compute_vqa_accuracy(
    question_data: List[Dict], predictions: Dict[int, str]
) -> Dict[str, float]:
    """
    Compute VQA accuracy with consensus scoring.

    Args:
        question_data: List of question annotations with answer distributions
        predictions: Dict mapping question_id to predicted answer

    Returns:
        Dict with overall accuracy and breakdown by question type
    """
    scores_by_type = defaultdict(list)
    all_scores = []

    for item in question_data:
        qid = item["question_id"]
        if qid not in predictions:
            continue

        pred = predictions[qid].lower().strip()

        # Build answer list with multiplicity
        answer_list = []
        for ans_dict in item["answers"]:
            answer_list.extend([ans_dict["answer"].lower()] * ans_dict["count"])

        # Count matches
        matching = sum(1 for ans in answer_list if ans == pred)

        # VQA accuracy formula: min(matching / 3, 1)
        score = min(matching / 3.0, 1.0)

        all_scores.append(score)
        scores_by_type[item["answer_type"]].append(score)

    # Calculate statistics
    results = {
        "overall_accuracy": np.mean(all_scores) if all_scores else 0.0,
        "total_questions": len(all_scores),
    }

    for ans_type, scores in scores_by_type.items():
        results[f"{ans_type}_accuracy"] = np.mean(scores)
        results[f"{ans_type}_count"] = len(scores)

    return results
In[15]:
Code
results = compute_vqa_accuracy(vqa_data, model_predictions)
Out[16]:
Console
VQA Evaluation Results
========================================
Overall Accuracy: 33.33%
Total Questions: 3

Breakdown by Answer Type:
  Yes/No:    0.00% (1 questions)
  Number:    0.00% (1 questions)
  Other:     100.00% (1 questions)
Out[17]:
Visualization
Bar chart of VQA accuracy for Yes/No, Number, and Other categories showing 0% for the first two and 100% for Other, with dashed overall accuracy line at 33%.
Accuracy breakdown by VQA answer type from the simulated evaluation. The model performs perfectly on descriptive 'other' questions (color identification) but completely fails on numerical counting and binary yes/no questions. The dashed line shows overall accuracy at 33%, showing how aggregate scores can mask complete failure on specific question categories.

The results reveal that our simulated model struggles with number recognition and yes/no questions while correctly identifying colors. In practice, we would analyze error patterns to identify whether failures stem from visual recognition, question understanding, or answer generation. The error on question 2 likely indicates that the model miscounted due to overlapping or occluded dogs in the image, while the error on question 3 might suggest the model failed to recognize the stop sign's shape and distinctive red color, or misunderstood the question polarity.

This per-category breakdown is precisely the kind of diagnostic information that makes VQA evaluation useful beyond a single number. A model achieving 33% overall accuracy on this mini-dataset looks much worse than one achieving 60% overall, but the per-category analysis reveals that both might have identical performance on color questions while differing on counting tasks. Without this breakdown, we would have no guidance on where to improve.

Evaluating Image Captioning with Multiple Metrics

Let's implement caption evaluation using n-gram based metrics. While libraries like pycocoevalcap give standard implementations, understanding the mechanics helps interpret scores and diagnose failure modes.

In[18]:
Code
import math


def get_ngrams(tokens: List[str], n: int) -> Counter:
    """Extract n-grams from token list."""
    ngrams = []
    for i in range(len(tokens) - n + 1):
        ngram = tuple(tokens[i : i + n])
        ngrams.append(ngram)
    return Counter(ngrams)


def compute_bleu(
    candidate: str, references: List[str], max_n: int = 4
) -> float:
    """
    Simplified BLEU score calculation for demonstration.
    (Full implementation would include brevity penalty)
    """
    candidate_tokens = candidate.lower().split()

    scores = []
    for n in range(1, max_n + 1):
        candidate_ngrams = get_ngrams(candidate_tokens, n)

        # Count clipped n-grams (max count in any reference)
        clipped_counts = Counter()
        for ref in references:
            ref_tokens = ref.lower().split()
            ref_ngrams = get_ngrams(ref_tokens, n)
            for ngram in candidate_ngrams:
                if ngram in ref_ngrams:
                    clipped_counts[ngram] = max(
                        clipped_counts[ngram], ref_ngrams[ngram]
                    )

        # Calculate precision
        total = sum(candidate_ngrams.values())
        matching = sum(
            min(candidate_ngrams[g], clipped_counts[g])
            for g in candidate_ngrams
        )

        if total > 0:
            scores.append(matching / total)
        else:
            scores.append(0.0)

    # Geometric mean of n-gram precisions
    if all(s > 0 for s in scores):
        return math.exp(sum(math.log(s) for s in scores) / len(scores))
    return 0.0
In[19]:
Code
# Example caption evaluation
candidate_caption = "a dog is playing with a ball in the park"
reference_captions = [
    "a dog plays with a ball in the park",
    "a puppy is playing with a toy in a grassy field",
    "dog running with ball in park",
]

bleu_score = compute_bleu(candidate_caption, reference_captions)
Out[20]:
Console
Image Captioning Evaluation
========================================
Candidate: 'a dog is playing with a ball in the park'

References:
  1. a dog plays with a ball in the park
  2. a puppy is playing with a toy in a grassy field
  3. dog running with ball in park

BLEU Score: 0.786

The BLEU score indicates moderate overlap with references. Notice that the candidate is semantically correct despite word differences ("playing" vs. "plays," "puppy" vs. "dog"). This illustrates why modern evaluation increasingly relies on semantic metrics like CLIPScore or BERTScore rather than n-gram overlap alone. BERTScore, which uses contextual embeddings from BERT to compute semantic similarity between candidate and reference tokens, would assign higher scores to semantically equivalent paraphrases. CLIPScore would evaluate alignment directly against the image without needing references at all. In practice, a research paper evaluating an image captioning model would typically report BLEU-4; METEOR; CIDEr; and either SPICE or CLIPScore, capturing both lexical and semantic dimensions of caption quality.

Computing CLIPScore for Text-Image Alignment

CLIPScore evaluates whether a caption describes an image without requiring reference captions. Let's simulate this workflow to understand the mechanics. In production, you would use the transformers library to load an actual CLIP model and compute real embeddings.

In[21]:
Code
import numpy as np


# Simulated CLIP embeddings (in practice, use transformers CLIPModel)
# These stand for normalized embeddings in a shared space
def simulate_clip_score(
    text_embedding: np.ndarray, image_embedding: np.ndarray
) -> float:
    """
    Compute cosine similarity between text and image embeddings.
    In practice, these come from CLIP's vision and text encoders.
    """
    # Cosine similarity of normalized vectors
    similarity = np.dot(text_embedding, image_embedding)
    # Scale to 0-100 range commonly used in papers
    return float((similarity + 1) / 2 * 100)


# Simulate embeddings for "a red fire hydrant"
# High alignment scenario
text_emb_aligned = np.array([0.8, 0.3, 0.5])  # Concept: red, fire, hydrant
text_emb_aligned = text_emb_aligned / np.linalg.norm(text_emb_aligned)

image_emb = np.array([0.75, 0.35, 0.55])  # Image of red fire hydrant
image_emb = image_emb / np.linalg.norm(image_emb)

# Misaligned text: "a blue car"
text_emb_misaligned = np.array([0.2, 0.1, 0.9])  # Concept: blue, car
text_emb_misaligned = text_emb_misaligned / np.linalg.norm(text_emb_misaligned)
In[22]:
Code
score_aligned = simulate_clip_score(text_emb_aligned, image_emb)
score_misaligned = simulate_clip_score(text_emb_misaligned, image_emb)
Out[23]:
Console
CLIPScore Evaluation (Simulated)
========================================
Image: Fire hydrant (red)

Prompt 1: 'a red fire hydrant'
  CLIPScore: 99.810/100 (Well aligned)

Prompt 2: 'a blue car'
  CLIPScore: 86.894/100 (Poorly aligned)
Out[24]:
Visualization
Unit circle with three vectors showing image embedding, aligned text, and misaligned text as arrows, with similarity scores annotated.
CLIP embedding space visualization showing text-image alignment via cosine similarity. The image embedding (blue) and aligned text embedding for ''red hydrant'' (teal) point in nearly the same direction, yielding high cosine similarity. The misaligned ''blue car'' text embedding (red) points in a substantially different direction, yielding low similarity. This geometric interpretation makes CLIPScore intuitive: well-aligned pairs cluster together in the shared space.

The CLIPScore successfully distinguishes between semantically aligned and misaligned text-image pairs. The geometric interpretation is intuitive: aligned pairs cluster together in the shared embedding space, while misaligned pairs point in divergent directions. In practice, researchers use CLIPScore for rapid iteration during model development, where the reference-free property lets evaluation on any new image without collecting human annotations. Final validation for publication still relies on human evaluation studies, which capture quality dimensions that CLIPScore cannot, including compositional accuracy, cultural context, and aesthetic judgment.