Multimodal Evaluation › Code Implementation: Loading and Evaluating Multimodal Data
Language AI HandbookAll chapters 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.
# 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.
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 resultsresults = compute_vqa_accuracy(vqa_data, model_predictions)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)

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.
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# 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)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.
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)score_aligned = simulate_clip_score(text_emb_aligned, image_emb)
score_misaligned = simulate_clip_score(text_emb_misaligned, image_emb)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)

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.