Evaluating Vision Models

FID and generative image metrics

FID compares two piles of images as a whole rather than one by one, which is the only sensible thing to do, and it misleads in several specific ways worth knowing.

Read these first

On this page 9
  1. The short answer
  2. The analogy you have lived
  3. Why one-by-one comparison is impossible
  4. What FID actually looks at
  5. The thing nobody tells beginners
  6. Where you have seen this
  7. The honest part
  8. Remember this
  9. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

The short answer

FID compares a pile of generated images against a pile of real ones, as two collections, not picture by picture.

The analogy you have lived

Two sweet shops each send you a tray of a hundred laddoos. You have to say which shop is closer to the real thing.

You cannot line them up one against one. Laddoo number seven from one tray has no partner in the other. The comparison is between the trays.

So you look at the trays as a whole. Are the sizes about the same? Is there the same spread of colours? Does one tray have five varieties while the other has only one, repeated a hundred times?

That last shop might make an excellent laddoo. Its tray is still wrong, because a real tray has variety in it. FID catches that. Judging one laddoo at a time never would.

Why one-by-one comparison is impossible

A generator invents pictures that were never taken. There is no correct answer to compare against.

Ask a model for a photo of a street. It gives you a street that does not exist. Which real street should it be scored against? None of them. The question has no answer.

So the whole field gave up on scoring one image and started scoring collections. Every serious generative metric works this way.

What FID actually looks at

Comparing raw pixels would be useless. Two photos of the same street from a step apart have almost no pixels in common.

So the images go through a trained network first, the same kind used for image classification. It turns each picture into a list of numbers describing what is in it, rather than where each pixel sits.

Then FID compares the two clouds of numbers.

   real photos    →  network  →  a cloud of points
                                      \
                                       how far apart are
                                       these two clouds?
                                      /
   generated ones →  network  →  a cloud of points

It measures two things about each cloud: where its centre sits, and how spread out it is. A generator making only one kind of picture has a tiny cloud, and the distance is large even if the centre is right.

Lower FID is better. Zero means the two clouds are indistinguishable. That is the opposite convention from most metrics on this site, and it catches people out constantly.

The thing nobody tells beginners

FID is not comparable across papers unless everything matches.

The score depends on how many images you used. It depends on how they were resized. It depends on whether they were saved as JPEG and at what quality. It depends on which version of which network produced the numbers.

Change any of these and the score moves, sometimes by more than the difference between two competing models. A FID of 12 from one codebase and 15 from another tell you nothing about which model is better.

Compare only against numbers you produced yourself, with your own pipeline, on the same day.

Where you have seen this

  • The image generators inside phone photo editors.
  • Tools that expand a photo beyond its original edges.
  • Video game texture generation.
  • Systems that create training images when real ones are scarce.

Every one was tuned against a score like this. Every one also had humans check the output before shipping.

The honest part

FID does not measure whether the picture is good. It measures whether the collection has the right statistics.

A generator producing beautiful, coherent images of the wrong subject can score well. A generator producing exactly one perfect image, copied a hundred times, scores badly — correctly. And a generator that memorised its training set scores brilliantly while creating nothing at all.

Nobody has solved this. FID is the best available convenience, not a judge.

Remember this

  • Generated images are scored as a collection, because no single correct answer exists.
  • Lower is better, and zero means indistinguishable.
  • The score is only comparable within one pipeline, one image count, one resizing rule.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install numpy scipy

The Fréchet distance is a short piece of linear algebra. Seeing it work on features you control, before adding a 90 MB Inception network, is the fastest way to understand what it can and cannot see.

The distance, plus the two failures it catches

fid_from_scratch.py
import numpy as np
from scipy import linalg

rng = np.random.default_rng(0)      # fixed seed, so these numbers repeat exactly
D = 64                              # real FID uses 2048-d Inception features

CENTRES = rng.normal(size=(4, D)) * 3       # four "kinds" of picture in the data


def sample(centres, n, spread=1.0):
    which = rng.integers(0, len(centres), size=n)
    return centres[which] + rng.normal(size=(n, D)) * spread


