正交子空间分解:提升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×dn 是图片数d 是特征维数。先按列中心化得到 X_c然后计算 SVDX_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, transformNone): 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, dim0), torch.tensor(labels)这里我故意省略了模型的 hook 部分因为不同预训练模型的取层方式不一样。我自己习惯了直接复制特征提取层把最后的分类头丢掉。用 hook 获取倒数第二层时记得只保留全局池化之后的向量不要拿最后一个卷积特征图直接展开那样维度太高且空间位置信息太碎。构建子空间的代码如下def estimate_subspace(feats, n_components): feats feats - feats.mean(dim0, keepdimTrue) U, S, Vh torch.linalg.svd(feats, full_matricesFalse) 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(dim0, keepdimTrue) p_true feat W_true p_fake feat W_fake score (p_fake ** 2).sum(dim1) - (p_true ** 2).sum(dim1) 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 换成在线更新的滑动窗口统计。关键要记住检测泛化性的前提是去掉把不同生成器绑在一起的“内容共因”只留下生成机制导致的“方向失衡”。这个方法后续还可以继续扩展。比如把子空间从固定的一组扩展到按图像内容类别分别维护或者在每次遇到新生成器时用极少量的新样本在线校正子空间方向。这些都是我目前正在尝试的方向目前看下来潜力不小尤其是“小样本增量更新”的思路对实际部署会很有帮助。希望这篇实操笔记能让你少走一些我走过的弯路。