☰
CNN卷积神经网络人脸识别代码实现:从数据管线到模型部署的完整链路
2026/9/29 3:11:25 网站建设 项目流程

简介:这份资源是《DeepLearning tutorial(5)CNN卷积神经网络应用于人脸识别》一文的配套代码包,面向具备一定深度学习基础、希望动手实践卷积神经网络图像分类的开发者与学习者,帮助其理解并复现从数据加载、网络搭建到训练与预测的完整人脸识别流程。压缩包共4个文件,包含2个Python脚本分别承担CNN模型训练与推理调用,1个pkl参数文件保存训练好的模型权重,另有1个gif图像文件用于展示数据集样本,整体约14.64MB,结构精简、便于直接运行调试。目前已有17745人学习下载,热度较高。借助这份代码,读者可对照博文梳理CNN在人脸识别任务中的实现细节,掌握卷积、池化、全连接层的组织方式,并基于已有参数文件快速验证模型效果,也可在此基础上替换数据集或调整网络结构进行二次实验,是入门CNN图像分类的实用参考。

1. 从一张 112×112 的灰度图说起:这套 CNN 人脸识别代码到底能跑出什么

很多人第一次接触人脸识别,脑子里浮现的是门禁机“滴”一声开门,但真到自己动手,卡住的地方往往特别具体:手里有一堆按人名分好文件夹的照片,想训一个能认出“这是谁”的模型,却不知道从哪张图开始喂、卷积核该设几层、最后那层 Softmax 到底输出什么。这套 CNN 卷积神经网络人脸识别代码实现,解决的正是这个从“有图”到“能识别”的完整链路问题——它把数据读取、网络搭建、训练循环、模型保存和单张推理串成了一条能直接跑通的流水线,而不是丢给你一个孤零零的model.py。

它适合两类人:一类是刚学完卷积神经网络原理、想找一个能改能调的真实项目练手的学生或转行者;另一类是做嵌入式或门禁类产品、需要先在小数据集上验证识别方案可行性的工程师。代码基于 Python 和主流深度学习框架,输入统一到 112×112 或 96×96 的灰度或 RGB 图,输出是每个类别人名对应的概率分布。下面我不讲教科书里的卷积公式,只讲这份代码怎么用、参数怎么改、哪里最容易翻车。

2. 数据管线与网络结构:把照片变成网络能吃的张量

2.1 目录结构决定读取逻辑,别急着改代码

这套代码默认的数据组织方式是每个类别一个文件夹,文件夹名就是标签名。常见做法是用ImageFolder或自己写一个Dataset子类来遍历。我一般会先确认三件事:图片格式是否统一、有没有损坏文件、类别之间数量是否严重失衡。如果某个人的照片有 800 张、另一个人只有 30 张,训练时模型会明显偏向多数类,这不是网络结构能救回来的。

import os from PIL import Image from torch.utils.data import Dataset class FaceDataset(Dataset): def __init__(self, root_dir, transform=None): self.samples = [] self.transform = transform # 遍历每个子文件夹,文件夹名即标签 self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {c: i for i, c in enumerate(self.classes)} for cls in self.classes: cls_dir = os.path.join(root_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): # 过滤非图片文件,避免读取时报错 if fname.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): self.samples.append((os.path.join(cls_dir, fname), self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert('RGB') # 统一转 RGB,防止灰度图混入 if self.transform: img = self.transform(img) return img, label

这段代码的关键在convert('RGB')和文件后缀过滤。前者保证通道数一致,后者避免读到.DS_Store或缩略图缓存导致训练中断。class_to_idx用sorted是为了让标签顺序可复现,否则每次运行类别编号可能变化,保存的模型再加载时就会对不上号。

2.2 网络结构:四层卷积够用,但别照搬

