ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

正交子空间分解:提升AI生成图像检测跨生成器泛化的实操指南

正交子空间分解:提升AI生成图像检测跨生成器泛化的实操指南

最近一段时间我一直在做 AI 生成图像检测相关的工作。坦白讲,这个方向最折磨人的不是准确率上不去,而是准确率“看起来很高,一换生成器就崩”。你今天在实验台上刷到 99% 的好成绩,下周换一个新出的生成模型再测,可能直接掉到 60%,比随机猜好不了多少。正交子空间分解是这套思路里我个人比较推荐的一种基础解法,它不用改复杂的网络结构,也不用堆大量标注数据,靠空间几何约束就能把“跨生成器泛化”这个硬骨头啃下来大半。这篇文章就把这个方法的原理、落地步骤和我实际踩过的坑全部摊开聊一聊,适合正在做图像检测、内容取证或者 AIGC 鉴伪的同学参考。

1. 项目概述与核心思路拆解

1.1 为什么 AI 生成图像检测这么难泛化

先从不那么常见的现象说起:很多检测模型在训练集同分布的测试集上表现非常好,AUC 能到 0.99 以上,但一旦换成新出现的生成器,性能就急剧下降。原因是大多数方法在做的是“记住指纹”,而不是“理解生成过程”。真实照片和生成假图之间的差异确实存在,但具体到不同生成器,这些差异的形态差别非常大。早期 GAN 生成的图像有很强的频谱伪影和棋盘格纹理,扩散模型生成的图像则在这些方面干净得多,但在高频细节和局部一致性上又有新的可辨识痕迹。

但如果只把这些痕迹当作分类线索,模型很容易过拟合。举个例子:训练数据里有一半图片来自某个扩散模型,这个模型在生成时总是倾向把亮度均值压到某个区间,分类器很聪明,它会在第一层卷积就抓到这个统计特征。可新的生成器没有这个亮度偏移,模型学到的判断依据就全部失效,成绩自然崩盘。

问题在于这些“痕迹”在高维特征空间里并不是均匀分布的。真实图像的特征分布很宽,因为照片内容千变万化;而生成图像由低维潜变量控制,即使图像内容看起来各不相同,其内部特征却往往被压缩在一个相对低秩的子空间里。这个“低秩集中”的现象是几乎所有生成器的通病,也是我们做泛化检测可以抓住的共性。

1.2 正交子空间分解到底在做什么

正交子空间分解这个名字听起来很数学,其实拿几何图像来理解非常简单。想象真实照片的特征分布是一大片起伏的山脉,生成图像的特征则像一条条蜿蜒的河流,河流一定被约束在山谷里。我们要做的,是找到这些“山谷”的方向,然后把特征投影到这些方向上,判断一个特征点落在山谷里的比例有多高。如果某个特征在“生成子空间”上的投影能量很大,在“真实子空间”上的投影能量很小,那它大概率是一张假图。

这里的关键词是“正交”。真实子空间和生成子空间不是各自独立随便找出来的,而是要求它们互相垂直,不能共享方向。这样做的好处很直接:生成方向里不会混入真实图像里的内容分量,真实方向里也不会残留生成痕迹。检测器使用的信号因此变得纯净,不会因为某个生成器恰好和训练集的内容分布重叠就被带偏。

从数学上看,我们把深度网络提取的特征记为向量 x,把它分解成两部分:

x = P_true(x) + P_fake(x)

其中 P_true 和 P_fake 是向两个正交子空间的投影算子,并且满足 P_true(x) 与 P_fake(x) 的内积为零。检测时不再直接看原始分类器输出,而是比较两个投影范数,生成图像在 P_fake 上的响应显著更高,真实图像则在 P_true 上更高。这个做法天然对生成器内容不敏感,因为它只在乎“方向占比”。

1.3 这个方法能解决什么问题

这个方案解决得最好的是跨生成器泛化问题。训练时我们可能只见过四五个生成器的图,但测试时会遇到全新的架构,甚至可能是混合了多种后处理的图。正交分解能保持稳定的原因在于,它提取的不是某一个生成器特有的细节,而是生成图像这个大类在特征空间中共通的低秩集中特性。

