ResNet18+LSTM活体检测实战:基于OULU-NPU视频时序建模

发布时间:2026/9/19 4:13:48

ResNet18+LSTM活体检测实战:基于OULU-NPU视频时序建模 1. 项目概述这不是一个“跑通就行”的Demo而是一次真实活体检测模型的端到端实战OULU-NPU数据集——这个名字在活体检测Liveness Detection领域几乎等同于“黄金标准”。它不是网上随手搜来的几十张人脸截图而是由芬兰奥卢大学University of Oulu与韩国浦项工科院POSTECH联合采集的、严格遵循ISO/IEC 30107-3标准的高质量视频级活体数据。63名受试者在不同光照、姿态、设备iPhone 6s、Samsung Galaxy S6、Moto G4下分别完成真实人脸视频、打印攻击paper print、屏幕重放攻击video replay三类样本采集。每段视频长达10秒分辨率统一为1280×720帧率30fps且所有攻击样本均经过专业防伪设备验证有效性。这意味着用它训练出来的模型不是在玩具数据上“自我感动”而是直面工业级对抗场景的真实考验。标题里说的“从打印攻击到视频重放”绝非修辞——它精准指向活体检测最核心的两大攻击类型静态伪造print attack和动态伪造replay attack。前者是把一张高清照片打印出来拿给摄像头拍后者是用另一台手机或平板播放一段录好的真人视频来欺骗系统。这两种攻击成本极低、隐蔽性强是金融开户、远程身份核验、门禁通行等高安全场景中最常被利用的漏洞。而OULU-NPU正是为系统性评估模型对这两类攻击的识别能力而生。它不只提供图片而是提供带时序信息的视频片段这就决定了单纯用CNN比如ResNet18提取单帧特征会丢失关键的微表情、眨眼节奏、屏幕摩尔纹、打印纸反光等动态线索必须引入能建模时间依赖关系的结构——LSTM正是为此而设。PyTorch作为当前深度学习研究与落地的主流框架其动态图机制、清晰的API设计和强大的社区生态让它成为本项目不可替代的底座。ResNet18不是随便选的“轻量级凑数模型”它在ImageNet上以仅11M参数量达到70% top-1准确率证明了其在有限计算资源下卓越的特征提取能力非常适合活体检测这种对实时性有硬要求的边缘部署场景。而LSTM则负责将ResNet18输出的每一帧特征向量按时间顺序“串起来”学习其中的运动模式、纹理变化规律——比如真实人脸眨眼时眼睑运动的平滑连续性而打印攻击中眼睛区域是完全静止的再比如视频重放时屏幕边缘常出现的细微闪烁或色彩偏移这些都体现在帧间特征序列的演化中。所以这个项目远不止是“调个包跑个代码”。它是一次完整的工程闭环从数据加载的时序对齐、视频帧采样策略的设计、ResNet18特征提取器的迁移微调、LSTM时序建模的结构搭建、多模态损失函数的组合设计到最终在OULU-NPU官方划分的Protocol 1cross-material, cross-pose上进行严格测试。整个过程我全程在Ubuntu 22.04 RTX 4090 PyTorch 2.1.0 CUDA 12.1环境下实操所有配置、参数、踩坑点都来自真实日志。如果你正准备做毕业设计、技术预研或是想真正理解活体检测背后的时间建模逻辑而不是停留在“调参侠”层面那么接下来的内容就是你该抄的作业。2. 整体架构设计与方案选型逻辑为什么是ResNet18LSTM而不是Transformer或3D-CNN2.1 核心思路双流时序建模——空间特征提取 时间动态建模活体检测的本质是区分“活的生物组织”与“静态/动态的伪造介质”。这个判断无法靠单张图像完成必须依赖跨帧的一致性与动态性。因此整个模型架构天然被拆解为两个耦合但职责分明的模块前端Spatial Stream负责“看懂”每一帧画面。它需要快速、鲁棒地提取出人脸区域的关键判别性特征——皮肤纹理、血管分布、光照反射特性、微小运动痕迹如呼吸导致的面部起伏。这正是卷积神经网络CNN的强项。我们选择ResNet18是因为它在精度与速度之间取得了极佳平衡。它的残差连接能有效缓解深层网络的梯度消失问题保证在微调时特征提取器的稳定性其18层结构带来的计算开销使得单帧前向推理在GPU上仅需约3ms为后续LSTM处理长序列留出充足余量。后端Temporal Stream负责“读懂”帧与帧之间的故事。它接收前端输出的特征序列例如对一段30帧的视频得到30个512维的向量并从中挖掘时间维度上的模式。真实人脸的特征序列是高度自相关的、具有生理节律性的如眨眼周期约4-6秒呼吸频率约12-20次/分钟而打印攻击的序列是完全平坦的所有帧特征几乎一致视频重放则可能表现出周期性伪影如屏幕刷新率导致的固定间隔闪烁。LSTM正是为建模此类长程依赖而生。它通过门控机制遗忘门、输入门、输出门有选择地记住或丢弃历史信息能有效捕捉上述生理节律与伪造伪影的差异。提示有人会问为什么不用更火的Transformer诚然ViT在图像分类上表现惊艳但其计算复杂度是O(n²)对30帧序列意味着900次注意力计算远超LSTM的O(n)线性复杂度。在活体检测这种对延迟敏感的场景LSTM的效率优势是压倒性的。至于3D-CNN它虽能直接处理视频体素但参数量巨大ResNet18 3D版参数量是2D版的3倍以上且对小样本数据OULU-NPU总样本仅约1万段视频极易过拟合。我们的目标是“可靠可用”而非“参数炫技”。2.2 数据流与模块衔接如何让CNN的“静态眼”与LSTM的“动态脑”无缝协作整个数据流并非简单的“CNN→LSTM”串联而是一个精心设计的管道视频加载与预处理使用decord库比OpenCV快3倍直接从.avi文件中高效解码。关键一步是时序对齐OULU-NPU的原始视频长度不一5-15秒我们统一采样为30帧。但绝非简单等间隔抽取采用“中心裁剪随机抖动”策略先定位视频中心10秒片段再在此区间内随机选取30个时间戳保证最小间隔≥0.1秒确保模型看到的是最具判别力的动态过程而非开头结尾的静止帧。人脸检测与裁剪使用RetinaFace比MTCNN精度更高、速度更快对每一帧进行检测。关键技巧在于跨帧跟踪对第一帧检测出的人脸框用光流法cv2.calcOpticalFlowFarneback预测后续帧中的人脸位置再微调。这避免了逐帧检测带来的框抖动保证了输入LSTM的特征序列空间坐标高度一致极大提升了时序建模的稳定性。特征提取与降维ResNet18的最后一个全连接层fc被移除取layer4输出的特征图7×7×512。经全局平均池化GAP后得到512维特征向量。这里有个重要细节不做BatchNorm冻结。虽然ResNet18是ImageNet预训练但OULU-NPU的光照、设备差异巨大冻结BN层会导致域偏移加剧。我们采用“微调式BN”训练时开启BN但学习率设为骨干网络的0.1倍让BN统计量缓慢适应新数据。LSTM输入构造将30帧的512维向量堆叠成形状为(30, 1, 512)的张量seq_len, batch, input_size。注意batch1是刻意为之。LSTM的隐藏状态hidden state在序列内是连续传递的若batch_size1不同视频的序列会被强行“打断”破坏时序连贯性。我们采用单样本批处理single-sample batching用torch.utils.data.DataLoader的sampler定制逻辑实现牺牲一点吞吐量换取建模质量。输出与决策LSTM最后一层的输出shape:(1, 512)送入一个两层MLP512→128→2输出[real, spoof]的概率分布。损失函数采用Focal Loss Center Loss组合Focal Loss解决正负样本极度不平衡真实样本约占60%攻击样本占40%但攻击内部又分print/replay存在子类别不平衡Center Loss则强制同类样本如所有print attack的特征在嵌入空间中聚拢提升类内紧凑性。2.3 方案取舍背后的硬道理为什么放弃“端到端视频CNN”和“纯Transformer”在动手前我对比了三种主流架构在OULU-NPU Protocol 1上的初步实验结果基于相同硬件与训练轮数架构方案参数量单样本推理耗时(ms)Protocol 1 ACER (%)主要瓶颈ResNet18 (单帧)11.2M2.128.7完全忽略时序无法区分print与replayResNet18LSTM (本方案)13.8M8.312.4LSTM门控计算引入少量开销但精度跃升I3D (3D-CNN)32.5M42.615.9显存占用翻倍训练易崩溃小数据下泛化弱ViTTimeSformer48.7M67.214.1注意力矩阵计算爆炸对30帧序列内存带宽吃紧ACERAttack Classification Error Rate是活体检测的黄金指标越低越好。数据清晰表明ResNet18LSTM在精度、速度、显存占用三个维度上达到了最佳平衡点。I3D和TimeSformer的失败并非模型能力不足而是它们的设计初衷是处理长视频如Kinetics的10秒以上动作识别而OULU-NPU的30帧序列太短无法发挥其时空建模优势反而因参数冗余导致过拟合。这印证了一个朴素真理没有最好的模型只有最适合任务的模型。活体检测不是学术竞赛而是工程落地必须把“能跑、能稳、能快”放在首位。3. 核心细节解析与实操要点从环境配置到数据加载的每一个魔鬼细节3.1 PyTorch环境搭建避开Anaconda与CUDA的“经典陷阱”标题里的“pytorch安装”、“ubuntu 26 安装pytorch环境”等热词暴露了无数新手卡在第一步的现实。我用RTX 4090Ada架构在Ubuntu 22.04上踩过的坑值得你花3分钟读完CUDA版本陷阱NVIDIA官网显示4090支持CUDA 11.8但PyTorch 2.1.0官方预编译包仅适配CUDA 12.1。若强行用conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia会报错libcudnn.so.8: cannot open shared object file。正确做法是先sudo apt install nvidia-cuda-toolkit安装CUDA 12.1驱动再执行pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121这条命令从PyTorch官方CUDA 12.1镜像源下载确保二进制兼容。Anaconda的“隐形污染”很多教程推荐用conda install pytorch但它默认安装的cudatoolkit版本如11.3与系统CUDA 12.1冲突。我的经验是彻底卸载Anaconda改用Miniforge轻量级Conda发行版。创建环境时指定Python 3.10PyTorch 2.1.0最稳定conda create -n livetorch python3.10 conda activate livetorch pip install torch2.1.0cu121 torchvision0.16.0cu121 torchaudio2.1.0cu121 --extra-index-url https://download.pytorch.org/whl/cu121VSCode调试加速在settings.json中添加python.defaultInterpreterPath: ./venv/bin/python, python.testing.pytestArgs: [tests/], python.formatting.provider: black配合code --install-extension ms-python.python可直接在VSCode中启动GPU调试断点查看model.lstm.weight_ih_l0的梯度流动这是理解LSTM门控机制最直观的方式。注意不要迷信“最新版即最好”。PyTorch 2.2.0在RTX 4090上偶发cudaErrorLaunchTimeout错误回退到2.1.0后问题消失。工程实践的第一原则是稳定压倒一切。3.2 OULU-NPU数据集的“正确打开方式”绕过官网下载的迷雾OULU-NPU官网https://www.ee.oulu.fi/~mikem/oulu-npu/提供的是加密压缩包需邮件申请密钥。但更关键的是原始数据结构极其反人类。它不是规整的train/val/test目录而是按session采集会话和subject受试者层层嵌套且.avi文件命名规则晦涩如1_1_1_1.avi表示session1, subject1, real, video1。手动整理会浪费半天。我的解决方案是编写一个data_preprocess.py脚本自动完成三件事协议划分严格按Protocol 1定义——将63名受试者分为6组每组约10-11人其中5组用于训练1组用于测试。脚本自动读取Protocols/Protocol_1.txt中的划分列表生成train_list.txt和test_list.txt。攻击类型标注解析文件名将1_1_1_1.avi映射为[session1, subject1, typereal, video_id1]1_1_2_1.avitype2为print attack1_1_3_1.avitype3为replay attack。最终生成CSV列名video_path, label, attack_typereal/print/replay。帧缓存优化为避免每次训练都实时解码视频I/O瓶颈脚本将每段视频的30帧JPEG图像按{session}_{subject}_{type}_{id}/frame_{0001}.jpg格式保存到cache/目录。后续Dataset类直接从缓存读取训练速度提升3倍。# data_preprocess.py 核心逻辑 import os, cv2, decord from decord import VideoReader from decord import cpu, gpu def extract_frames(video_path, output_dir, num_frames30): vr VideoReader(video_path, ctxcpu(0)) total_frames len(vr) # 中心10秒采样再随机抖动 start_frame max(0, (total_frames // 2) - 150) # 150帧≈5秒 end_frame min(total_frames, start_frame 300) # 300帧≈10秒 frame_indices sorted(random.sample(range(start_frame, end_frame), num_frames)) for i, idx in enumerate(frame_indices): frame vr[idx].asnumpy() # BGR format cv2.imwrite(os.path.join(output_dir, fframe_{i1:04d}.jpg), frame)3.3 ResNet18的“活体化”改造不只是去掉fc层那么简单直接加载torchvision.models.resnet18(pretrainedTrue)是危险的。ImageNet预训练的ResNet18其最后的全连接层fc是为1000类分类设计的而活体检测只有2类real/spoof。若直接移除fc用layer4输出做特征会面临两个问题特征尺度失配ImageNet的输入是224×224而OULU-NPU人脸ROI裁剪后是224×224看似匹配。但ImageNet的归一化参数mean[0.485,0.456,0.406], std[0.229,0.224,0.225]是针对自然图像统计的人脸皮肤区域的像素分布高亮、低饱和与此差异巨大。实测发现直接使用会导致layer4输出的特征方差极小0.01LSTM几乎学不到有效信号。我的解决方案是在ResNet18前端插入一个“人脸自适应归一化层”class FaceNorm(nn.Module): def __init__(self): super().__init__() # OULU-NPU人脸ROI的均值与标准差实测统计 self.mean nn.Parameter(torch.tensor([0.52, 0.43, 0.39]), requires_gradFalse) self.std nn.Parameter(torch.tensor([0.21, 0.19, 0.18]), requires_gradFalse) def forward(self, x): return (x - self.mean.view(1,3,1,1)) / self.std.view(1,3,1,1) # 在模型定义中 self.face_norm FaceNorm() self.resnet models.resnet18(pretrainedTrue) # 移除fc层 self.resnet.fc nn.Identity()这个FaceNorm层的参数是我对OULU-NPU训练集所有帧做了一次遍历统计得出的。它让输入特征的分布更接近ResNet18预训练时的期望layer4输出的特征方差稳定在0.8-1.2之间为LSTM提供了高质量的“原材料”。3.4 LSTM的“时序建模”精调门控、层数与初始化的艺术LSTM不是黑箱它的每个门控参数都影响着时序信息的流动。OULU-NPU的30帧序列既不够长到需要多层LSTM易梯度消失也不够短到单层LSTM就足够。我的实测结论是单层LSTM 手动权重初始化是最优解。层数选择尝试了1层、2层、3层LSTM。2层LSTM在训练初期ACER下降更快但第30轮后开始震荡验证集ACER比1层高0.8%。原因是第二层LSTM接收到的输入是第一层输出的“已加工”特征其时序模式已被部分抽象反而丢失了原始帧间差异的物理意义如打印攻击的绝对静止性。隐藏层大小hidden_size512。理由很实在ResNet18的layer4输出是512维若LSTMhidden_size设为256则信息通道被强制压缩导致判别力下降设为1024则参数量暴增且无证据表明更大容量能提升性能。权重初始化PyTorch默认的LSTM初始化orthogonal在活体检测任务上表现平庸。我采用门控特定初始化def init_lstm_weights(lstm_layer): for name, param in lstm_layer.named_parameters(): if weight_ih in name: # input-to-hidden weights nn.init.xavier_uniform_(param.data) elif weight_hh in name: # hidden-to-hidden weights nn.init.orthogonal_(param.data) elif bias in name: # biases param.data.zero_() # 设置forget gate bias为1利于长期记忆 param.data[lstm_layer.hidden_size:2*lstm_layer.hidden_size] 1这个技巧源自LSTM原始论文Hochreiter Schmidhuber, 1997的建议将遗忘门forget gate的初始偏置设为1能让网络在训练初期更倾向于记住历史信息避免因随机初始化导致的“健忘症”对捕捉眨眼等慢速生理节律至关重要。4. 实操过程与核心环节实现从零开始的完整训练流水线4.1 数据集类OULUDataset如何优雅地加载30帧序列一个健壮的Dataset类是整个训练流程的基石。它必须解决三个核心问题时序一致性、内存效率、数据增强。以下是OULUDataset的核心实现class OULUDataset(Dataset): def __init__(self, csv_file, transformNone, seq_len30): self.df pd.read_csv(csv_file) # 包含video_path, label, attack_type self.transform transform self.seq_len seq_len def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] video_dir row[video_path].replace(.avi, ) # 对应缓存目录 label row[label] # 0real, 1spoof # 1. 加载30帧图像 frames [] for i in range(1, self.seq_len 1): frame_path os.path.join(video_dir, fframe_{i:04d}.jpg) img Image.open(frame_path).convert(RGB) if self.transform: img self.transform(img) frames.append(img) # 2. 堆叠为 (seq_len, C, H, W) video_tensor torch.stack(frames, dim0) # shape: (30, 3, 224, 224) # 3. 时序增强随机帧丢弃模拟摄像头丢帧 if self.transform and hasattr(self.transform, random_drop): drop_mask torch.rand(self.seq_len) 0.1 # 10%概率丢弃 video_tensor video_tensor[~drop_mask] # 补齐到30帧 if len(video_tensor) self.seq_len: pad_len self.seq_len - len(video_tensor) video_tensor torch.cat([video_tensor, video_tensor[-pad_len:]], dim0) return video_tensor, label # 使用示例 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), FaceNorm(), # 我们自定义的归一化 ]) dataset OULUDataset(train_list.csv, transformtransform) dataloader DataLoader(dataset, batch_size8, shuffleTrue, num_workers4)关键点解析FaceNorm()被集成在transform链中确保所有帧使用同一套归一化参数。时序增强random_drop模拟真实场景中摄像头可能发生的轻微丢帧迫使模型学习更鲁棒的时序模式而非死记硬背固定帧序。实测此增强使ACER降低0.6%。num_workers4是经验值num_workers过高如8会导致decord解码进程竞争反而降低吞吐过低如1则CPU成为瓶颈。在RTX 409032GB RAM机器上4是最佳平衡点。4.2 模型定义ResNet18LSTM清晰的模块化与梯度控制模型代码必须体现“空间-时间”分离的设计哲学并便于调试class ResNet18LSTM(nn.Module): def __init__(self, num_classes2, lstm_hidden512, dropout0.5): super().__init__() self.face_norm FaceNorm() self.resnet models.resnet18(pretrainedTrue) self.resnet.fc nn.Identity() # 移除fc # 冻结resnet前3层只微调layer4 for param in self.resnet.layer1.parameters(): param.requires_grad False for param in self.resnet.layer2.parameters(): param.requires_grad False for param in self.resnet.layer3.parameters(): param.requires_grad False # layer4保留梯度学习人脸特有纹理 self.lstm nn.LSTM(input_size512, hidden_sizelstm_hidden, num_layers1, batch_firstFalse, dropoutdropout) self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(lstm_hidden, 128), nn.ReLU(), nn.Dropout(dropout), nn.Linear(128, num_classes) ) def forward(self, x): # x: (seq_len, batch, C, H, W) - (30, 8, 3, 224, 224) seq_len, batch_size, c, h, w x.shape # 重塑为 (seq_len * batch, C, H, W) 以便批量处理 x x.view(seq_len * batch_size, c, h, w) x self.face_norm(x) x self.resnet(x) # 输出 (seq_len * batch, 512) # 重塑回 (seq_len, batch, 512) x x.view(seq_len, batch_size, -1) # LSTM前向传播 lstm_out, (h_n, c_n) self.lstm(x) # lstm_out: (seq_len, batch, 512) # 取最后一帧的输出 last_output lstm_out[-1] # (batch, 512) return self.classifier(last_output) # 初始化与梯度检查 model ResNet18LSTM() init_lstm_weights(model.lstm) # 应用我们定制的初始化 # 检查layer4是否可训练 print(layer4 grad:, next(model.resnet.layer4.parameters()).requires_grad) # True梯度控制是关键layer4是唯一被微调的CNN部分因为它负责提取最高阶的人脸判别特征如皮肤毛孔、血管纹路这些特征在伪造攻击中变化最剧烈。而layer1-layer3的特征边缘、纹理基元在ImageNet上已足够通用冻结它们能防止小样本数据下的灾难性遗忘。4.3 训练循环与损失函数Focal Loss Center Loss的协同作战标准的CrossEntropyLoss在OULU-NPU上效果很差因为攻击样本printreplay内部也存在不平衡print attack有1200段replay attack有1800段。Focal Loss通过调节难易样本的权重能有效聚焦于难分样本如高仿真replayclass FocalLoss(nn.Module): def __init__(self, alpha1, gamma2, reductionmean): super().__init__() self.alpha alpha self.gamma gamma self.reduction reduction def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-ce_loss) focal_weight (self.alpha * (1-pt)**self.gamma) focal_loss focal_weight * ce_loss if self.reduction mean: return focal_loss.mean() return focal_loss.sum() # Center Loss实现简化版 class CenterLoss(nn.Module): def __init__(self, num_classes, feat_dim, device): super().__init__() self.num_classes num_classes self.feat_dim feat_dim self.centers nn.Parameter(torch.randn(num_classes, feat_dim).to(device)) def forward(self, x, labels): # x: (batch, feat_dim), labels: (batch,) batch_size x.size(0) # 计算每个样本到其类别中心的距离 distmat torch.pow(x, 2).sum(dim1, keepdimTrue).expand(batch_size, self.num_classes) \ torch.pow(self.centers, 2).sum(dim1, keepdimTrue).expand(self.num_classes, batch_size).t() distmat.addmm_(1, -2, x, self.centers.t()) classes torch.arange(self.num_classes).long().to(x.device) labels labels.unsqueeze(1) mask labels.eq(classes.expand_as(labels)) dist distmat * mask.float() loss dist.clamp(min1e-12).sum() / batch_size return loss # 训练循环核心 criterion_focal FocalLoss(alpha1, gamma2) criterion_center CenterLoss(num_classes2, feat_dim512, devicedevice) optimizer torch.optim.Adam([ {params: model.resnet.layer4.parameters(), lr: 1e-4}, {params: model.lstm.parameters(), lr: 1e-3}, {params: model.classifier.parameters(), lr: 1e-3}, {params: criterion_center.parameters(), lr: 0.5} # center loss学习率单独设置 ], weight_decay1e-4) for epoch in range(100): for batch_idx, (data, target) in enumerate(dataloader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 前向传播 output model(data) feat model.lstm[0].output # 获取LSTM最后一层输出512维特征 # 计算损失 loss_focal criterion_focal(output, target) loss_center criterion_center(feat, target) loss loss_focal 0.1 * loss_center # center loss权重设为0.1 loss.backward() optimizer.step()参数选择依据gamma2是Focal Loss的经典值能有效抑制易分样本如明显打印攻击的梯度贡献。center loss weight0.1过大如0.5会导致特征过度聚类丧失类间判别力过小如0.01则起不到正则化作用。0.1是多次消融实验的最优值。分层学习率layer4参数更新最慢1e-4因其已是强特征提取器LSTM和Classifier更新较快1e-3因其需从头学习时序模式与决策边界。4.4 测试与评估Protocol 1的ACER计算拒绝“假高分”OULU-NPU的Protocol 1评估是活体检测领域的“高考”。它要求测试集中的所有受试者在训练集中完全未出现过。这意味着模型无法通过记忆人脸ID来作弊必须真正学会活体与攻击的本质差异。ACERAverage Classification Error Rate计算公式为ACER (APCER BPCER) / 2 APCER FP / (FP TN) # Attack Presentation Classification Error Rate (printreplay误判为real) BPCER FN / (FN TP) # Bona Fide Presentation Classification Error Rate (real误判为spoof)其中TP真实人脸正确识别TN攻击正确识别FP攻击误判为真实FN真实误判为攻击。我的测试脚本evaluate_protocol1.py严格遵循此定义def evaluate_protocol1(model, test_loader, device): model.eval() all_preds [] all_targets [] with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) pred torch.softmax(output, dim1)[:, 1] # spoof概率 all_preds.extend(pred.cpu().numpy()) all_targets.extend(target.cpu().numpy()) # 计算APCER/BPCER # APCER: 在所有attack样本中误判为real的比例 attack_mask np.array(all_targets) 1 attack_preds np.array(all_preds)[attack_mask] # 设定阈值找到使APCERBPCER最小的点 thresholds np.arange(0.1, 0.9, 0.01) acer_scores [] for th in thresholds: fp np.sum((attack_preds th)) # attack被当成real (pred0.5) tn np.sum((attack_preds th)) apcer fp / (fp tn) if (fp tn) 0 else 0 # BPCER: 在所有real样本中误判为spoof的比例 real_mask np.array(all_targets) 0 real_preds np.array(all_preds)[real_mask] fn np.sum((real_preds th)) # real被当成spoof (pred0.5) tp np.sum((real_preds th)) bpcer fn / (fn tp) if (fn tp) 0 else 0 acer_scores.append((apcer bpcer) / 2) best_acer min(acer_scores) best_th thresholds[np.argmin(acer_scores
延伸阅读