代码里的 CNN 主干通常是 3 到 4 个卷积块,每个块是“卷积 + 批归一化 + 激活 + 池化”的组合。以输入 112×112 为例,第一层卷积核常见设 3×3、步长 1、填充 1,输出通道 32 或 64。这里有个容易被忽略的点:人脸识别和通用物体分类不同,五官的相对位置比纹理更重要,所以池化层不要一上来就压得太狠,否则眼睛、嘴角这些关键区域在特征图上只剩几个像素。

import torch.nn as nn class FaceCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( # 输入 3×112×112 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56×56 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 28×28 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 14×14 nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1) # 全局池化,输出 128×1×1 ) self.classifier = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x)

AdaptiveAvgPool2d(1)是这份代码里比较聪明的一处:它把任意空间尺寸压成 1×1,这样输入图片尺寸稍有变化也不会导致全连接层维度报错。num_classes必须等于你实际文件夹的数量,改错这个参数是新手最常见的翻车点之一,训练时 loss 不降、准确率卡在随机水平,八成就是它。

2.3 训练循环:学习率和批大小怎么定

训练部分一般用交叉熵损失配 Adam 或 SGD。我一般会先把学习率设成 1e-3 跑几轮看 loss 曲线,如果前三轮 loss 几乎不动,就降到 1e-4;如果 loss 剧烈震荡甚至变成 nan,就再降一个数量级。批大小在显存允许的前提下取 32 或 64,太小会让批归一化统计不稳定,太大则在小数据集上容易过拟合。

import torch from torch.utils.data import DataLoader from torchvision import transforms transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.RandomHorizontalFlip(), # 人脸左右翻转是合理增强 transforms.ToTensor(), transforms.Normalize(mean=[0.5]*3, std=[0.5]*3) ]) dataset = FaceDataset('data/train', transform=transform) loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=2) model = FaceCNN(num_classes=len(dataset.classes)).cuda() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(30): model.train() total_loss = 0 for imgs, labels in loader: imgs, labels = imgs.cuda(), labels.cuda() optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() total_loss += loss.item() print(f'epoch {epoch}, loss {total_loss/len(loader):.4f}')

RandomHorizontalFlip对人脸是安全的增强,因为左右翻转后仍是一张合理的人脸;但如果你做的是带方向性的任务,比如识别车牌或文字,这个增强就会帮倒忙。Normalize的均值和方差用 0.5 是通用做法,如果换成自己数据集的统计值,收敛通常会更快一点。

3. 训练完到能用:模型保存、加载与单张推理

3.1 保存的不只是权重,还有类别映射

很多人训完模型只存state_dict,推理时却忘了当时类别顺序是什么,结果把张三识别成李四。正确做法是把类别列表和权重一起存,或者至少存一个 json 记录class_to_idx。

# 保存 torch.save({ 'model_state': model.state_dict(), 'classes': dataset.classes, 'input_size': (112, 112) }, 'face_cnn.pth') # 加载 ckpt = torch.load('face_cnn.pth', map_location='cpu') classes = ckpt['classes'] model = FaceCNN(num_classes=len(classes)) model.load_state_dict(ckpt['model_state']) model.eval()

map_location='cpu'是为了在没有 GPU 的机器上也能加载,部署到边缘设备时这一步很关键。model.eval()必须调用,否则批归一化会继续用当前批的统计量,单张推理时结果会飘。

3.2 单张图片推理与置信度阈值

推理阶段要把图片做和训练时完全一致的预处理,少一步归一化都会让结果偏掉。输出经过 Softmax 后取最大概率的类别,同时建议设一个置信度阈值,低于阈值就判为“未知”,而不是硬塞进某个已知类别。

from PIL import Image import torch.nn.functional as F def predict(img_path, model, classes, threshold=0.6): img = Image.open(img_path).convert('RGB') img = transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): logits = model(img) probs = F.softmax(logits, dim=1) conf, idx = probs.max(dim=1) if conf.item() < threshold: return 'unknown', conf.item() return classes[idx.item()], conf.item()