它也适合检测那些经过压缩、缩放、加噪处理的图。图像被压缩后,像素域指纹会被破坏,但高维语义特征里的子空间投影关系通常还能保留,所以鲁棒性比端到端二分类好不少。对于数字取证的落地场景,比如社交平台审核、内容溯源、版权保护,这个方法都能派上用场。

当然它也有局限。它不是万能钥匙,对极端裁剪或严重重绘的图像,效果会大打折扣。但作为泛化检测的基础模块,它可以和传统的逐像素伪造取证方法互补,作为整个检测链路里最不需要频繁重训的那个环节。

2. 方法设计:正交子空间分解的完整流程

2.1 整体架构搭法

整个流程分三段:特征提取、子空间估计、判别打分。首先用预训练的图像骨干网络把真实图和生成图都转成高维特征向量;然后在特征空间里分别估计“真实子空间基”和“生成子空间基”,并做正交化处理;最后对新的输入图像计算它在两个子空间上的投影能量差,超过阈值就判定为 AI 生成。

这个结构最省算力的地方在于,骨干网络不需要重新训练。我们可以直接使用在通用分类任务上预训练好的模型,比如常见的卷积网络或者 Transformer 架构网络。因为正交子空间本身是在特征层面做统计建模,底层的语义表示越通用,子空间估计越稳定。整个训练过程本质上是“特征统计”而不是“端到端学习”,这大大降低了对标注数据和算力的要求。

设计时我倾向于把特征维度降到一个可控范围,比如先做一次 PCA 白化,将特征从几千维压缩到二三百维。因为 SVD 在高维空间里容易受到噪声维度干扰,降维能让主方向更干净,后续正交化也更稳定。

2.2 特征提取器选型与数据组织

骨干网络的选择直接影响子空间质量。我对比过几类预训练模型,结论是没有绝对最优,但鲁棒性排序大概是:在大型数据集上预训练的纯分类模型 > 对比学习模型 > 自训练小模型。对比学习模型的特征表达很丰富,但它的特征空间结构更适合做图像检索,不一定是生成痕迹最敏感的;分类任务训练的模型特征里有更多“真/假”可分的纹理信息,反而更适合。

数据组织上不需要追求巨大数量。我的经验是每个生成器准备五千到一万张图,真实图一万张左右就够了。图片可以来自公开数据集的未标注子集,但要做随机裁剪和水平翻转,防止模型记住数据集的构图习惯。更重要的是,必须留出“不可见生成器”作为测试集,这是验证泛化性的前提。

还有一个容易忽略的细节:真实图与生成图的数量不要差太远。如果真实图是生成图的十倍,SVD 估计出来的真实子空间会覆盖大部分自然图像变化方向,生成的残差方向被极大压缩,正交化后生成子空间可能只剩几个分量。所以我在每个批次里都保持真图、假图数量接近,这样才能得到公平的子空间估计。

2.3 SVD 计算与正交子空间基的构建

子空间估计用的是 SVD,核心操作其实不复杂。我以真实图特征矩阵为例说明过程。假设真实图特征矩阵为 X,大小是 n×d,n 是图片数,d 是特征维数。先按列中心化得到 X_c,然后计算 SVD:

X_c ≈ U Σ V^T

取 V 的前 k 个列向量作为真实子空间的基 W_true。同理,对生成图特征矩阵 Y 做同样的操作得到生成子空间基 W_fake。不过此时两个基并不一定正交,它们之间有重叠,因为真实图里也可能出现类似生成图的纹理集中现象。

所以要做一步关键操作:把生成子空间的基向量逐一投影到真实子空间上去并减去。具体来说,对 W_fake 的第 i 个列向量 w,计算它在 W_true 上的投影,然后更新:

w_new = w - W_true (W_true^T w)

对所有列做完之后,W_fake 的每个基向量都和真实子空间正交了,同时仍然保留自身的主要变化方向。这一步是整套方法的核心,也是跟普通“域分类”最大的区别。正交化之后,生成子空间里剩下的成分不再包含真实图像常见的语义变化,它更接近纯粹的“生成残留”。

2.4 判别规则和分数融合

子空间构建完之后,判别过程非常轻量。对任意输入图像,先经骨干网络得到特征 x,中心化后计算它在两个子空间上的投影向量:

p_true = W_true^T x p_fake = W_fake^T x

然后定义判别分数:

score = ||p_fake||² - ||p_true||²

得分大于 0,判定为生成图像;小于 0,判定为真实图像。实际使用中我会学一个线性分类器放在这两个投影特征后面,而不是手动定阈值,因为两个投影范数的尺度可能不一样,用一个简单逻辑回归去拟合是更稳的做法。

如果想进一步提高鲁棒性,可以把骨干网络从一层换成多层特征融合。浅层特征包含大量像素级伪影,深层特征包含语义级不自然,两者结合能抵御更多后处理类型。我实验下来,单纯使用层数越多并不总是更好,浅层特征容易受压缩噪声干扰,所以我最终选择倒数第二层输出,兼顾语义和纹理。

另一条经验是:如果训练时的生成器数量很少,建议把子空间维度 k 设置得大胆一些,比如取 64 到 128。生成器少时,特征变化方向也少,维度太低会把区分信息丢掉。如果生成器种类很多,k 可以适当减小,因为大量生成器的共同规律其实很集中,保留 32 维就足够。

3. 实操过程:训练与评估全记录

3.1 环境准备与数据目录结构

整个实验我用的是单张卡的环境,框架用 PyTorch。依赖很少,主要是 torch、torchvision、numpy 和 sklearn,不需要额外装特殊库。数据目录我一般整理成下面这种结构:

data/ train/ real/ gen_a/ gen_b/ gen_c/ gen_d/ test_unseen/ real/ gen_e/ gen_f/

train 下面放可见生成器的假图和真实图,test_unseen 下面放完全没有参与子空间构建的新生成器。这个划分非常关键,因为如果新生成器的图参与过训练,再谈泛化就没有意义了。

我需要提醒一个问题:AI 生成图和真实照片往往混有 EXIF 信息、分辨率差异、文件格式差异等无关变量。如果这些差异在训练和测试之间分布不均匀,模型可能学到的根本不是生成痕迹,而是文件命名习惯或编码质量。我的做法是在数据读取时统一转成 RGB、统一缩放到短边 512 像素、统一保存为 PNG,尽可能让真图和假图经过相同的预处理管线。

3.2 特征提取与子空间构建代码实现

下面这段代码是我实验里最核心的部分,去掉了工程壳子,保留主干逻辑。特征提取部分比较简单,直接加载预训练模型,拿倒数第二层输出就行。

import os import torch import numpy as np from torchvision import transforms, models from torch.utils.data import Dataset, DataLoader class ImageListDataset(Dataset): def __init__(self, path_list, transform=None): self.path_list = path_list self.transform = transform def __len__(self): return len(self.path_list) def __getitem__(self, idx): img = load_img(self.path_list[idx]) # 自行实现:读取图像并转RGB if self.transform: img = self.transform(img) return img def make_transform(): return transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def extract_features(model, loader, device): model.eval() feats, labels = [], [] with torch.no_grad(): for x in loader: x = x.to(device) feat = model(x) # 取倒数第二层输出,需要自定义 hooks feats.append(feat.cpu()) labels.extend([1] * x.size(0)) return torch.cat(feats, dim=0), torch.tensor(labels)

这里我故意省略了模型的 hook 部分,因为不同预训练模型的取层方式不一样。我自己习惯了直接复制特征提取层,把最后的分类头丢掉。用 hook 获取倒数第二层时,记得只保留全局池化之后的向量,不要拿最后一个卷积特征图直接展开,那样维度太高且空间位置信息太碎。

构建子空间的代码如下:

def estimate_subspace(feats, n_components): feats = feats - feats.mean(dim=0, keepdim=True) U, S, Vh = torch.linalg.svd(feats, full_matrices=False) basis = Vh[:n_components].T return basis def orthogonalize(W_fake, W_true): # 使 W_fake 的每一列与 W_true 正交 W_fake = W_fake - W_true @ (W_true.T @ W_fake) # 对 W_fake 再做一次正交化,保证内部基向量不相关 Q, R = torch.linalg.qr(W_fake) return Q

使用的时候先在训练数据集上分别收集两个特征矩阵,然后估计两个子空间,最后做一次正交化。整个过程很快,几千张图的特征矩阵做 SVD 也就几秒到十几秒。

