简介面向图像识别初学者压缩包内是一套基于PyTorch的ResNet卷积神经网络训练代码用于完成真假图片的分类识别。整包共7个文件、仅190KB包含3个Python脚本第一个负责扫描数据集文件夹中的图片路径并生成训练/验证txt文件第二个自动读取txt并执行CNN训练第三个提供PyQt图形界面用于展示结果另有说明文档、依赖清单以及两张用于提示图片放置位置的示例图结构一目了然。代码采用逐行注释并适配任意数量的分类文件夹即使自行增加类别也无须修改训练脚本训练时每个epoch会显示进度条、准确率和损失值结束后自动保存日志与模型文件方便对比多轮训练效果。当前已有46人学习下载。注意压缩包不含数据集使用者需按提示图将真/假图片放入对应文件夹再从数据准备到训练完成即可顺畅跑通适合希望快速上手图像分类实战的PyTorch学习者。1. 真假图片识别为什么绕不开 ResNet拿到“通过CNN训练识别真假图片”这样的需求多数人第一反应是找一套现成的分类网络把数据丢进去跑 baseline。实际做下来你会发现普通人脸照片和 AI 生成图片的差异并不像猫狗分类那样体现在整体轮廓上而存在于皮肤纹理、边缘混叠、光照一致性这些非常细微的局部区域。ResNet 这类带残差连接的 CNN 结构恰恰能在不过度加深网络的前提下把这种细粒度差异一层层保留下来所以它成了真假识别项目里最稳的起点也常被用来跟视觉 Transformer 做精度对比。这套方案适合谁适合手头有一批来源复杂的真实图片和生成图片想快速搭一个二分类验证可行性的人也适合想把训练代码、注释和说明文档整理成可交付文件的人。项目压缩包里不含数据集图片意味着你要自己解决数据组织、划分和加载这反而是工程里最容易翻车的部分。本文从数据目录开始把 ResNet 训练全流程拆开讲完并附上逐行注释思路和具体踩坑记录。2. 不含数据集图片的工程自建真假图像目录与训练/验证划分的落地方法压缩包里没有数据集图片不等于数据准备可以跳过。常见做法是让用户按固定目录放置图片训练脚本用 PyTorch 的ImageFolder自动读取。这个步骤看似简单但目录结构一错标签就会全部打乱而且训练过程不会报错只会让准确率卡在 50% 上下。所以先花十分钟把数据组织规则讲清楚比直接写训练循环更值得。2.1 ImageFolder 对目录结构的硬性要求两个文件夹决定二分类标签torchvision.datasets.ImageFolder有一套严格的目录约定根目录下每个子文件夹代表一个类别子文件夹内的所有图片都属于该类。对于真假识别任务我一般是这么组织的data/ ├── train/ │ ├── real/ │ │ ├── real_0001.jpg │ │ └── ... │ └── fake/ │ ├── fake_0001.jpg │ └── ... └── val/ ├── real/ └── fake/在训练代码里加载时只需一行from torchvision import datasets, transforms train_dataset datasets.ImageFolder( rootdata/train, transformtransforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), ]) ) print(train_dataset.class_to_idx) # {fake: 0, real: 1}class_to_idx是ImageFolder按文件夹名字典序生成的映射这里fake为 0、real为 1。如果不关心顺序直接用就行但如果你要在测试阶段单独计算准确率必须保证fake对应的索引和模型输出层第 0 个节点一致否则混淆矩阵会看起来完全颠倒。我在项目里习惯手动固定类别顺序避免后续写评测脚本时还要猜。还有一点需要注意ImageFolder不会检查图片是否损坏。某张图片如果读取失败DataLoader会在训练中途抛PIL.UnidentifiedImageError导致训练中断。稳妥的办法是数据准备阶段加一次完整性检查把坏图直接移动到corrupted/目录而不是等训练时再处理。2.2 用脚本完成按比例划分并固定随机种子没有现成数据集时你拿到的可能是一个混合目录里面真图和假图放在一起。此时要先按文件名或子目录区分再划分为训练集和验证集。推荐写一个独立脚本把划分结果落地成train.txt和val.txt这样后续每次训练用的都是同一份数据结果才能复现。import os import random from collections import defaultdict random.seed(42) data_root data/all_images val_ratio 0.2 # 按标签分桶假设文件名前缀区分real_xxx.jpg / fake_xxx.jpg buckets defaultdict(list) for fname in os.listdir(data_root): if fname.startswith(real_): buckets[real].append(fname) elif fname.startswith(fake_): buckets[fake].append(fname) for label in buckets: random.shuffle(buckets[label]) val_count int(len(buckets[label]) * val_ratio) with open(fval_{label}.txt, w) as f: for fname in buckets[label][:val_count]: f.write(fdata/all_images/{fname}\n) with open(ftrain_{label}.txt, w) as f: for fname in buckets[label][val_count:]: f.write(fdata/all_images/{fname}\n)random.seed(42)是这里的灵魂没有它每次运行得到不同的划分实验对比就失去意义。val_ratio取 0.2 是经验值数据量不足一万张时 20% 的验证集足够稳定。如果你在做人脸伪造识别建议按视频序列或者生成器来源分组而不是把同一批图片随机拆开否则验证集会泄漏身份信息导致准确率虚高。3. ResNet 残差结构如何在真假识别里放大局部伪造痕迹真假图片识别的难点在于伪造痕迹通常出现在局部区域眼睛瞳孔里的不规则高光、头发边缘的锯齿、皮肤纹理的过度平滑。普通 CNN 随着层数加深浅层细节会被逐步丢弃最后全连接层看到的可能只剩下“这个人脸很圆”这类粗粒度特征。ResNet 的残差连接正是为了解决这个问题出现的。3.1 残差块、粗粒度与细粒度特征从瓶颈单元看网络如何“盯住”边缘伪影一个标准的 ResNet 残差块可以写成import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 输入输出维度不一致时用 1x1 卷积调整 self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity self.shortcut(x) out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out identity # 残差相加 out self.relu(out) return out这段代码里out identity是残差网络与普通 CNN 最关键的区别。identity是原始输入out是经过两层卷积后的特征二者相加让梯度在反向传播时多了一条高速公路。对真假识别来说生成图像的人工痕迹可能只在 3x3 像素范围内如果网络在浅层就把它提取到了残差连接可以保证这个信号不会在后续层被“洗掉”。这就是为什么 ResNet 比 VGG 在伪造检测上更受青睐。很多人在调参时只看最终准确率忽略了模型到底在关注什么。ResNet 前几层提取的是边缘、纹理这类细粒度特征后几层叠加成五官布局这类粗粒度特征。真假识别的理想状态是细粒度特征占主导所以冻结前几层不一定会坏事有时反而能逼模型放弃对背景内容的依赖。如果你试过 ResNet 和 Vision Transformer 做对比会发现 ResNet 在数据量只有几千张时更稳因为卷积的归纳偏置不需要太多数据就能学。3.2 预训练权重、冻结与微调resnet 预训练模型真正省时间的地方训练图片只有几千张时从零训练 ResNet50 很容易过拟合。常规方案是加载 ImageNet 预训练权重把最后一层换掉然后只微调后半段。代码里通常这样写import torchvision.models as models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_classes 2 model.fc nn.Linear(model.fc.in_features, num_classes) # 冻结前四层只训练 layer4 和 fc for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad Falseweightsmodels.ResNet50_Weights.IMAGENET1K_V1是新版 torchvision 指定预训练权重的标准写法旧写法的pretrainedTrue会弹 DeprecationWarning。model.fc原来输出 1000 类这里换成 2 类但要注意替换后该层的参数是随机初始化的初始 loss 会比纯微调略高需要稍大一点的学习率来适配。冻结策略不是死的。如果这里的图像是高清人脸冻结layer1、layer2就足够如果是小尺寸截图或带有明显压缩伪影建议只冻结conv1和bn1让后面所有层都参与训练。我一般会用脚本分别跑一组冻结对比观察验证 loss 的差距而不是靠感觉决定。如果你想进一步提升细粒度特征提取能力可以在 ResNet 之后加一个简单自注意力模块对最后一层特征图做通道注意力加权。这在 torch 里就是一行x x.mean(dim[2, 3]) # 全局平均池化 weight torch.sigmoid(self.fc_att(x)).unsqueeze(-1).unsqueeze(-1) x x * weight但要注意加入自注意力后参数量增加且需要更多的训练轮次才能收敛。项目标题里写的是 ResNet所以不建议一开始就改结构先把标准 ResNet 跑通再考虑扩展更合适。4. 训练脚本逐行注释从 dataloader 到 loss 曲线的最小可跑版本这一章提供一个可以直接抄改的训练脚本核心片段并解释每一行的作用和参数选择逻辑。如果你手里就是那个 zip 包里的train.py你会发现大部分注释对应的正是这些内容。4.1 数据增强与归一化真实图片与生成图片的像素分布差异真假识别里很多人直接把 ImageNet 的归一化参数拷过来结果训练总是差一口气。原因是 ImageNet 的均值方差来自自然照片而生成图片的像素分布往往更集中对比度更低。加载数据时应该同时做两件事轻量数据增强提高泛化性归一化参数根据实际数据计算。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # 先用 ImageNet 参数再根据训练集重新计算 ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomHorizontalFlip和RandomRotation对真假识别是安全的增强因为真实照片和生成图片在翻转后真假属性不会变。ColorJitter要慎用过大的亮度扰动会让模型误以为“颜色鲜艳 假图”。如果发现验证集准确率低于训练集 5 个百分点以上优先把ColorJitter的幅度调小。归一化的均值和标准差最好在训练开始前用脚本跑一遍所有训练图片的通道统计量再用真实值替换 ImageNet 默认值。生成图片普遍偏灰用 ImageNet 均值去减会把像素值推到负区间影响第一层卷积的响应。计算方式如下import numpy as np from PIL import Image means np.zeros(3) stds np.zeros(3) count 0 # 假设 img_paths 是训练集图片路径列表 for path in img_paths: img np.array(Image.open(path).convert(RGB)).astype(np.float32) / 255.0 for c in range(3): means[c] img[:, :, c].mean() stds[c] img[:, :, c].std() count 1 means / count stds / count print(real mean/std:, means, stds)这段脚本在数据量大时跑得慢但只跑一次。如果图片数量和原始数据集差不多有数万张也可以抽样 2000 张估算足够用了。4.2 训练循环与验证循环epoch、batch size、学习率和早停训练循环是所有代码里注释密度最高的部分因为参数调节的“玄学”都藏在这里。下面这段是经过简化但结构完整的 PyTorch 训练代码项目 zip 里的train.py核心逻辑基本就是这个骨架。import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct 0.0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() return total_loss / len(loader.dataset), correct / len(loader.dataset) # 模型、损失函数、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)CrossEntropyLoss已经内置了 softmax所以模型最后一层不需要额外加。AdamW相比普通 Adam 在weight_decay实现上更规范推荐优先使用。lr1e-4是微调 ResNet 预训练模型的经验起点如果是从零训练可以尝试3e-4但风险更大容易 loss 不降。CosineAnnealingLR把学习率在一个周期内从初始值余弦降到接近 0适合训练 30 到 50 个 epoch 的场景。训练时应该在每个 epoch 结束后跑一次验证集记录验证 loss只有当验证 loss 连续下降时才保存模型权重这就是“早停”的雏形best_loss float(inf) for epoch in range(30): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device) # 验证集评估 model.eval() val_loss, val_correct 0.0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) val_correct (outputs.argmax(1) labels).sum().item() val_loss / len(val_loader.dataset) val_acc val_correct / len(val_loader.dataset) print(fepoch {epoch}: val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_model.pth) scheduler.step()验证循环里的with torch.no_grad()必不可少它告诉 PyTorch 不需要计算梯度省显存也大幅提高速度。model.eval()和model.train()的切换同样重要否则 BatchNorm 在验证时仍用当前 batch 的统计量结果会非常不稳定。保存模型时用state_dict()而不是整个 model 对象这样换环境加载更安全也避免把 Python 版本相关的序列化信息带上。5. 训练真假识别模型时的 5 个避坑记录下面这 5 个问题来自实际做识别项目时最容易遇到的情况整理成“现象 → 原因 → 解决”以便对照排查。5.1 现象训练 loss 降到 0.2验证 loss 反而升高训练集准确率很快到 98%验证集却只有 80%这是典型的过拟合。原因通常是模型容量过大、训练数据太少或增强不足。ResNet50 在只有几千张图片时即使加了预训练权重也足够把训练集彻底“背下来”。解决思路分几步一是增大增强强度比如加入RandomResizedCrop让模型看到不同尺度二是减少模型容量换成 ResNet18 或 ResNet34 试一次三是加大weight_decay到1e-3。在这些操作后验证 loss 如果开始下降说明方向正确。5.2 现象验证集准确率 95% 以上但一测新生成的图片就翻车这是真假识别项目里最坑的问题。原因多半是数据划分没隔离来源。举例来说如果 1000 张假图都来自同一个生成器模型学到的可能是“这个生成器特有的水印”而不是“假的痕迹”。验证集只要也被这个生成器的图片瓜分准确率自然虚高。解决方法是按生成器或采集设备分组同一生成器产生的图片只能出现在训练集或验证集不能两头都有。代码上可以给每张图片附加一个source_id用GroupShuffleSplit或手工按 source_id 分桶。很多开源数据集已经按来源分好直接使用即可。5.3 现象显卡显存不够batch size 调到 8 还报 OOMResNet50 输入 256x256batch size 为 64 时显存占用接近 10GB这在消费级显卡上很常见。除了换卡还有三个更务实的办法把输入分辨率降到 224x224显存直接减少 20% 以上准确率损失通常在 1% 以内。使用梯度累积虚拟增大 batch size。在循环里每 4 个 batch 反向一次实际效果等同 batch size 扩大 4 倍。开启torch.backends.cudnn.benchmark True对固定输入尺寸能小幅提速。代码层面梯度累积只改两个位置accumulation_steps 4 for i, (images, labels) in enumerate(loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()注意除以accumulation_steps让梯度累积后的平均值等效于单次 batch 的梯度避免损失数值突然变大。5.4 现象加载预训练权重后报size mismatch for fc.weight这个错误很直接model.fc还是 1000 类预训练权重里最后一行是[1000, 2048]而你的model.fc是[2, 2048]形状对不上。解决方法是先替换fc层再加载权重。正确顺序是import torchvision.models as models model models.resnet50(weightsNone) model.fc nn.Linear(model.fc.in_features, 2) state_dict torch.load(resnet50_weights.pth) # 去掉 fc 层的键后加载 state_dict.pop(fc.weight, None) state_dict.pop(fc.bias, None) model.load_state_dict(state_dict, strictFalse)strictFalse允许缺失fc键只加载匹配的层。如果你用的是torchvision的 API建议直接用第 3 章里的weights写法它已经帮你处理好了模型结构和预训练权重配套的问题。5.5 现象loss 一直不降准确率稳定在 50%二分类准确率 50% 意味着模型在瞎猜。先排除最容易忽略的问题标签是否反向。ImageFolder按文件夹名字典序映射如果你把真实图片放在real文件夹但训练代码里误写class_to_idx[fake]那标签就完全相反。排查方法是在训练前打印一批(images, labels)手动确认图像内容与标签对应。另一个常见原因是学习率过大或过小。预训练模型换掉fc层后新层随机初始化学习率太大容易震荡太小则迟迟不收敛。给fc层设置单独的更高学习率是常见做法。也可以先用1e-4训练 5 个 epoch观察 loss 是否下降再决定调大还是调小。如果 5 个 epoch 后 loss 纹丝不动优先检查数据加载和标签而不是继续调学习率。6. 真伪识别的进阶验证用 Grad-CAM 定位伪造区域并校准阈值准确率不是真假识别模型的终点。实际部署时一张图被判定为假固然重要但用户往往更关心“哪里看起来假”。Grad-CAM 可以把 ResNet 最后一层卷积的注意力热力图叠加到原图上直观看到模型依据哪些区域做出判断。from torchvision.transforms.functional import to_pil_image import matplotlib.pyplot as plt # 目标层选 layer4 的最后一个卷积块 model.eval() features dict() def hook_fn(module, input, output): features[feat] output hook model.layer4[-1].register_forward_hook(hook_fn) output model(image.unsqueeze(0)) model.zero_grad() one_hot torch.zeros_like(output) one_hot[0, 1] 1 # 假设类别 1 是 fake output.backward(gradientone_hot) grad model.layer4[-1].weight.grad # 实际取的是 fc 层对 feature map 的梯度 # 对特征图通道求平均得到权重再加权求和得到热力图跑 Grad-CAM 时有两个点容易搞混注册 hook 拿的是 layer4 输出的特征图而 backword 梯度要单独获取该特征图对应的梯度。如果梯度对象拿不到可以换成在hook_fn里同时注册register_full_backward_hook。热力图生成后如果模型判断一张假图靠的是背景文字而不是人脸区域说明训练数据里真图和假图的背景构成有系统性差异需要重新清洗数据。识别模型部署前还要看置信度分布而不是只看分类结果。真实图片与生成图片的分数往往不是完美分成 0 和 1而是一片重叠。先把验证集所有样本的 softmax 输出保存下来画直方图观察两个峰的重叠程度。如果重叠明显把默认阈值从 0.5 改为 0.7低于 0.3 的判为“不确定”只对高置信度结果做自动化处理能显著减少误判带来的运营成本。这个项目的代码结构里其实还应该有一个predict.py做的事情无非是加载best_model.pth对单张图片做和训练时相同的预处理然后输出softmax概率。我在第一次做真伪识别时把阈值调参这一步完全忽略结果在线上被连续的误报折腾到怀疑模型。后来养成的习惯是每次训练完先把验证集的分数分布打印出来再决定阈值设多少。希望这个方法论对你能派上用场——先看分布再定阈值最后再谈准确率。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Apache DolphinScheduler 核心特性深度解析:从可视化 DAG 到去中心化高可用架构 Apache DolphinScheduler 核心特性深度解析:从可视化 DAG 到去中心化高可用架构 【免费下载链接】dolphinscheduler Apache DolphinScheduler is the modern data orchestration platform. Agile to create high performance workflow with low-code 项目地址: ht… · 2026/9/23 10:33:31
Docker 搭建实时监控链路:Filebeat+Kafka+Zookeeper 简介:本资源是一套基于Docker构建的实时监控系统完整工程,面向计算机相关专业在校生、教师及初级大数据开发人员,解决多组件协同下的日志采集、流式处理与可视化监控落地难题,适用于毕业设计、课程设计、项目演示及FlinkSpringBoo… · 2026/9/23 10:33:31
SpreadJS v19.2 正式发布! 我们很高兴地宣布 SpreadJS v19.2 正式发布。本次版本聚焦于帮助开发者构建更贴近 Excel、更适合数据分析、更具协作能力的 JavaScript 电子表格应用。
新版本扩展了「模拟分析」能力,新增方案(Scenario)支持;为设计器功能区组件… · 2026/9/23 10:33:24
权利的游戏第一季迅雷手写实现:3个完整示例搞定项目 权利的游戏第一季迅雷手写实现:3个完整示例搞定项目 看了一堆教程还是不会写项目?别急,问题不在你笨,在于没人给你看 完整示例 。 我见过太多学员,理论背得滚瓜烂熟,一动手就抓瞎。今天这篇,不整虚的,直接上干货。… · 2026/9/23 11:11:03
签到图标避坑指南:拆解前端状态同步核心逻辑 签到图标避坑指南:拆解前端状态同步核心逻辑 版本升级后 API 全变了?别慌,很多开发者在重构老旧项目时,最头疼的不是业务逻辑,而是那些看似简单却暗藏玄机的 UI 状态同步问题。尤其是 签到图标… · 2026/9/23 11:11:03
3步搞定爱普生l383图解原理,拒绝配置卡半天 3步搞定爱普生l383图解原理,拒绝配置卡半天 配置环境就卡半天?爱普生l383驱动装不上,打印测试页全黑,这时候别急着砸打印机。很多开发者在处理打印驱动底层逻辑或嵌入式控制时,往往被“黑盒”状态劝退。今天不聊虚的,直接上 图解原理… · 2026/9/23 11:10:56
激光设备电源系统设计:从负载特性到工程调试的完整指南 1. 激光设备电源系统到底在供什么1.1 从激光器的负载特性说起很多人第一次接触激光设备时,会把注意力全放在光学谐振腔、泵浦源、冷却系统上,觉得电源不过是个“插上就能用”的配角。但实际在产线上待久了就会发现,激光器出光功率不稳、脉冲波… · 2026/9/23 11:10:50
可调试的NLP词云联想实战源码包 简介:本资源是一个面向高校计算机专业初学者的自然语言处理实践项目,聚焦词云生成与语义联想功能实现,适用于NLP入门学习、课程设计参考及AI可视化教学场景。项目基于Python开发,完整包含757个文件,以347张PNG词云效果… · 2026/9/23 11:10:43
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29