☰
视频生成评估避坑:FVD、IS、KID计算与数值稳定性
2026/10/29 4:37:17 网站建设 项目流程

做视频生成这几年,我被问得最多的问题不是“模型怎么训”,而是“你这个 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 的默认配置是这样的:

参数默认值可选范围影响
特征维度1024400 / 1024400 维数值偏高,不与主流可比
采样帧数1616 / 32 / 64帧数越多 FVD 越大,不可跨配置比较
输入分辨率224224 固定改动需重新校准
参考集样本数2048≥512低于 512 方差过大
协方差正则1e-61e-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
特征全是 NaNfp16 溢出或输入含异常帧逐批检查 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 所以模型很好”的结论。后来我在所有结果表格里都加了单位说明和“不可跨指标比较”的标注。评估这件事,数字本身不难算,难的是让看数字的人不误解它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询