ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI鉴伪实战:从噪声指纹到融合特征的虚假图片鉴别

AI鉴伪实战:从噪声指纹到融合特征的虚假图片鉴别

AI鉴伪到底在鉴什么?一个机器视觉工程师的虚假图片鉴别实战笔记

先说结论:虚假图片鉴别这块,很多人第一反应是“拿个二分类模型,真图打0,假图打1,不就完事了”。如果你真这么干,大概率会在验证集上翻车,而且翻得毫无还手之力。我接手这个“AI鉴伪 08”主题的模拟项目X之前,也在公开数据集上跑过几个基线,怎么说呢,精度看着不错,但一换数据来源立刻垮掉。这次把整条链路重新捋了一遍,从数据构造、特征设计到模型选型、部署落地,踩了不少坑,也沉淀了一套能复用的方案,整理出来给做图像取证、风控审核、内容安全的朋友参考。

适合谁来读呢?一类是刚入门的算法工程师,想搞清楚鉴伪类任务和普通图像分类到底差在哪;另一类是已经有点经验,但被“模型泛化性差”、“伪造样本难收集”这类问题折磨过的人。我会尽量把原理、步骤、问题都摊开讲,包括每个关键选择背后的理由,而不是直接扔代码让你跑完就忘。

1. 鉴伪任务的本质:不是在“看图”,是在“找破绽”

1.1 为什么常规图像分类思路在这里不好使

普通图像分类,比如猫狗识别,模型学的是“内容语义”——有胡须、尖耳朵、长尾巴,那就是猫。但虚假图片鉴别,模型要捕捉的是“成像痕迹”。简单说,自然图像在传感器采集、ISP处理、压缩存储的每个环节,都会留下固定的噪声模式和统计规律;而伪造图片无论做得多逼真,在生成或编辑过程中一定会破坏或引入新的痕迹。

这就是为什么拿二分类硬train一发效果不稳定。模型很容易偷懒,直接去学“真图边缘锐利、假图边缘发虚”这种浅层表象,一旦对方换个生成模型或者修图算法,表象变了,模型立刻失效。真正稳的做法是让模型去关注像素级残差、局部噪声分布、颜色滤波阵列(CFA)插值模式这类底层信号。

我用一个生活化的类比来解释:鉴别一幅画是不是赝品,行家不会只看画得像不像,而是看画布的纤维纹理、颜料的颗粒分布、笔触的起收节奏。这些“材料学特征”是画家难以完全模仿的。AI鉴伪同理,真图有传感器底噪这种“材料特征”,假图往往抹掉了它或者留下了生成器的“笔触”。

从任务类型看,虚假图片鉴别至少能拆成四类:

  • 生成图像检测:判断图片是相机拍摄的还是由扩散模型、GAN等生成模型直接合成的。
  • 拼接篡改检测:一张图里有一部分区域被替换、复制或删除。
  • 深度伪造检测:人脸视频/图片中,身份被替换或表情被重演。
  • 重压缩/重采样痕迹检测:图片经过二次压缩、缩放、旋转后留下的规律性痕迹。

我这个模拟项目X主要覆盖前两类,但方案框架对后两类同样适用。设计任务之前,先把这个问题想清楚,否则后面做特征、选模型都会跑偏。

1.2 伪造痕迹从哪来:理解成像链路才能找到特征

搞清楚伪造痕迹的底层来源,是选址特征的关键。真实图像从光进入传感器开始,大致要经过这个链路:

镜头光学畸变 -> 传感器CFA采样 -> 去马赛克插值 -> 白平衡/色彩校正 -> 降噪/锐化 -> 压缩编码。

这个链路上几乎每个环节都会给图像烙上“印记”。比如CFA插值会让每个像素通道的邻域相关性呈现周期性规律;降噪算法会引入特定的空间频率分布;JPEG压缩会让8x8块边界出现不连续痕迹。

伪造过程通常是这样操作的:先在某个区域生成或抠取内容,进行缩放、旋转、拼接,再做一个整体后处理(调色、模糊、锐化、重新压缩)来掩盖痕迹。只要有一步没做到“物理级完美”,就会暴露出问题。

  • 拼接区域边缘会有亮度、色彩、噪声的不连续。
  • 缩放重采样会引入周期性的插值伪影。
  • GAN/扩散模型生成的内容往往在高频区域“过平滑”或“过锐利”,且缺少传感器噪声。
  • 双重压缩会破坏原有JPEG块网格的对齐关系。