更多相关文章

2026/9/19 4:03:23

Altium Designer死铜清理全攻略:从判定逻辑到实战排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 5:28:50

FusionCompute 8.0.0国产虚拟化平台部署核心原理与实战指南

1. 这不是“装个虚拟化平台”那么简单:FusionCompute 8.0.0到底在解决什么问题?FusionCompute 8.0.0不是一套拿来就用的桌面软件,它是一套面向中大型企业数据中心的、全栈自主可控的服务器虚拟化平台。你看到的“CNA”和“VRM”,表…

2026/9/19 5:28:50

Windows系统部署Elasticsearch 9.2.1全攻略

1. 环境准备与前置检查在Windows系统上部署Elasticsearch 9.2.1之前,需要确保运行环境满足基本要求。我建议先检查以下几个关键点:操作系统版本:Windows 10 1809或更高版本,Windows Server 2019/2022(实测在LTSC版本上…

2026/9/19 5:28:50

外接屏幕闪烁怎么排查?从线材、驱动到电源的完整解决指南

1. 外接屏幕闪烁到底是怎么回事外接屏幕闪烁这个问题,我从入行到现在少说遇到过几十次。它跟显卡驱动崩溃导致的黑屏不一样,也跟显示器老化出现的坏点不一样,闪烁的表现形式很多样:有的是间歇性闪一下,有的是持续高频抖…

2026/9/18 14:13:01

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/19 0:03:10

验证 OpenSpec 兼容性,Cursor 的 Token 从 TaoToken 出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

书桌角落的 Mac mini,OpenClaw 通过 TaoToken 跑任务。

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

oh-my-hermes:打造跨工具的命令编排与插件化工作流

1. 项目概述与设计初衷1.1 它到底是什么先说结论:oh-my-hermes 是一个面向开发者日常终端操作的效率工具套件,核心定位是“把分散在各类命令行工具里的高频操作,统一收拢成一套插件化、可编排的工作流”。项目灵感来源很明显——oh-my-zsh 重…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码