做视频生成这几年,我被问得最多的问题不是“模型怎么训”,而是“你这个 FVD 是怎么算出来的,为什么跟我算的对不上”。同一个 checkpoint,两个人跑出 380 和 452 两个数,然后开始互相怀疑对方的模型有问题,最后发现是一个用 16 帧采样、一个用 32 帧采样,一个把像素归一化到 [-1,1]、一个用了 ImageNet 的 mean/std。这类事情在视频生成任务里极其常见,因为 FVD、IS、KID 这三个指标在图像时代就已经有一堆实现分歧,搬到视频上之后又叠加了帧数、帧率、时序编码器这几层变量,坑的数量直接翻倍。
这篇内容围绕 MiniSora 这套视频生成训练框架里评估模块的实现来讲,把 FVD、IS、KID 三个指标从“它到底在算什么”一路拆到“代码怎么写、参数怎么定、分数飘了怎么查”。适合两类人看:一类是刚接手视频生成项目、需要给模型出一个可信评估报告的同学;另一类是已经在跑评估、但发现自己的数字和论文或同行对不上、想搞清楚差异来源的同学。文中会给出可直接复用的计算流程、关键参数取值、数值稳定性处理方式,以及我在实际调评估脚本时踩过的具体坑。所有实现细节都以“能复现、能对比、能解释”为目标,不追求花哨。
1. 先把三个指标量的是什么说清楚
1.1 从“人眼看”到“特征空间看”的迁移
评价生成视频,最直觉的办法是找人打分,但人打分贵、慢、不可复现,而且不同人对“这段视频自不自然”的容忍度差异巨大。于是业界换了个思路:找一个已经在大量真实视频上训练过的神经网络,把视频喂进去,取它中间某一层的激活值当作这段视频的“特征向量”,然后比较生成视频的特征分布和真实视频的特征分布有多接近。这个思路的核心假设是:如果两个分布在一个强判别能力的特征空间里足够接近,那么它们在感知层面也足够接近。FVD、KID 都是这个思路的不同数学形式,IS 稍微特殊一点,它只看生成样本本身。
这个假设当然不完美。特征提取网络是在分类任务上训的,它关注的东西未必等于人眼关注的“真实感”,尤其是运动模糊、细微抖动、长时间遮挡这些视频特有的瑕疵,分类网络未必敏感。但在没有更好替代方案的前提下,这套基于特征分布的距离仍然是目前最通用、最可横向比较的评估方式。理解这一点很重要:指标低不代表视频一定好看,指标高基本能说明有问题,它是一个“排雷工具”而不是“审美裁判”。
1.2 FVD:让一个懂动作的编码器来当裁判
FVD 全称 Fréchet Video Distance,数学形式跟图像领域的 FID 完全一致,区别在于特征提取器换成了能处理时序的 3D 卷积网络。MiniSora 里默认用的是 I3D 结构,在通用动作识别数据集上预训练。为什么必须用 3D 网络?因为 2D 网络逐帧提特征再平均,会把时序信息彻底抹平——一段画面正确但帧序错乱的视频,逐帧特征平均后和正常视频几乎一样,而这种错乱恰恰是视频生成最典型的失败模式之一。3D 卷积在时间维度上做感受野,才能真正“看到”运动。
FVD 的计算公式是两个多元高斯分布之间的 Fréchet 距离:
FVD = ||μ_r - μ_g||² + Tr(Σ_r + Σ_g - 2·(Σ_r·Σ_g)^(1/2))其中 μ_r、Σ_r 是真实视频特征的均值和协方差,μ_g、Σ_g 是生成视频特征的均值和协方差。第一项衡量均值偏移,第二项衡量分布形状差异。实际实现里,协方差矩阵的尺寸是 d×d,d 是特征维度,1024 维的话就是 1024×1024,矩阵平方根那一步是整个计算里最耗时也最容易出数值问题的环节,后面会专门讲。
这里有个必须知道的细节:I3D 输出的特征维度不是唯一的。早期实现取的是分类头之前的 400 维 logits,后来普遍改成取平均池化后的 1024 维。这两个版本算出来的数值完全不可比,400 维版本的 FVD 通常显著偏高。所以看到别人报的 FVD 数字,第一件事是问清楚特征维度,第二件事是问清楚帧数和分辨率,缺一个都没法比。
1.3 IS 和 KID:老指标搬进视频任务后的变形
IS(Inception Score)本来是图像指标,用 Inception 网络对生成图做分类,希望两件事同时成立:单张图的类别预测足够“尖锐”(说明生成的物体明确、不含糊),整个批次的类别分布足够“平坦”(说明生成多样性好)。公式是对 KL 散度取期望再取指数。它在视频任务里的用法一般是逐帧计算再求平均,但这么做丢掉了全部时序信息,而且它完全不看真实数据分布,只评价生成样本自身的“清晰度+多样性”,所以模型只要生成一堆高置信度的猫狗就能刷高分,哪怕画面在抖。
KID(Kernel Inception Distance)用最大均值差异(MMD)代替 Fréchet 距离,同样是拿特征,但不假设高斯分布,直接算两个特征集合在核空间里的均值嵌入距离。它的最大优点是无偏估计——小样本下不会系统性低估,这对视频任务特别友好,因为视频样本收集成本高,很多时候手上只有几百条真实视频。缺点是数值很小,通常要乘以 1000 来报告,而且对核函数的选择比较敏感。MiniSora 里默认用三次多项式核。
把这三个指标放一起看,它们的分工其实很清楚:
| 指标 | 是否对比真实分布 | 是否含时序信息 | 小样本稳定性 | 主要用途 |
|---|---|---|---|---|
| FVD | 是 | 是 | 差(需大样本) | 主指标,横向对比模型 |
| KID | 是 | 取决于特征提取器 | 好 | 小数据集、消融实验 |
| IS | 否 | 否(逐帧平均) | 中等 | 辅助看清晰度与多样性 |
提示:不要用 IS 做主指标,它的设计目标里根本没有“贴近真实分布”这一项,单独看它会得出错误结论。MiniSora 的评估报告里把 IS 放在辅助栏,只在 FVD 出现异常时用来交叉验证。
2. 计算流程拆解:一次 FVD 到底经历了什么
2.1 特征提取的四个硬约束
很多人以为 FVD 计算就是“把视频喂进模型拿特征”,实际上有四个约束必须固定,任何一个变了,结果就不可比。
分辨率。I3D 的原生输入是 224×224。生成视频的分辨率往往不是这个数,需要 resize。这里就出现了第一个分歧:用双线性插值还是双三次插值?先 resize 再中心裁剪,还是直接拉伸?MiniSora 的做法是先按短边等比缩放到 256,再中心裁剪 224,双线性插值,理由是这套流程和预训练时的预处理最接近,特征偏移最小。如果你的模型输出是 512×512,直接拉伸到 224 会让物体形变,FVD 会虚高。
帧数。这是影响最大的一个参数。I3D 在预训练时用的是 64 帧输入,但实际评估中出于显存和速度考虑,很多人用 16 帧或 32 帧。MiniSora 默认 16 帧,理由是大多视频生成模型的输出长度在 16 到 32 帧之间,用 64 帧需要循环填充。必须强调的是,16 帧和 32 帧算出的 FVD 差值可以达到 30% 以上,绝对不是可以忽略的量级。
帧率与采样策略。给定一段 5 秒、25fps 的视频,要取 16 帧,是均匀间隔抽还是取前 16 帧?MiniSora 用时间维度均匀采样,公式是idx = round(linspace(0, T-1, 16))。如果视频长度不足 16 帧,就循环重复最后一帧补齐。这个策略要写死在配置里,换一次采样方式就等于换了一把尺子。
归一化。I3D 预训练时用的归一化方式决定了推理时必须一致。常见两种:像素值缩放到 [-1, 1],或者用 ImageNet 的 mean=[0.5,0.5,0.5]、std=[0.5,0.5,0.5]。这两种在小数点后几位就有差异,累积到协方差上会被放大。我在早期版本里因为生成端输出了 [0,255] 的 uint8 而提取端期望 [0,1] 的 float,导致 FVD 直接飙到 900 多,排查了大半天。
2.2 协方差矩阵与 Fréchet 距离的数值实现
特征提取完成后,会得到两个矩阵:真实集 R(形状 N_r × 1024)和生成集 G(形状 N_g × 1024)。接下来算均值和协方差,然后套公式。看起来简单,但有几个数值细节必须处理。
第一,协方差的估计方式。用np.cov(R, rowvar=False)得到的是无偏估计(除以 N-1),某些实现用有偏估计(除以 N)。在样本量上千时差异很小,但在几百样本时会有可见差别。MiniSora 统一用无偏估计,并在配置文件里注明。
第二,矩阵平方根。scipy.linalg.sqrtm返回的可能是复数(由于浮点误差导致极小的虚部),必须取实部:sqrtm_result = np.real_if_close(sqrtm_result, tol=1000)。如果不处理,后续的 trace 计算会得到复数,有些实现直接报错,有些静默取实部,行为不一致。
第三,协方差的秩亏问题。1024 维特征、500 个样本,协方差矩阵的秩最多 499,是奇异的。数学上 Fréchet 距离对奇异矩阵仍然有定义,但数值求解会不稳定,可能出现负值。标准做法是加一个极小的对角正则:cov += np.eye(d) * 1e-6。这个 eps 取多大有讲究,太大(比如 1e-3)会系统性压低 FVD,太小又起不到稳定作用。MiniSora 用 1e-6,并且在样本数少于特征维度时在日志里打警告。
第四,计算精度。float32 累加上千个样本的协方差会有明显误差,MiniSora 的特征在落盘时存 float32(省空间),但在计算均值和协方差前会转成 float64。这一步对最终数值的影响在 0.1 到 1 之间,虽然不大,但既然要做可复现的评估,就没理由省这一步。
2.3 参考集构建:最容易被忽视的一环
FVD 是“生成分布 vs 真实分布”的距离,真实分布那一端的样本集合就是参考集。参考集的质量直接决定指标有没有意义,但这一环经常被随手糊弄。
第一个问题是参考集规模。理论上越多越好,实践中 2048 条是一个常见的平衡点。低于 512 条时,FVD 的方差会大到让两次评估的差异失去解释力,我实测过用 256 条真实视频做参考集,同一个模型跑三次评估,FVD 分别是 412、389、447,波动接近 15%。
第二个问题是参考集与生成内容的匹配度。如果你训的是人脸短视频,参考集却混入了大量风景和体育素材,那 FVD 衡量的就不只是生成质量,还包含了内容分布差异。MiniSora 的做法是参考集尽量从同一数据源的验证集里抽,确保内容域一致。
第三个问题是参考集特征要不要缓存。答案是必须缓存。参考集特征算一次要几分钟到几十分钟,而评估过程中它会反复用到。MiniSora 把参考集特征存成.npy文件,同时存一份元信息(样本数、特征维度、帧数、分辨率、归一化方式、提取器权重版本哈希),加载时校验元信息,不匹配就拒绝使用。这个校验机制救过我好几次——有一次我改了帧数配置但忘了重新提特征,如果没有校验,那次评估的结果就是完全错误的,而且不会有任何报错。
3. 实操:把评估脚本跑起来
3.1 特征提取模块的骨架
先把提取器写成一个独立、无状态的类,输入是视频张量,输出是特征矩阵。它不负责读文件、不负责采样帧,这些交给数据管道。职责分离的好处是后面做缓存、做分布式、做半精度都很容易改。
import torch import torch.nn as nn class VideoFeatureExtractor(nn.Module): def __init__(self, backbone, feat_dim=1024, input_size=224, num_frames=16): super().__init__() self.backbone = backbone self.feat_dim = feat_dim self.input_size = input_size self.num_frames = num_frames self.backbone.eval() for p in self.backbone.parameters(): p.requires_grad = False @torch.no_grad() def forward(self, video): # video: (B, C, T, H, W), 已归一化, float32 assert video.shape[2] == self.num_frames assert video.shape[3] == self.input_size feat = self.backbone(video) # 取 pool 之后的 (B, 1024) return feat.float()几个必须写死的地方:eval()一定要调用,否则 BatchNorm 会用当前批次的统计量,导致同一段视频在不同批次里算出的特征不一样,这是最隐蔽的 bug 之一。no_grad()也要加,否则显存会炸。另外 backbone 的输出必须明确是哪一层,建议在封装时就把取特征的逻辑固定下来,别让调用方去猜。
3.2 完整流程脚本与参数取值
整个评估的主流程分五步:加载参考集特征(或现场提取并缓存)、逐批提取生成视频特征、拼接成矩阵、计算统计量、套公式。下面这段是核心计算部分:
import numpy as np from scipy import linalg def compute_stats(feats, eps=1e-6): feats = np.asarray(feats, dtype=np.float64) mu = feats.mean(axis=0) cov = np.cov(feats, rowvar=False) cov = cov + np.eye(cov.shape[0]) * eps return mu, cov def frechet_distance(mu1, cov1, mu2, cov2): diff = mu1 - mu2 covmean, _ = linalg.sqrtm(cov1.dot(cov2), disp=False) if not np.isfinite(covmean).all(): offset = np.eye(cov1.shape[0]) * 1e-6 covmean = linalg.sqrtm((cov1 + offset).dot(cov2 + offset)) if np.iscomplexobj(covmean): covmean = covmean.real fid = diff.dot(diff) + np.trace(cov1 + cov2 - 2.0 * covmean) return float(fid)sqrtm失败时加 offset 重试的逻辑很重要,在实际使用中大概有 1% 到 3% 的概率会触发,尤其在样本量偏小、协方差接近奇异的时候。如果不加重试,脚本会偶发崩溃,而且崩溃位置随机,很难排查。
参数取值方面,MiniSora 的默认配置是这样的:
| 参数 | 默认值 | 可选范围 | 影响 |
|---|---|---|---|
| 特征维度 | 1024 | 400 / 1024 | 400 维数值偏高,不与主流可比 |
| 采样帧数 | 16 | 16 / 32 / 64 | 帧数越多 FVD 越大,不可跨配置比较 |
| 输入分辨率 | 224 | 224 固定 | 改动需重新校准 |
| 参考集样本数 | 2048 | ≥512 | 低于 512 方差过大 |
| 协方差正则 | 1e-6 | 1e-8 ~ 1e-4 | 过大压低分数 |
| 计算精度 | float64 | — | float32 会有可见误差 |
3.3 KID 与 IS 的实现要点
KID 的实现比 FVD 简单,不需要矩阵平方根,但要选核函数。三次多项式核k(x,y) = (x·y/d + 1)^3是原论文的默认选择,MiniSora 沿用它。注意d是特征维度,用于归一化内积,漏掉这一步会导致核值爆炸、MMD 数值失真。
def polynomial_mmd(x, y, degree=3): d = x.shape[1] def k(a, b): return (a @ b.T / d + 1.0) ** degree k_xx = k(x, x); k_yy = k(y, y); k_xy = k(x, y) n, m = x.shape[0], y.shape[0] # 无偏估计:去掉对角项 sum_xx = (k_xx.sum() - np.trace(k_xx)) / (n * (n - 1)) sum_yy = (k_yy.sum() - np.trace(k_yy)) / (m * (m - 1)) sum_xy = k_xy.mean() return float(sum_xx + sum_yy - 2 * sum_xy)无偏估计里“减掉对角项”这一步特别容易漏。漏了之后得到的是有偏估计,小样本下会系统性偏小,报告的 KID 会虚低。我见过有实现直接算k_xx.mean() + k_yy.mean() - 2*k_xy.mean(),严格说那是 MMD 的有偏版本,不能叫 KID。
IS 的实现在视频任务里要注意两点。一是特征来源必须是分类 softmax 输出,不是 pool 特征,维度是类别数(通常是 1000)。二是分批策略,KL 散度是对整个批次的平均预测分布求的,所以批次太小会让结果方差极大。MiniSora 的做法是把所有帧的预测拼起来,按 10000 帧一批算,取多次的均值和标准差一起报。IS 的标准差往往比均值更有信息量,方差大的时候说明生成结果的类别分布很不稳定。
3.4 可复现性配置清单
评估结果要能复现,光有代码不够,还得有一份“实验指纹”。MiniSora 每次评估结束会往日志里写一段 JSON,包含:特征提取器权重文件的哈希值、帧数、分辨率、采样策略、归一化参数、参考集特征文件路径及其哈希、样本数、计算精度、库版本。这份东西看着繁琐,但当你三个月后回头找“当时那个 FVD 是怎么来的”,它就是唯一能救你的东西。
提示:库版本一定要记。不同版本的
scipy在sqrtm的实现上有差异,同一份数据算出的结果在第四位有效数字上可能不同。数值不大,但在做消融对比时,0.5 的差异有时就是结论翻转的临界点。
4. 参数与数值稳定性:分数飘移的真实来源
4.1 样本量、批次大小与偏差
先说结论:批次大小不应该影响结果,样本量会显著影响结果。
批次大小影响的是显存和速度,不影响特征值,前提是模型处于 eval 模式且没有用到跨样本的操作。这一点在纯卷积网络上成立,但如果你用的提取器里有任何依赖批次的模块(比如某些归一化变体),就必须警惕。检验方法很简单:同一批视频分别用 batch=4 和 batch=16 提特征,对比结果,差异应该在 1e-5 量级以内。
样本量的影响是实打实的。FVD 的估计量是有偏的,样本越少,估计出的协方差越偏离真实协方差,而且偏离方向不固定。我做过一组对照:用同一个模型生成 2000 条视频,然后分别取前 256、512、1024、2000 条算 FVD,结果是 468、431、412、398。也就是说,从 256 加到 2000,分数降了 70。这意味着两件事:一是报 FVD 必须同时报样本量;二是比较两个模型时,必须用相同数量的样本,最好还是同一批参考集。
KID 在这方面表现好得多,同样这四组样本量,KID 的值是 42.1、40.8、40.2、39.9(乘 1000 后),波动在 5% 以内。这也是我建议在小样本场景优先看 KID 的原因。
4.2 sqrtm 的三种失败模式与处理
矩阵平方根是整个计算链路上最脆弱的一环,我遇到过三种失败。
第一种,返回复数。这是最常见的,原因是浮点误差让协方差矩阵出现极小的负特征值。处理方式是np.real_if_close,但要注意 tolerance 的设定,tol 太大会把真实的复数结果也当成实数,tol 太小又过滤不干净。用 1000 是比较稳妥的经验值。
第二种,返回 NaN。这通常发生在协方差严重奇异、或者输入特征里有 NaN/Inf 的时候。前者靠加 eps 解决,后者必须在提取特征后立刻检查:assert np.isfinite(feats).all()。特征里出现 NaN 的原因五花八门,可能是输入视频里有全黑帧导致某些归一化除零,可能是模型在 fp16 下溢出,也可能是数据管道里 resize 的边界处理有问题。早检查早定位,别等到 sqrtm 报错再回头找。
第三种,耗时长。1024×1024 的矩阵平方根在单核上大概需要几十秒,评估频繁的时候会很难受。优化手段有两个:一是把参考集的协方差和它的平方根预先算好存起来,每次只算生成集那一侧;二是对特征做 PCA 降到 256 维再算,速度能快一个数量级,但降维后的数值不能和未降维的直接比,只能在同一降维配置下横向对比。
4.3 改分辨率、改帧数之后为什么不能比
这是我在项目里反复强调的一条纪律:任何影响特征提取配置的改动,都会让历史数据失去可比性。原因很直接——FVD 衡量的是特征空间里的分布距离,而特征空间本身是由“输入什么样的视频”决定的。同一个视频,用 16 帧提特征和用 32 帧提特征,得到的向量完全不同,均值、协方差全变,最终距离当然不同。
实际发生过的事:某次为了省显存,把评估帧数从 32 降到 16,然后发现所有模型的 FVD 都“变好了”,兴奋了半天以为是优化生效,结果一查是配置改了。从那以后我在评估脚本里加了一道硬性检查:如果当前配置与参考集元信息里记录的配置不一致,直接抛出异常,必须显式传--force才能继续,并且结果文件会打上NON_COMPARABLE标记。
如果确实需要换配置,正确做法是把所有候选模型在新配置下重新评估一遍,用一套全新的基线,而不是拿新数字和旧数字比。工作量是大了点,但这是唯一诚实的做法。
5. 常见问题排查速查表
5.1 分数异常的症状与定位路径
评估出问题时,症状往往很模糊——“FVD 特别高”“FVD 是负数”“两次跑结果不一样”。下面这张表是我整理的高频问题和对应定位路径,基本覆盖了 90% 的情况。
| 症状 | 最可能原因 | 定位方法 | 处理 |
|---|---|---|---|
| FVD 高到离谱(>800) | 归一化不一致或像素范围错 | 打印输入张量的 min/max/mean | 统一到 [-1,1] 或 ImageNet 标准化 |
| FVD 为负 | 协方差正则过大或 sqrtm 未取实部 | 检查 eps 取值、检查虚部 | eps 降到 1e-6,取 real |
| 同模型两次结果差异大 | 样本量不足或采样未固定随机种子 | 固定 seed 后重跑 | 提高样本量至 ≥1024 |
| 特征全是 NaN | fp16 溢出或输入含异常帧 | 逐批检查 isfinite | 关闭 fp16,过滤坏帧 |
| KID 恒为 0 | 用了有偏估计或特征全同 | 检查是否减了对角项 | 改成无偏公式 |
| IS 方差极大 | 批次太小或类别分布极不均衡 | 增大评估批次数 | 报均值+标准差 |
| 评估速度极慢 | 未缓存参考集特征 | 检查是否有 npy 缓存 | 加缓存层 |
这里重点说“FVD 为负”这个症状。理论上 Fréchet 距离非负,出现负数一定是实现问题。最常见的是np.trace(cov1 + cov2 - 2*covmean)里 covmean 取错了,或者 eps 加到协方差上之后改变了迹的关系。还有一种少见情况:两个协方差矩阵都非常接近零矩阵(比如所有生成视频几乎一模一样),此时数值误差就会主导结果。后者其实是模型多样性崩溃的信号,值得单独关注。
5.2 数据管道的隐性错位
评估脚本本身没问题、参数也统一了,分数还是不对,那八成是数据管道的问题。这类问题最讨厌,因为它不报错,只是悄悄给出错误结果。
第一种错位:生成视频和真实视频的解码方式不同。生成端可能是模型直接输出的张量,真实端是 mp4 文件用 ffmpeg 解码。不同解码器、不同版本的 ffmpeg,解出来的像素值在小数点后几位就有差异,色彩空间的转换矩阵也可能不同(尤其是 YUV 到 RGB 的转换系数)。MiniSora 的做法是把生成结果也先编码成 mp4 再用同一套解码流程读回来,牺牲一点精度换取一致性。
第二种错位:帧序。有些视频容器存了旋转元数据,解码时不应用旋转会得到横竖颠倒的画面。这类问题在手机拍摄的数据里特别常见。检查方法是随机抽几条视频可视化,别嫌麻烦。
第三种错位:时长不一致。真实视频平均 5 秒,生成视频固定 2 秒,如果采样策略是“均匀取 16 帧”,那真实视频的 16 帧覆盖 5 秒内容,生成视频的 16 帧覆盖 2 秒内容,两者的运动幅度天然不同,FVD 会被内容差异污染。解决办法是截取真实视频的前 2 秒做参考,保持时长分布一致。
5.3 多人协作时的口径统一
一个评估脚本在自己机器上跑得通,不代表团队里其他人跑出来是同一个数。我经历过的协作翻车包括:有人用自己的 conda 环境,torch 版本不同导致 I3D 的权重加载顺序有微妙差异;有人把torch.backends.cudnn.deterministic关了,卷积结果不完全一致;有人在自己机器上改了采样帧数忘了提交配置。
解决方案是把评估做成一个容器化的、配置驱动的黑盒。输入是“模型输出目录 + 参考集路径 + 配置文件名”,输出是“指标 JSON + 完整指纹”。配置文件名里带上版本号,所有参数从配置文件读,不允许命令行临时覆盖关键参数。跑完之后把 JSON 提交到统一的记录表里,横向对比只从表里读。这套流程建立起来之后,“你算的和我算的不一样”这类扯皮基本消失了。
6. 让评估既快又稳的几个优化手段
6.1 特征缓存与增量评估
评估慢的主要成本在特征提取,而特征提取的结果是确定性的——同一段视频、同一套配置,提多少次特征都一样。所以缓存是最直接有效的优化。
缓存的键怎么设计是关键。用视频文件路径当键不安全,因为文件可能被覆盖。MiniSora 用的是hash(文件路径 + 文件大小 + 修改时间 + 提取配置哈希),前三个保证文件内容没变,第四个保证配置没变。缓存文件按批次存成.npy,另外维护一个索引文件记录哪些视频已经提过特征。
增量评估的价值在调模型阶段特别明显。参考集特征只算一次,之后每次评估只算新生成的那批视频。我们的实际数据是:2000 条视频、16 帧、224 分辨率,单卡提特征大概 4 分钟,算 sqrtm 大概 20 秒。加了缓存之后,日常评估的耗时从 5 分钟降到 40 秒左右,迭代速度提升非常明显。
有一个细节要注意:缓存要有失效机制。改了提取器权重、改了归一化方式、甚至升级了 torch 版本,都应该让缓存失效。把配置哈希放进键里能自动处理大部分情况,但库版本变化不会体现在配置里,所以我在缓存目录下额外放一个env.lock文件,记录关键库版本,不一致就整体清空。
6.2 分布式提取与精度取舍
样本量上千之后,单卡提特征会变成瓶颈。分布式提取的思路很直接:每张卡处理一部分视频,各自算出局部特征,最后 all_gather 拼成完整矩阵,再在主卡上算统计量。注意统计量必须在完整矩阵上算,不能各卡算各的再平均——协方差的平均不等于平均的协方差,这个错误犯一次就会得到完全错误的结果。
半精度能提速约 1.5 到 2 倍,但要小心。I3D 这类 3D 卷积网络在 fp16 下容易出现数值溢出,尤其是深层。我的建议是:只在特征提取阶段用 fp16,且必须验证 fp16 和 fp32 算出的 FVD 差异在可接受范围内(实测差异通常在 0.5 以内)。如果差异超过 2,就别用了,这个精度损失会掩盖真实的模型差异。另外,fp16 的结果不能和 fp32 的历史数据混在一起比,要么全用 fp16,要么全用 fp32。
还有一个不太起眼但很有效的优化:把参考集的协方差矩阵、以及它和自身的 sqrtm 结果预先算好存下来。每次评估时只需要算生成集的统计量,以及一次交叉项的 sqrtm,能省掉一半的矩阵运算时间。
6.3 从单一分数到分层诊断
FVD 是个标量,标量的好处是便于比较,坏处是信息量太低。两个模型 FVD 都是 400,可能一个是“画面清晰但运动僵硬”,另一个是“运动自然但细节糊”,改进方向完全相反。所以评估做到一定程度之后,必须从单一分数走向分层诊断。
MiniSora 里加了几层拆分。第一层是按运动幅度分桶:用相邻帧的光流均值把生成视频分成低、中、高三档,分别算 FVD。实践中经常能看到模型在低运动样本上表现很好、高运动样本上崩掉,这种情况在总 FVD 里只体现为“稍微高一点”,但分层之后问题一目了然。
第二层是外观与时序解耦。用逐帧的 FID 衡量单帧画质,用帧间光流一致性和 warping error 衡量时序连贯性。FVD 低但逐帧 FID 高,说明时序建模好、单帧质量差;反过来则是画质尚可但运动不自然。这两个诊断方向对应的优化手段完全不同,前者要调生成器容量和解码器,后者要调时序模块和损失函数。
第三层是按内容类别或时长分桶。如果训练数据里短视频占多数,长视频的生成质量往往明显更差,但总 FVD 会被短视频拉低,掩盖问题。按视频长度分档统计,能立刻暴露这类不均衡。
这些分层统计不会让指标变得“更好看”,但能让它变得“更有用”。我在实际使用中的体会是,FVD 适合放在报告首页给决策者看,分层结果适合放在附录给自己看——真正指导下一步做什么的,永远是那些被拆开的分项。
最后分享一个我在配置评估脚本时踩过的坑:早期我把 KID 的结果乘 1000 之后直接和 FVD 并排展示,结果有人误以为两者是同一量纲、可以直接比较大小,得出“KID 只有 40 所以模型很好”的结论。后来我在所有结果表格里都加了单位说明和“不可跨指标比较”的标注。评估这件事,数字本身不难算,难的是让看数字的人不误解它。