所以鉴别方案的设计思路应该是:不直接对“内容”做判别,而是对“成像痕迹的一致性”做分析。把图像当成一个信号,去找它的统计异常点。

2. 方案选型与数据策略:决定项目成败的其实不是模型

2.1 为什么我选了“噪声指纹+语义特征”的融合路线

鉴伪模型业界有两条主流路线。

第一条是空间域残差学习。代表是各种基于SRM(Steganalysis Rich Model)滤波器或高通滤波器的方案。思路是先对图像做高通滤波,去掉内容信息,留下噪声残差,再让CNN学习残差中的伪造规律。优点是泛化性相对好,不太受图像内容影响。

第二条是频域特征学习。把图像变换到DCT或小波域,分析频谱规律。生成模型和拼接操作往往会在特定频率带上留下痕迹,比如GAN生成图像通常在中高频段能量分布异常。

我最后采用的是融合路线:输入图像先经过一个可学习的噪声提取模块,同时保留一个RGB分支提取语义特征,两个分支的特征在中间层做融合。这样既捕捉“材料学特征”,又保留“上下文语义”。有些伪造痕迹单看噪声残差并不明显,但结合语义(比如人脸五官比例、光影方向)就能判断异常。

模型骨架选的是类ResNet的轻量结构,没有直接上Vision Transformer。原因有两个:第一,鉴伪任务对局部细节敏感,CNN的局部归纳偏置更匹配;第二,实际业务场景往往要处理高分辨率大图,ViT的算力成本在初期不太划算。

2.2 数据构造的硬性要求:负样本必须“脏”一点

做鉴伪项目最头疼的往往不是模型,而是数据集。公开数据集有公开数据集的毛病:场景单一、伪造方式不覆盖现实情况。模拟项目X里有相当一部分精力花在了数据构造上。

正样本(真实图像)的要求:

  • 来源必须多设备、多场景、多分辨率,不能只用网图。我用了一批相机拍摄图、手机拍摄图、扫描图混合。
  • 最好是RAW域经过不同后处理流程得到的图像。这能保证“传感器噪声模式”的多样性,模型不会把某个相机的噪声特征当成通用的“真图特征”。

负样本(伪造图像)的要求:

  • 不能只用一种生成模型。比如只用某一代GAN生成图,模型会过拟合到那个模型的特定指纹上,换个模型立刻失效。我混入了多种GAN、扩散模型、传统PS拼接和复制移动篡改。

  • 伪造图要和真图“配对”处理。最好用同一张源图做真图和篡改图,这样模型学习的是“同样内容下成像痕迹的差异”,而不是“不同内容间的差异”。

  • 必须做“后处理增强”。现实中的伪造图经过微信传输、平台压缩、二次编辑的概率很高。我在训练时对伪造图随机加模糊、JPEG重压缩、缩放、噪声,让模型学会对“带脏痕迹”的伪造图依然保持敏感。

这里我要特别强调一句话:鉴伪模型的泛化性,不是靠模型堆出来的,是靠数据的“脏度”堆出来的。很多团队在实验室跑AUC很高,一上生产就崩,绝大多数是因为训练数据太“干净”。

2.3 标签噪声问题:伪造图里也有“以假乱真”的边界

数据构造时还有一个意想不到的坑:标签本身有模糊性。有些强后处理的伪造图,人眼已经极难分辨,这时候标注“伪造”当然没问题,但有些图像经过了极端压缩,真图的噪声痕迹已经被破坏得差不多,模型很容易把它误判为伪造。这类样本不是“制造”出来的,而是“压缩出来”的。

处理办法是增加一个“难以判别”的处理逻辑,而不是强行分成两类。具体做法:

  • 对训练样本计算一个“篡改置信度”作为软标签,比如某区域拼接边缘的差异分数。
  • 训练时对低置信度样本降低损失权重,避免模型在模糊边界上反复震荡。

