
简介这份资源面向具备Python与PyTorch基础、希望上手图像分类实战的开发者与学习者聚焦用CNN训练ResNet模型识别图片真伪这一典型场景。压缩包共7个文件约190KB包含3个py脚本、2张示例图、1个txt依赖清单和1份docx说明文档脚本分别负责生成数据索引、CNN训练与PyQt界面演示说明文档提供逐行注释与使用指引。资源不含数据集图片需自行搜集并按文件夹分类放置代码已适配分类文件夹数量变化无需改动即可训练训练过程带进度条并输出每轮准确率与损失值结束后保存日志与模型权重。目前已有46人学习下载适合想快速跑通图像识别流程、理解数据组织与训练监控细节的读者参考。1. 真假图片识别为什么用 ResNet 而不是自己搭 CNN电商平台每天要拦掉大量 AI 生成的商品图社交产品要判断用户头像是不是网图二手交易要识别卖家上传的实拍图有没有被 PS 过。这些场景的共同点是你手上只有一堆「真图」和「假图」的文件夹没有标注框没有分割掩码就是一个二分类问题。标题里这个「resnet模型-通过CNN训练识别真假图片」讲的就是这件事——用 ResNet 预训练模型做迁移学习把真假图片分类跑通附带逐行注释和说明文档但不含数据集图片需要你自己准备数据。为什么不是从零搭一个 CNN我试过。三层卷积加两层全连接在几千张图上训练准确率卡在 70% 上下晃验证集 loss 跟过山车一样。换成 ResNet18 预训练权重同样的数据三轮就上 90%。原因不玄学ResNet 的残差结构让梯度能跨层回传预训练权重已经把 ImageNet 上千万张图的纹理、边缘、色彩分布学到了你只需要微调最后的分类头。对于真假图片这种「纹理级差异」的任务底层特征复用率极高。这个方案适合谁适合手上有几百到几万张标注图、想快速验证一个真假识别想法的人。不适合指望零样本直接上线的场景也不适合图片里真假差异只在语义层面比如「这张图里的猫是画的还是拍的」的情况——那种得换 CLIP 类方案。下面从数据准备一路讲到推理部署中间会重点说清楚逐行注释里那些参数到底在调什么。2. 数据准备与 ResNet 迁移学习的前置判断2.1 真假图片的数据集怎么组织才不翻车标题里明确写了「不含数据集图片」意味着你得自己攒数据。真假图片识别最怕的不是数据少是数据分布偏。我见过一个项目真图全是手机实拍假图全是 StyleGAN 生成模型学到的其实是「有没有手机噪点」而不是「是不是生成的」。换一批相机拍的假图直接崩。目录结构按 ImageFolder 的标准来这是最省事的做法dataset/ ├── train/ │ ├── real/ # 真图建议 500 张起步 │ │ ├── 001.jpg │ │ └── ... │ └── fake/ # 假图数量与真图尽量 1:1 │ ├── 001.jpg │ └── ... ├── val/ │ ├── real/ │ └── fake/ └── test/ ├── real/ └── fake/训练集、验证集、测试集按 7:1.5:1.5 切。注意验证集和测试集必须来自不同的生成批次或不同的拍摄设备否则你测出来的准确率是虚高的。真图来源尽量杂不同手机、不同光照、不同压缩质量。假图来源也要杂不同生成模型、不同分辨率、有没有经过二次压缩。提示真假比例严重失衡时比如真图 5000 张、假图 300 张不要直接上采样先试试给假图类别加权CrossEntropyLoss(weighttorch.tensor([1.0, 5.0]))这种比复制图片温和。2.2 ResNet 预训练权重选哪个版本ResNet 有 18、34、50、101、152 几个常见深度。真假图片任务里我一般从 ResNet18 或 ResNet34 起步。原因假图检测的判别特征集中在纹理和频域异常不需要太深的语义抽象。ResNet50 参数量是 18 的 3 倍多在小数据集上更容易过拟合训练也慢。版本参数量适合数据量真假识别经验ResNet1811M5005000首选快够用ResNet3421M200010000比 18 稳一点ResNet5025M10000数据少时容易过拟合ResNet10144M50000真假任务一般用不上预训练权重用 torchvision 自带的ResNet18_Weights.IMAGENET1K_V1就行。不要用随机初始化那是给自己找麻烦。加载方式后面代码里会写。2.3 数据增强里哪些能用哪些会帮倒忙真假图片识别有个反直觉的点颜色抖动ColorJitter要慎用。假图往往在色彩统计上有细微异常你一抖动这个信号就被抹掉了。水平翻转可以用随机裁剪可以用但不要用太激进的旋转和缩放。我一般用这套from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), # 统一尺寸ResNet 输入 224 transforms.RandomCrop(224), # 随机裁剪到 224 transforms.RandomHorizontalFlip(p0.5), # 水平翻转真假图都适用 transforms.ToTensor(), # 转 tensor归一化到 [0,1] transforms.Normalize( # ImageNet 统计量 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ), ]) 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,256))再RandomCrop(224)是标准做法给裁剪留空间。Normalize的均值和方差必须和预训练权重匹配不然底层特征分布对不上微调效果打折。验证集不要做随机增强保证每次评估一致。3. 用 ResNet 搭真假图片分类器的完整代码与逐行说明3.1 模型改造把 ResNet 的 fc 层换掉ResNet18 原始输出是 1000 类我们要改成 2 类。改法很简单但有几个细节容易错。import torch import torch.nn as nn from torchvision import models def build_model(num_classes2, freeze_backboneTrue): # 加载预训练 ResNet18weights 参数指定用 ImageNet 权重 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结 backbone 参数只训练最后的 fc 层 if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换 fc 层原版是 512 - 1000改成 512 - num_classes in_features model.fc.in_features # 512 model.fc nn.Sequential( nn.Dropout(p0.3), # 防过拟合小数据集必加 nn.Linear(in_features, num_classes) ) return modelfreeze_backboneTrue是第一阶段策略只训练 fc 层学习率可以设大一点1e-3。跑几轮后如果验证集准确率不动了再解冻后面几层做微调学习率降到 1e-4。Dropout(0.3)在数据量小于 2000 时很关键我试过不加训练集准确率 99% 验证集 75%加了之后差距缩到 5% 以内。3.2 训练循环损失函数、优化器和学习率调度import torch.optim as optim from torch.optim.lr_scheduler import StepLR from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 数据加载 train_dataset ImageFolder(dataset/train, transformtrain_transform) val_dataset ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 设备 device torch.device(cuda if torch.cuda.is_available() else cpu) # 模型 model build_model(num_classes2, freeze_backboneTrue).to(device) # 损失函数交叉熵类别不平衡时加 weight criterion nn.CrossEntropyLoss() # 优化器只优化 requires_gradTrue 的参数 optimizer optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4 ) # 学习率调度每 5 个 epoch 降一半 scheduler StepLR(optimizer, step_size5, gamma0.5) # 训练循环 for epoch in range(20): model.train() running_loss 0.0 correct 0 total 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() _, predicted outputs.max(1) # 取最大 logit 的索引 total labels.size(0) correct predicted.eq(labels).sum().item() scheduler.step() # 更新学习率 train_acc 100. * correct / total print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Train Acc: {train_acc:.2f}%) # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted outputs.max(1) val_total labels.size(0) val_correct predicted.eq(labels).sum().item() val_acc 100. * val_correct / val_total print(fVal Acc: {val_acc:.2f}%)filter(lambda p: p.requires_grad, ...)这行很重要。冻结 backbone 后如果把所有参数都传给优化器虽然梯度是 None 不会更新但 Adam 的动量状态会浪费显存。StepLR的step_size5, gamma0.5意思是每 5 轮学习率乘 0.5。真假图片任务一般 1520 轮就收敛了再训下去验证集准确率反而掉。batch_size32是 8G 显存下的安全值。显存够可以上 64训练更稳。num_workers4在 Windows 上有时会报错改成 0 就行代价是数据加载慢一点。3.3 微调阶段解冻哪些层、学习率怎么设第一阶段跑完验证集准确率如果卡在 85% 左右不动了进入微调。解冻layer4和fc学习率降到 1e-4。# 解冻 layer4 for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True # 重新定义优化器学习率降一个数量级 optimizer optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay1e-4 ) scheduler StepLR(optimizer, step_size3, gamma0.5) # 继续训练 10 轮 for epoch in range(10): # ... 同上训练循环 pass为什么只解冻layer4ResNet18 的layer1到layer3学的是边缘、纹理、局部形状这些对真假图片通用。layer4学的是高层语义需要适配你的数据分布。全解冻容易把预训练权重带偏尤其是数据量小于 1000 的时候。微调阶段的学习率不能大1e-4 是上限。我试过 5e-4验证集 loss 直接震荡前几轮的成果全白费。weight_decay1e-4保持正则化强度防止微调时过拟合。4. 真假图片识别训练中的避坑与排查4.1 验证集准确率远高于测试集现象训练时验证集 95%换测试集只有 70%。原因验证集和测试集的数据来源不同或者验证集在切分时混入了和训练集同批次的图片。真假图片识别里同一批生成的假图往往有相似的伪影模式模型记住了这批模式换一批就失效。解决切分数据时按「生成批次」或「拍摄设备」分组同一组的图片只能出现在一个集合里。用sklearn.model_selection.GroupShuffleSplit按组切分不要用随机切分。4.2 训练 loss 不降一直卡在 0.69现象loss 在 0.69 附近不动准确率 50%。原因0.69 是二分类交叉熵在随机猜测时的值-ln(0.5)。说明模型没学到任何东西。常见原因是Normalize的均值和预训练权重不匹配或者标签映射反了real 和 fake 的文件夹顺序导致 label 0/1 颠倒但模型输出也跟着颠倒理论上不影响但如果评估代码写死了类别名就会出错。解决检查transforms.Normalize的 mean/std 是不是 ImageNet 的。检查ImageFolder.class_to_idx的输出确认 real 和 fake 对应的索引。如果标签没问题把学习率调大 10 倍试一轮看 loss 有没有变化。4.3 GPU 显存够但训练速度极慢现象batch_size328G 显存只用了 3G但一个 epoch 要跑 10 分钟。原因num_workers设成了 0数据加载在主线程里串行执行。或者图片分辨率太大Resize之前是 4000x3000解码耗时。解决num_workers设成 CPU 核数的一半Linux 下 48 都行。图片在训练前先统一缩放到 512 长边存一份不要每次从原图解码。用transforms.Resize之前加一个transforms.Resize(512)做预缩放或者离线处理。4.4 假图检测率很高但真图误杀严重现象假图召回 98%真图准确率只有 80%大量真图被判成假。原因训练时假图样本太单一模型学到了「不像这批假图的都是真图」的决策边界遇到没见过的真图比如新手机拍的就判假。或者类别权重设得太偏向假图。解决增加真图的多样性不同设备、不同压缩、不同光照。检查CrossEntropyLoss的 weight 参数如果给假图加了 5 倍权重先降到 2 倍试试。另外推理时不要只取 argmax看 softmax 概率设置一个阈值比如假图概率 0.8 才判假给真图留缓冲。4.5 模型在验证集上表现好但推理时结果随机现象验证集准确率 90%部署成 API 后同一张图两次请求结果不一样。原因推理时忘了model.eval()Dropout 和 BatchNorm 还在训练模式。BatchNorm 在训练模式用当前 batch 的统计量单张图推理时统计量就是它自己输出完全乱套。解决推理前必须model.eval()并且用torch.no_grad()包住。如果用了Dropouteval 模式下会自动关闭。BatchNorm 在 eval 模式下用训练时累积的 running_mean 和 running_var所以训练时batch_size不能太小否则 running 统计量不准。5. 推理部署与真假图片识别的进阶技巧5.1 单张图片推理的完整代码from PIL import Image import torch.nn.functional as F def predict(image_path, model, device): model.eval() # 必须关闭 Dropout/BatchNorm 训练行为 img Image.open(image_path).convert(RGB) img_tensor val_transform(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits model(img_tensor) probs F.softmax(logits, dim1) # 转概率 fake_prob probs[0][1].item() # 假设索引 1 是 fake return fake_prob # 使用 model build_model(num_classes2, freeze_backboneFalse) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.to(device) prob predict(test.jpg, model, device) print(f假图概率: {prob:.4f})unsqueeze(0)补上 batch 维度因为模型定义时输入是 4 维。F.softmax把 logits 转成概率方便设阈值。map_locationdevice在 CPU 推理时避免加载 GPU tensor 报错。5.2 用 TTA 把准确率再抬 2 个点测试时增强TTA对真假图片识别很有效。同一张图做几次不同变换取平均概率。def predict_tta(image_path, model, device): model.eval() img Image.open(image_path).convert(RGB) tta_transforms [ val_transform, 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]) ]), ] probs [] with torch.no_grad(): for t in tta_transforms: img_tensor t(img).unsqueeze(0).to(device) logits model(img_tensor) probs.append(F.softmax(logits, dim1)[0][1].item()) return sum(probs) / len(probs)水平翻转 TTA 对真假识别特别管用因为假图的伪影往往在特定方向上有规律翻转后模型能看到不同视角。我实测在 ResNet18 上TTA 能把测试集准确率从 91% 抬到 93% 左右。代价是推理时间翻倍实时性要求高的场景慎用。5.3 模型保存与加载的坑保存时用torch.save(model.state_dict(), best_model.pth)不要保存整个模型对象。保存整个模型会把类定义也序列化进去换环境加载时经常报Cant get attribute build_model。加载时先实例化模型结构再load_state_dict。如果改了fc层的结构加载旧权重会报 key 不匹配。这时候用strictFalse但要注意fc层的权重会随机初始化必须重新训练。# 保存 torch.save(model.state_dict(), best_model.pth) # 加载 model build_model(num_classes2, freeze_backboneFalse) state_dict torch.load(best_model.pth, map_locationcpu) model.load_state_dict(state_dict, strictTrue) # 结构一致时用 True5.4 真假图片识别的边界在哪里ResNet 做真假图片识别能解决的是「纹理级、频域级」的伪造检测。对于经过强压缩、强滤波的假图或者生成模型已经迭代到伪影极小的版本ResNet18 的准确率会明显下降。这时候需要上频域特征DCT、FFT或者换更大的模型。我自己的习惯是先用 ResNet18 跑一个 baseline如果测试集准确率低于 85%不要急着换模型先检查数据质量和切分方式。大部分时候问题出在数据上不是模型上。数据干净、切分合理ResNet18 足够撑起一个可用的真假图片识别服务。希望帮到你。本文还有配套的精品资源点击获取