def stats(x):
    return x.mean(0), np.cov(x, rowvar=False)


def frechet(mu1, s1, mu2, s2, eps=1e-6):
    diff = mu1 - mu2
    covmean, _ = linalg.sqrtm(s1.dot(s2), disp=False)
    if not np.isfinite(covmean).all():                  # numerical rescue
        covmean = linalg.sqrtm((s1 + np.eye(len(s1)) * eps).dot(
                               s2 + np.eye(len(s2)) * eps))
    covmean = covmean.real
    return float(diff.dot(diff) + np.trace(s1) + np.trace(s2) - 2 * np.trace(covmean))


def fid(a, b):
    return frechet(*stats(a), *stats(b))


N = 5000
real = sample(CENTRES, N)

sets = {
    "another sample of the real thing": sample(CENTRES, N),
    "good generator (all 4 modes)":     sample(CENTRES, N),
    "mode collapse (only 2 of 4)":      sample(CENTRES[:2], N),
    "blurry (right modes, too spread)": sample(CENTRES, N, spread=1.6),
    "over-sharp (too tight)":           sample(CENTRES, N, spread=0.5),
}

print(f"FID against {N} real samples ({D}-dimensional features)")
for name, x in sets.items():
    print(f"  {name:<34} {fid(real, x):8.3f}")

print("\nFID between two samples of the SAME distribution, by sample size:")
for n in (100, 500, 1000, 5000, 20000):
    a, b = sample(CENTRES, n), sample(CENTRES, n)
    print(f"  n = {n:>6}   FID = {fid(a, b):8.3f}")


def kid(a, b, subsets=50, subset_size=500):
    """Kernel Inception Distance: an unbiased MMD with a polynomial kernel."""
    d = a.shape[1]
    k = lambda x, y: (x.dot(y.T) / d + 1) ** 3
    out = []
    for _ in range(subsets):
        x = a[rng.choice(len(a), subset_size, replace=False)]
        y = b[rng.choice(len(b), subset_size, replace=False)]
        m = subset_size
        kxx, kyy, kxy = k(x, x), k(y, y), k(x, y)
        np.fill_diagonal(kxx, 0)
        np.fill_diagonal(kyy, 0)
        out.append(kxx.sum() / (m * (m - 1)) + kyy.sum() / (m * (m - 1))
                   - 2 * kxy.mean())
    out = np.array(out)
    return float(out.mean()), float(out.std(ddof=1) / np.sqrt(len(out)))


print("\nKID: 0 means indistinguishable, and it is allowed to go negative.")
print("Shown as mean and standard error over 50 random subsets of 500:")
for name, x in sets.items():
    m, se = kid(real, x)
    print(f"  {name:<34} {m:10.4f}  se {se:.4f}")
Output
FID against 5000 real samples (64-dimensional features)
  another sample of the real thing      0.638
  good generator (all 4 modes)          0.483
  mode collapse (only 2 of 4)         436.159
  blurry (right modes, too spread)     22.433
  over-sharp (too tight)               15.723

FID between two samples of the SAME distribution, by sample size:
  n =    100   FID =   54.869
  n =    500   FID =    5.891
  n =   1000   FID =    3.895
  n =   5000   FID =    0.750
  n =  20000   FID =    0.259

KID: 0 means indistinguishable, and it is allowed to go negative.
Shown as mean and standard error over 50 random subsets of 500:
  another sample of the real thing       0.4556  se 0.4057
  good generator (all 4 modes)          -0.0807  se 0.4118
  mode collapse (only 2 of 4)          278.8716  se 4.3637
  blurry (right modes, too spread)       0.3238  se 0.4697
  over-sharp (too tight)                -0.1037  se 0.4174

The three things that output proves

Mode collapse is caught, loudly. Dropping two of the four modes scores 436 against a floor of about 0.5. Every individual sample in that set came from a perfectly good mode. The tray is wrong because it lacks variety, and no per-image score would have noticed.

Being too tight is penalised as much as being too blurry. The blurry set scores 22.4 and the over-sharp set 15.7. A generator that produces cleaner, more confident images than reality is not doing better. FID compares the spread of the two clouds, in both directions. This is the mathematical reason "sharpen the output" does not improve FID.

