“鉴定伪人”在图像安全领域是一个严肃课题:判断一张人脸照片是真实拍摄,还是由生成模型伪造。随着扩散模型和生成对抗网络的发展,普通人已经很难用肉眼分辨一张高分辨率人脸图像的真假,于是“伪人图像鉴定”就变成一个工程问题。这篇文章会从零搭建一个可运行的人脸真伪二分类模型,用它区分真实人脸和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 环境要求
训练深度学习模型时,环境差异会造成很多“能跑但结果不对”的问题。这里先给出一份推荐环境,落地前先对照确认。
| 依赖项 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.9 或 3.10 | 3.11+ 需要确认 PyTorch 轮子支持 |
| PyTorch | 2.0 及以上 | 这里使用 CPU/GPU 通用代码 |
| torchvision | 与 PyTorch 版本配套 | 提供预训练模型和图像变换 |
| OpenCV | 4.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映射到 0,fake映射到 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, transform=None): 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(p=0.5), transforms.RandomRotation(degrees=5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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_classes=2): model = models.resnet18(weights=models.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(), lr=1e-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, desc="Training"): 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, desc="Validating"): 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", transform=train_transform) val_dataset = FaceDataset("data/val", transform=val_transform) train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True ) val_loader = torch.utils.data.DataLoader( val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True ) model = build_model().to(device) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-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(f"Epoch {epoch + 1}/{epochs} " f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} " f"val_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(f"Saved best model with val_acc={best_acc:.4f}")4. 训练超参数说明和常见结果分析
4.1 关键超参数如何影响训练
很多人调参只看准确率,不够。需要理解每个参数改动的代价。
| 参数 | 常见值 | 调大影响 | 调小影响 | 推荐场景 |
|---|---|---|---|---|
| batch_size | 16 或 32 | 训练更快,显存占用更高,也可能降低梯度噪声 | 训练更慢,梯度波动更大 | 按显存选择,不够时减小 |
| learning_rate | 1e-4 到 3e-4 | 收敛快但容易震荡,可能 loss 不降或发散 | 收敛慢,最终效果可能更好 | 预训练微调用小学习率 |
| epochs | 20 到 50 | 可能过拟合 | 可能欠拟合 | 以验证集准确率不再提升为止 |
| num_workers | 2 到 8 | 数据加载更快,但会增加内存和线程开销 | 可能 GPU 等待数据 | 小数据集 2 即可 |
训练时不要只看训练集 loss。如果训练准确率接近 100%,但验证准确率明显低,说明过拟合。此时应该增加数据增强、使用 Dropout、减少 epoch 或换用小模型。
4.2 训练日志示例
下面是一段可能的训练日志格式,具体数值与你的数据分布和随机种子有关,不能作为统一标准。
Epoch 1/30 train_loss=0.6852 train_acc=0.5321 val_loss=0.6023 val_acc=0.6915 Epoch 5/30 train_loss=0.3824 train_acc=0.8425 val_loss=0.3105 val_acc=0.8821 Epoch 10/30 train_loss=0.2115 train_acc=0.9320 val_loss=0.1907 val_acc=0.9213 Epoch 20/30 train_loss=0.1228 train_acc=0.9632 val_loss=0.1188 val_acc=0.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_path="models/best_model.pth"): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=None) in_features = model.fc.in_features model.fc = torch.nn.Linear(in_features, 2) model.load_state_dict(torch.load(model_path, map_location=device)) 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, dim=1) _, 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_location=device,能在没有 GPU 的机器上加载保存自 GPU 的权重。这是最常见的坑之一。
5.2 命令行调用
在predict.py最后加一段命令行入口,方便直接对指定图片执行鉴定。
if __name__ == "__main__": import sys image_path = sys.argv[1] label, confidence = predict_image(image_path) print(f"result: {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 memory | batch_size 过大;图像尺寸过大 | 查看nvidia-smi显存占用 | 减小 batch_size,或改用更小输入尺寸 |
| 加载训练好的模型报 key 错误 | 保存了不同结构的模型权重 | 打印 state_dict 的 key | 保证模型类一致后再 load_state_dict |
| 训练很慢 | num_workers 太少;没有 GPU | CPU/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, device=device) 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_version=12, )导出后,ONNX Runtime 推理比直接加载 PyTorch 权重更轻量,也更容易部署在无 GPU 的服务器上。但要注意,ONNX 导出时需要固定输入尺寸,如果后续要换输入尺寸,需要重新导出。
7.2 生产环境还需要考虑什么
在真实业务里,模型准确率只是其中一环。上线前至少要补齐以下能力:
- 输入校验:接收图片前检查格式、大小、是否包含可读人脸区域。
- 日志和监控:记录每个请求的推理耗时、类别、置信度,方便后续分析。
- 失败兜底:置信度低于阈值时返回“不确定”,不要强制给一个结果。
- 模型版本管理:保存模型权重、数据版本、训练参数和测试指标,方便回滚。
- 阈值调整:根据业务对误报和漏报的容忍度,设置不同的判决阈值。
举例来说,如果业务是核身风控,把合成人脸误判为真实人脸的代价很高,那么就把“fake”的判定阈值从 0.5 调低,并让置信度接近阈值的样本走人工审核,而不是简单输出二分类结果。
7.3 从二分类扩展到更复杂的深伪鉴定
二分类只是基础版本,实际深伪检测会面对更多问题:
- 视频伪造检测:需要把单帧结果聚合到视频级别,并考虑帧间抖动。
- 局部伪造检测:换脸、表情驱动、口型同步属于不同伪造类型,可能需要分割或注意力机制。
- 多模态一致性:结合音频和文本,判断视频中的人、声、唇形是否一致。
- 对抗攻击:生成方可能针对检测器做对抗扰动,需要不断更新训练数据。
学习路径上,建议先把这个二分类项目完整跑通,再尝试在数据集里加入不同生成模型、不同压缩方式的图像,观察哪些因素导致准确率下降。这种基于失败案例的迭代训练,才是深伪检测最有价值的部分。
7.4 项目落地最佳实践清单
最后给出一个可以直接用于项目自检的清单,每个工程阶段都值得对照一遍。
- 数据清单:真实人脸和合成人脸是否来自不同生成方式和拍摄设备;是否划分了独立测试集;是否有重复图像跨训练集和测试集。
- 预处理清单:训练和推理是否完全一致;图像格式是否统一;是否记录了标准化参数。
- 训练清单:是否设置随机种子;是否保存了验证集最优模型;是否记录了每个 epoch 的日志。
- 评估清单:是否输出混淆矩阵;是否统计每类召回率和精确率;是否分析失败样本。
- 部署清单:是否导出轻量模型;是否设置置信度阈值;是否记录请求日志和模型版本。
- 更新清单:是否有周期性收集新样本的流程;是否能从线上错误样本触发重新训练。
“鉴定伪人”这个任务不会因为一个二分类模型而彻底解决,但一个能跑通、能分析、能迭代的流程,是解决此类问题的基础。先把手里的数据、模型、评估和部署串起来,再逐步增加复杂能力,会比一开始追求高精度更实用。