ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

图像质量评价七指标:PSNR/SSIM/LPIPS/IS/FID/P-R实战

图像质量评价七指标:PSNR/SSIM/LPIPS/IS/FID/P-R实战 第一次认真做图像复原的评测是给一个去噪模型做验收。测试集跑完PSNR 从 26.8 涨到 28.4我当时挺高兴把成对的对比图导出来一看人直接愣住了——数字涨了图反而更糊头发丝和睫毛的位置糊成一坨皮肤上的细纹被抹平得像个塑料模特。后来复盘才发现涨的那 1.6 dB 主要来自一个更激进的平滑策略它在像素误差这个尺子上确实赢了代价是把高频细节全丢掉了。人眼能看出来的糊在 PSNR 的眼里是更准了。这就是图像评价指标最有意思的地方每一个数都在回答一个特定问题没有哪个数能回答所有问题。PSNR、SSIM、LPIPS、IS、FID再加上生成模型评测里常用的 Precision 和 Recall这七个指标覆盖了从像素差多少到整批图的分布像不像的完整光谱。但它们各自的计算口径、输入要求、取值范围、解释方式完全不同混着用、乱着比几乎必然得出错误结论。下面我就按每个指标到底在量什么、怎么算、什么情况下会骗你这条线把这七个指标挨个拆开讲一遍最后给一套可以直接抄的评测流水线。不管你是刚入门做超分、去噪、风格迁移还是在跟 GAN、扩散模型打交道这套东西都用得上。1. 先搞清楚这七个指标各自站在哪个位置1.1 三类评价目标像素保真、感知相似、分布吻合评价一张图好不好其实混着三件完全不同的事。第一件是像素级保真输出图和参考图逐像素差多少。它假设越接近参考图越好衡量的是重建的忠实度。PSNR 和 SSIM 属于这一类业内叫全参考指标Full-ReferenceFR因为它们必须有一张对应的参考图才能算。第二件是感知相似两张图在人眼看来像不像。人眼对低频结构、纹理、语义内容敏感对某些高频噪声反而不敏感。LPIPS 就是为这件事设计的它用预训练深度网络的特征距离来近似人的感知。第三件是分布级吻合不关心单张图对应哪张参考图只关心生成的一万张图整体上是不是同一个分布。IS、FID、Precision、Recall 都属于这一类它们不需要成对参考图只需要两个图像集合。这个分类不是学术洁癖它直接决定你能不能用某个指标。举个最常见的误用给超分模型算 FID。超分任务有明确的低分-高分配对你要的是保真度结果去算分布距离得到的数既不受控也不敏感纯属自娱自乐。反过来给无条件生成的 GAN 算 PSNR 更没意义——它本来就没有对应的参考图你只能去和最近邻硬凑凑出来的数毫无解释力。1.2 一张表看懂七个指标的分工指标类型需要参考图典型取值范围方向主要反映PSNR全参考是20~40 dB越大越好逐像素误差SSIM全参考是0~1越大越好亮度/对比度/结构相似LPIPS全参考是0~1越小越好深度特征距离感知IS分布级否1~几十越大越好可识别性 类间多样性FID分布级否0~几百越小越好特征分布距离Precision分布级否0~1越大越好生成样本的保真度Recall分布级否0~1越大越好对真实分布的覆盖度看方向这一列就能避免一半的低级错误LPIPS 和 FID 是越小越好其余五个是越大越好。我在组里见过不止一次有人把 FID 从 8.2 优化到 15.6 还觉得模型进步了就是因为脑子里默认数大就是好。看是否需参考图能避免另一半分布级指标要的是一整个文件夹的图样本量不够时波动极大绝不是随便抽几十张就能算的。1.3 指标选错会出现哪些反直觉现象我在实际项目里踩过的、也见过别人踩的典型症状有这么几种你可以对照着自己的实验看看PSNR 涨、观感变差几乎一定是用 L2/MSE 类损失训练的结果。MSE 对大误差惩罚重模型倾向于输出平均值也就是把所有可能的高频细节平均掉得到一张平滑但安全的脸。这时候 PSNR 会很好看LPIPS 却会恶化。SSIM 平稳、LPIPS 剧烈抖动SSIM 是个局部窗口统计量对全局的纹理质感变化不敏感而 LPIPS 抓特征的深层差异对材质、纹理的变化很敏感。两者背离时通常说明模型在结构位置对了但纹理风格变了。FID 很低、Recall 也很低这是模式崩塌的典型指纹。模型只学会了生成一小撮特别标准的样本每一张都很像训练集里的典型图所以 FID 好看但真实分布的边角它一个都没覆盖Recall 就掉下去了。IS 很高但肉眼全是废图IS 只认 ImageNet 的 1000 类模型生成纹理非常强烈的狗鼻子特写这类图会让 Inception 非常自信IS 就上去了。IS 高不代表好看它只代表容易被分类器认出来而且认出来之后类别分布很分散。2. PSNR最容易被误用的那个数2.1 从 MSE 到 dB公式里每一步的实际含义PSNR 的全称是峰值信噪比Peak Signal-to-Noise Ratio定义非常朴素先算均方误差再用峰值把误差归一化最后取对数转成分贝。$$\text{MSE} \frac{1}{N}\sum_{i1}^{N}(x_i - y_i)^2$$$$\text{PSNR} 10 \cdot \log_{10}\frac{\text{MAX}^2}{\text{MSE}} 20\cdot\log_{10}(\text{MAX}) - 10\cdot\log_{10}(\text{MSE})$$这里的MAX是像素的动态范围峰值。8 位图像里MAX 255归一化到[0,1]的浮点图里MAX 1.0。有意思的是只要MAX和数据范围匹配两种写法算出来的 PSNR 完全一样。推一下你就明白了把[0,255]的数据除以 255 归一化MAX从 255 变成 1MSE 缩小为原来的 1/65025比值不变对数结果自然不变。这也是为什么20*log10(255) ≈ 48.13 dB这个常数很值得记住——它代表全部像素平均差 1 个灰度级的水平。也就是说8 位图像下的 PSNR 每提高约 6 dBMSE 就缩小到原来的四分之一提高约 3 dBMSE 减半。实际工作中JPEG 高质量压缩大概在 35~40 dB超分任务里 26~32 dB 是常见区间去噪任务因为没有分辨率放大环节普遍能到 30 dB 以上。看 PSNR 的时候一定要带上在什么数据集、什么尺度因子下否则这个数没法横向比。2.2 为什么 PSNR 对模糊和轻微位移这么宽容PSNR 只对误差的平方和负责所以它对误差的空间分布完全不敏感。这个性质导致两个非常反直觉的后果。第一个后果整体偏暗一点点比局部细节全丢更吃亏。假设一张图整体亮度降低了 2 个灰度级均匀分布在所有像素上MSE 就是 48 位下的 PSNR 约 42 dB看起来还行。但如果只在边缘 5% 的像素上产生了 20 个灰度级的误差MSE 0.05 × 400 20PSNR 掉到 35 dB。反而是后者在人眼里更难接受因为前者你可能根本看不出来。第二个后果一像素平移会让 PSNR 断崖式下跌。一张精细纹理图平移一个像素人眼看几乎没区别但逐像素对齐之后 MSE 会暴涨到几十PSNR 直接掉十几个 dB。这也是为什么做超分评测时必须确保低分图和高分图是严格像素对齐的任何 resize、半步偏移、裁剪边界处理不一致都会让 PSNR 假性暴跌。我见过最离谱的一次是有人在读图时用了一个把uint8转float32再乘 255 的预处理结果两张图的数值范围差了 255 倍PSNR 算出来是 0.03 dB排查了半天。2.3 算 PSNR 时最容易踩的四个坑提醒下面这四条里任意一条出错PSNR 都不具备任何可比性而且错误往往不会报异常只会静静地给你一个错的数。数据范围必须和 MAX 对齐。如果输入是[0,255]的 float就必须用MAX255如果输入是[0,1]就必须用MAX1。混用的话误差恰好差20*log10(255) ≈ 48.13 dB。位深不同不能直接比。8 位数据和 16 位数据算出的 PSNR 天然不同因为动态范围不同。做跨论文对比时先看清对方报告的位深和数值范围。通道怎么平均要明确。三通道图有两种做法先把三个通道的 MSE 各自算完再平均或者把所有通道的像素拼成一个大向量统一算 MSE。数学上这两者等价但如果先算每个通道的 PSNR 再取平均结果就不一样了因为对数不是线性运算。统一口径别混着用。边界像素的处理要一致。如果做的是分块处理或者有 padding 的重建任务边缘区域可能不是有效输出。裁剪掉多少、是否补零、补零算不算进 MSE这些都要在所有对比方法之间保持一致。3. SSIM把结构这件事单独拎出来量化3.1 亮度、对比度、结构三项拆解SSIM 的出发点是人眼主要从局部区域的亮度、对比度和结构三个角度去理解图像而不是逐像素比对。所以它把这三件事拆开各自算一个相似度再乘起来。SSIM 最经典的写法是$$\text{SSIM}(x,y)\frac{(2\mu_x\mu_y C_1)(2\sigma_{xy}C_2)}{(\mu_x^2\mu_y^2C_1)(\sigma_x^2\sigma_y^2C_2)}$$其中mu是局部均值代表亮度sigma是局部标准差代表对比度sigma_xy是局部协方差代表结构相关性。分子分母刻意写成对称的形式保证当xy时结果恰好等于 1且结果落在[-1,1]之间实践上因为像素非负几乎总是非负。你把它拆成三个因子看会更清楚2μxμy/(μx²μy²)是亮度比较2σxσy/(σx²σy²)是对比度比较σxy/(σxσy)就是余弦相似度形式的结构比较。乘积的前两项恰好化简为公式里的样子。理解这一点之后你就明白SSIM 高的图不一定是细节一模一样也可能是亮度对比度形似、结构大致相关。3.2 C1、C2 这两个小常数为什么存在C1 (K1·L)²、C2 (K2·L)²L是动态范围论文里取K1 0.01、K2 0.03。归一化L1时C1 1e-4、C2 9e-4。它们存在的唯一目的是防止除零。当局部区域是一块纯色平坦区μ和σ都可能接近 0分母就会趋近于 0比值会数值爆炸。加上小常数之后平坦区之间的比较会平滑地趋近于 1避免数值问题。代价是小常数会让平坦区域对差异不敏感。两块都很平的区域即使亮度差了十几个灰度级只要都在C1的量级附近SSIM 也可能给出接近 0.99 的分数。这就是为什么大片天空、白墙、纯色背景的图SSIM 普遍虚高。3.3 窗口、均值滤波与shave实现层面的差异SSIM 是局部指标必须有窗口。原版论文用11x11的高斯窗sigma 1.5然后对全图的 SSIM map 取平均得到最终分数。但这里有几个实现层面的自由选择会直接改变数值。第一个是用高斯窗还是均匀窗。有些实现图快直接用11x11的均值滤波代替高斯滤波。这会得到一个不同的分数两者之间可能差 0.01~0.02在 SSIM 这个量级上已经不算小了。第二个是是否 shave 边界。卷积需要 padding边界那一圈像素的统计量是靠补零或镜像凑出来的本身不可靠。很多实现会裁掉边界 5 个像素再做平均对应11x11窗口的半径。原版 MATLAB 代码和后来流行的 Python 实现在这件事上并不完全一致。第三个是多通道怎么合成。稳妥的做法是把 RGB 转到 YCbCr只用 Y 通道算 SSIM因为亮度通道才承载结构信息。图省事直接用 RGB 三通道平均也很常见但结果会偏高而且对色度失真不敏感。实操建议如果你要跟别人的 SSIM 数对比别只对数值先确认窗口类型、是否 shave、用不用 Y 通道这三件事。三个都对齐之后数字才可比。我个人的习惯是直接在代码里注释清楚这三项配置写进实验记录的表格里。3.4 SSIM 的变体与它不擅长处理的情况SSIM 有几个常用变体值得一提。MS-SSIM在多尺度上算 SSIM 再加权对分辨率变化更鲁棒常用于评估下采样相关的任务。SSIMULACRA 2和Butteraugli这类专门为压缩质量设计的指标对块效应、振铃、模糊的排序和人眼主观评分MOS的相关性明显高于 SSIM如果你做的是压缩或画质增强值得优先考虑。SSIM 不擅长的场景也很明确严重模糊结构位置对了糊了SSIM 依然能给 0.9 以上、纹理替换把草地纹理换成纹理类似的另一片草地SSIM 几乎不动、风格化颜色和对比度被整体调整SSIM 会大幅下降但观感可能更好。所以现在做复原类任务的论文基本是PSNR SSIM LPIPS三件套一起报用 PSNR 对齐像素、SSIM 对齐结构、LPIPS 对齐感知。4. LPIPS用预训练网络给像不像打分4.1 从像素空间到特征空间感知差距的本质LPIPSLearned Perceptual Image Patch Similarity想解决的问题很直白两个在像素空间距离相同的失真为什么会让人眼觉得一个能接受、一个不能忍答案在于人眼视觉系统其实是在做多级的特征提取。低频轮廓、边缘、纹理统计量各自在不同的脑区被处理最终的像不像是所有层级特征的共同判断。逐像素比较相当于把这一切压扁成了最底层的一个特征。LPIPS 的做法就是借助一个已经在 ImageNet 上训练好的分类网络AlexNet、VGG、SqueezeNet把两张图分别喂进去取出多个中间层的特征图在特征空间里算距离。因为分类网络为了分类必须学会提取语义上有意义的特征所以特征空间里的距离自然比像素空间更接近人的感知。4.2 LPIPS 的完整计算链路具体算起来有这么几步两张图输入骨干网络取出L个中间层的激活比如 AlexNet 取relu1到relu5这五层。对每一层的激活在通道维度做 L2 归一化。也就是把每个通道的特征图除以它自己的 L2 范数。这一步很关键因为不同通道的激活幅值可能差好几个数量级不归一化的话距离会被少数大值通道主导。沿着通道维度做一次逐元素的缩放每个通道乘一个独立的权重。这些权重是用人眼主观判断数据两选一的相似性判断训练出来的线性层是 LPIPS 唯一学习到的部分参数量极小。把每层的特征差异在空间上求平均、在通道上求和得到每层的距离再把这L个距离加起来就是最终的 LPIPS 值。整条链路里骨干网络是冻结的只有那组通道权重是训练出来的。这也是 LPIPS 能在很小的标注数据集上训出来、而且泛化还不错的原因。4.3 输入范围、backbone、版本三个决定数字大小的开关这三个开关任何一个人动了LPIPS 的数值都会变且不同配置之间完全不可比。输入范围LPIPS 标准要求输入是[-1,1]。如果你的图是[0,1]的必须先做x*2-1。这一步忘了做计算结果会明显偏差——因为网络的批归一化统计量是按 ImageNet 的分布预设的输入范围不对所有中间激活都会跑偏。Backbonealex、vgg、squeezenet三个选项给出的数值量级不同。AlexNet 版本最常用也最轻速度最快VGG 版本对细节更敏感但显存和耗时会明显上升。选哪个要在实验里固定跨 backbone 比较数字毫无意义。版本lpips这个 pip 包有几个版本迭代包内部还有一个normalize参数控制是否对输入再做一次 ImageNet 均值方差归一化不同版本的默认值变化过。这直接导致同一个模型、同一批图、升级包之后数字变了。这个问题在开源社区的 issue 里被反复讨论过。我的做法在requirements.txt里把 LPIPS 的版本号钉死评测脚本里显式传入normalize参数不依赖默认值。同时在实验记录里写明 backbone 和版本。这三行信息的成本几乎为零能省下后面几天的困惑。4.4 我踩过的LPIPS坑跨backbone比较与resize陷阱第一个坑是跨 backbone 比较。有一次我们组内两个人在同一批图上跑 LPIPS一个用alex一个用vgg结果差了 0.05 左右讨论了半天模型效果最后发现是配置不一致。现在我们的评测脚本里把 backbone 写死在配置文件里不允许命令行覆盖。第二个坑是resize 顺序。如果生成图和参考图尺寸不一致必须先 resize 到统一尺寸再算 LPIPS。但 resize 用双线性还是最近邻、在[0,1]空间还是[-1,1]空间做都会影响结果。更麻烦的是如果你的任务本身就是超分x4放大resize 会把生成图的高频细节直接抹掉LPIPS 会变得不敏感。这种情况要么报分块 LPIPS要么对齐尺寸后只报局部裁剪区域。第三个反直觉的发现是LPIPS 对亮度偏移比 PSNR 更宽容对纹理变化比 PSNR 敏感得多。所以当你看到 PSNR 掉了但 LPIPS 涨了别急着下结论说模型变差了先看看是不是纹理更锐利了——这往往是感知质量提升的信号只是在像素误差这个尺子上被惩罚了。5. IS与FID把一整批图当成一个分布5.1 IS它测的其实是可识别性加多样性Inception Score 的逻辑是这样的用 ImageNet 上训练的 Inception v3 对每张生成图做分类得到 1000 类的概率分布p(y|x)。如果生成质量高这个分布应该是尖的——某一张图很明确地属于某一类条件熵低。如果生成多样性好把所有图的p(y|x)平均起来得到的p(y)应该是平的——各类都有边缘熵高。IS 就是条件分布和边缘分布之间的 KL 散度的指数$$\text{IS} \exp\left(\mathbb{E}x \left[ D{KL}(p(y|x) | p(y)) \right]\right)$$实践上有两个必须注意的点。一是样本量。IS 对样本量非常敏感标准做法是至少 50000 张并且分成 10 份分别计算报告均值和标准差。只算一次、只用几千张得到的数波动大得没法用。二是它只在类 ImageNet 数据上有意义。人脸、医学影像、遥感图像这些不在 ImageNet 1000 类里的数据Inception 分类本身就是乱猜的IS 就变成了噪声。我见过在 CelebA 上报告 IS 的论文那个数字一点参考价值都没有。IS 最致命的缺陷是它可以被欺骗。对抗性地生成一些让分类器极度自信的样本就能把 IS 刷得很高而图像本身可能完全不像自然图像。所以现在 IS 已经很大程度上被 FID 和 PR 取代了只在需要和早期工作对齐时才报。5.2 FID的推导从Inception特征到Fréchet距离FIDFréchet Inception Distance是目前最主流的生成质量指标。它的思路是不比较单张图而是比较两个图像集合在 Inception 特征空间里的分布。具体步骤是用 Inception v3 去掉最后的分类层取全局池化层的 2048 维激活向量作为每张图的特征表示。然后假设真实图像的特征分布和生成图像的特征分布都是多元高斯分布用它们的均值向量和协方差矩阵计算 Fréchet 距离也叫 2-Wasserstein 距离$$d^2 |\mu_r - \mu_g|^2 \text{Tr}\left(\Sigma_r \Sigma_g - 2(\Sigma_r \Sigma_g)^{1/2}\right)$$第一项衡量两个分布的中心差多远第二项衡量两个分布的形状差多少。矩阵的平方根通过特征值分解实现对Σr Σg做特征分解取特征值的平方根再重组回矩阵。这个公式有几个很好的性质它是真的度量满足三角不等式和非负性两个分布完全相同时距离为 0而且它同时惩罚质量差和多样性差不需要分别设计。这也是它比 IS 可靠得多的原因。5.3 FID的样本量、分辨率与权重来源问题FID 虽然好用但坑也不少而且都挺隐蔽。样本量FID 是有偏估计量样本越少估计出来的值系统性偏高方差也大。用 5000 张算出来的 FID 通常比 50000 张高好几个点。社区的标准做法是 50000 张对 50000 张而且真实集和生成集的数量最好一致。如果你只有几千张真实图那就别用 FID用 KID 更合适。分辨率与插值Inception v3 的输入是299x299而你的图可能是1024x1024或者64x64。这个 resize 怎么做对 FID 的影响大得离谱。用双线性、双三次还是 Lanczosresize 是直接拉伸还是先裁剪还是先抗锯齿不同的选择能带来好几个点的差异。有一篇专门讨论这个问题的论文指出了各种实现之间的不一致后来出现了clean-fid这个库把预处理流程统一了。如果你要和别人的数对比务必确认这一点。权重来源TensorFlow 版的 Inception 权重和 PyTorch 移植版比如pytorch-fid用的那个在数值上有细微差异导致同样的数据算出来的 FID 不完全一致。同一个实验里必须用同一个实现跨实现比较要谨慎。数据范围和量化真实图通常是uint8生成图往往是float。如果是float但范围是[0,1]需要先转回uint8再送进网络否则分布对不上。这个转换里的四舍五入方式也会带来轻微差异。5.4 KID与CMMD小样本场景下的替代方案KIDKernel Inception Distance用最大均值差异MMD代替 Fréchet 距离用多项式核在 Inception 特征空间上计算无偏估计。它的好处是样本量小时不会系统性偏高而且可以给出多次采样的均值和标准差稳定性明显更好。代价是数值量级和 FID 完全不同通常在0.0x这个量级别和 FID 混着看。CMMDCLIP Maximum Mean Discrepancy是近几年出现的做法用 CLIP 的图像嵌入代替 Inception 特征。好处是 CLIP 的语义空间更贴近人类对图像内容的理解而且不像 Inception 那样只在 ImageNet 类别上有效。对文生图这类任务CMMD 通常比 FID 更有解释力。选择上我的经验是样本量大于 3 万、做的是自然图像生成用 FID样本量小或者做的是特定领域图像用 KID 或 CMMD无论用哪个都在论文或报告里写清楚实现库和版本。6. Precision与Recall把保真度和多样性拆成两个数6.1 这里的P/R不是分类任务里的P/R先说清楚这两个指标和分类任务里的 precision/recall 完全不是一回事只是名字撞了。在生成模型评测里它们来自 Kynkäänniemi 等人 2019 年提出的改进的精确率与召回率用来把 FID 那个混合在一起的分数拆成两个正交的维度Precision精确率生成的图有多少落在真实数据的流形范围内衡量保真度。Recall召回率真实数据有多少落在生成数据的流形范围内衡量多样性/覆盖度。两者都基于同一个想法把真实图像和生成图像各自映射到特征空间通常还是 Inception 的 2048 维池化特征然后在特征空间里估计出各自的流形——具体做法是对每个样本找到它到最近的k个邻居的距离用这个 k 近邻半径作为一个超球所有样本的超球并集就构成了对分布支撑集的近似。6.2 kNN流形估计的计算过程具体实现拆成四步用固定的特征提取器把真实集和生成集都编码成特征向量。必须用同一个特征提取器通常是 Inception v3 的池化层输出。分别对真实集和生成集计算集合内部每个样本到其k近邻的距离作为该样本的流形半径得到两组半径。对生成集的每一个样本判断它是否落在真实集流形内。判断方式是拿它的特征向量去真实集里找 k 近邻如果这个距离小于真实集里对应邻居的半径就判为在流形内。落在内部的样本比例就是 Precision。反过来对真实集的每一个样本判断它是否落在生成集流形内落在内部的真实样本比例就是 Recall。这个定义的好处是它同时考虑了两端的尺度不是简单地用一个全局阈值。而且它不需要真实集和生成集一一对应只要数量足够就行。6.3 k值、样本量与计算成本之间的取舍k的取值通常在 3~5 之间。k太小流形估计对应的超球太小会低估覆盖范围导致 Precision 和 Recall 同时被压低k太大超球变大两个指标都会虚高。论文实验下来k3是个不错的默认值社区也基本沿用了这个设置。换k值时一定要在报告里写明因为不同k之间数值不可比。样本量方面标准做法是用 50000 张真实图和 50000 张生成图其中一部分用于估计流形另一部分用于查询。数量减少会让指标方差快速上升尤其是 Recall——它统计的是真实样本被覆盖的比例样本少了之后这个比例的估计非常不稳定。计算成本是这个指标最大的短板。核心操作是两组五万维向量之间的两两距离计算也就是一个50000 x 50000的距离矩阵用 NumPy 在 CPU 上跑基本等于放弃治疗。我一般用torch.cdist在 GPU 上算分块处理避免显存溢出五万对五万大概几十秒到一分钟。但如果你要做网格搜索或者反复调参这个时间成本还是要计入的。一个降低成本的实用技巧把特征先存成float16能省一半显存对最终数值影响极小。另外计算 k 近邻时可以用torch.topk而不是完整排序能省掉不少时间。6.4 怎么用P/R组合去读FID把 Precision、Recall 和 FID 放在一起看能解读出单看 FID 时完全看不出来的信息。我总结了几种典型的组合FIDPrecisionRecall解读低高高理想情况质量和多样性都对低高低模式崩塌只生成少数高质量样本覆盖不全低低高少数样本质量差但整体覆盖好FID 被拉低了需要单独检查坏样本高高低覆盖不全但覆盖到的部分很准说明训练不充分或容量不足高低高多样性够但质量不行典型的生成器训练不足我印象最深的一次是调一个条件生成模型。FID 从 12.4 降到 9.8看起来不错但 Precision 几乎没变Recall 从 0.71 掉到 0.42。画了一下生成样本发现模型学会了专攻三类特别常见的条件剩下的条件全靠瞎猜。如果只看 FID这个问题完全被掩盖了。7. 把七个指标串成一条能复现的评测流水线7.1 数据准备阶段的统一规范指标本身算对不难难的是整套流程的一致性。我在项目里踩过的绝大多数坑最后都归到数据准备的某个环节不一致上。所以现在我会强制做这几件事统一读取函数。所有评测脚本共用一个load_image函数明确规定读成 RGB不是 BGR不是灰度、uint8或float32二选一并全程统一、数值范围明确写在函数 docstring 里。任何一次cv2.imread和PIL.Image.open混用都可能引入通道顺序问题。统一尺寸与裁剪策略。成对指标PSNR/SSIM/LPIPS必须保证输入和参考图严格同尺寸同对齐。分布指标IS/FID/PR则要明确所有图是 resize 到统一尺寸还是保持原尺寸FID 会内部 resize但你的预处理也会影响结果。固定随机种子和采样方式。分布指标从大集合里抽样时必须固定种子。不同随机子集之间 FID 的差异可能有几个点不固定的化实验结果完全没法复现。预处理参数写入实验记录。包括插值方式、是否抗锯齿、是否做了归一化、是否有色彩空间转换。这些东西写下来只要两分钟但能省掉后面几天的扯皮。7.2 一份可直接改的评测脚本骨架全参考指标部分下面这段是我常用的最小实现import torch import torch.nn.functional as F def psnr(pred, target, max_val1.0): pred/target: (N,3,H,W) float tensor in [0, max_val] mse F.mse_loss(pred, target) return 10.0 * torch.log10(max_val ** 2 / mse) def gaussian_window(size11, sigma1.5, channels3, devicecpu): coords torch.arange(size, dtypetorch.float32, devicedevice) - size // 2 g torch.exp(-(coords ** 2) / (2.0 * sigma ** 2)) g (g / g.sum()).unsqueeze(0) w2d (g.t() g).unsqueeze(0).unsqueeze(0) return w2d.expand(channels, 1, size, size).contiguous() def ssim(pred, target, max_val1.0, shave5): pred/target: (N,3,H,W), 局部高斯窗 11x11, sigma1.5 c pred.shape[1] win gaussian_window(channelsc, devicepred.device) k1, k2 0.01, 0.03 c1, c2 (k1 * max_val) ** 2, (k2 * max_val) ** 2 mu_x F.conv2d(pred, win, groupsc) mu_y F.conv2d(target, win, groupsc) mu_x2, mu_y2, mu_xy mu_x ** 2, mu_y ** 2, mu_x * mu_y sigma_x2 F.conv2d(pred * pred, win, groupsc) - mu_x2 sigma_y2 F.conv2d(target * target, win, groupsc) - mu_y2 sigma_xy F.conv2d(pred * target, win, groupsc) - mu_xy ssim_map ((2 * mu_xy c1) * (2 * sigma_xy c2)) / \ ((mu_x2 mu_y2 c1) * (sigma_x2 sigma_y2 c2)) if shave 0: ssim_map ssim_map[..., shave:-shave, shave:-shave] return ssim_map.mean(dim(1, 2, 3)).mean()LPIPS 用现成的库就行关键是范围转换不要漏import lpips loss_fn lpips.LPIPS(netalex, verboseFalse).to(device).eval() with torch.no_grad(): # pred/target 是 [0,1]必须先转 [-1,1] d loss_fn(pred * 2 - 1, target * 2 - 1)FID 直接调库但注意目录里的图格式必须完全一致from pytorch_fid import fid_score fid fid_score.calculate_fid_given_paths( [data/real_50k, data/fake_50k], batch_size50, devicecuda, dims2048, num_workers8 )PR 的核心是两两距离和 k 近邻半径用 GPU 分块算def knn_radius(feats, k3, block4096): feats: (N, D) L2-normalized on GPU返回每个样本的 k 近邻距离 N feats.shape[0] radius torch.empty(N, devicefeats.device) for i in range(0, N, block): d torch.cdist(feats[i:i block], feats) # (b, N) d[torch.arange(d.shape[0]), torch.arange(i, i d.shape[0])] float(inf) radius[i:i block] d.topk(k, largestFalse).values[:, -1] return radiusPrecision 就是生成样本的 k 近邻距离 对应真实邻居半径的比例Recall 反过来两边都算完之后取平均即可。7.3 结果表格与结论的写法评测跑完最忌讳的就是只写一行我们的方法最好。我会要求实验表格至少包含这几列方法名、PSNR、SSIM、LPIPS、FID、Precision、Recall再加一列推理耗时或者参数量。另外分布指标的表格里必须标出样本量比如FID-50k否则读者没法判断可比性。写结论的时候我一般遵循一个原则不要在多个指标上都说显著优于而是挑出最相关的两三个指标重点分析其余指标说明持平或者略有差距但可接受。原因很简单人眼评价本身就充满权衡一套方法在 PSNR 上赢、LPIPS 上输这是非常正常的结果写清楚权衡比强行全赢更可信也更经得起复现。7.4 我在真实项目里总结的几条经验第一先定指标再训模型。很多人是先训完再看指标结果发现指标不合适来不及重训。我的习惯是在开训之前就把评测脚本跑通用 baseline 的输出跑一遍全流程确认所有指标都是合理量级再开始正式训练。第二永远保留一组人眼主观对照图。指标是辅助不是裁判。我见过太多次 PSNR 高 0.3 dB 但人眼明显更差的案例。我的做法是每次评测固定挑同一个位置的四张裁剪图包含文字、细纹理、平滑渐变、人脸各一张和指标一起放进实验记录。几个月后回头看这个比任何表格都有用。第三警惕跨论文对比。不同论文的测试集、裁剪方式、下采样核、评测代码实现都可能不同。论文里说 FID 从 10.2 降到 8.7不代表你用同样的思路也能复现这个差距。真正能信的只有你自己控制变量跑出来的数。第四LPIPS 和 FID 不要用来选 checkpoint。它们计算成本高、方差大用作训练过程中的早停判据非常不稳。选 checkpoint 我用验证集上的 PSNR 或者一个轻量的感知损失最终评测才跑全套指标。还有一个小技巧如果你要在论文或者报告里报 PSNR建议同时报一下它在验证集上的标准差。不同方法的 PSNR 差异如果是 0.1 dB 而标准差是 0.3 dB这个提升基本可以认为是噪声。我吃过这个亏费了很大劲调出来的 0.15 dB 提升最后被三次不同种子的复现打脸打得很惨。指标这种东西看着简单用起来全是细节。
返回列表