The sample-size table is the most important thing on this page. Both sets are drawn from the identical distribution. The true answer is zero at every row. FID reports 54.9 at n=100 and 0.26 at n=20000.

FID is a biased estimator. With too few samples it reports a large distance where there is none, and the bias shrinks as you add samples. So a FID of 30 computed on 1,000 images and a FID of 12 computed on 50,000 images cannot be compared. This is why the field settled on 50,000 generated samples as a convention, and why any paper reporting FID without the sample count has told you nothing.

KID fixes exactly that. Its estimator is unbiased, so it is allowed to go negative and it sits near zero for genuinely matched distributions. Look at the standard errors: 0.456 and -0.081 both have an error bar of about 0.41, so they are indistinguishable from each other and from zero, which is the correct conclusion. Mode collapse at 278.87 with an error of 4.36 is unambiguous. Reporting an error bar costs one line and prevents most bad conclusions.

Doing it for real

bash
pip install clean-fid
python
from cleanfid import fid
score = fid.compute_fid("path/to/real", "path/to/generated")

No output block. The number depends entirely on your images and your machine, and inventing one would teach you to expect something that will not happen.

Use clean-fid rather than rolling your own. Parmar, Zhang and Zhu (CVPR 2022) showed that the standard implementations resize images without a low-pass filter, which introduces aliasing, and that the JPEG quality you save your samples at measurably shifts the score. Their library fixes both. Two implementations of "FID" disagreeing by several points, on identical images, is the normal state of affairs.

Practical rules that make your numbers mean something:

  • Fix the sample count and print it next to every score. 50,000 is conventional.
  • Save generated images as PNG, or hold them in memory. Never JPEG.
  • Compute the real-side statistics once and cache them.
  • Report the FID between two disjoint halves of your real data as a floor. Anything at or below that floor is noise.

Common mistakes

Treating FID as a per-image score. It is defined on distributions. There is no such thing as the FID of one picture, and code that appears to compute one is computing something else.

Comparing FID across resolutions or datasets. A 256-pixel model and a 512-pixel model produce incomparable numbers. So do FFHQ and ImageNet.

Using Inception Score. It uses an ImageNet classifier's confidence and diversity over its 1,000 classes as a proxy for quality. It is insensitive to mode collapse within a class, it ignores the real data entirely, and it is meaningless on any domain that is not ImageNet-like. It survives in papers for historical comparison only.

Optimising FID directly. Since it is a differentiable function of features, you can push it down without improving anything a human would notice. Treat a large FID improvement with no visible change as a bug report.

Forgetting that memorisation scores perfectly. A model that reproduces training images has a near-zero FID and has generated nothing. Check for near-duplicates against the training set before believing any strong result.

Try it yourself

Add a sixth set: the real samples with a small constant added to every feature, a shift rather than a change in spread. Predict which term of the distance moves before running it. Then rerun the whole script with D = 2048 to match Inception's feature width and watch how much more data the covariance estimate needs.

What to learn next

Researcher — Mathematics and papers.

Fréchet Inception Distance

Heusel et al. (2017) model both feature sets as multivariate Gaussians and take the Fréchet (Wasserstein-2) distance between them:

$$ \mathrm{FID} = \lVert \mu_r - \mu_g \rVert_2^2 + \operatorname{Tr}!\left(\Sigma_r + \Sigma_g - 2\left(\Sigma_r \Sigma_g\right)^{1/2}\right) $$

$\mu_r, \Sigma_r$ are the mean and covariance of features from real images, $\mu_g, \Sigma_g$ the same for generated ones. Features are the 2048-dimensional pool3 activations of an Inception-V3 network trained on ImageNet. $(\Sigma_r\Sigma_g)^{1/2}$ is the matrix square root, which is why scipy.linalg.sqrtm appears in every implementation and why numerical rescue terms are standard.

The Gaussian assumption is false. Deep features are neither Gaussian nor unimodal. The estimator is used anyway because the closed form makes it cheap, and because it correlates with human judgement well enough to be useful.

