Evaluating generative models
Be able to use FID, human judgement and their pitfalls.
Prerequisites
- EGenerative models — an overviewrequired
- EModel evaluationrequired
Intuition
How do you measure «good images»? There is no answer key per image — only distributions to compare.
| Metric | Measures | Misses |
|---|---|---|
| FID | the distance between the feature statistics of genuine and generated | mode collapse, memorisation, semantic errors |
| Precision/recall (generative) | precision = quality, recall = coverage | requires a choice of feature space |
| CLIP score | how well the image matches the prompt | says nothing about image quality |
| Human pairwise | actual preference | expensive, subjective, has to be blinded |
FID alone is never enough. A model that has memorised the training images gets an excellent FID. A model that has lost half the distribution can also get a decent FID if what it does produce is sharp.
Formal
FID assumes that the feature vectors (pool3 in Inception-v3) are normally distributed and compares the mean and the covariance:
Pitfalls that make published FID numbers hard to compare:
- The sample size: FID is biased downwards with more samples — 10 k and 50 k images give different numbers for the same model.
- Preprocessing: resizing and the interpolation method have a measurable effect.
- The feature space: Inception is trained on ImageNet and fits badly for, say, medical images or faces.
Precision and recall for generative models (Kynkäänniemi et al. 2019) separate the two errors: low precision = ugly or implausible images, low recall = whole modes are missing. That is exactly the split FID lacks.
A memorisation test that should always be run: for every generated image, find the nearest neighbour in the training data (in feature space) and look manually at the hundred closest. A model that reproduces training data is both a quality problem and a copyright problem.
Code
import numpy as np
from scipy import linalg
def fid(feat_real: np.ndarray, feat_gen: np.ndarray) -> float:
mu_r, mu_g = feat_real.mean(0), feat_gen.mean(0)
s_r, s_g = np.cov(feat_real, rowvar=False), np.cov(feat_gen, rowvar=False)
covmean, _ = linalg.sqrtm(s_r @ s_g, disp=False)
if np.iscomplexobj(covmean):
covmean = covmean.real
return float(((mu_r - mu_g) ** 2).sum() + np.trace(s_r + s_g - 2 * covmean))
def nearest_neighbour_check(feat_gen, feat_train, k=1):
"""A memorisation test: how close do the generated images lie to the training data?"""
d = ((feat_gen[:, None, :] - feat_train[None, :, :]) ** 2).sum(-1)
nearest = np.sort(d, axis=1)[:, :k].mean(1)
return {"median_distance": float(np.median(nearest)),
"suspect": int((nearest < np.percentile(nearest, 1)).sum())}
# Always report: FID (with n), precision, recall, the CLIP score for text→image,
# human pairwise on a sample, and the memorisation check.
Mastery means
- Uses FID and knows its limitations
- Complements it with precision/recall and human judgement
- Designs an evaluation that catches mode collapse
Sign in to do the exercises and build your mastery up.
Sources
- arXiv — GANs Trained by a Two Time-Scale Update Rule Converge to a Local Nash Equilibrium (FID) — arXiv (open access; licence per article)
- arXiv — Improved Precision and Recall Metric for Assessing Generative Models — arXiv (open access; licence per article)