计算判别分数的部分更简单:

def compute_score(feat, W_true, W_fake): feat = feat - feat.mean(dim=0, keepdim=True) p_true = feat @ W_true p_fake = feat @ W_fake score = (p_fake ** 2).sum(dim=1) - (p_true ** 2).sum(dim=1) return score

实验里我给每个子空间选择了 64 个主成分,最后得到的结果已经相当稳定。

3.3 泛化能力应该怎么测

只测同分布数据没有说服力。我建议至少采用三种测试方式:

第一种是“同生成器测试”,也就是训练时见过的生成器,确保实现没有写错,特征提取管线没有 bug。第二种是“跨生成器测试”,用完全没见过的新生成器去测,这是整个文章的重点。第三种是“跨后处理测试”,在跨生成器的基础上再叠加 JPEG 压缩、尺寸缩放、高斯模糊等操作,模拟线上真实环境。

报告指标最好同时给出 AUC、AP 和阈值 0.5 下的准确率。AUC 适合看整体排序能力,AP 适合样本不平衡场景,准确率则直观但受阈值影响大。我做实验常常只用一个指标自我安慰,后来发现跨生成器测试下,AUC 和 AP 的排名会存在分叉,所以至少要同时记两个指标。

跨生成器实验的具体做法是:选定两个完全不参与训练的生成器,把它们的图全部归入测试集,连同真实图一起测;真实图可以和训练集有重复,但为了让测试更严格,最好用另一批独立真实图;这样既能测出模型区分真伪的能力,也能看出模型有没有记住训练图上的一些特有问题。

3.4 从实验结果看正交性到底带来了什么

我第一版实验没有做正交化,只是分别取真实子空间和生成子空间,然后直接拿投影范数差做分类,效果其实也还行,但在跨生成器测试中会虚高一点。做正交化之后,AUC 反而没有明显变化,这让人一度怀疑正交化不重要。后来我看了测试集上具体误判的样本才明白:正交化压低的是误报率,尤其是那些内容非常复杂、包含大量纹理的真实图片,它们容易被判别为“生成图”。正交化把真实内容方向从生成子空间里剔除后,这类误报显著减少。

这个现象解释起来也顺:真实照片内容差异大,特征变化方向非常多;生成图内容虽然有差异,但整体被限制在低维空间。如果不做正交化,两个子空间的重叠部分会被重复计分,导致一个本身也是“真实内容”的方向被当成生成方向。正交化之后,生成子空间里只剩下与真实方向垂直的纯生成痕迹,这样在内容多样性较高的真实图上就能明显减少误判。

从性能上看,我观察到的提升主要是在精准率上,而不是召回率。也就是说,模型还是能稳定地把假图找出来,关键是它不再动辄把真实图冤枉成假图。对线上鉴伪任务来说,误报往往比漏报更致命,所以这一步的价值其实被低估了。

4. 踩坑记录与调参心得

4.1 特征没白化,子空间全被亮度差分占满

第一个大坑是特征中心化与白化不彻底。我第一次跑的时候直接把原始特征丢给 SVD,结果生成子空间的第一主成分几乎就是“全局亮度偏移”。这是因为不同生成器在平均亮度上确实和真实照片有细微差异,而 SVD 会把方差最大的方向排在前面。亮度这种全局统计量虽然也能区分一部分图,但它的泛化性并不好,一旦把图像再次标准化,这些分量就失去意义。

我的解决办法是两步:先做特征标准化,让每个维度均值为 0、方差为 1,再做 PCA 降维到 256 维。标准化保证了亮度、对比度这种幅度信息不会占据主导地位;PCA 则把无关的噪声维度压缩掉。实测下来,跨生成器 AUC 在标准化之后提升了大约两个点。这个操作很便宜,但效果非常显著。

4.2 正交性约束不是越强越好

如果字面意义上的“完全正交”,理论上最严格,把所有重叠分量全部剔除,但实际上不可取。因为特征空间里真实方向和生成方向不是绝对井水不犯河水,它们重叠那一部分里既有内容信息,也可能包含少量的生成痕迹。过度正交化会把这一部分有效信号也丢掉,导致生成子空间变得过于稀疏。表现在实验上就是,训练集 AUC 尚可,但跨生成器测试反而下降。