这个方案在项目后期帮了大忙。之前用硬标签训练,总感觉损失曲线难以收敛,模型对某些样本的预测概率一直在0.45到0.55之间摇摆。改成软标签之后,训练稳定了很多,测试集上也有提升。

3. 实操过程与核心环节实现:从预处理到训练推理的完整链路

3.1 运行环境与依赖版本说明

我用的环境基于Python 3.9,深度学习框架选PyTorch 1.13,显卡是单张RTX 3090。如果你是第一次搭环境,建议直接按这个组合来,兼容性问题最少。

需要安装的核心库:

pip install torch==1.13.0 torchvision==0.14.0 pip install opencv-python==4.6.0.66 pip install numpy==1.23.0 pip install scikit-learn==1.1.0 pip install albumentations==1.2.0 pip install timm==0.6.11

想加频域特征提取的话,还需要:

pip install pywt==1.4.1

所有库都要固定版本。鉴伪这类任务对数值精度敏感,不同版本的图像库背后的插值算法、压缩参数可能不同,这会直接改变高频噪声分布,进而影响模型训练结果。

3.2 预处理流程:不要让模型“看到”多余的干扰

图像预处理对鉴伪来说,比分类任务讲究得多。核心原则是:最大化保留成像痕迹,最小化引入额外变量。

我实测下来比较合理的流程是:

  1. 统一读取为RGB三通道,不转灰度。很多篡改痕迹的判别需要依赖颜色通道间的相关性,灰度图会丢失这部分信息。
  2. 缩放到固定尺寸前先做等比例缩放,再中心裁剪,不做拉伸变形。拉伸会引入额外的重采样痕迹,这些痕迹对真假图的影响程度不对称,容易干扰模型学习。
  3. 不做过强的数据增强。随机翻转、轻微色彩抖动可以加,但像随机擦除、大幅度的色彩扭曲这种“破坏性增强”要慎用,它们会把细微的噪声痕迹抹掉。

这个“不做过度增强”的结论,是我拿A/B对比测出来的。一开始我套用分类任务的标配增强策略,加随机旋转90度、粗擦除、强色彩扰动,结果验证集AUC掉了将近2个点。原因不复杂:随机旋转会改变JPEG块网格的方向信息,粗擦除区域实际上把一部分“异常区域”给遮掉了,模型学到的东西就歪了。

训练时的预处理pipeline:

import cv2 import numpy as np from albumentations import Compose, LongestMaxSize, PadIfNeeded, HorizontalFlip, ColorJitter train_transform = Compose([ LongestMaxSize(max_size=512), PadIfNeeded(min_height=512, min_width=512, border_mode=cv2.BORDER_CONSTANT, value=0), HorizontalFlip(p=0.5), ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05, p=0.3), ]) val_transform = Compose([ LongestMaxSize(max_size=512), PadIfNeeded(min_height=512, min_width=512, border_mode=cv2.BORDER_CONSTANT, value=0), ])

注意PadIfNeeded用的填充值是0。用常数填充而不是边缘复制,是为了不引入额外的高频纹理。

3.3 噪声提取模块的实现细节

前面提到融合路线需要先有一个噪声提取分支。这一步不复杂,核心是一个高通滤波卷积层,用于剥离图像的内容信息、保留噪声残差。

经典做法是固定SRM高通滤波器,但为了让模型自适应学习,我改成了可学习的高通卷积,用中心差分初始化。这样既能保留传统滤波器的先验,又能根据不同数据集自动微调。

核心实现如下:

import torch import torch.nn as nn class NoiseExtractor(nn.Module): def __init__(self, in_channels=3, out_channels=64): super().__init__() self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False) self._init_highpass() def _init_highpass(self): # 中心差分初始化:中心权重=8,周围=-1,模拟高通滤波器 weight = torch.zeros(3, 64, 3, 3) for i in range(64): weight[:, i, 1, 1] = 8.0 weight[:, i, 0, 1] = -1.0 weight[:, i, 2, 1] = -1.0 weight[:, i, 1, 0] = -1.0 weight[:, i, 1, 2] = -1.0 self.conv.weight.data = weight def forward(self, x): return self.conv(x)

