KID Domain Gap: Measuring the Distance Between Two Rendered Worlds, Part 1
最直接的”两个图集差多少”是逐像素算 L1/L2 距离:
其中 是两张图在 处的像素值, 是图像高宽。这个距离的问题在于它把”画面内容”和”成像风格”混在一起:
像素距离对几何与光度的微小扰动过敏,对语义变化反而迟钝——敏感轴正好是错的。
本研究关心的是”模型眼里的两个世界差多少”:SimLingo 这类端到端模型读图靠预训练学到的特征表示,不靠裸像素。所以域差距应该在模型能感知的表示空间里量,而不是在像素空间里量。这正是感知度量(perceptual metric)的标准做法:用一个在大型数据集上预训练的网络提取特征,在特征空间里算距离。LPIPS 已经证明,深度特征空间里的距离与人类感知判断高度一致——像素空间做不到这一点。
InceptionV3 是 ImageNet 预训练的分类网络,原始结构末端是 pool → fc(2048→1000) → softmax。做特征提取时把最后的分类头 fc 摘掉(换成恒等映射),留下全局平均池化层的输出:
是输入图像, 是截断后的网络。这 2048 维是网络在倒数第二层对图像内容的压缩表示——分类头之前的最后一站,语义信息最完整又没有被动类别化。
from torchvision.models import inception_v3, Inception_V3_Weights
model = inception_v3(weights=Inception_V3_Weights.DEFAULT).to(device).eval()
model.aux_logits = False
model.AuxLogits = None # 推理时不用辅助分类头
model.fc = torch.nn.Identity() # 摘分类头,输出即 2048-d pool 特征
(tools/t32_kid.py 实测代码,ImageNet 权重用 torchvision 自带的 DEFAULT。)
特征可比的前提是两侧图像逐位一致地过同一条预处理链:
arr = np.asarray(Image.open(p).convert("RGB").resize((299, 299), Image.BILINEAR))
x = torch.from_numpy(arr).permute(2, 0, 1).float() / 255.0 # HWC->CHW, [0,1]
x = (x - 0.5) / 0.5 # 归一化到 [-1,1]
三个要点:统一转 RGB(排除灰度/RGBA 混杂)、统一 resize 到 299×299(InceptionV3 的训练输入尺寸)、统一归一化。任何一侧换了插值方式或归一化常数,量出来的”域差距”里就掺进了预处理差,不再是渲染差。
Inception 特征作图像分布的”裁判模型”是 FID/KID 一脉的业界惯例:ImageNet 上学到的表示对自然图像的语义差异敏感,且全网有统一权重可复现。本项目 UE4 侧 12636 张、UE5 侧 21056 张各过一个特征矩阵,后续所有距离计算都在这两个 矩阵上进行。
aux_logits=False 必须在建网时设置,否则 torchvision 会要求辅助头参与前向,输出变成元组。比较两堆点 和 是否同分布,最朴素的想法是比均值向量 。死穴在于:均值只是分布的一阶信息。一个圆环形分布和一个集中在圆心的分布可以有完全相同的均值,形状却天差地别。要比出形状差异,需要均值以外的矩(方差、偏度……直至无穷阶)。
MMD(Maximum Mean Discrepancy,最大均值差异)的招是换一个空间比均值。选一个再生核希尔伯特空间(RKHS,),把每个点 映射成 ,然后定义分布的均值嵌入(kernel mean embedding):
是分布 在 里的”重心”。对足够丰富的核(特征核,characteristic kernel,如高斯核、次数足够高的多项式核),这个映射是单射——不同的分布必有不同的均值嵌入。于是”两个分布是否相同”这个无穷维问题,被压缩成了”两个点距离是否为零”:
在原空间比均值只能看见一阶差异;在 里比均值,因为 编码了 的各阶矩信息,均值差里自动携带了全部阶次的分布差异。多项式核对应的空间编码了所有不超过 3 阶的单项式——均值、协方差、三阶矩的差异都会贡献进 MMD。这就是”高维空间里,均值差能看见形状差异”的准确含义。
“最大”来自 MMD 的等价定义:在所有单位范数函数 中,找让 最大的那个(最能区分两分布的”证人函数”),这个最大差值就是 MMD。均值嵌入形式是它可计算的等价写法。
MMD 要把点映射到高维(甚至无穷维)特征空间再算均值差。如果 真的有无穷维,连把它存下来都做不到,更别说算内积。核技巧(kernel trick)的关键观察是:整个算法只需要内积 ,从不需要 本身。
定义核函数 。对某些精心选择的 ,左边可以用 直接算出来,右边对应的高维映射 永远不必显式构造。本项目用的是 3 次多项式核(polynomial kernel):
是 InceptionV3 特征,。展开 ,它是 所有不超过 3 次单项式的加权和——数学上严格等于某个显式高维空间里的内积 ,其中 包含 的所有 0~3 阶单项式分量。这个空间的维度是 量级(约 ),但计算 只需要一次 2048 维点积。
一次矩阵乘 x @ y.T 就得到所有点对的高维内积。这就是 SVM 的同款技巧,也是 KID 能在 CPU 级统计量成本下跑完三万张图的原因。
MMD² 定义是 RKHS 里两个均值嵌入的距离平方:。把范数平方按内积展开,再把 代回、用 替换,得到纯核函数表达式:
其中 是独立地从 抽的两个样本( 同理)——“独立”这个条件后面去对角线时会用到。
如果 和 来自同一个分布,那么”堆内取两点”和”跨堆取两点”在统计上没有区别——三个期望相等,。两堆点真正分开时,跨堆相似度掉下来,前两项不变,MMD² 变大。
直觉读法:MMD² = 两堆各自的抱团程度 − 两堆之间的抱团程度。互相渗透的点云里,堆内和跨堆一样亲密,差为零;各居一方时,跨堆项拖后腿,距离显现。
三项各自都是 O(1) 的正数,而 MMD² 是它们的微小差值——典型的”大数相减得小数”结构,对浮点精度敏感。项目代码里特征矩阵转 float64 再算核矩阵,就是为这一步服务的。
三项核均值展开里的期望,采样计算时变成对核矩阵求平均。堆内项的核矩阵 是 方阵,它的对角线是 ——点和自己的相似度,恒为整个矩阵的最大值(多项式核下 )。
问题在于定义要求 是独立同分布的两个样本: 和 必须是两次独立抽样。把对角线算进去,等于允许”抽两次抽到同一个点”,这不是定义里的量。后果是系统性偏高:对角线项总是正且最大,混进来必然抬高估计,样本越少抬得越明显。这是有偏估计(V-statistic 路线)。
抠掉对角线,只对 求和,分母相应地从 改成 :
是两堆各自的样本数。前两项”总和减迹、除以 “,第三项没有对角线问题( 和 本来就不是同一批点),直接全体平均。可以证明 :任何样本量下期望都精确等于真值,这就是”无偏”。
无偏是有代价的: 不再非负。当真值 (两堆同分布)时,估计值以零为期望上下波动,取到负数完全正常——负数本身就是”测不出差异”的证据。本项目域内基线实测 UE5 对 UE5 得到 ,正是教科书式的零值抖动。如果用有偏版本,同分布时也恒为正,反而无法区分”真差异”和”估计器自带的底噪”。
term1 = (k_xx.sum() - np.trace(k_xx)) / (m * (m - 1)) # 总和减迹 = 去对角线
term3 = k_xy.mean() # 跨堆项无对角线问题
(tools/t32_kid.py。np.trace 取的就是对角线之和。)
tools/t32_kid.py(全文件 148 行)的核心函数,逐行对应”MMD² 三项展开 + 无偏估计”的数学:
def kid_unbiased(x, y, splits=10):
"""无偏 KID(poly kernel, gamma=1/d, coef=1),子集均值/方差(arXiv:1801.01401 Alg.1 简化)。"""
d = x.shape[1] # 特征维度 2048,多项式核的 gamma=1/d
n = min(len(x), len(y)) # 两侧对齐到同样规模,保证 m=n
vals = []
rs = np.random.RandomState(0) # 固定种子:重跑逐位一致
for _ in range(splits): # 重复抽 10 个子集
xi = x[rs.choice(len(x), n, replace=False)] # 无放回抽 n 张
yi = y[rs.choice(len(y), n, replace=False)]
k_xx = (xi @ xi.T / d + 1.0) ** 3 # X 堆内核矩阵
k_yy = (yi @ yi.T / d + 1.0) ** 3 # Y 堆内核矩阵
k_xy = (xi @ yi.T / d + 1.0) ** 3 # 跨堆核矩阵
m = n
term1 = (k_xx.sum() - np.trace(k_xx)) / (m * (m - 1)) # 去对角线
term2 = (k_yy.sum() - np.trace(k_yy)) / (m * (m - 1))
term3 = k_xy.mean() # 跨堆项:1/m² 全体平均
vals.append(term1 + term2 - 2 * term3)
return float(np.mean(vals)), float(np.std(vals))
n = min(...)):UE4 有 12636 张、UE5 有 21056 张,直接全量算会让两项堆内项的自由度不同。统一抽 张使 ,三项分母一致,公式形式最干净,也让每次抽样的统计量可比。(xi @ xi.T / d + 1.0) ** 3 就是多项式核 的向量化写法。一个 n×n 矩阵的每个元素都是一对点的高维内积,升维从不发生。sum() - trace() 即”全体和减去对角线”,配合 m(m-1) 得到无偏项(详见”无偏估计”一篇)。splits=10 重复抽样:每次无放回抽样都有随机性,单次值是一次”掷骰子”。10 次的均值是稳定点估计,标准差是这次测量的抽样噪声——返回值同时给 (mean, std) 就是为这个。np.random.RandomState(0) 让抽样序列确定,整个测量逐位可复现——别人重跑同一批图,得到的每个小数位都相同。没有 GPU、没有梯度、没有训练——纯统计量。全部计算是三个 矩阵的求和, 取几千时秒级完成。三万张图的测量瓶颈在特征提取(GPU 前向),KID 本身几乎免费。
FID(Fréchet Inception Distance)先把两个图集的特征各自拟合成多元高斯 、,再算两个高斯之间的 Fréchet 距离:
是特征均值, 是特征协方差。两个软肋:
KID(Kernel Inception Distance)= 多项式核 MMD² 的无偏估计,逐条对上:
| FID | KID | |
|---|---|---|
| 分布假设 | 高斯拟合 | 无(核方法,分布自由) |
| 有限样本 | 有偏,随 漂移 | 无偏,期望恒等于真值 |
| 不确定度 | 无原生方差 | 多次子集抽样自带均值±标准差,可接 bootstrap 出置信区间 |
第三点是工程刚需:有了逐次抽样的方差,才能算置信区间、才能做”跨侧 vs 域内基线的区间是否重叠”这种统计判断。
路线级分组后一组只有几百到两千张,场景组最少的一组(右转)UE5 侧 1731 张。这个量级正落在 FID 偏差最陡的区间,而 KID 的无偏性与样本量无关——小样本稳健是硬需求,选型没有悬念。
点估计 KID = 0.0743 只是单次测量。要问”真值大概在哪个范围”,需要知道统计量 的抽样分布——但它是三项核均值组合的 U-statistic,有限样本下服从什么分布没有好用的闭式答案。理论分布在,算不动。
不知道总体分布,就把样本当总体:手里的一万多张图就是我们对”UE4 世界全部可能画面”的全部知识。从这个经验分布里有放回地重采样出一批”平行宇宙”的图集,每个平行宇宙算一遍统计量,看它在这些平行宇宙里抖成什么样——抖动的分布就近似了真实抽样分布。这就是 bootstrap(自助法)。
def bootstrap_ci(x, y, n_boot=1000, subsample=2000, seed=0):
rs = np.random.RandomState(seed)
pts = []
for _ in range(n_boot): # 1000 个平行宇宙
xi = x[rs.choice(len(x), min(subsample, len(x)), replace=True)] # 有放回!
yi = y[rs.choice(len(y), min(subsample, len(y)), replace=True)]
pts.append(kid_unbiased(xi, yi, splits=1)[0]) # 每次只算一遍
lo, hi = np.percentile(pts, [2.5, 97.5]) # 分位数即 95% CI
return float(lo), float(hi)
(tools/t32_kid.py。)三个细节:
replace=True 是 bootstrap 的定义:有放回重采样让同一张图可以在一个伪样本里出现多次,这正是”从经验分布独立抽样”的模拟。写成无放回就是另一件事(子采样,subsample),方差结构不同。splits=1:单遍快算,分布信息不来自单次的内部重复,而来自 1000 次外层重复本身。总体 KID 的 bootstrap 95% CI 为 ,宽度约 ±0.003——三万张级样本量下估计非常稳。这个区间后来直接用作判据:与域内基线(≈0.0001)的区间完全不重叠,才敢说域差距是物理事实而非采样噪声。
KID 的数字不是”两个图集的内在属性”,而是在特定特征空间里量出来的距离。换一套特征提取器,同样的两堆图会量出不同的数。本项目的完整口径是:
Inception_V3_Weights.DEFAULT),pool 层 2048 维;文献里常见的 KID 实现(如 torch-fid 附带版本)用的是另一套 custom Inception 权重。结构同为 InceptionV3,但训练细节不同 → 特征空间不同 → 同两堆图算出的 KID 绝对值不同。两套数字之间没有换算关系。
| 比较 | 是否合法 |
|---|---|
| 跨侧 KID(UE4 vs UE5)vs 域内基线(UE4 vs UE4) | ✅ 同一特征空间内,核心结论 |
| 场景组之间(直行/左转/右转) | ✅ 同口径分组,相对排序有效 |
| 路线之间 | ✅ 同上(注意小样本方差更大) |
| 与论文里别人报的 KID/FID 绝对值 | ❌ 特征空间不同,不可比 |
换言之:本项目所有结论都是同一把尺子内部的相对比较,尺子本身的刻度零点由域内基线标定。跨研究的绝对值对标需要逐位复刻对方的特征口径(权重、预处理、核参数全对齐),没有意义也没有必要。
指标口径属于实验方法的一部分:不报权重来源的 KID 数字无法复现、无法审计,读者可能拿去做非法的跨文献比较。这类”口径声明”在评测类研究里是刚需——和”用了哪个数据集划分""指标在多少样本上算”同级。