ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深度学习实战:基于PyTorch搭建人脸真伪二分类模型

深度学习实战:基于PyTorch搭建人脸真伪二分类模型 “鉴定伪人”在图像安全领域是一个严肃课题判断一张人脸照片是真实拍摄还是由生成模型伪造。随着扩散模型和生成对抗网络的发展普通人已经很难用肉眼分辨一张高分辨率人脸图像的真假于是“伪人图像鉴定”就变成一个工程问题。这篇文章会从零搭建一个可运行的人脸真伪二分类模型用它区分真实人脸和AI合成人脸包含数据准备、模型训练、单图推理和常见问题排查。文章假设你具备基础的 Python 和 PyTorch 使用经验不需要接触过深度伪造检测。读完可以自己准备数据集训练一个能区分真实人脸与合成人脸的分类器并知道如何把它封装成可调用的推理接口。1. 先理解“伪人人脸”为什么难辨认1.1 从“伪人”这个说法出发网络语境里的“伪人”常指看起来像真人、但实际不是人类的角色比如游戏里的 NPC、虚拟偶像、AI 换脸视频里的人物。在图像安全领域它对应的是“AI 合成人脸”或“深度伪造人脸”。这类图像来源通常是 StyleGAN、Stable Diffusion 等生成模型生成结果已经具备真实照片的纹理、光照和边缘信息。如果目标是做内容审核、人脸核身或防欺诈系统就需要从技术层面回答一个问题这张人脸图像是否由机器生成而不是相机拍摄。传统手段靠分析 JPEG 压缩痕迹、传感器噪声或光照一致性但这些方法在高质量生成图像面前越来越不可靠。深度学习二分类模型是当前更通用的做法。1.2 为什么选择图像二分类方案把“鉴定伪人”转化成图像二分类任务输入是一张人脸图像输出是“真实”或“合成”两个类别。这个方案适合大多数入门项目原因有三点任务定义清晰模型只需要学一个决策边界。可以复用成熟的图像分类网络如 ResNet、EfficientNet。推理速度快单张图像在 CPU 上也能完成预测。相比之下像素级伪造区域定位、频域分析、多模态一致性校验更适合作为后续进阶方向。对于第一版鉴定工具先做好二分类再把错误样本找出来分析是性价比最高的路径。1.3 技术实现路线总览整个项目按下面几步推进准备真实人脸图像和合成人脸图像。按训练集、验证集、测试集划分目录。使用 PyTorch 加载图像并做数据增强。基于预训练的 ResNet18 微调分类头。训练后保存模型权重。编写推理脚本对单张图像输出类别和置信度。下面每一节都会给出可以运行的代码和需要关注的关键点。2. 环境准备和项目结构2.1 环境要求训练深度学习模型时环境差异会造成很多“能跑但结果不对”的问题。这里先给出一份推荐环境落地前先对照确认。依赖项推荐版本说明Python3.9 或 3.103.11 需要确认 PyTorch 轮子支持PyTorch2.0 及以上这里使用 CPU/GPU 通用代码torchvision与 PyTorch 版本配套提供预训练模型和图像变换OpenCV4.x用于人脸区域裁剪也可用 Pillow 代替tqdm任意较新版本训练进度显示GPU推荐显存 8GB 以上没有 GPU 也可用 CPU 训练只是更慢不建议直接安装最新版本而不确认兼容性。安装前先到 PyTorch 官网选择与自身 CUDA 驱动匹配的安装命令。下面给出 CPU 环境的安装命令GPU 环境按官网提示替换安装源。pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python tqdm注意如果电脑有 NVIDIA 显卡但nvidia-smi输出的 CUDA 版本与 PyTorch 要求的版本不匹配优先按 PyTorch 官网给出的 CUDA 版本安装而不是按系统全局版本安装。2.2 项目目录结构建议按下面的目录组织数据、代码和输出文件。这样数据路径、模型路径和日志路径都清晰后续排查问题也更方便。face-forensic/ ├── data/ │ ├── train/ │ │ ├── real/ │ │ └── fake/ │ ├── val/ │ │ ├── real/ │ │ └── fake/ │ └── test/ │ ├── real/ │ └── fake/ ├── models/ │ └── best_model.pth ├── train.py ├── predict.py └── requirements.txt训练脚本从data/train读取数据在data/val上保存最优模型最终通过predict.py对单张图像推理。这个结构可以原样用于其他图像二分类任务。2.3 数据集怎么准备如果之前没有做过图像数据准备最容易的问题是正负样本来源不明确。对于真实人脸可以使用公开人脸数据集或自行拍摄并授权的人像照片对于合成人脸可以借助生成模型离线生成一批图像作为负样本。学术研究中常见的做法是使用 FFHQ 作为真实人脸来源使用 StyleGAN 生成图像作为合成人脸来源但实际落地时要先确认数据版权和使用条款。数据量上建议每类不少于 1000 张。网络模型虽然能通过预训练权重降低数据需求但样本太少仍然容易过拟合。如果每类只有几百张模型可能在训练集上表现很好在真实业务数据上却完全失效。训练集、验证集、测试集的划分建议按 8:1:1 或 7:2:1。验证集用来选模型测试集只用来评估最终效果。不要拿测试集做调参否则评估结果会乐观得不真实。3. 核心代码实现3.1 数据加载与数据增强图像分类任务中数据加载的关键是类别映射必须稳定。目录名real映射到 0fake映射到 1不能每次运行都动态推断。import os import torch from torch.utils.data import Dataset from PIL import Image from torchvision import transforms class FaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] self.class_to_idx {real: 0, fake: 1} for class_name, label in self.class_to_idx.items(): class_dir os.path.join(root_dir, class_name) for file_name in os.listdir(class_dir): if file_name.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(class_dir, file_name), label)) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] image Image.open(path).convert(RGB) if self.transform: image self.transform(image) return image, label训练阶段需要数据增强目的是让模型不要死记训练图像。对于人脸真伪任务随机水平翻转、轻微旋转、随机颜色扰动是常用的增强方式。注意不要使用会破坏人脸语义的增强比如随机裁剪到只有下巴这会让模型学到错误信号。train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(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]), ])这里使用了 ImageNet 的标准化参数。如果预训练模型是 torchvision 提供的就必须沿用对应参数否则模型输出会被严重干扰。3.2 模型定义与训练准备模型部分直接使用 torchvision 的预训练 ResNet18。ResNet18 层数适中在入门级硬件上训练速度快也能达到不错的分类效果。import torchvision.models as models def build_model(num_classes2): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc torch.nn.Linear(in_features, num_classes) return model这里注意两点。第一不使用随机初始化而是加载 ImageNet 预训练权重这样模型已经有基础特征提取能力。第二只替换最后一层全连接输出类别数改为 2。损失函数使用交叉熵损失优化器使用带动量的 SGD 或 Adam。对于二分类任务交叉熵期望输出是每个类别的得分而不是一个 0 到 1 之间的单值。如果之后要输出置信度再用 Softmax 处理。device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model().to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4)学习率不一定要从默认值开始。预训练模型微调时1e-4左右通常比较稳如果同时微调全部层过大的学习率容易把预训练特征破坏掉。3.3 训练循环训练循环按“训练一个 epoch然后在验证集上评估”的方式组织。每次验证后比较准确率如果当前模型比历史最优更好就保存权重。from tqdm import tqdm def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in tqdm(dataloader, descTraining): images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / total def evaluate(model, dataloader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in tqdm(dataloader, descValidating): images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / total验证阶段必须用torch.no_grad()否则会自动构建计算图导致显存占用成倍增长还可能拖慢推理速度。主训练函数负责控制 epoch 循环、保存最优模型和打印日志。def main(): train_dataset FaceDataset(data/train, transformtrain_transform) val_dataset FaceDataset(data/val, transformval_transform) train_loader torch.utils.data.DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue ) val_loader torch.utils.data.DataLoader( val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue ) model build_model().to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) best_acc 0.0 epochs 30 for epoch in range(epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(fEpoch {epoch 1}/{epochs} ftrain_loss{train_loss:.4f} train_acc{train_acc:.4f} fval_loss{val_loss:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), models/best_model.pth) print(fSaved best model with val_acc{best_acc:.4f})4. 训练超参数说明和常见结果分析4.1 关键超参数如何影响训练很多人调参只看准确率不够。需要理解每个参数改动的代价。参数常见值调大影响调小影响推荐场景batch_size16 或 32训练更快显存占用更高也可能降低梯度噪声训练更慢梯度波动更大按显存选择不够时减小learning_rate1e-4 到 3e-4收敛快但容易震荡可能 loss 不降或发散收敛慢最终效果可能更好预训练微调用小学习率epochs20 到 50可能过拟合可能欠拟合以验证集准确率不再提升为止num_workers2 到 8数据加载更快但会增加内存和线程开销可能 GPU 等待数据小数据集 2 即可训练时不要只看训练集 loss。如果训练准确率接近 100%但验证准确率明显低说明过拟合。此时应该增加数据增强、使用 Dropout、减少 epoch 或换用小模型。4.2 训练日志示例下面是一段可能的训练日志格式具体数值与你的数据分布和随机种子有关不能作为统一标准。Epoch 1/30 train_loss0.6852 train_acc0.5321 val_loss0.6023 val_acc0.6915 Epoch 5/30 train_loss0.3824 train_acc0.8425 val_loss0.3105 val_acc0.8821 Epoch 10/30 train_loss0.2115 train_acc0.9320 val_loss0.1907 val_acc0.9213 Epoch 20/30 train_loss0.1228 train_acc0.9632 val_loss0.1188 val_acc0.9490看到第 1 个 epoch 准确率只有 0.53 是正常的因为分类头刚替换需要几个 epoch 才能适应新任务。如果前几个 epoch 训练 loss 还在 0.69 附近波动先检查数据加载是否正确再检查学习率是否过小。4.3 训练完成后如何判断效果仅仅看总准确率不够还要看混淆矩阵。对“伪人鉴定”场景漏掉合成人脸把 fake 判成 real的代价通常大于把真实人脸误判为合成人脸。因此要额外关注召回率和精确率之间的取舍。在测试集上写一个统计脚本输出每个类别的准确率、召回率、F1以及真实和合成两类的误分类样本。这样可以发现模型是不是只学会了分辨背景或色调而不是人脸的真正差异。5. 编写单张图像推理脚本5.1 推理代码训练完成之后models/best_model.pth就是可用的权重文件。推理脚本需要做和训练时相同的数据预处理包括尺寸缩放、标准化然后用torch.no_grad()推理。import torch import torchvision.models as models from PIL import Image from torchvision import transforms def predict_image(image_path, model_pathmodels/best_model.pth): device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsNone) in_features model.fc.in_features model.fc torch.nn.Linear(in_features, 2) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.to(device) model.eval() 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]), ]) image Image.open(image_path).convert(RGB) input_tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probabilities torch.softmax(outputs, dim1) _, predicted torch.max(outputs, 1) class_names [real, fake] label class_names[predicted.item()] confidence probabilities[0][predicted.item()].item() return label, confidence推理时load_state_dict传入map_locationdevice能在没有 GPU 的机器上加载保存自 GPU 的权重。这是最常见的坑之一。5.2 命令行调用在predict.py最后加一段命令行入口方便直接对指定图片执行鉴定。if __name__ __main__: import sys image_path sys.argv[1] label, confidence predict_image(image_path) print(fresult: {label}, confidence: {confidence:.4f})执行效果类似python predict.py data/test/fake/example.png result: fake, confidence: 0.9743如果加载模型时报错优先检查保存时的模型结构是否与加载时一致比如输出类别数有没有改成 2或者保存的是否是model.state_dict()而不是整个模型对象。5.3 对一张图做失败分析推理本身不是终点。遇到误判时把图像另存到一个badcase目录然后对比真实样本和误判样本的差异。通常能发现三类问题训练数据和业务数据分布不一致比如训练时全是高清正脸业务里却是低分辨率侧脸。图像预处理不一致比如推理时没有做相同尺寸缩放。类别不平衡导致模型偏向样本更多的一方。分析失败样本是提升模型最有用的手段比盲目调参效果好得多。6. 常见问题排查和完整排错链路6.1 训练阶段高频问题下面的表格整理了从数据准备到训练结束最容易踩的坑。问题现象常见原因检查方式处理建议训练 loss 一直不降学习率太大或太小数据标签错误输出每个 batch 的标签分布学习率调到 1e-4 左右检查目录映射验证准确率远低于训练准确率过拟合数据增强太弱观察 train_acc 和 val_acc 差距增加增强、早停、减小模型程序报 CUDA out of memorybatch_size 过大图像尺寸过大查看nvidia-smi显存占用减小 batch_size或改用更小输入尺寸加载训练好的模型报 key 错误保存了不同结构的模型权重打印 state_dict 的 key保证模型类一致后再 load_state_dict训练很慢num_workers 太少没有 GPUCPU/GPU 是否被占用增加 num_workers确认训练在 GPU 上执行推理结果和训练时完全不一致预处理不同对比训练和推理的 transform统一 Resize 和 Normalize 参数6.2 完整排错链路如果训练脚本报错按以下顺序检查不要第一反应就去改网络结构。检查数据路径。os.path.exists(data/train/real)是否存在目录名是否拼写正确。检查类别映射。打印一个 batch 的标签确认real对应 0、fake对应 1。检查图像可读性。部分图片损坏时Image.open会抛异常可以在 Dataset 里加一个异常捕获并打印路径。检查张量形状。输入到模型前的张量应该是[batch_size, 3, 224, 224]如果不是说明 transform 顺序有问题。检查设备。模型和输入数据必须都在同一个 device 上混用 CPU 和 GPU 会报错。检查 loss 是否出现 nan。出现 nan 通常是因为学习率过大或输入图像包含异常像素可以用更小学习率训练几轮验证。检查评测指标。不要只打印总体准确率打印每个类别的精确率和召回率避免类别不平衡造成的虚假高分。6.3 针对“伪人鉴定”特有问题的排查这类项目的特殊性在于普通图像分类模型容易学到“图片背景”“照片风格”这类表面特征。例如训练集里真实人脸大多是室内照片合成人脸大多有统一背景模型就会偷懒。验证时准确率高但一换环境立刻失效。如果怀疑模型学到了表面特征可以做一个小实验把真实人脸和合成人脸都裁剪成只有人脸区域再训练一遍。如果准确率大幅下降说明之前的模型利用了非人脸区域如果准确率保持说明模型学到了人脸的生成痕迹。这个实验值得在调优阶段做一次。7. 生产环境部署建议和后续扩展7.1 从训练脚本到可用接口训练好的模型不能只停留在.pth文件生产环境需要把它封装成服务。常见做法是导出为 ONNX然后用 FastAPI 对外提供 HTTP 接口。pip install onnx onnxruntime fastapi uvicorn导出 ONNX 的核心代码如下dummy_input torch.randn(1, 3, 224, 224, devicedevice) torch.onnx.export( model, dummy_input, models/best_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version12, )导出后ONNX Runtime 推理比直接加载 PyTorch 权重更轻量也更容易部署在无 GPU 的服务器上。但要注意ONNX 导出时需要固定输入尺寸如果后续要换输入尺寸需要重新导出。7.2 生产环境还需要考虑什么在真实业务里模型准确率只是其中一环。上线前至少要补齐以下能力输入校验接收图片前检查格式、大小、是否包含可读人脸区域。日志和监控记录每个请求的推理耗时、类别、置信度方便后续分析。失败兜底置信度低于阈值时返回“不确定”不要强制给一个结果。模型版本管理保存模型权重、数据版本、训练参数和测试指标方便回滚。阈值调整根据业务对误报和漏报的容忍度设置不同的判决阈值。举例来说如果业务是核身风控把合成人脸误判为真实人脸的代价很高那么就把“fake”的判定阈值从 0.5 调低并让置信度接近阈值的样本走人工审核而不是简单输出二分类结果。7.3 从二分类扩展到更复杂的深伪鉴定二分类只是基础版本实际深伪检测会面对更多问题视频伪造检测需要把单帧结果聚合到视频级别并考虑帧间抖动。局部伪造检测换脸、表情驱动、口型同步属于不同伪造类型可能需要分割或注意力机制。多模态一致性结合音频和文本判断视频中的人、声、唇形是否一致。对抗攻击生成方可能针对检测器做对抗扰动需要不断更新训练数据。学习路径上建议先把这个二分类项目完整跑通再尝试在数据集里加入不同生成模型、不同压缩方式的图像观察哪些因素导致准确率下降。这种基于失败案例的迭代训练才是深伪检测最有价值的部分。7.4 项目落地最佳实践清单最后给出一个可以直接用于项目自检的清单每个工程阶段都值得对照一遍。数据清单真实人脸和合成人脸是否来自不同生成方式和拍摄设备是否划分了独立测试集是否有重复图像跨训练集和测试集。预处理清单训练和推理是否完全一致图像格式是否统一是否记录了标准化参数。训练清单是否设置随机种子是否保存了验证集最优模型是否记录了每个 epoch 的日志。评估清单是否输出混淆矩阵是否统计每类召回率和精确率是否分析失败样本。部署清单是否导出轻量模型是否设置置信度阈值是否记录请求日志和模型版本。更新清单是否有周期性收集新样本的流程是否能从线上错误样本触发重新训练。“鉴定伪人”这个任务不会因为一个二分类模型而彻底解决但一个能跑通、能分析、能迭代的流程是解决此类问题的基础。先把手里的数据、模型、评估和部署串起来再逐步增加复杂能力会比一开始追求高精度更实用。
返回列表