ARTICLE DETAIL

资讯详情

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

Deepfake视频检测实战:卷积Vision-Transformer从训练到部署

Deepfake视频检测实战:卷积Vision-Transformer从训练到部署 简介面向深度学习与计算机视觉研究者及AI内容安全从业者这是一份围绕Deepfake视频检测的完整工程包基于卷积Vision-Transformer架构将CNN局部特征与ViT全局建模结合有效提升伪造视频识别的精度与鲁棒性。包体共196个文件约80.29MB含167个jpg图像样本、13个mp4测试视频、9个py源码脚本、pth预训练权重、pptx汇报材料、csv预测结果、json与pkl/npy辅助数据以及md运行说明覆盖数据准备、模型训练、评估与推理全流程。已有400人浏览学习。使用者可直接调用权重对新视频进行检测也可基于源码扩展训练、调整参数或接入自有流程配上说明文档与汇报文件可快速理解网络结构、数据组织方式与环境配置显著降低复现门槛。对从事Deepfake检测研究、毕业设计或安全产品预研的人员兼具工程实用性与学术参考价值。1. 为什么deepfake视频检测绕不开卷积Vision-Transformerdeepfake视频检测这个方向表面看是个二分类问题实际做起来却是个「既要看局部纹理、又要看全局时序」的复合问题。早期基于CNN的方案能在单帧上抓出伪影但遇到换脸后逐帧重编码的视频就失效纯Transformer能建模长时序却容易忽略面部皮肤纹理、光照边缘这些细微线索。卷积Vision-Transformer把两者拼在一起用卷积枝提取帧内空间特征用Transformer枝建模帧间时序依赖恰好补上了单一架构的盲区。这个方案适合手里有GPU、想从零训练或微调检测模型的工程师也适合需要把检测模型落到视频审核、内容安全场景的团队。本文不聊论文复现直接讲怎么做、参数怎么给、坑在哪。2. 把视频切成可学的信号数据采集、人脸对齐与帧级预处理2.1 训练集怎么搜集公开数据集与自建负样本的配比做deepfake检测数据是第一道坎。常见做法是用FaceForensicsFF作为主训练集它的c23压缩档是最常用的实验基准再叠加FaceShifter、DeepFakeDetection这些生成方式不同的子集做正样本扩展。负样本不一定要从头采集直接用FF里的real视频切帧即可。但只靠公开数据集的坑在于模型会记住压缩痕迹所以我一般会在训练集里混入5%到10%的自建负样本——用手机拍一段真人视频或者从无版权视频站下载日常场景专挑光照变化大、运动模糊多的片段。配比上有个经验值正负样本比例控制在1:1到1:1.5之间。正样本太多模型会把「生成痕迹」当特征遇到新换脸工具直接失效负样本太多模型会偏向判真召回率上不去。实际操作时按视频而非按帧做配比更稳——一个视频里相邻帧高度相似按帧配比容易造成同源数据在训练集和验证集里同时出现指标虚高。数据集目录结构我一般这样组织dataset/ ├── train/ │ ├── real/ │ │ ├── video_0001/ │ │ │ ├── frame_0000.jpg │ │ │ ├── frame_0001.jpg │ │ └── ... │ └── fake/ │ ├── video_0002/ │ └── ... ├── val/ └── test/每个视频单独建文件夹文件名带帧序号。这样做的好处是后续做人脸对齐、滑动窗口采样、时序建模时路径里的帧顺序天然就是时序信息不用额外维护映射表。2.2 人脸检测与对齐RetinaFace的五点对齐与112×112裁剪视频检测不能直接把整帧丢给模型——背景信息会干扰模型学习人脸区域的伪造痕迹。标准做法是先人脸检测再按关键点对齐最后裁剪成固定尺寸。推荐用RetinaFace做检测器它在侧脸、遮挡、低分辨率下的关键点稳定性比MTCNN好很多。对齐时取双眼、鼻尖、左右嘴角五个关键点做一个仿射变换映射到标准位置最后resize到112×112或224×224。import cv2 import numpy as np from retinaface import RetinaFace def extract_aligned_face(frame, target_size112): faces RetinaFace.detect_faces(frame) if not faces: return None # 取面积最大的人脸避免背景人物干扰 best max(faces.values(), keylambda f: f[score]) keypoints best[landmarks] # 标准五点坐标按112x112输出参考系 ref_pts np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) src_pts np.array([ keypoints[left_eye], keypoints[right_eye], keypoints[nose_bridge], keypoints[mouth_left], keypoints[mouth_right] ], dtypenp.float32) # 仿射变换对齐等价于图片配准 transform cv2.estimateAffinePartial2D(src_pts, ref_pts)[0] aligned cv2.warpAffine(frame, transform, (target_size, target_size), borderModecv2.BORDER_REPLICATE) return aligned这段代码的核心是estimateAffinePartial2D它只估计平移、旋转、缩放不引入剪切变形能保持人脸五官比例不变。borderMode用BORDER_REPLICATE而不是默认的黑色填充是为了避免对齐过程中边缘产生黑边、形成模型误学的「边缘伪影」。target_size取112是平衡计算量和精度的常用值224会显著增加Transformer分支的计算负担但检测精度提升有限。对齐这一步最容易出问题的不是代码而是检测器的score阈值。默认0.9会把大量模糊帧直接过滤掉导致一个视频只剩零星几帧时序建模根本跑不起来。我一般降到0.5同时保留得分最高的那个人脸作为目标避免多人同框时模型学到错误的人脸身份特征。2.3 帧采样策略均匀采样还是随机采样视频帧不能全部送进模型显存不允许时序上也存在大量冗余。常见的帧采样策略有两种均匀采样每隔N帧取一帧和随机裁剪从一个视频里随机取连续K帧。均匀采样覆盖时间长但相邻帧语义变化小容易让模型偷懒只学「肤色平均」随机裁剪连续片段能强化时序判别但视频短的时候容易重复采样。我的做法是组合策略每段视频先按固定步长抽帧抽完再做一次随机偏移。具体来说设定采样窗口长度为16帧步长为4从第0帧开始候选训练时随机选一个起点取连续16帧作为一条样本验证时固定起点保证可复现。这样做的好处是每次训练的epoch里模型看到的帧组合都不一样相当于数据增强。预处理做完序列数据落盘时注意存储格式。推荐直接存为npy的数组文件shape是(N, C, T, H, W)比逐帧读jpg快一个数量级。代价是磁盘占用大112×112的灰度图序列1000个视频大概占10GB左右但换来的是训练时几乎零等待。数据集够大时也可以只存对齐后的人脸图训练时用DataLoader在线切帧省存储但费CPU。3. 卷积Vision-Transformer模型搭建网络结构与参数设计3.1 为什么用卷积做stem局部纹理建模与patch embed的冲突Vision Transformer原版的做法是把图片切成16×16的patch每个patch线性映射成token。但对deepfake检测来说这个设计是吃亏的——换脸生成器最常暴露问题的地方是面部皮肤纹理、牙齿边缘、头发丝这些恰恰是高频细节而16×16的patch已经把高频信息模糊成平均色块了。卷积Vision-Transformer的常见做法是保留一个轻量卷积stem用几层步长为2的卷积把空间尺寸逐级压到14×14或7×7再做patch embedding。这样做的核心收益是卷积核天然具有局部感受野能先提取纹理、边缘信息再交给Transformer去建模全局依赖。我一般用三层卷积做stem通道数取64→128→256最后一层输出的特征图尺寸为14×14然后flatten成196个token。import torch.nn as nn class ConvStem(nn.Module): def __init__(self, in_channels3, hidden_dim256): super().__init__() self.conv1 nn.Conv2d(in_channels, 64, kernel_size3, stride2, padding1) self.bn1 nn.BatchNorm2d(64) self.conv2 nn.Conv2d(64, 128, kernel_size3, stride2, padding1) self.bn2 nn.BatchNorm2d(128) self.conv3 nn.Conv2d(128, hidden_dim, kernel_size3, stride2, padding1) self.bn3 nn.BatchNorm2d(hidden_dim) self.act nn.ReLU(inplaceTrue) def forward(self, x): # x: (B, T, C, H, W)逐帧过卷积 B, T, C, H, W x.shape x x.reshape(B * T, C, H, W) x self.act(self.bn1(self.conv1(x))) x self.act(self.bn2(self.conv2(x))) x self.act(self.bn3(self.conv3(x))) _, C, H_, W_ x.shape x x.reshape(B, T, C, H_ * W_) # 转置为 (B, T, H_*W_, C) 进入Transformer x x.permute(0, 1, 3, 2) return x输入shape(B, T, C, H, W)我习惯把时间维度T放在batch后面让卷积在处理空间维度的同时保持时间维独立。reshape(B * T, ...)是因为卷积的batch维不区分时空直接合并计算效率最高BN层在时间维度上做归一化等价于对不同帧共享同一套统计量不会引入帧间不一致偏差。第三层卷积后没有直接池化是因为14×14的feature map后续要展平成token序列池化会进一步丢失位置信息而且Transformer分支能处理长序列没必要提前压缩。3.2 时序建模模块的选择temporal attention、TCN与轻量GRU卷积stem处理完单帧后核心问题变成怎么让模型学会帧与帧之间的变化模式。最直接的做法是把每帧的token序列拼起来直接送进标准Transformer encoder。但这样做有两个问题一是序列长度等于帧数×单帧token数计算量太大二是标准encoder的self-attention对所有token一视同仁帧内空间注意力和帧间时序注意力混在一起模型不易收敛。更可控的做法是分层建模先用标准Transformer处理单帧token提取空间特征再把空间特征按时间维做时序建模。时序建模层有三个常见选择temporal attention只在时间维做attention、TCN时间卷积网络和轻量GRU。我实际测试下来TCN是最稳的——时间维只有16帧远小于空间token数卷积核在时序维直接滑过感受野可控不会出现attention在短序列上不稳定的问题。import torch import torch.nn as nn class TCNBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, dilation2, dropout0.2): super().__init__() self.pad (kernel_size - 1) * dilation self.conv nn.Conv1d(in_channels, out_channels, kernel_size, dilationdilation, paddingself.pad) self.bn nn.BatchNorm1d(out_channels) self.act nn.ReLU(inplaceTrue) self.dropout nn.Dropout(dropout) def forward(self, x): # x: (B, T, C) - (B, C, T) 适合Conv1d x x.permute(0, 2, 1) x self.conv(x) x x[:, :, :x.shape[2] - self.pad] # 裁剪因果卷积多余部分 x self.act(self.bn(x)) x self.dropout(x) x x.permute(0, 2, 1) return xpaddingself.pad配合裁剪是因果卷积的标准做法保证当前帧不会被未来帧影响。检测任务虽然离线不需要因果约束但因果卷积在时间维上的感受野方向是固定的等价于「只看过去的帧」可以防止模型学到「某帧造假」时过度依赖「下一帧露馅」这种非鲁棒特征。dilation2让感受野从前一帧扩展到前3帧左右16帧序列里用两层TCN就能覆盖到接近全部历史。TCN的kernel_size取3就够这个参数不是越大越好——时间维本身只有十几帧kernel_size5会把感受野撑得过大导致模型只关注整体趋势、忽略单帧突变。换脸视频的破绽往往出现在某个瞬间比如眨眼异常、嘴角抖动这种局部突变恰恰需要小kernel才能捕捉。3.3 模型参数表patch、深度、DropPath与分类头的最佳范围参数选择这块直接给一张我常用的配置表按这个配置在FF c23上训练单卡RTX 3090大概12小时能到90%以上的AUC参数推荐值说明输入尺寸112×112低于这个尺寸纹理信息不够高于224训练开销翻倍卷积stem通道64→128→256通道增长过快会导致小数据集过拟合Transformer encoder层数68层以上在FF上收益微弱且显存压力大attention头数8配合隐藏维256每个头32维时序TCN层数2每层dilation递增1→2DropPath率0.2对Transformer分支的过拟合抑制效果比Dropout好分类头全局平均池化→Linear(256→2)不用额外FC堆叠小数据集上FC层越多越容易崩DropPath是一个容易被忽略的参数。它的作用是在训练时随机丢弃整条残差路径等价于训练多个子网络的集成。在Transformer分支里DropPath比Dropout更有效因为它作用的粒度是block级而不是token级能防止模型对某一层特征过度依赖。DropPath率从0.1加到0.3在FF上验证集AUC能从88.3涨到90.1超过0.5模型欠拟合严重AUC直接掉回85以下。补一个大多数人不注意的点分类头不要用Sigmoid加单输出节点。deepfake检测的正负样本在特征空间里往往是相近的都是人脸二输出节点加CrossEntropyLoss能保留更多梯度信息训练初期收敛快得多。4. 从训练到复现损失函数、学习率与训练流程4.1 损失函数设计二元交叉熵、Focal Loss与标签平滑deepfake检测最常见的问题是正负样本不均衡——真实视频的帧数远多于伪造视频。如果你的训练集配比已经做到了1:1那直接上二元交叉熵BCEWithLogits就够了简单可靠。但如果数据集分布不均衡Focal Loss是更好的选择它会给难分类的样本更大的梯度权重而多数类里那些已经分对的简单样本权重自动降低。import torch.nn.functional as F def focal_loss(logits, labels, alpha0.25, gamma2.0): probs torch.sigmoid(logits) bce F.binary_cross_entropy_with_logits(logits, labels, reductionnone) p_t probs * labels (1 - probs) * (1 - labels) modulating (1 - p_t) ** gamma alpha_t alpha * labels (1 - alpha) * (1 - labels) loss alpha_t * modulating * bce return loss.mean()gamma2.0是Focal Loss的默认值作用是压低易分样本的损失贡献alpha0.25用于平衡正负样本的权重通常配合正样本比例调。我的经验是在配比1:1的FF上Focal Loss相比BCE涨点不明显大约0.3到0.5个AUC但在自建的小数据集或类别严重失衡时涨点能有2个点以上。如果训练集配比已经调好BCE加标签平滑label smoothing设为0.05反而是更稳的选择损失曲线更平滑不容易在最后几个epoch出现震荡。4.2 训练超参与优化器AdamW、CosineAnnealing与warmup优化器这里没什么悬念用AdamW就对了。CNN部分的学习率可以给大一点3e-4Transformer分支的学习率要小一些1e-4因为Transformer对学习率更敏感给大了直接loss发散。两个分支用不同的参数分组是落地时常见但容易漏的操作。optimizer torch.optim.AdamW([ {params: model.conv_stem.parameters(), lr: 3e-4}, {params: model.transformer.parameters(), lr: 1e-4}, {params: model.tcn.parameters(), lr: 2e-4}, {params: model.classifier.parameters(), lr: 2e-4}, ], weight_decay5e-4) scheduler torch.optim.lr_scheduler.SequentialLR( optimizer, schedulers[ torch.optim.lr_scheduler.LinearLR(optimizer, start_factor0.1, total_iters5), torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max45) ], milestones[5] )warmup设为5个epoch让学习率在前5个epoch从10%线性爬升到目标值。这一步不能省——预训练权重不是从头训练时Transformer分支一上来就用大学习率会把预训练学到的特征直接冲掉。CosineAnnealing的T_max45对应总共50个epoch减去5个warmup学习率按余弦曲线从峰值降到接近0末尾几个epoch模型参数在极小范围内震荡能收敛到更平滑的极小值。Batch size在单卡上取32比较合理对应到每条样本是16帧112×112显存占用大约在16GB左右。Batch太小4或8BN的统计量不稳定训练曲线抖得厉害Batch太大64以上要调大学习率容易在Transformer分支上踩梯度震荡的坑。模型并行或者显存不够的时候先把帧数从16降到8而不是降batch size——时间维砍半对指标的影响比batch减半小得多。4.3 训练脚本与日志监控断点续训与最佳模型保存训练脚本按「每epoch保存一次 记录最佳指标」的标准流程来。断点续训是必须的——训练到第40个epoch因为掉卡中断没有checkpoint的话一切重来这种翻车经历我不希望任何人再体验一次。best_auc 0.0 start_epoch 0 if resume_path is not None: ckpt torch.load(resume_path) model.load_state_dict(ckpt[model]) optimizer.load_state_dict(ckpt[optimizer]) scheduler.load_state_dict(ckpt[scheduler]) start_epoch ckpt[epoch] 1 best_auc ckpt[best_auc] for epoch in range(start_epoch, total_epochs): train_one_epoch(model, train_loader, optimizer, criterion) auc evaluate(model, val_loader) if auc best_auc: best_auc auc torch.save({ epoch: epoch, model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), best_auc: best_auc, }, best_model.pt)这里有一个关键技巧optimizer和scheduler的状态也要一起存不能只存模型权重。CosineAnnealingLR恢复时必须知道当前走到的epoch位置否则学习率重新从头算模型直接被带偏。保存最佳模型时用AUC而不是accuracy做指标——deepfake检测的类别分布不是绝对均衡accuracy容易被多数类带跑AUC对阈值不敏感更适合作为筛选标准。验证集评估时有一个隐藏问题帧级预测和视频级预测要分开算。单帧预测准确率高不代表视频判断准确率高因为一个视频里可能只有个别帧露出破绽。评估时先按帧出概率再对同一个视频的所有帧取平均或取最大值得到视频级预测分再用这个分算AUC。两个指标都记训练阶段看帧级AUC验证阶段看视频级AUC两者差超过5个点说明时序分支没有学到有效信息要回头查TCN层。5. 复现时最容易翻车的5个坑现象、原因与解决5.1 换一个数据集精度暴跌压缩痕迹过拟合现象在FF c23上训完验证集AUC 92%拿到FaceShifter生成的新视频上一测AUC直接掉到68%。原因模型学到的是数据集的压缩特征而不是伪造痕迹。FF c23统一用H.264压缩到固定码率模型记住了块效应和模糊分布新数据的压缩参数一变这些特征全部失效。解决训练时做压缩增强。每帧随机做一次JPEG重压缩质量因子从70到95随机取也可以叠加轻微的高斯模糊或锐化模拟不同编码器的预处理差异。压缩增强是这条赛道上性价比最高的防过拟合手段成本几乎为零但能把你拉回正常基线。5.2 训练Loss在降、验证ACC不动帧序错乱现象训练第10个epoch loss从0.8降到0.3验证集acc一直停在72%左右上不去。原因数据加载时把视频帧按文件名排序但文件名是字符串排序frame_10.jpg排到了frame_2.jpg前面。时序建模吃到的帧顺序是乱的TCN学到的「时序模式」全是噪声。解决文件名补零到固定位数例如frame_0001.jpg或者读取时直接用数字索引排序而不是字符串排序。这一点在预处理的落盘阶段就要定好后面训练脚本里加sorted(..., keylambda x: int(x.split(_)[1].split(.)[0]))是无奈的后悔药但至少能救回来。5.3 低显存跑推理精度漂移fp8卷积回退问题现象用FP16或混合精度训练时精度正常但部署到低显存环境开FP8推理前几层卷积的输出和训练时不一致检测率掉了3%到5%。原因某些推理框架在FP8模式下对卷积核大小有限制7×7这类大卷积会自动回退到FP16或FP32但回退发生在本层内部和邻近算子的精度对不上误差逐层累积。这不是模型问题是精度切换的边界问题。解决推理前用脚本扫描一遍模型里的所有卷积层把kernel size大于等于7的层固定为FP16只让3×3和1×1卷积走FP8。或者更省事整个推理链路统一用FP16别在FP8上省那点显存精度才是检测场景的生命线。5.4 人脸对齐失败导致误检检测器阈值与关键点质量现象一张侧脸或者低分辨率帧模型直接判fake但人眼一看就是真人。原因人脸检测器在低质量帧上没有输出5点关键点预处理代码会跳过这一帧但有些流水线会用「失败」信号填充成一个全黑图或上一帧的对齐结果这个脏数据被模型当成输入特征输出随机。解决对齐失败时不要补帧直接丢弃该帧。如果丢弃后单个视频剩余帧数不足8帧把样本整体丢弃不要硬凑。全黑图会让模型学到「黑假」的捷径这个在测试阶段遇到完全相同的低质量输入时等于埋了一颗定时炸弹。5.5 数据泄露同源视频切帧混进训练集现象训练集AUC 98%测试集AUC 88%但换了外部数据集立刻掉到75%。原因开源数据集内部有同源视频的不同版本——同一个原始视频被不同换脸工具处理或同一生成视频在多个子集里重复出现。按视频划分数据集时没查重训练集和验证集共享了一批同源帧模型记住的是视频ID而不是伪造特征。解决严格按视频ID去重同一个原始视频派生出的所有版本只能进同一个集合。一般开源数据集每个样本都带时间戳或原始视频ID按这个字段做hash分组宁可少几个训练视频也不要让泄露混进来。训练前这一步查重值得花半天时间做干净。6. 把模型推到推理端滑窗检测、阈值校准与ONNX导出训练出来的模型在验证集上AUC好看只能说明判别能力存在真实视频是任意长度、任意压缩档、任意分辨率推理时的窗口策略和阈值选择才是决定线上效果的关键。我做推理时会用滑窗检测以16帧为窗口、步长8帧滑动每帧得到一个预测概率窗口内取平均作为该窗口的得分整个视频的得分序列再做一次滑窗平滑用后验的多数表决代替单帧硬判。这样做的原因是换脸破绽往往只延续几帧单帧为单位的判断会被剧烈抖动干扰窗口化之后输出的置信度稳定得多。阈值不要拍脑袋定用验证集做一次校准。统计验证集所有视频得分的分布把阈值定在让「假视频全部拦截但真视频误伤率低于5%」的位置。这个阈值要按业务场景调整——内容安全场景宁可误杀不可漏过阈值定低影视审核场景要减少人工二次复核阈值定高。调完之后把阈值存进配置文件而不是写死在代码里每次重训后单独调这一项。导出ONNX时要注意配合滑窗做batch推理。把16帧拼成一个batch送进onnx模型一次推理出16个概率比逐帧推理快16倍。导出时把动态轴设成batch维避免固定batch size导致推理端性能浪费。实测RTX 3090上用TensorRT跑FP16的ONNX模型16帧112×112推理一次约3毫秒比PyTorch原始模型快6倍左右。最后说一个我踩过的坑部署时一定要用「真实采集视频当天拍当天测」做烟雾测试不要只拿数据集里的验证集测。数据集里的真实视频都是采集设备拍好、剪辑过的和摄像头实时流的噪声分布完全不同。我做过一次线上部署模型在验证集AUC 91%上线后对低照度会议室场景的误检率高到不可用最后排查发现是训练数据里从来没有「室内暗光灯具频闪」这类帧。补了500条真实环境视频做微调后误检率才压下来。现在我把这个教训固定成了流程每次模型交付必须带一份新采集样本的评估报告没有这份报告不上线。希望这个习惯连同上面的参数和排错思路能帮你在deepfake视频检测这条路上少走几段弯路。本文还有配套的精品资源点击获取
返回列表