threshold设 0.6 是经验值,类别多、类间相似度高时可以调到 0.7 以上。这个机制在门禁场景里特别重要:宁可拒识,也不要把陌生人放进来。

3.3 用混淆矩阵定位问题类别

训练准确率高不代表每个类别都好。我习惯在验证集上跑一遍混淆矩阵,看哪些类别互相混淆。常见现象是戴眼镜和不戴眼镜的同一人被分到不同类,或者光照差异大的图片被误判。这时候优先补数据,而不是加网络深度。

指标含义关注点
训练 loss模型拟合程度持续下降但验证不降即过拟合
验证准确率泛化能力与训练差距超过 10% 要警惕
混淆矩阵类别间误判分布定位具体哪两类容易混
置信度分布预测把握程度低置信样本可用于主动学习

4. 避坑与排查:这几处翻车我见过太多次

4.1 现象:loss 一直是 nan

原因:学习率过大,或者输入没有归一化,像素值 0-255 直接进网络导致梯度爆炸。 解决:把学习率降到 1e-4,确认ToTensor()后再接Normalize,检查数据里有没有全黑或损坏图片。

4.2 现象:训练准确率 99%,实际用一张新照片全错

原因:数据泄漏,训练集和验证集里有同一张图或同一人的高度相似图,模型只是记住了人脸而不是学到特征。 解决:按人划分训练验证集,而不是随机按图片划分;同一个人不同角度的照片要分到同一侧。

4.3 现象:推理时类别编号对不上

原因:保存模型时没存classes列表,或者重新运行时文件夹顺序变了。 解决:始终把类别映射和权重一起保存,加载后打印classes确认顺序。

4.4 现象:GPU 上训练正常,CPU 部署报错

原因:模型保存时张量在 GPU 上,加载时没有指定map_location。 解决:加载时统一加map_location='cpu',部署前在目标设备上跑一遍推理。

4.5 现象:输入图片尺寸不一致导致全连接层维度报错

原因:网络里用了固定尺寸的view或Linear输入维度写死。 解决:用AdaptiveAvgPool2d(1)替代固定展平,或者在预处理里强制Resize到统一尺寸。

5. 进阶技巧:把人脸识别从“能跑”推到“敢用”

真正落地时,纯 CNN 分类有个天然短板:类别是固定的,新增一个人就得重新训练。我一般会在这份代码基础上做一步改造——把 CNN 当特征提取器,去掉最后的分类层,输出 128 维或 512 维嵌入向量,然后用余弦相似度做比对。这样新增人员只需要注册一张底图,不用重训模型。

# 去掉 classifier,只保留特征 feature_extractor = nn.Sequential(*list(model.children())[:-1]) feature_extractor.eval() def get_embedding(img_path): img = Image.open(img_path).convert('RGB') img = transform(img).unsqueeze(0) with torch.no_grad(): feat = feature_extractor(img) return feat.view(-1) # 比对 emb1 = get_embedding('a.jpg') emb2 = get_embedding('b.jpg') similarity = F.cosine_similarity(emb1.unsqueeze(0), emb2.unsqueeze(0)) print('相似度', similarity.item())

相似度阈值一般设在 0.6 到 0.75 之间,具体取决于你的特征维度和数据分布。验证方法是拿同一人的两张不同照片和不同人的照片各跑一批,画一个相似度分布直方图,看两类分布的交叠区域在哪里,阈值就取在交叠最少的位置。

另一个实用技巧是测试时增强:对同一张图做水平翻转和轻微裁剪,各取一次嵌入再平均,能小幅提升稳定性。代价是推理时间翻倍,门禁这种对延迟不敏感的场景完全值得。

从那以后我每次训完人脸模型,都会强制走一遍“同一人不同图 + 不同人图”的相似度分布验证,确认阈值不是拍脑袋定的,才敢往设备上刷。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询