我最后用的折中方案是只对前若干个主成分强制正交,比如生成子空间的前三分之二做正交化,后面三分之一保留原样。这个做法有点像一个软约束,既剔除了最明显的重叠方向,又留下了必要的容错空间。如果你发现自己的实验里正交化之后假图召回率明显下降,可以考虑调小正交化分量的数量。

4.3 批次大小对 SVD 稳定性影响很大

做 SVD 之前需要把所有特征堆叠成一个矩阵,特征矩阵的行数就是参与统计的图片数量。这里有个隐藏问题:如果参与统计的图片太少,SVD 的结果会有很大随机波动,换一批图片重新做,得到的子空间基向量都变了,自然很难泛化。我最初用小批量特征去估计子空间,结果换一个测试子集分数就抖得厉害。

后来我把参与估计的图片数量提升到每类一万张左右,并在抽取时用了固定随机种子。对大多数真实场景,每个生成器 8000 到 10000 张图已经够用。再多固然更好,但边际收益会快速递减。还有一个小技巧:不要一次性把所有图片的特征全部塞进 GPU,可以先分批次抽取特征存成内存数组,最后统一做 SVD,这样既不爆显存,也不影响计算。

4.4 提高工程效率的几个习惯

整个实验写成熟之后,速度非常快。真正麻烦的是反复跑不同骨干网络、不同主成分数量、不同正交化策略的组合。我自己习惯把每一组配置的输出结果存成单独的 JSON 文件,记录特征提取时间、SVD 时间、各测试集 AUC、AP 以及误报样本路径。否则实验多了根本记不清哪组设置对应哪个结果。

另一个习惯是记录“梯度般的敏感性”。对超参数稍微扰动一下,如果 AUC 抖动超过 0.01,说明当前设置处于不稳定区域。这个信号比单次实验的绝对精度重要得多。一个稳定的方案,哪怕绝对精度不是最高,在真实场景里反而更值得部署。

还有个容易被忽略的地方:预训练骨干网络的影响比想象中的大。同样一套子空间流程,换一个骨干网络之后,最优主成分数量可能要完全重新调。我建议把骨干网络当成最重要的超参数来对待,不要固定在同一个网络上调其他参数,那样很容易调出一组只对当前网络有效的参数。

5. 常见问题排查速查表

实际落地过程中会遇到很多“看起来没道理”的情况,我整理了一张速查表,方便直接对照。

现象可能原因解决建议
跨生成器 AUC 和同分布 AUC 差距过大模型记住了生成器特有指纹增加正交化强度,降低主成分数量,使用更多生成器参与训练
真实图误报率高真实内容方向和生成方向重叠对生成子空间与真实子空间做正交化,尤其是主要方向
换测试子集后分数波动大用于估计子空间的图片太少每类样本增加到 8000 张以上,固定随机种子
压缩后效果断崖式下降特征太依赖高频纹理改选高层语义特征,加入数据增强模拟压缩
生成子空间只有少数几个有效方向不同生成器的共性被低维主成分过滤掉了适当提高保留维度,或用软正交化方案
在某个生成器上特别差该生成器训练数据过少单独多收集该生成器的样本,再重新估计子空间
分数分布极度偏斜两个子空间范数尺度不一致用逻辑回归拟合投影特征,不手动设阈值

除了表格里的问题之外,我更想强调一个宏观思路:不要把正交子空间分解当成一个固定公式,它更像是一个思考框架。你可以把“正交化”这个动作替换成任何想让不同语义方向互相排斥的约束,也可以把单层特征换成多层特征,甚至可以把简单的 SVD 换成在线更新的滑动窗口统计。关键要记住,检测泛化性的前提是去掉把不同生成器绑在一起的“内容共因”,只留下生成机制导致的“方向失衡”。

这个方法后续还可以继续扩展。比如把子空间从固定的一组扩展到按图像内容类别分别维护;或者在每次遇到新生成器时,用极少量的新样本在线校正子空间方向。这些都是我目前正在尝试的方向,目前看下来潜力不小,尤其是“小样本增量更新”的思路,对实际部署会很有帮助。希望这篇实操笔记能让你少走一些我走过的弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进