1. 先搞清楚FID到底在衡量什么
做图像生成、超分辨率、图像修复或者风格迁移的朋友,应该都绕不开一个词:FID,全称是Fréchet Inception Distance。这两年无论在论文里还是实际项目中,FID几乎成了生成图像质量评分的默认标准。这个指标能火起来不是没原因的——它确实比早期的IS(Inception Score)靠谱得多,也比PSNR、SSIM这些传统指标更贴近人眼感知。
先说人话版本:FID这个数值反映的是“你生成的这一批图像”和“真实图像”在特征空间里有多像。数值越小,说明两批图像的特征分布越接近,生成质量越好。它跟PSNR/SSIM这种逐像素比较的指标有本质区别——FID不是拿一张生成图和一张真实图做一对一对比,而是把一整批生成图和一整批真实图分别丢进一个预训练好的Inception网络里,在网络的某一层提取出高维特征,然后去比较这两组特征的统计分布差异。
我最早接触FID是在做GAN相关的项目,当时用的是IS(Inception Score)作为主要评价指标。IS有个明显缺陷:它只衡量生成图像的类别清晰度和多样性,不关心生成分布和真实分布之间的距离。也就是说,就算你生成的图像类别很明确、种类也丰富,但如果整体风格跟真实图像差得远,IS照样可以很高。这在很多场景下会给出误导性的结论。FID正是为了解决这个问题而设计的——它显式地计算生成特征分布与真实特征分布之间的差异,本质上是在回答一个问题:生成的数据分布到底离真实数据分布有多远。
这个特性让FID特别适合用来监控GAN的训练过程、对比不同生成模型的优劣、以及评估超分模型的感知质量。如果你正在做人脸生成、图像编辑、文生图模型评测,或者任何跟“生成图像真实性”强相关的项目,FID基本是你绕不开的一个核心指标。
接下来我会把FID从数学原理、代码实现到实际使用中的坑,全部拆开讲一遍。这篇文章面向的是已经跑通过基本图像生成流程、但想深入理解FID原理和正确使用方式的开发者。
2. Inception网络为什么会被拉来当特征提取器
FID这个名字里带着“Inception”,很多人会误以为这个指标依赖某个神秘的特殊网络结构。其实没那么玄乎——它用的是Google在2015年提出的Inception V3网络,而且用的是在ImageNet上预训练好的权重,不需要重新训练。
为什么非要用Inception V3而不是VGG、ResNet或者别的分类网络?这里有几个原因。
最重要的是历史原因。FID的提出是建立在IS的基础上的。IS最早提出时就用Inception V3作为特征提取器,因为Inception V3在ImageNet分类任务上表现优异,而且它的中间层特征被证明具有很强的语义表征能力。后来FID的作者在构建指标时,自然而然沿用了同一个网络和相同的特征提取层,这样前后两个指标就能在相同条件下做对比。
其次,Inception V3的网络结构有它的独特优势。它在设计上采用了多尺度卷积核并行处理的方式——同一个层里用不同尺寸的卷积核去捕捉不同尺度的视觉信息。这种多尺度特性让Inception V3提取出的特征比VGG那种纯堆叠式网络更丰富,对图像内容的表征更全面。对于衡量生成图像和真实图像之间的分布差异来说,特征越丰富,统计出来的距离越有区分力。
在实际代码里,用到的具体实现一般是这样的逻辑。将一批图像缩放统一到合适尺寸后,送入Inception V3,然后在模型末尾的pool3层(也就是全局平均池化层之前的那一层)提取2048维的特征向量。每个特征向量代表一张图的语义特征。对整批图像提取特征后,计算这些特征向量的均值和协方差矩阵,再用Fréchet距离公式计算两个分布的距离。
这里要提一个容易踩的坑:不同框架、不同版本的FID实现,特征提取的层不太一样,导致同一批图像算出来的FID数值有差异。比如PyTorch官方torchmetrics库的实现和原始的TensorFlow实现,在预处理细节、是否使用fid-specific Inception权重、特征层选择上都有细微差别。这导致论文里报告的FID数值跟你在本地复现的数值经常对不上。这个我在后面的实操部分会详细讲。
3. FID计算公式拆解
FID的数学定义不复杂,但要真正理解它,需要先明白什么是Fréchet距离。
Fréchet距离最早是用来衡量两条曲线之间相似度的几何概念。有一个经典的比喻:一个人牵着一条狗走在曲线上,人和狗各自沿着一条曲线走,他们的速度可以不同但方向必须一致,那么“人和狗之间的最短连线长度的最大值”的最小可能值就是这两条曲线的Fréchet距离。简单理解就是:两条曲线有多“同步”,如果形状越接近,同步走起来越轻松,距离越短。
FID把这个概念推广到了高维空间的概率分布上。我们有两组特征向量,一组是真实图像的2048维特征,一组是生成图像的2048维特征。假设这两组特征都服从多维高斯分布——真实情况可能不是严格高斯,但这个近似在实践中的效果非常好——那么Fréchet距离就有了闭合解。
公式长这样:
FID = ||μ_r - μ_g||² + Tr(C_r + C_g - 2(C_r * C_g)^(1/2))
其中:
- μ_r、μ_g 分别是真实图像特征和生成图像特征的均值向量
- C_r、C_g 分别是两组特征的协方差矩阵
- Tr表示矩阵的迹,也就是对角线元素之和
- (C_r * C_g)^(1/2) 表示矩阵的平方根,这里针对的是两个协方差矩阵乘积的平方根
这个公式有三层含义值得展开讲。
第一项 ||μ_r - μ_g||² 衡量的是均值偏移。它度量两批特征向量的中心点相距多远。对应到视觉上,可以理解为“平均风格”的差异。如果生成的图像整体偏亮、偏红、或者整体构图有固定模式,这一项就会变大。
第二项 Tr(C_r + C_g) 是在衡量两个分布各自的方差。方差代表了特征的多样性程度。如果生成图像的多样性不足——比如生成的全是同一张脸的不同角度——特征的方差就会明显小于真实图像的方差,这一项就会变大。
第三项 -2(C_r * C_g)^(1/2) 是在衡量两个协方差矩阵之间的耦合程度。协方差矩阵刻画了特征维度之间的相关性结构。比如“眼睛大小”和“嘴巴宽度”这两个特征在真实人脸中有特定的相关性模式,如果生成模型打破了这个模式,这个耦合项就会变大。
把这三项综合起来看,FID实际上是从三个维度——均值、方差、相关性结构——来全面衡量两个分布的距离。这比单看均值或者单看方差要严密得多,也正是它为什么能比IS更准确地反映生成质量。
补充一个细节:理论上计算FID要求特征维度远小于样本数量,否则协方差矩阵会不准确。2048维特征要求样本量至少要大于2048,实践中一般建议至少用5000到10000张图像来评估,才能得到稳定的FID值。
4. 从零实现FID计算
理论讲完,直接上代码。我用PyTorch写一个完整的FID计算流程,从图像加载到最终分数输出,全部走通。这个实现的风格偏教学,没有过度优化,但每一步都给出了清晰的解释。
先说环境依赖:
pip install torch torchvision numpy scipy pillow opencv-python核心代码实现:
import torch import torch.nn as nn import numpy as np from scipy import linalg from torchvision import models, transforms from PIL import Image import os class FIDCalculator: def __init__(self, device='cuda', batch_size=64, dims=2048): self.device = device self.batch_size = batch_size self.dims = dims self._load_inception() self._load_transform() def _load_inception(self): # 加载预训练的Inception V3,并截断到pool3层 from torchvision.models import inception_v3 self.inception = inception_v3(weights=models.Inception_V3_Weights.IMAGENET1K_V1) # 去掉全连接层,只保留到特征提取部分 self.inception.fc = nn.Identity() self.inception.to(self.device) self.inception.eval() def _load_transform(self): # Inception V3要求的预处理: # 1. 缩放到299x299 # 2. 归一化到[-1, 1]范围(Inception默认) self.transform = transforms.Compose([ transforms.Resize((299, 299)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def _load_images_from_dir(self, path): images = [] for fname in os.listdir(path): fpath = os.path.join(path, fname) if not os.path.isfile(fpath): continue try: img = Image.open(fpath).convert('RGB') img = self.transform(img) images.append(img) except Exception as e: print(f"跳过文件 {fname}: {e}") if not images: raise ValueError(f"目录 {path} 中没有可读取的图像") return torch.stack(images) def _get_features(self, images): features = [] with torch.no_grad(): for i in range(0, len(images), self.batch_size): batch = images[i:i+self.batch_size].to(self.device) # Inception V3的输入范围是[-1, 1],这里需要注意 features.append(self.inception(batch).cpu().numpy()) return np.concatenate(features, axis=0) def compute_fid(self, real_path, fake_path): # 1. 加载图像 real_images = self._load_images_from_dir(real_path) fake_images = self._load_images_from_dir(fake_path) # 2. 提取特征 real_feats = self._get_features(real_images) fake_feats = self._get_features(fake_images) # 3. 计算均值 mu_real = np.mean(real_feats, axis=0) mu_fake = np.mean(fake_feats, axis=0) # 4. 计算协方差矩阵 sigma_real = np.cov(real_feats, rowvar=False) sigma_fake = np.cov(fake_feats, rowvar=False) # 5. 计算Fréchet距离 diff = mu_real - mu_fake # 均值项 mean_term = np.dot(diff, diff) # 协方差项 covmean, _ = linalg.sqrtm(sigma_real.dot(sigma_fake), disp=False) # 处理数值误差 if not np.isfinite(covmean).all(): print("协方差矩阵平方根存在数值问题,添加对角噪声") offset = np.eye(sigma_real.shape[0]) * 1e-6 covmean, _ = linalg.sqrtm((sigma_real + offset).dot(sigma_fake + offset), disp=False) # 处理复数结果 if np.iscomplexobj(covmean): covmean = covmean.real trace_term = np.trace(sigma_real + sigma_fake - 2 * covmean) fid = mean_term + trace_term return float(fid) if __name__ == '__main__': calculator = FIDCalculator(device='cuda' if torch.cuda.is_available() else 'cpu') fid_score = calculator.compute_fid('path/to/real_images', 'path/to/generated_images') print(f"FID: {fid_score:.4f}")这段代码有几个细节值得重点解释。
第一,Inception V3的输入尺寸是299x299,不是224x224。很多人习惯性用224x224去resize,算出来的特征完全不对。这是因为Inception V3的原始设计是ImageNet的299x299分辨率,它的结构(尤其是前面的卷积层感受野)是针对299x299设计的。
第二,预处理归一化方式。torchvision的Inception V3预训练权重是在ImageNet上训练的,归一化必须用mean=[0.485, 0.456, 0.406]和std=[0.229, 0.224, 0.225]。这一点在torchmetrics等库里会做内部处理,如果自己实现就必须显式写出来。
第三,使用torchmetrics库的替代方案。如果你不想从零实现,直接用现成库也行:
from torchmetrics.image.fid import FrechetInceptionDistance fid_metric = FrechetInceptionDistance(feature=2048) fid_metric.update(real_images, real=True) fid_metric.update(fake_images, real=False) fid_score = fid_metric.compute()这里要注意一个细节:torchmetrics默认要求输入图像的值范围是[0, 255]的uint8格式,或者[0, 1]的float格式,并且需要是RGB三通道。如果你传入的是[-1, 1]范围的归一化图像,会得到错误结果。
5. FID计算中的关键参数与选择逻辑
实战中,有四个参数会直接影响FID的计算结果,需要仔细对待。
5.1 特征维度选择
Torch官方实现默认提取2048维特征。这个维度来自Inception V3的pool3层输出。2048维是一个比较高的维度,能捕捉到图像丰富的语义特征,但相应地,它要求的样本量也更大。如果样本数少于2048,协方差矩阵就会是奇异的,无法可靠计算。
有一个经验规律:样本量至少是特征维度的5到10倍,才能得到统计上比较稳定的结果。也就说要得到可靠的FID,至少需要10000张以上的真实图像和同等数量的生成图像。很多论文里用1000张图像算FID,结果其实抖动很大,算出来的数值不具备严谨的对比意义。
如果你手里的样本量有限,可以考虑在Inception V3更早期的层提取特征。比如在某些实现中,可以选择取Inception V3的192维特征层或者768维特征层,这样在样本量较少时协方差矩阵更容易估计。但要注意:不同层的特征分布差异在感知层面反映的信息不同,改变特征层会导致FID数值的整体偏移,跨论文对比时要特别小心。
5.2 真实图像与生成图像的数量
理想情况下,真实图像和生成图像应该数量相等。但实际项目中两者数量经常不一致。FID对样本量的敏感性体现在协方差矩阵的估计误差上:当两组样本数量相差悬殊时,数量少的那一组的协方差估计方差会很大,直接影响最终得分。
建议是:如果条件允许,两组各取5000到10000张图像。如果实在不够,至少保证两组数量都在3000张以上,并且固定评测时的种子,保证可复现。
5.3 图像尺寸与预处理
Inception V3要求输入是299x299。你的生成图可能是256x256、512x512甚至更高分辨率。在做resize时,建议用高质量插值算法。
PyTorch默认的Resize用的是bilinear插值,对于缩小图像来说效果尚可。但如果你从512缩小到299,信息损失还是比较大的。实测如果用nearest插值,FID数值会明显变差,因为图像内容在缩小过程中丢失了大量细节。建议显式指定PIL的LANCZOS插值:
from torchvision.transforms import InterpolationMode self.transform = transforms.Compose([ transforms.Resize((299, 299), interpolation=InterpolationMode.LANCZOS), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])另外,如果你的生成图像本身带有透明度通道(RGBA),一定要先转成RGB。直接丢给Inception V3会报错或者产生奇怪的输入。
5.4 Inception权重的来源
这里有个容易踩的大坑。原始的FID实现使用的是TensorFlow的Inception V3预训练权重,这个权重和PyTorch官方torchvision里的Inception V3权重虽然都是在ImageNet上训练的,但细节上有差别。一些知名的FID复现仓库(比如pytorch-fid)使用的是自己转换的权重,跟torchvision的版本就不一样。
实际影响是:同一个项目,用不同来源的Inception权重计算FID,数值可以相差0.5到2.0不等。对于追求极致精度的对比实验(比如方法A的FID是10.2,方法B的FID是10.5,你想证明A更好),这个差异已经足够改变结论。
我的建议是:
- 在同一篇文章内的横向对比,统一使用同一个库和同一份权重,保证相对公平
- 跟论文里的数值做绝对对比时,尽量找到该论文使用的FID实现和版本,否则对比没有意义
- 写报告时明确标注FID版本和库的来源,方便他人复现
6. FID与其他常见图像评估指标的横向对比
说到FID,就不得不把PSNR、SSIM、LPIPS这几个常见指标放在一起比较。每个指标的侧重点不同,适用的场景也完全不同。先看一个直观的对比表:
| 指标 | 全称 | 比较粒度 | 是否需要参考图 | 主要衡量内容 | 适用场景 |
|---|---|---|---|---|---|
| PSNR | 峰值信噪比 | 逐像素 | 需要 | 像素级误差 | 图像压缩、去噪 |
| SSIM | 结构相似性 | 局部区域 | 需要 | 亮度/对比度/结构 | 图像质量评估 |
| LPIPS | 感知相似度 | 特征空间 | 需要 | 感知相似性 | 图像修复、超分 |
| FID | Fréchet距离 | 分布级别 | 不需要 | 生成分布与真实分布的距离 | GAN、文生图模型评估 |
这张表的信息量很大,我逐一展开。
PSNR是最老的指标之一,它的逻辑极其朴素:逐像素比较生成图和真实图的均方误差,再换算成对数域的分贝值。PSNR的数值越高代表误差越小。它的优点是非常简单、计算快、结果确定,但缺点也致命:它对图像的全局结构、纹理、边缘等重要感知因素完全不敏感。同一张图稍微平移一个像素,PSNR就会剧烈下降,但人眼根本感知不到这种平移。所以在生成任务中,PSNR已经很少作为主要指标了,最多作为一个参考。
SSIM比PSNR进一步,它把图像分成局部窗口,比较窗口内的亮度、对比度和结构三个维度。SSIM的设计假设是人眼对局部结构的敏感性远高于全局。它的数值范围是-1到1,越接近1代表越相似。SSIM比PSNR人眼友好得多,但它仍然是在像素空间做局部比较,对纹理的感知能力还是有限的。在超分任务中,SSIM可以用来衡量重建图像和真实高分辨率图像的相似度,但它无法回答“重建图像是否足够自然”这个问题。
LPIPS(Learned Perceptual Image Patch Similarity)是近几年的新锐指标。它不直接在像素空间比较,而是把图像送入一个预训练的网络(如AlexNet或VGG),在多个层提取特征,然后计算特征之间的加权距离。LPIPS的设计思路是:如果两张图的特征在深层网络中都激活很相似,那人眼也会觉得它们相似。这个指标的厉害之处在于,它衡量的不是“像素是否相同”,而是“内容是否相同”。在做图像修复、超分辨率任务时,LPIPS能够更好地反映感知质量。但它仍然是一个“一对一”的指标,需要真实参考图,而且背后的特征提取网络同样会影响结果。
FID和前三个有本质区别:它是一个分布级别的指标。它不需要“一一对应”的参考图,而是比较“一组真实图”和“一组生成图”的整体分布。这个特性让它特别适合没有配对真实图、或者关注整体分布而不是单图一致性的生成任务。比如做文生图模型的评估,你这个模型生成的所有图像和真实的自然图像在整体分布上有多接近,这正是FID能回答的问题。
刚才那张表的最后一列给出了建议的使用方式。实践中的最佳实践是多指标互补:做超分任务,PSNR+SSIM+LPIPS三个都报;做GAN评估或文生图评测,FID+IS(如果适用)一起报。单一的指标无法完整评估生成模型的质量,这个观点被越来越多的论文验证。
7. FID使用中的典型坑与排错指南
下面这些坑是我在实际项目里踩过或者见到别人踩过的,整理出来供大家排错参考。
7.1 样本量过小导致FID不稳定
样本量是影响FID可靠性的最大因素。我曾经在一组实验里只用了500张真实图和500张生成图来计算FID,同样的模型跑了两次,数值差了将近3.0。这个误差已经超过了方法之间的真实差距,直接导致那次实验结论可信度很低。
解决办法是至少用5000张以上图像做评估。如果项目本身只有几千张测试数据,可以考虑用数据增强扩充——但要注意增强方式对不同模型的影响可能不均衡,尽量只做翻转和裁剪这类对语义影响小的操作。
7.2 batch size过小导致显存溢出或速度过慢
Inception V3在推理时的显存占用不高,但如果batch size设置的太大,GPU显存仍然可能吃紧。一般建议batch size设置为32或64。对于4GB显存的卡,batch size设32,分辨率299x299,完全没问题。
批量推理时使用no_grad上下文可以节省大量显存和计算时间。上面的代码里已经加了with torch.no_grad(),但如果你改用OnlineFID这种支持增量计算的库,它内部会自动处理。
7.3 混合精度与数值稳定性
默认情况下,Inception V3的权重是float32。如果你用混合精度训练(AMP),但计算FID时忘了切回float32,可能出现数值异常。强烈建议在调用模型推理前显式执行self.inception.float(),并且在代码入口做好数据类型控制。
协方差矩阵的平方根计算(linalg.sqrtm)是数值不稳定的重灾区。当协方差矩阵接近奇异时,sqrtm可能返回复数结果,或者产生NaN。上面的代码里已经做了应对:检测到非有限值就添加对角噪声、检测到复数就取实部。这个处理在scipy版本之间有一定差异,如果你用的是旧版scipy,建议手动升级到1.9以上。
7.4 真实图与生成图的预处理不一致
把真实图和生成图做不同的预处理,是FID计算里最容易“作弊”也最容易“翻车”的点。比如有人生成图像时做了归一化到[0, 1],而真实图还是[0, 255],直接丢给网络,结果会偏差巨大。
一个稳妥的做法是:所有图像统一走同一个transform pipeline,并且在代码里用同一个函数load和preprocess。上面的代码里load_images_from_dir已经保证了这一点。如果你用torchmetrics库,它要求输入必须是[0, 255]的uint8或[0, 1]的float,随便混合会报错,这算是一个强制性的防错机制。
7.5 FID数值的版本一致性
最后再强调一次:FID的绝对数值没有跨实现的可比性。你的代码用的是PyTorch官方Inception权重,论文A用的是pytorch-fid的权重,论文B用的是Clean-FID的校正版本,三者的数值不能直接拿来对比。
Clean-FID这个库值得一提,它校准了Inception网络对图像预处理方式(比如bicubic vs bilinear resize)的敏感性,修改了resize的实现方式,使得FID对预处理方式更鲁棒。论文报告的FID如果用Clean-FID算的,跟你用普通库算出来的结果会有显著差异。在和论文对比时优先看对方代码仓库里用的是哪个库、哪个版本。
我的建议是在自己的项目里固定一套评测脚本,用固定的库版本,只做相对比较。跨项目、跨论文的绝对数值对比要格外谨慎。
8. FID的局限性与未来方向
FID虽然是当前的主流指标,但它绝不是完美的。理解它的局限性,才能在工作中正确使用它。
最大的争议点在于:Inception V3是在ImageNet上训练的,它的特征对“自然图像”比对“其他域图像”更敏感。如果你做的是医学影像生成、卫星图像生成、或者艺术风格图像生成,Inception V3的特征可能无法很好地区分细微的域内差异。这时候FID可能给出一个看起来不错、但实际感知质量很差的分数。
为了解决这个问题,现在有一些域自适应版本的FID,比如在目标域数据上微调特征提取器,或者使用在其他大规模数据集(如CLIP)上训练的模型作为特征提取器。CLIP-based的指标(比如基于CLIP embedding计算Fréchet距离)正在成为一个新方向,它对域的适应能力更强。
另一个局限性是:FID基于高斯假设。虽然这个假设在大多数情况下工作良好,但如果特征分布呈现出明显的多模态或长尾特性,高斯假设就会引入系统误差。这也是为什么有些生成质量差异在FID上反映不明显、但人眼能明显感知到的原因。
还有一点:FID对几何变换(如平移、旋转)比较敏感。如果你生成的图像在构图、位置分布上和真实图有差异,FID会给出较高的分数。这在有些任务中可能是合理的(比如物体位置本身就是评估内容的一部分),但在另一些任务中可能带来误判。
从我个人的使用经验来看,FID是分布级评价的有力工具,但面对具体任务时最好搭配任务特定的指标一起使用。比如做人脸生成,除了FID还看人脸关键点距离和身份保持度;做医学图像生成,还看Dice系数和医生盲评。没有哪个指标能包打天下,FID也不例外。
9. 实操心得:我的FID评测流程总结
根据我过去做过的多个生成项目,我整理了一套相对稳定的FID评测流程,供参考。
第一,固定评测集。提前选好真实的评测图像集合,一般从测试集中随机采样5000到10000张。这个集合在项目期间不变,确保每次实验的FID数值有可比性。生成图像也保持一致数量,用固定的随机种子生成。
第二,统一代码版本。团队内统一FID计算库和版本,最好封装成一个公共函数,避免每个人用的库版本不一致导致报告上的分数无法互相验证。
第三,每训练一定步数自动计算一次FID,并记录到日志中。这样能观察到模型训练过程中FID的变化曲线。我实际观察到的典型情况是:训练刚开始FID很高,随着训练推进逐渐下降,到某个节点后开始波动或上升(过拟合)。这个曲线对决定何时停止训练很有帮助。
第四,对比实验时尽量在同一次运行中完成。比如方法A和方法B在同一个脚本里、相同的随机种子、相同的评测集下计算FID,减少系统误差。
第五,报告FID时明确写出实现细节。包括使用的库(torchmetrics / pytorch-fid / clean-fid)、特征维度、图像数量、Inception权重来源。这个信息对其他人复现和验证非常重要。
说到底,FID是一个工具,不是一个信仰。它在你需要快速判断“生成分布是否逼近真实分布”时极为有效,但它不能替代人工评估。在实际项目中,我会用FID做快速筛选,用人工盲评做最终决策,这样既高效又可靠。