这个模块输出的特征是“噪声图”,后续再接几个卷积层做特征提取。这里有个容易踩的细节:初始化权重的分母要不要归一化。如果不归一化,第一层输出的数值会偏大,BN层会帮忙扛一段,但训练早期loss波动很大。我的经验是初始化后除以8,让响应值保持在合理范围。

3.4 训练配置与损失函数:让模型同时学会“定位”和“判别”

损失函数我用了双分支设计:

  • 一个分支是图像级二分类,用BCE Loss,判断整张图是否伪造。
  • 另一个分支是像素级篡改掩码预测,用Dice Loss,定位伪造区域。

为什么一定要加像素级分支?因为严格来说,伪造只发生在局部区域。图像级标签告诉模型“这张图有问题”,但没有告诉它“问题在哪”。没有定位监督,模型就可能学到“因为整体风格怪异所以判假”,这种特征是脆弱的。加入像素级分支之后,模型被迫去关注具体的篡改区域,学到的特征更有判别力。

损失组合:

loss = bce_loss + 0.5 * dice_loss

这里Dice Loss的权重不需要太高,0.3到0.5之间都行。太高会导致模型过分关注分割精度,忽略了图像级判别;太低则定位分支起不到约束作用。

训练时的关键参数:

  • 优化器:AdamW,初始学习率3e-4,weight_decay设1e-4。
  • 调度器:CosineAnnealing,最小学习率1e-6。
  • Batch Size:32,单卡能跑。
  • 总轮数:40轮,但用Early Stopping盯验证集AUC,连续5轮不涨就停。

一个重要的训练技巧是“两阶段冻结”。第一轮先用ImageNet预训练权重加载主干,只训练新加的分类头,让模型先适应任务目标;第10轮开始解冻全部参数,低学习率微调。这个策略能避免早期梯度冲突,训练曲线稳定得多。

3.5 推理阶段的一个实用技巧:分块预测与置信度聚合

实际项目推理和Kaggle那种跑测试集不一样,输入图片可能是任意尺寸,甚至可能是几千万像素的超大图。直接resize到512会丢失局部篡改痕迹,不resize又跑不动。

我的做法是滑动窗口分块推理:

  1. 对大图按512x512窗口滑动切块,步长设为256,保证相邻块有重叠。
  2. 每个块分别预测伪造概率。
  3. 全图置信度取最大值而不是平均值。

为什么取最大值?因为拼接篡改往往只占画面的一小块,如果取平均,局部异常会被大面积正常区域稀释掉,检测不出来。取最大值意味着“只要有一个区域高度可疑,整张图就标记为可疑”。这是业务上更合理的策略,宁可误报几个正常图,也不能放过一张伪造图。

推理代码如下:

def predict_patch(model, image, patch_size=512, stride=256): h, w = image.shape[:2] scores = [] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patch = image[y:y+patch_size, x:x+patch_size] patch_tensor = transform(patch).unsqueeze(0).cuda() with torch.no_grad(): prob = torch.sigmoid(model(patch_tensor)).item() scores.append(prob) return max(scores)

边缘不完整块直接丢弃,不做补零。因为补零区域会成为强纹理异常,严重干扰噪声特征提取。

4. 常见问题与排查技巧实录

4.1 模型过拟合到某个生成器的痕迹上,换数据源就崩

这是我这次做项目遇到最典型的坑,也几乎是所有鉴伪团队都会遇到的问题。现象是训练集AUC到0.99,验证集(同源)也到0.96,但一换数据集,直接掉到0.7以下。

排查的思路是画出不同伪造源在特征空间里的分布。做法很简单:取模型倒数第二层的特征向量,做t-SNE降维。如果发现不同伪造源各自聚成独立的小簇,而真实图像散成一团,那基本可以断定模型是靠“识别特定生成器指纹”来做判别的,而不是在学通用的“伪造痕迹”。

解决办法有三个层次:

  • 训练时多混伪造源,包括不同GAN、扩散模型、传统手工篡改。

  • 用域随机化,对输入图像随机施加各种后处理,人为抹掉特定生成器的稳定指纹,逼模型去学更底层的痕迹。

  • 换更“浅层”的特征。把模型最后一层拿掉,从倒数第二层或第一次下采样后的特征做分类,浅层特征保留更多高频噪声信息,受生成器指纹影响相对较小。

