ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于ResNet迁移学习的真假图片识别实战:从数据组织到频域增强

基于ResNet迁移学习的真假图片识别实战:从数据组织到频域增强 简介这份资源面向具备一定Python与PyTorch基础、希望入门图像真伪识别实战的开发者与学习者提供了一套基于ResNet与CNN训练识别真假图片的完整代码方案。压缩包共7个文件约190KB包含3个py脚本、2张示例提示图、1个txt依赖文件与1份docx说明文档分别承担数据路径生成、模型训练、界面演示及环境依赖说明等用途。代码不含数据集图片需自行搜集图片放入对应分类文件夹每个文件夹内附有提示图指引放置位置便于快速上手。运行数据路径生成脚本可自动划分训练集与验证集训练脚本会读取文本内容并适配分类文件夹数量即使新增类别也无需改动代码训练过程带进度条每个epoch结束后输出准确率与损失值并保存日志记录。目前已有46人学习适合想掌握CNN图像分类流程、理解数据组织与训练监控细节的读者参考。1. 真假图片识别从 ResNet 迁移学习切入的一条可复现路径手里有一批图片想判断哪些是真实拍摄、哪些是屏幕翻拍或 AI 生成这是内容审核、电商验货、证件核验场景里反复出现的需求。标题里的方案用 ResNet 做骨干网络通过 CNN 训练一个二分类器来识别真假图片并且不含数据集图片、带逐行注释和说明文档。这意味着拿到的是一个可运行的训练框架而不是一份打包好的数据。对一线工程师来说这反而是好事数据自己控标签自己定模型结构透明出了问题能定位到具体层。本文围绕这条路径把 ResNet 迁移学习做真假图片二分类的选型理由、数据组织、训练脚本、参数设置和踩坑记录讲清楚适合已经会跑 Python 和 PyTorch、想快速搭一个可用基线的人。整套流程在单张消费级显卡上就能跑通不需要分布式环境。2. ResNet 做真假图片分类为什么选它而不是从零搭 CNN2.1 真假图片的判别难点与 ResNet 的适配性真假图片分类和常规的猫狗分类不一样。真实拍摄的图片带有传感器噪声、镜头畸变、自然光照渐变屏幕翻拍会引入摩尔纹、色偏、边缘锐化痕迹AI 生成图在频域有周期性伪影、局部纹理过于平滑。这些线索分布在不同的空间尺度上——摩尔纹是高频细节光照不自然是低频全局特征。普通浅层 CNN 感受野有限抓不住全局光照一致性堆太深又容易梯度消失训练不动。ResNet 的残差连接解决了深层网络的退化问题让 50 层甚至 101 层的网络能稳定训练。它的层级结构天然适合这个任务浅层卷积核响应边缘和纹理对应摩尔纹、锐化痕迹深层特征图感受野覆盖整张图对应光照和色彩分布。用 ImageNet 预训练权重初始化浅层的边缘检测器、纹理描述子可以直接复用相当于把自然图像的基础视觉先验免费拿过来。这就是迁移学习的价值——你不需要百万级真假图片就能训出一个可用的分类器几千到几万张标注图通常就能看到明显效果。选 ResNet 而不是 VGG 或 MobileNet还有一个工程上的理由ResNet 的模块化结构清晰BasicBlock 和 Bottleneck 两种块定义明确改起来方便。比如你想在某个 stage 后面接注意力模块或者把最后的全连接层换成多任务头代码改动量很小。MobileNet 虽然轻但在真假图片这种需要捕捉细微频域线索的任务上深度可分离卷积的表达能力有时不够容易欠拟合。2.2 迁移学习的三种策略与选择依据拿到预训练 ResNet 后怎么用它训练自己的数据常见有三种做法。第一种是冻结全部卷积层只训练最后的全连接层。把 ResNet 当固定特征提取器训练快、显存占用小适合数据量很少每类几百张的情况。缺点是特征不针对你的任务调整真假图片的特定线索可能没被充分表达。第二种是冻结浅层微调深层。比如冻结 conv1 到 layer2只训练 layer3、layer4 和全连接层。浅层通用特征保留深层任务相关特征微调。这是最常用的折中方案学习率设小一点通常 1e-4 到 1e-5。第三种是全部层都微调用很小的学习率。数据量足够每类几千张以上时效果最好但训练慢且容易过拟合。我一般先用第二种跑一个基线看验证集准确率和损失曲线。如果欠拟合训练损失降不下去再解冻更多层如果过拟合验证损失回升加数据增强或冻结更多层。这个决策过程比一上来就全量微调要稳。2.3 用 torchvision 加载 ResNet 并替换分类头下面这段代码完成三件事加载预训练 ResNet50、冻结浅层、替换全连接层为二分类输出。逐行注释说明每个参数的作用。import torch import torch.nn as nn from torchvision import models def build_model(num_classes2, freeze_layersTrue): # 加载 ImageNet 预训练的 ResNet50weights 参数指定预训练权重版本 # 如果环境无法联网下载可先手动下载权重文件再指定路径 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 冻结浅层conv1、bn1、layer1、layer2 的参数不参与梯度更新 # 这些层提取的是通用边缘、纹理特征对真假图片任务同样有效 if freeze_layers: for name, param in model.named_parameters(): if name.startswith((conv1, bn1, layer1, layer2)): param.requires_grad False # 替换全连接层原版输出 1000 类改成 2 类真/假 # in_features 是 ResNet50 最后一层池化后的通道数固定为 2048 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(p0.5), # 随机丢弃 50% 神经元抑制过拟合 nn.Linear(in_features, 256), # 降维到 256减少参数量 nn.ReLU(inplaceTrue), # 非线性激活 nn.Dropout(p0.3), # 再丢弃 30% nn.Linear(256, num_classes) # 输出 2 类 logits ) return model # 实例化并打印可训练参数数量确认冻结生效 model build_model(num_classes2, freeze_layersTrue) trainable sum(p.numel() for p in model.parameters() if p.requires_grad) total sum(p.numel() for p in model.parameters()) print(f可训练参数: {trainable:,} / 总参数: {total:,})逻辑说明named_parameters()返回参数名和参数张量通过前缀匹配冻结指定层。替换后的分类头用了两层全连接加 Dropout比直接Linear(2048, 2)多一层非线性变换在真假图片任务上通常能提升 1 到 2 个百分点。inplaceTrue的 ReLU 节省少量显存。打印可训练参数是为了确认冻结是否生效——ResNet50 总参数约 2550 万冻结浅层后应降到 2000 万以下。参数说明weights参数在新版 torchvision 中替代了旧的pretrainedTrue指定IMAGENET1K_V2使用改进的预训练权重。Dropout的 p 值根据数据量调整数据少就调大。num_classes保持 2如果要做多分类比如真图、翻拍、AI 生成三类改这个值即可。3. 数据组织与训练脚本从文件夹结构到第一个 checkpoint3.1 不含数据集图片时怎么组织自己的数据标题明确说不含数据集图片所以你需要自己准备数据。真假图片二分类的数据组织遵循 ImageFolder 约定根目录下两个子文件夹分别放两类图片。dataset/ ├── real/ # 真实图片 │ ├── 0001.jpg │ ├── 0002.png │ └── ... └── fake/ # 假图片翻拍、AI 生成、篡改 ├── 0001.jpg ├── 0002.png └── ...真实图片可以来自自己拍摄、公开数据集中的自然图像。假图片的来源要明确如果是屏幕翻拍用手机对着显示器拍如果是 AI 生成用生成模型批量产出如果是篡改用图像编辑工具处理。关键是两类图片的采集条件要尽量一致避免模型学到与真假无关的捷径特征。比如真实图片全是白天拍的假图片全是晚上拍的模型可能学会看亮度而不是看真假。数据量建议每类至少 500 张理想情况 2000 到 5000 张。划分训练集、验证集、测试集比例 7:1.5:1.5 或 8:1:1。验证集用于调参和早停测试集只在最后评估一次。3.2 数据增强哪些增强对真假识别有效哪些会帮倒忙数据增强是防止过拟合的关键但不是所有增强都适合真假图片任务。有效的增强随机水平翻转、小角度旋转±10 度、随机裁剪从原图裁 224×224、轻微色彩抖动亮度、对比度、饱和度各 ±0.1。这些模拟了拍摄时的自然变化不破坏真假线索。要慎用的增强强高斯模糊会抹掉摩尔纹和频域伪影这些恰恰是判别假图的重要线索。JPEG 压缩增强会引入新的压缩伪影和翻拍伪影混在一起让模型困惑。强色彩偏移比如把色调转 180 度会破坏光照一致性线索。我一般用 torchvision 的 transforms 组合训练时用带增强的 pipeline验证和测试时只做 resize 和归一化。from torchvision import transforms # 训练集增强翻转、旋转、裁剪、轻微色彩抖动 train_transform transforms.Compose([ transforms.Resize((256, 256)), # 先缩放到 256再随机裁 224 transforms.RandomCrop(224), # 随机裁剪模拟构图变化 transforms.RandomHorizontalFlip(p0.5), # 50% 概率水平翻转 transforms.RandomRotation(degrees10), # ±10 度旋转 transforms.ColorJitter( brightness0.1, # 亮度抖动 ±10% contrast0.1, # 对比度抖动 ±10% saturation0.1 # 饱和度抖动 ±10% ), transforms.ToTensor(), # 转成张量像素值归一化到 [0,1] transforms.Normalize( mean[0.485, 0.456, 0.406], # ImageNet 均值 std[0.229, 0.224, 0.225] # ImageNet 标准差 ) ]) # 验证/测试集只做 resize 和归一化不做随机增强 val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ])逻辑说明Resize(256)加RandomCrop(224)是标准做法先放大再裁剪让每次看到的区域略有不同。Normalize用的均值和标准差必须和预训练模型一致否则预训练权重的统计分布对不上效果会下降。验证集不做随机增强保证评估结果可复现。参数说明RandomRotation的角度不要超过 15 度太大旋转会引入黑边且真实拍摄很少有大角度倾斜。ColorJitter的幅度控制在 0.1 到 0.2再大就可能破坏真假线索。3.3 训练循环损失函数、优化器与学习率调度真假图片二分类用交叉熵损失优化器用 AdamW 或 SGD。AdamW 对学习率不那么敏感适合快速实验SGD 加动量在调好后泛化略好但需要更仔细地调学习率。我一般先用 AdamW 跑基线。import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 加载数据集 train_dataset ImageFolder(rootdataset/train, transformtrain_transform) val_dataset ImageFolder(rootdataset/val, transformval_transform) train_loader DataLoader( train_dataset, batch_size32, # 显存不够就降到 16 或 8 shuffleTrue, # 训练集打乱 num_workers4, # 数据加载线程数根据 CPU 核数调整 pin_memoryTrue # 锁页内存加速 GPU 传输 ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, # 验证集不打乱 num_workers4, pin_memoryTrue ) # 设备选择 device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes2, freeze_layersTrue).to(device) # 损失函数交叉熵label_smoothing 缓解过拟合 criterion nn.CrossEntropyLoss(label_smoothing0.1) # 优化器只优化 requires_gradTrue 的参数 optimizer optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, # 微调学习率比从头训练小一个量级 weight_decay1e-4 # L2 正则抑制过拟合 ) # 学习率调度余弦退火从 1e-4 降到 1e-6 scheduler optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, # 周期设为总 epoch 数 eta_min1e-6 # 最小学习率 ) # 训练循环 best_acc 0.0 patience 5 # 早停耐心值 counter 0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() # 清空梯度 outputs model(images) # 前向传播 loss criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss loss.item() * images.size(0) scheduler.step() # 更新学习率 train_loss running_loss / len(train_dataset) # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1:02d} | Train Loss: {train_loss:.4f} | Val Acc: {val_acc:.4f}) # 保存最佳模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(f早停触发最佳验证准确率: {best_acc:.4f}) break逻辑说明filter(lambda p: p.requires_grad, ...)确保优化器只更新未冻结的参数冻结层的梯度不会被计算。label_smoothing0.1把硬标签 0/1 变成 0.05/0.95防止模型对训练样本过度自信在真假图片这种边界模糊的任务上有帮助。余弦退火让学习率平滑下降比阶梯下降更稳定。早停机制在验证准确率连续 5 个 epoch 不提升时终止训练避免过拟合。参数说明batch_size根据显存调整ResNet50 在 8GB 显存上 batch_size 32 通常够用。lr1e-4是微调的常用起点如果损失震荡就降到 5e-5。weight_decay1e-4是 AdamW 的推荐值。T_max设为总 epoch 数让学习率在一个完整周期内降到最低。4. 真假图片识别的避坑与排查五条血泪经验4.1 验证集准确率虚高测试集一塌糊涂现象训练时验证集准确率到 95%换一批新数据测试只有 60% 多。原因验证集和训练集来自同一批数据划分时没有考虑采集条件。比如同一部手机拍的图片被随机分到训练和验证集模型学到了手机型号相关的特征而不是真假线索。或者数据增强只用在训练集验证集分布和训练集不一致。解决按采集批次划分数据集同一批次的数据要么全在训练集要么全在验证集。如果假图片来自多个生成模型确保每个模型的图片在训练和验证集中都有。验证集的 transform 要和训练集保持一致除了随机增强部分。4.2 模型把屏幕边框当成了判别依据现象翻拍图片的识别准确率很高但把翻拍图裁剪掉边框后准确率骤降。原因翻拍图片往往带有显示器边框、桌面背景等非内容区域模型学会了看边框而不是看图片内容。这是典型的捷径学习。解决数据预处理时统一裁剪到图片内容区域去掉边框。或者在训练时用 RandomCrop 强制模型关注中心区域。更彻底的做法是收集不带边框的翻拍样本让模型必须依赖摩尔纹、色偏等内容线索。4.3 损失不下降梯度全是零现象训练几个 epoch损失一直在 0.69 附近二分类的随机水平准确率 50%。原因冻结层设置错误把不该冻的层冻了或者优化器没有拿到可训练参数。另一种可能是学习率太小参数几乎不动。解决打印可训练参数数量和名称确认全连接层和深层卷积在列。检查优化器的参数列表是否为空。把学习率调大一个量级试试比如从 1e-5 调到 1e-4。如果还是不动检查数据标签是否正确有没有全部标成同一类。4.4 显存溢出batch_size 降了还是爆现象CUDA out of memory把 batch_size 从 32 降到 8 还是报错。原因没有释放中间变量或者验证时没有用torch.no_grad()计算图一直在累积。另一种可能是图片分辨率太高224 的输入实际被 resize 到了 512。解决验证和测试循环必须包在with torch.no_grad():里。训练循环中loss.item()而不是直接用loss避免持有计算图。检查 transforms 里的 Resize 尺寸确保和模型输入匹配。如果还不行用torch.cuda.empty_cache()手动清理缓存。4.5 假图片类型单一模型只认一种假现象训练时用的假图片全是屏幕翻拍上线后遇到 AI 生成图完全识别不了。原因训练数据的假图片只覆盖了一种伪造方式模型学到的决策边界只针对翻拍特征。解决假图片要覆盖多种类型屏幕翻拍、AI 生成、图像拼接、重压缩。每类至少几百张。如果某类数据难获取可以用数据增强模拟比如对真实图片做重压缩、加噪声、调色偏来生成伪假样本。但要注意增强生成的假样本和真实假样本的分布差异最好混合使用。5. 进阶技巧用频域特征和测试时增强把准确率再推一截5.1 频域分支给 ResNet 加一条 FFT 输入通道真假图片在频域的区别往往比空域更明显。AI 生成图在频域有周期性峰值翻拍图有显示器刷新率相关的频率成分。可以在 ResNet 之外加一个频域分支对输入图片做二维 FFT取幅度谱送进一个小 CNN最后和 ResNet 的空域特征拼接。import torch.fft class FreqBranch(nn.Module): def __init__(self): super().__init__() # 输入是单通道幅度谱输出 256 维特征 self.conv nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1) # 全局平均池化输出 128 维 ) self.fc nn.Linear(128, 256) def forward(self, x): # x: [B, 3, H, W]转灰度后做 FFT gray x.mean(dim1, keepdimTrue) # [B, 1, H, W] fft torch.fft.fft2(gray) # 复数频谱 amp torch.abs(fft) # 幅度谱 amp torch.log1p(amp) # 对数压缩动态范围太大 amp torch.fft.fftshift(amp, dim(-2, -1)) # 低频移到中心 feat self.conv(amp) # [B, 128, 1, 1] feat feat.flatten(1) # [B, 128] return self.fc(feat) # [B, 256]逻辑说明torch.fft.fft2对灰度图做二维傅里叶变换torch.abs取幅度谱。log1p做对数压缩因为频谱动态范围极大不压缩的话大部分值接近零。fftshift把零频移到中心方便卷积核处理。频域分支输出的 256 维特征和 ResNet 的 2048 维特征拼接后送进分类头。参数说明频域分支的卷积核数量可以调整32/64/128 是轻量配置。如果频域线索重要可以加大到 64/128/256。AdaptiveAvgPool2d(1)把任意尺寸的特征图压成 1×1保证全连接层输入固定。5.2 测试时增强多尺度多裁剪投票测试时增强TTA是在推理阶段对同一张图做多种变换分别预测后取平均。对真假图片任务常用的 TTA 包括原图、水平翻转、多尺度缩放224、256、288、中心裁剪和四角裁剪。def predict_with_tta(model, image, device): 对单张图片做 TTA 预测返回平均概率 model.eval() probs [] # 定义多种变换 tta_transforms [ transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((288, 288)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])]), ] with torch.no_grad(): for t in tta_transforms: inp t(image).unsqueeze(0).to(device) # 加 batch 维度 out model(inp) prob torch.softmax(out, dim1) # 转概率 probs.append(prob) avg_prob torch.mean(torch.stack(probs), dim0) # 平均 return avg_prob逻辑说明对每种变换分别推理softmax转成概率后取平均。多尺度覆盖不同大小的真假线索水平翻转消除方向偏差。TTA 通常能提升 1 到 3 个百分点代价是推理时间乘以变换数量。参数说明尺度选择根据训练时的输入尺寸训练用 224 就测 224、256、288。变换数量控制在 4 到 8 个太多收益递减。如果延迟敏感只用原图和水平翻转两个。5.3 一个我反复用的验证习惯每次训完模型我不会只看测试集准确率。我会单独看假图片的召回率和真图片的精确率。在内容审核场景里漏掉假图假图被判成真图的代价通常比误杀真图高。所以我会调整分类阈值让假图的召回率优先。具体做法是在验证集上画 P-R 曲线选一个召回率 95% 以上的阈值再看精确率能不能接受。这个习惯帮我避免了好几次“准确率很好看但上线就翻车”的情况。模型文件里的说明文档如果只写了准确率没有混淆矩阵和阈值建议用起来是要打折扣的。希望帮到你。本文还有配套的精品资源点击获取
返回列表