Bias, and why the sample count is part of the metric

The plug-in estimator of $\Sigma$ from $n$ samples in $d$ dimensions is biased, and the trace term inherits it. Chong and Forsyth (2020), Effectively Unbiased FID and Inception Score, show that $\widehat{\mathrm{FID}}n$ decreases roughly as $1/n$ and propose $\mathrm{FID}\infty$, obtained by computing FID at several sample sizes and extrapolating the linear fit in $1/n$ to zero. The runnable table above reproduces this decay on synthetic data where the true value is exactly zero.

Practical consequence: with $d = 2048$ and $n = 50{,}000$, the covariance estimate uses about 24 samples per dimension. It is a noisy estimate, and the residual bias is not negligible, only conventionalised.

Kernel Inception Distance

Bińkowski et al. (2018) replace the Gaussian assumption with a kernel two-sample test. For the polynomial kernel $k(x,y) = \left(\frac{1}{d}x^\top y + 1\right)^3$, the unbiased MMD estimator is

$$ \widehat{\mathrm{MMD}}^2 = \frac{1}{m(m-1)}\sum_{i \ne j} k(x_i, x_j) + \frac{1}{m(m-1)}\sum_{i \ne j} k(y_i, y_j) - \frac{2}{m^2}\sum_{i,j} k(x_i, y_j) $$

Excluding the diagonal is what makes it unbiased, and is why it can be negative. KID is therefore usable at small $n$, comes with a standard error for free, and is the right choice when you have a few thousand samples rather than fifty thousand.

Implementation-induced variance

Parmar et al. (2022), On Aliased Resizing and Surprising Subtleties in GAN Evaluation, document two mechanisms that shift FID independently of image quality:

  • Aliased downsampling. Bilinear and bicubic resizing in TensorFlow and PyTorch do not adapt the filter support to the scale factor, so downsampling aliases. Different libraries alias differently, and the resulting FID gap can exceed the gap between competing models.
  • JPEG quantisation. Generators cannot reproduce JPEG artefacts. If the reference set was stored as JPEG, saving samples as JPEG at a tuned quality lowers FID without improving anything.

They also note that the Inception weights themselves differ between the original TensorFlow checkpoint and the common PyTorch port, giving a third source of disagreement. clean-fid pins all three.

What FID does not separate

FID collapses fidelity and diversity into one scalar. Two orthogonal decompositions exist:

Precision and recall for distributions (Sajjadi et al., 2018; refined by Kynkäänniemi et al., 2019). Precision is the fraction of generated samples falling inside the support of the real manifold, estimated with $k$-nearest-neighbour hyperspheres; recall is the fraction of real samples inside the generated manifold's support. A mode-collapsed model has high precision and low recall; an over-smoothed model the reverse. FID cannot tell them apart.

Density and coverage (Naeem et al., 2020) fix the sensitivity of the above to outliers in the $k$-NN radius estimate, and are the better default.

Domain and perceptual alternatives

  • CLIP-FID substitutes CLIP image features for Inception features. Better behaved on non-ImageNet domains, and less sensitive to the resizing pathology.
  • CLIPScore (Hessel et al., 2021) measures text-image agreement for conditional generation, which FID does not test at all. A text-to-image model can have excellent FID while ignoring the prompt.
  • LPIPS (Zhang et al., 2018) is a paired perceptual distance. It is the right tool for reconstruction, super-resolution and inpainting, where a reference image exists, and the wrong tool for unconditional generation, where none does.
  • HYPE (Zhou et al., 2019) and the various two-alternative-forced-choice protocols remain the ground truth for perceptual quality. Every automated metric is a proxy for one of these.

Memorisation

Near-zero FID is achieved by a model that reproduces its training set. Somepalli et al. (2023) and Carlini et al. (2023) demonstrate verbatim training-image extraction from diffusion models, particularly for images duplicated in the training data. Any generative evaluation should be accompanied by a nearest-neighbour check in feature space against the training set, reported as the fraction of samples within a similarity threshold of a training image.

Papers

What to learn next