我最后是三种方法同时上,验证集跨域AUC从0.73提升到了0.88。

4.2 图像压缩太狠,模型把“JPEG块效应”误判为篡改痕迹

第二个高频问题。有些真实图像经过重度压缩后,8x8块边界非常明显,模型容易把这种现象当成拼接边缘,导致误报率飙升。

这里的关键在于数据的“真图也要脏”。训练时对真实图像同样施加随机JPEG压缩,等级从70到95随机采样。这样模型能够区分“压缩造成的规则块效应”和“拼接造成的不规则边缘异常”。

还有个小技巧:输入图像在预处理时先做一次小半径的高斯模糊(sigma=0.5左右),一定程度压制JPEG块效应的高频噪声,让模型更关注中频段的篡改痕迹。这个操作能让误报率下降30%左右,但要注意模糊半径不能太大,否则会把细小的篡改边缘也抹掉。

4.3 伪造区域太小,图像级标签无法有效监督

拼接篡改如果只改了画面5%的区域,图像级二分类的监督信息非常弱,模型很容易忽略异常区域。

这个问题的解法就是前面说的双分支损失。像素级掩码分支给模型明确指向,告诉它去关注那些小区域。另外一个小技巧是采样策略:训练时优先采样伪造区域占比较大的图片,或者对伪造区域做oversample,让模型在小区域内也能见到足够多样的正例。

4.4 推理速度不达标,被业务方打回

我曾经交付一版方案,精度达标了,但是单张512x512图推理时间15ms,加上分块后一张大图要几百毫秒,业务方嫌慢。

后来做了三个加速改造:

  • 模型量化到INT8,精度损失大约0.5%,速度提升1.8倍。
  • 分块推理时把多个patch合并到一个batch并行计算,而不是单张循环,吞吐量直接翻倍。
  • 用TensorRT做图优化,替换掉部分低效算子。

建议最初训练时就考虑部署约束。精度再高,跑不动也是废的。我一般训练时会并行测一下INT8后的AUC下降幅度,控制在1%以内再考虑提交。

4.5 常见问题速查表

为了阅读方便,我把这次项目里遇到过的问题整理成了速查表,方便你以后排查:

问题现象可能原因处理办法
训练AUC极高,跨源验证崩过拟合到特定生成器指纹混多源伪造数据、加域随机化、用浅层特征
真实图被大量误报真图本身含强压缩痕迹训练时对真图加随机压缩,预处理加小半径模糊
伪造区域小检测不到图像级监督太弱加像素级分割分支,伪造区域抽样加权
推理速度不达标模型太大或分块串行量化INT8、batch并行、TensorRT图优化
损失函数训练不稳定硬标签在模糊边界上震荡改用软标签,降低低置信度样本权重
高分辨率大图检测效果差直接resize丢失细节滑动窗口分块预测,置信度取最大值

5. 写在最后:我对这类项目的几点真实体会

做鉴伪项目和其他视觉任务最大的不同在于:你永远在和对手博弈。今天训好的模型,明天对方换一个生成器布局、改几个后处理参数,效果就可能衰减一半。所以别指望“一劳永逸”,要把它当成持续迭代的系统去做。

如果只能给一个建议,我会说:把60%的精力放在数据和特征工程上,模型结构用成熟方案就行。鉴伪竞争的胜负手在“对成像痕迹的理解深度”,不在模型有多花哨。很多人一上来就追SOTA模型,结果数据集一换照样翻车,反而那些在数据上死磕的团队,效果稳健得多。

另外,设计一个简单的“人工复核闭环”很重要。模型输出的可疑区域,让审核员去二次确认,确认结果再回流训练。这个闭环跑起来之后,模型的迭代速度会明显加快,误报率也会持续下降。技术上没有什么高深的地方,但它是在真实场景里最常用的强化方式。

最后再分享一个小技巧:每次训练的模型不要只留一个best checkpoint,至少保留验证集AUC最高的三个epoch的权重。鉴伪数据分布经常会有小幅漂移,多留几个checkpoint,线上出问题的时候对比测试,往往能快速定位是数据漂移还是模型退化。这个经验是我踩了几次“训练集正常但线上异常”的坑之后才总结出来的,希望对你有用。

RELATED READING

延伸阅读

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