
做图像生成、超分辨率、图像修复或者风格迁移这类项目的朋友应该都有一个共同的执念到底怎么量化“生成得好不好”早年我用PSNR和SSIM后来发现这两个指标在GAN和扩散模型面前经常失灵。直到开始认真用FIDFréchet Inception Distance评估生成模型我才算找到一个相对靠谱的衡量手段。FID的核心思路很直接把真实图片集和生成图片集分别送进Inception-v3网络提取高层特征再把两组特征看成两个高斯分布计算它们之间的Fréchet距离。分数越低说明两个分布越接近也就是生成图片在“整体特征分布”上越像真实图片。它同时兼顾了真实性生成图片是否像真的和多样性生成图片是否千篇一律所以很快成了GAN、扩散模型、图像编辑等领域的事实标准。如果你也在跑生成模型或者正在对比不同算法的效果这篇内容值得看完。1. FID到底在衡量什么先理解它的设计哲学1.1 从“像素差异”到“分布差异”很多人第一次接触FID时都会困惑为什么不直接对比像素原因很简单生成任务不是一个“一对一”的问题而是一个“一对多”的问题。比如一张真实的人像照片你让模型生成一张“看起来差不多”的图合法结果可能有无数种——表情可以不同、背景可以微调、光照可以变化但在像素层面可能差得非常大。PSNR这类指标是按像素算MSE的稍微有一点位移或光照变化分数就会暴跌可是人眼看过去完全没问题。FID换了一个思路我们不要求生成图和某张真实图在像素上一一对应而是看“真实图片整体”和“生成图片整体”的统计特征是否接近。打个比方PSNR像是核对身份证照片必须五官位置、光线都对齐FID像是比较两个班级的考试成绩分布不关心某个学生对某个学生只看整体均值和方差是否相似。这个思路天然适配生成模型因为生成模型的目标本来就不是复刻某张图而是学到目标数据分布。1.2 Inception网络为什么被选中FID里的“I”就是Inception具体来说是Inception-v3网络。为什么偏偏选它因为它是在ImageNet上训练出来的图像分类模型网络前面的层学到的是边缘、纹理、颜色这些底层视觉特征越往后的层越接近语义特征。FID取的是Inception-v3最后一个池化层之前的特征也就是去掉分类层之后得到的2048维向量。这个特征空间里相似的语义内容会聚集在一起而光照变化、小范围平移、轻微旋转的影响会被大幅削弱。用一个更直白的说法Inception网络就像一个“有经验的老师”它把每张图浓缩成2048个维度的“评语”。FID要做的就是比较真实图集的“评语分布”和生成图集的“评语分布”差多少。这里还有一个隐藏假设这些高维特征近似服从高斯分布。为什么可以这么假设因为特征向量维度高受中心极限定理影响很多实际分布在高维空间里确实能较好地用均值和协方差来刻画。高斯分布只需要两个参数——均值和协方差矩阵计算上非常方便。虽然现实中特征分布未必是严格高斯但实验证明用这个近似已经足够管用。2. FID的计算全流程公式、代码与坑2.1 Fréchet距离的直观理解与公式拆解FID的数学定义来自Fréchet距离也叫Wasserstein-2距离的一种形式。对于两个多元高斯分布它有闭式解FID ||μ_r - μ_g||^2 Tr(C_r C_g - 2(C_r * C_g)^(1/2))其中μ_r和C_r是真实图片特征的均值向量和协方差矩阵μ_g和C_g是生成图片特征的均值向量和协方差矩阵。公式可以拆成两部分理解左边第一项是两个分布中心点的欧氏距离它衡量“平均特征”差多少右边第二项是协方差矩阵的差异它衡量两个分布在特征空间里的“延展形态”差多少。这两项加起来既惩罚了生成图片整体偏移也惩罚了生成图片多样性不足或过度分散。矩阵的平方根(C_r * C_g)^(1/2)是计算里最容易出问题的地方。它不是元素开根号而是矩阵的平方根需要用scipy.linalg.sqrtm来计算。由于是数值计算经常会出现微小的复数虚部代码里一般要取实部来消除数值误差。2.2 完整计算流程PyTorch实战我把自己常用的FID计算流程整理一下你照着跑就能得到可复现的结果。这里假设你已经有了真实图片路径和生成图片路径生成图片可以是你模型刚输出的一批结果。第一步准备特征提取函数。用PyTorch加载预训练的Inception-v3去掉最后的全连接层把输出改成平均池化后的2048维特征import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image import numpy as np from scipy import linalg import os class InceptionV3Feature(nn.Module): def __init__(self): super().__init__() self.model models.inception_v3(weightsmodels.Inception_V3_Weights.IMAGENET1K_V1) # 去掉最后的全连接层只保留到平均池化层为止 self.model.fc nn.Identity() self.model.aux_logits False # 关闭辅助分支避免影响输出 def forward(self, x): # x: [N, 3, 299, 299] 且像素归一化到 [0, 1] x self.model(x) return x # [N, 2048]这里有两个关键点。一是输入尺寸必须是299x299Inception-v3的原始设计要求就是这个尺寸很多人习惯性用224特征虽然也能出来但和标准FID用法不一致指标会偏离。二是输入归一化要用ImageNet的标准方式normalize到[0,1]后再按mean[0.485, 0.456, 0.406]和std[0.229, 0.224, 0.225]做标准化这个细节直接影响特征质量。第二步批量提取特征。图片数量多的时候不要一张张前向显存够就尽量用大batch速度差好几倍def extract_features(image_dir, model, batch_size64, devicecuda): model.to(device).eval() 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]), ]) features [] image_paths [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.lower().endswith((.png, .jpg, .jpeg))] for i in range(0, len(image_paths), batch_size): batch_paths image_paths[i:i batch_size] batch [] for p in batch_paths: img Image.open(p).convert(RGB) batch.append(transform(img)) batch_tensor torch.stack(batch).to(device) with torch.no_grad(): feat model(batch_tensor).cpu().numpy() features.append(feat) return np.concatenate(features, axis0)第三步计算FID。这里有个容易踩的坑np.cov默认把每一列当成一个变量而我们的特征是“每行一个样本”所以必须指定rowvarFalsedef calculate_fid_from_features(real_feats, fake_feats): mu_real real_feats.mean(axis0) mu_fake fake_feats.mean(axis0) sigma_real np.cov(real_feats, rowvarFalse) sigma_fake np.cov(fake_feats, rowvarFalse) diff mu_real - mu_fake covmean, _ linalg.sqrtm(sigma_real.dot(sigma_fake), dispFalse) if np.iscomplexobj(covmean): covmean covmean.real fid diff.dot(diff) np.trace(sigma_real sigma_fake - 2.0 * covmean) return fid我自己第一次写的时候忘了关Inception-v3的aux_logits结果输出维度莫名其妙多了一个分支特征维度都对不上。这里提醒所有用PyTorch的人inception_v3默认在训练模式下会返回辅助输出推理前务必设置model.aux_logits False否则你会被折磨很久。2.3 数值稳定性与实操注意事项FID的计算不只是“跑通”就行真正要得到可信的数字有几个细节必须注意。样本量是第一大问题。官方FID基准通常用5万张真实图和5万张生成图因为协方差矩阵要估计2048x2048的参数样本太少会导致估计方差极大。实际项目中如果只是对比同一个模型的不同checkpoint我建议最少也要1000张图5000张以上会比较稳定。曾经有一次我贪快只用了200张测试图同一个模型跑了两次FID一次27.5一次32.1差距大到完全没法做决策。后来我把真实图特征缓存下来把生成图数量提到3000张分数才稳定下来。提取器权重必须固定。FID是相对指标它的绝对值依赖于提取特征的Inception-v3权重。TorchVision的预训练权重和TensorFlow官方权重虽然在ImageNet上性能接近但提取出的特征分布有细微差异算出来的FID会差零点几甚至更多。所以同一个项目里所有实验必须使用同一个框架、同一份权重。如果某天你换了框架之前跑过的FID全部不能直接对比只能重新跑。数值误差也可能让你怀疑人生。矩阵平方根计算如果协方差矩阵接近奇异会得到很小的复数虚部代码里取real就好了。但如果你发现FID出现负数——理论上FID最小就是0——那基本是数值稳定性问题可以给协方差矩阵加一个很小的单位阵扰动比如sigma 1e-6 * np.eye(dim)再做矩阵平方根。3. PSNR、SSIM、FID、LPIPS四个指标怎么选3.1 四类指标的底层逻辑差异很多刚入门的朋友习惯把PSNR、SSIM、FID、LPIPS混在一起讨论其实它们衡量东西的层级完全不同。PSNR是最朴素的“像素级差异”它只看每个像素的误差大小完全忽略结构和语义。SSIM稍微聪明一点从亮度、对比度、结构三个维度比较局部窗口但它本质上仍然要求两幅图在空间上是“对准”的属于同一位置的局部比较。FID则跳出了“逐对比较”它比较的是两个图片集合的整体特征分布。LPIPS是另一种深度特征指标它用预训练网络比如AlexNet、VGG提取中间层特征在这些特征空间里算加权距离它可以理解成“感知版”的逐对距离。这里最关键的区别在于PSNR和SSIM回答的是“这两张图在像素上像不像”FID回答的是“这两批图在整体分布上像不像”LPIPS回答的是“这张图和那张图在人眼感知上像不像”。它们的计算粒度不同适用任务自然不同。3.2 指标对比速查表我把四个指标的关键属性整理成了一张表项目里做选择时直接查就行指标比较粒度核心思想数值方向主要适用场景PSNR逐像素像素级MSE取对数越大越好图像压缩、去噪、超分SSIM局部结构亮度、对比度、结构三因子越大越好图像压缩、去噪、超分FID整体分布Inception特征空间Fréchet距离越小越好GAN、扩散模型、生成质量评估LPIPS深度特征逐对预训练网络感知距离越小越好图像编辑、风格迁移、感知相似度这张表里有个容易忽略的细节FID是唯一一个“集合对集合”的指标其余三个都是“单图对单图”。所以在做超分或去噪任务时原图和结果是一一对应的用PSNR和SSIM非常自然但做GAN生成时生成图没有“标准答案”只能用FID这类分布指标。3.3 真实项目里的组合用法实际项目中我不建议只盯一个指标因为单一指标都有盲区。比如FID对纹理细节不敏感你生成一堆模糊但有正确语义的图FID可能还不错但人眼一看就露馅。反过来PSNR可以在某张图上很高但生成模型整体可能早就模式崩塌了只是崩塌的那几张图恰好和真实图很像。我在扩散模型项目里的习惯是FID做主线指标LPIPS做辅助指标。主线看整体分布是否收敛辅助看重建或编辑结果是否贴合人类感知。比如评估图像修复模型时我会同时报LPIPS和FIDLPIPS衡量修复结果和原图在感知上是否接近FID衡量修复结果的整体风格是否和真实数据分布一致。如果只报PSNR你很可能被高PSNR的“涂抹感”结果欺骗那个指标在模糊图像上经常虚高。有一个真实案例一次我做图像超分模型A的PSNR比模型B高0.3dB但人眼一看模型A的结果过度平滑纹理细节全丢了。后来我加上LPIPS和FID对比模型A的LPIPS明显更差FID也略差这才发现问题。从那以后我再也不单独用PSNR做生成模型的最终裁决。4. FID的局限与变体从静态图到视频生成4.1 FID的典型局限与使用禁忌先说清楚FID不是万能的它有四个让人头疼的局限。第一个是样本量敏感这个前面提过了。第二个是特征空间依赖ImageNet分类能力如果你的任务和自然图像差异很大比如医学影像、卫星遥感图、显微图像Inception-v3提取的特征未必能捕捉到对任务最关键的信息FID分数可能和人的主观感受脱节。第三个是高斯假设不一定成立虽然大部分时候管用但如果特征分布是多模态的只用均值和协方差刻画会丢掉高阶信息。第四个是最容易忽略的FID不衡量内容对齐。它只看整体分布完全不关心“生成图集合”和“真实图集合”里的图是否存在一一对应关系。一张图语义被完全改掉但只要整体分布仍然接近FID也可能很低。还有一个使用禁忌不要用FID做跨数据集对比。你在FFHQ数据集上训的模型FID是8在CelebA-HQ上训的模型FID是6这不代表后者更好因为不同数据集的真实分布难度完全不同。FID只能在同一数据集、同一预处理、同一评估协议下比较。另外生成图片时的随机种子也会带来分数波动。如果要严格对比两个模型我建议固定好所有随机种子并且每个模型各跑三次取平均值这个习惯能让你少做很多无用功。4.2 视频生成中的FID扩展视频生成模型这几年火得不行但静态图的FID没法直接拿来用因为视频还有时间维度。业内比较常用的做法是把FID扩展到视频域最典型的就是FVDFréchet Video Distance。它的思想和FID完全一致只是把提取特征的基础网络从Inception-v3换成了I3DInflated 3D ConvNet输入也从单帧图像换成了连续的视频片段。“vid fid”这个词在热词里出现我理解基本就是指这个方向——用Fréchet距离评估视频生成质量。实际使用中和静态FID有几个明显区别第一视频片段怎么截取固定帧数还是随机采样现在常见的是采样16帧左右作为一个clip。第二帧率怎么对齐生成视频和真实视频如果帧率不同I3D提取的特征会产生偏差。第三评估方差更大因为视频内容本身存在时序变动所以更需要固定采样策略。我自己评估视频生成模型时会把FVD配合“固定起始帧”的策略来做。具体做法是从真实视频里随机抽起点生成模型从同一条件帧开始预测后续帧这样FVD的对比更公平。如果不这么做模型可能只是生成了一堆和真实视频分布接近但时序完全对不上的画面FVD看着不错实际生成能力一塌糊涂。4.3 那些FID的变体指标值不值得追除了FVD社区里还有不少FID思想的变体。比如热词里提到的SID在不同的细分领域指的东西不完全一样但核心思路都是找一个新的特征空间然后在那个空间里度量两个集合的分布距离。有人用CLIP的特征替代Inception特征做文本到图像生成的评估有人用DINO这种自监督特征来度量得到的指标在某些场景下和人眼偏好相关度更高。我的观点是新指标可以试但不要盲追。FID的优势在于它已经有大量历史实验数据积累社区里任何一个新模型都会报FID你可以横向对比自己的结果。换一个新特征空间虽然可能更契合某个细分任务但很难和别人发布的结果直接对比。我的做法是主线继续报FID辅助实验里可以加一个CLIP或DINO特征版本的距离指标用来验证结论的稳健性。如果两个指标都指向同一个模型更好那这个结论基本可靠。至于LPIPS它在超分、编辑、复原任务里几乎成了标配因为它和人类感知的相关度确实比PSNR高。但它仍然是单图对单图比较没法替代FID在GAN训练里的位置。理想的做法是FID和LPIPS一起用FID看整体LPIPS看单张感知两个都过了再上人工评估这样踩坑概率最小。5. 我现在的工作流FID怎么用才不踩坑最后分享一点我个人的使用经验。经过这么多项目的打磨我现在评估生成模型时基本固定了这样一套流程先固定真实图片特征缓存保证不同实验之间只有一个变量生成图片统一数量、统一随机种子、统一采样温度然后同时报告FID和LPIPS必要的时候加一个人工评估小样本。实际操作中我发现FID更像是一个“体检指标”而不是“考试分数”。它适合帮你在模型迭代过程中快速发现方向性错误比如训练不收敛、模式崩塌、采样参数不对FID会给你明确信号。但它不适合告诉你“这张生成图哪里好哪里坏”也不适合作为唯一标准去判断一个模型的商业价值。如果你只是盯着FID调参最后可能得到一个分数好看但实际效果平庸的模型。还有一个小技巧调采样参数时特别有用。用FID对比候选checkpoint时建议每个候选模型在多个随机种子下各跑一遍FID取中位数和标准差。有一次我在扩散模型上调CFG引导系数单次FID显示7.8最好但多跑几次后发现8.0到8.3都很接近反而是7.8那次是随机波动出来的。取中位数之后真实最优值其实是8.2。这种波动在你做论文或者发布模型对比时影响不大但在工程上决定部署哪个版本时差一个点就可能选错模型。FID也一直在演进。视频生成领域的FVD、文本到图像生成里的CLIP-based分布距离、自监督特征上的距离度量都在尝试补足原版FID的短板。我觉得这不代表FID会被淘汰反而是这个指标思想足够扎实的证明。核心的“分布距离”想法不会过时只是特征空间和距离定义会随任务不同而调整。对做生成模型的人来说打好FID这套基本功后面再学任何变体都会快很多。