简介这份资源是《DeepLearning tutorial5CNN卷积神经网络应用于人脸识别》一文的配套代码包面向具备一定深度学习基础、希望动手实践卷积神经网络图像分类的开发者与学习者解决从理论到代码落地、跑通人脸识别完整流程的问题。压缩包共4个文件约14.64MB包含2个Python脚本分别负责训练与推理、1个pkl模型参数文件以及1个gif数据集预览图覆盖数据加载、网络搭建、训练与测试等关键环节。目前已有17748人学习下载热度较高。借助这份代码读者可以对照博文复现基于Olivetti Faces数据集的CNN人脸识别实验理解卷积、池化与全连接层的组织方式掌握模型训练与参数保存、加载推理的完整链路并在此基础上替换数据集或调整网络结构快速迁移到自己的图像分类任务中。1. 从一张 112×112 的灰度图说起这套 CNN 人脸识别流程到底能跑通什么很多人第一次做人脸识别卡住的地方不是模型结构而是数据从哪来、怎么对齐、训练完怎么验证。这套 CNN 卷积神经网络人脸识别流程核心解决的就是「从原始人脸图到可推理模型」这条完整链路数据预处理、网络搭建、训练调参、模型保存、单张图片推理每一步都有可复现的代码。它适合两类人一是刚接触深度学习、想拿一个完整项目练手的开发者二是已经会调库、但没自己从零搭过 CNN 的从业者。整套流程基于 Python 和主流深度学习框架输入是固定尺寸的人脸灰度图或 RGB 图输出是身份分类结果。下面我按实际跑通的顺序拆开讲包括参数怎么设、哪里容易翻车、验证时看什么指标。2. 数据准备与预处理把人脸图变成网络能吃的张量2.1 为什么人脸识别第一步不是搭网络而是对齐和归一化CNN 对人脸的姿态、光照、尺度非常敏感。同一张脸如果训练集里是正脸、测试集里是侧脸准确率会断崖式下跌。常见做法是先用检测器把人脸框出来再做仿射变换对齐到标准姿态最后统一缩放到固定尺寸。这个尺寸不是随便定的112×112 是很多人脸识别网络的常用输入再小会丢细节再大会增加计算量。灰度化还是保留 RGB取决于你的数据量和任务难度数据少、光照差异大时灰度图反而更稳数据充足、需要区分肤色和妆容时保留 RGB 更好。归一化是另一个容易被忽略的点。像素值从 0-255 直接送进网络梯度会不稳定。我一般会做两步先减均值再除以标准差或者直接缩放到 [-1, 1]。均值不要用 ImageNet 的用人脸数据集自己的均值否则颜色分布对不上。2.2 用代码把原始图片整理成训练集下面这段代码做三件事读取指定目录下的人脸图、检测并对齐、保存成统一尺寸。依赖 OpenCV 和 NumPy检测器用常见的 Haar 级联或 DNN 检测器都行这里用 Haar 做示例因为它不需要额外下载大模型。import cv2 import os import numpy as np # 输入输出目录按自己的数据集路径改 input_dir ./raw_faces output_dir ./aligned_faces target_size (112, 112) # 加载人脸检测器Haar 级联文件随 OpenCV 安装包提供 detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) os.makedirs(output_dir, exist_okTrue) for label in os.listdir(input_dir): label_dir os.path.join(input_dir, label) if not os.path.isdir(label_dir): continue save_dir os.path.join(output_dir, label) os.makedirs(save_dir, exist_okTrue) for img_name in os.listdir(label_dir): img_path os.path.join(label_dir, img_name) img cv2.imread(img_path) if img is None: continue gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(60, 60) ) # 只保留检测到的第一张人脸多人脸场景需要额外策略 for (x, y, w, h) in faces[:1]: face gray[y:yh, x:xw] face cv2.resize(face, target_size) # 归一化到 [0,1]后续训练时再减均值 face face.astype(float32) / 255.0 save_path os.path.join(save_dir, img_name) cv2.imwrite(save_path, (face * 255).astype(uint8))逻辑说明detectMultiScale的scaleFactor控制金字塔缩放步长1.1 是速度和召回率的折中minNeighbors越大误检越少但漏检越多5 是常用起点。minSize过滤掉太小的误检框。只取faces[:1]是因为分类任务通常一张图对应一个人如果一张图有多人需要改成按人脸分别保存并重新分配标签。参数说明target_size要和后面网络输入层一致改这里就必须改网络。归一化在保存时做了缩放训练时如果再做一次均值和标准差处理注意不要重复归一化导致数值范围异常。提示Haar 检测器对侧脸和遮挡比较吃力如果数据里这类样本多换 DNN 检测器或 MTCNN检测率会明显提升。3. 搭一个能收敛的 CNN层数、卷积核和正则化怎么定3.1 网络结构不是越深越好先看你的数据量人脸识别分两类任务一是闭集分类比如 100 个人、每人 50 张图输出就是 100 类二是开集验证判断两张图是不是同一个人。这套流程主要针对闭集分类网络不需要 ResNet 那么深。我一般用 4 到 6 个卷积块每个块包含卷积、批归一化、ReLU、池化。卷积核统一用 3×3步长 1padding 设为 same这样特征图尺寸只由池化决定计算起来心里有数。通道数从 32 开始每经过一次池化翻倍32、64、128、256。再往上加小数据集上参数量会爆炸过拟合几乎必然。全连接层前用全局平均池化替代展平能显著减少参数这是很多人忽略的省参技巧。3.2 用 Keras 把网络搭出来并解释每个参数下面代码用 Keras Sequential API 搭一个 4 块 CNN输入 112×112×1输出类别数由num_classes决定。import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape(112, 112, 1), num_classes100): model models.Sequential([ # 第 1 块32 通道池化后 56×56 layers.Conv2D(32, (3, 3), paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), # 第 2 块64 通道池化后 28×28 layers.Conv2D(64, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), # 第 3 块128 通道池化后 14×14 layers.Conv2D(128, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), # 第 4 块256 通道池化后 7×7 layers.Conv2D(256, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), # 全局平均池化替代展平减少参数量 layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model model build_cnn(input_shape(112, 112, 1), num_classes100) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) model.summary()逻辑说明每个卷积块后接批归一化是为了让每层输入分布稳定学习率可以设大一点而不容易发散。Dropout 放在全局平均池化之后、全连接之前比例 0.5 是分类任务的常用值数据量小可以提到 0.6。优化器用 Adam初始学习率 1e-3如果训练 loss 震荡降到 1e-4。参数说明num_classes必须等于你实际的身份类别数多一类少一类都会导致训练时报错或预测错位。input_shape的通道数要和预处理一致灰度图是 1RGB 是 3。paddingsame保证卷积后尺寸不变池化层负责降维。注意如果训练集类别极度不均衡比如某些人只有 5 张图、某些人有 200 张要在fit里传class_weight否则模型会偏向多数类。4. 训练、保存与推理把模型跑起来并验证它真的能用4.1 数据生成器和训练循环的关键参数数据量不大时可以用ImageDataGenerator做在线增强但人脸识别里水平翻转要慎用因为左右脸特征不对称翻转可能引入噪声。我一般只做小角度旋转和亮度微调。下面代码用生成器读数据并训练。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, # 如果预处理已归一化这里改成 1.0 rotation_range10, # 小角度旋转避免人脸结构失真 width_shift_range0.05, height_shift_range0.05, brightness_range[0.9, 1.1], validation_split0.2 ) train_gen train_datagen.flow_from_directory( ./aligned_faces, target_size(112, 112), color_modegrayscale, batch_size32, class_modecategorical, subsettraining ) val_gen train_datagen.flow_from_directory( ./aligned_faces, target_size(112, 112), color_modegrayscale, batch_size32, class_modecategorical, subsetvalidation ) history model.fit( train_gen, validation_dataval_gen, epochs50, callbacks[ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience4, min_lr1e-6 ) ] ) model.save(face_cnn.h5)逻辑说明validation_split0.2从训练目录里切 20% 做验证注意生成器会按类别分层切分比手动切更稳。EarlyStopping的patience8表示验证 loss 连续 8 轮不降就停restore_best_weightsTrue保证保存的是最优轮次的权重而不是最后一轮。ReduceLROnPlateau在验证 loss 停滞时把学习率减半帮助跳出局部最优。参数说明batch_size32是显存和梯度稳定性的折中显存小就降到 16。epochs50配合早停实际可能二三十轮就结束。color_mode必须和网络输入通道一致否则会报维度错误。4.2 单张图片推理和批量验证的写法训练完要验证模型是不是真的能用不能只看训练日志。下面代码加载模型对单张图做预测并输出置信度。import numpy as np import cv2 from tensorflow.keras.models import load_model model load_model(face_cnn.h5) class_names list(train_gen.class_indices.keys()) def predict_face(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (112, 112)) img img.astype(float32) / 255.0 img np.expand_dims(img, axis(0, -1)) # 变成 (1,112,112,1) preds model.predict(img, verbose0) idx np.argmax(preds) return class_names[idx], float(preds[0][idx]) name, conf predict_face(./test.jpg) print(f预测身份: {name}, 置信度: {conf:.4f})逻辑说明np.expand_dims两次第一次加 batch 维度第二次加通道维度顺序不能反否则形状对不上。verbose0关闭预测进度条批量推理时更干净。置信度低于 0.6 时建议人工复核不要直接采信。参数说明class_names从生成器的class_indices取顺序和训练时一致不能自己手动排序。如果换了数据集重新训练这个映射会变推理代码要同步更新。提示批量验证时把所有测试图整理成和训练集相同的目录结构用flow_from_directory跑evaluate得到的准确率比单张抽查更有说服力。5. 避坑与排查这几处翻车我踩过不止一次5.1 训练准确率很高但推理一塌糊涂现象训练集准确率 99%验证集也有 95%但拿新图片预测全错。原因通常是预处理不一致。训练时用了生成器的rescale1./255推理时又做了一次归一化或者训练用灰度、推理用 RGB。解决把预处理逻辑抽成一个函数训练和推理共用不要各写一套。5.2 loss 变成 NaN 或者一直不降现象训练几轮后 loss 突然变成 NaN。原因多半是学习率太大或者输入数据里有异常值。解决先把学习率降到 1e-4检查数据里有没有全黑、全白或尺寸不对的图。批归一化层加在卷积之后、激活之前能明显缓解这个问题。5.3 类别数对不上导致维度报错现象model.fit时报logits and labels must have the same shape。原因num_classes设成了 10但实际目录下有 12 个人。解决训练前先打印train_gen.num_classes用这个值去建模型不要凭记忆写。5.4 验证集划分后某些类只剩一张图现象验证集准确率波动极大一会儿 60% 一会儿 90%。原因validation_split随机切分小类别可能被切得只剩一两张。解决数据量少于每类 20 张时改用 K 折交叉验证或者手动按类别分层切分保证验证集每类至少 3 张。5.5 保存的模型加载后预测结果和训练时不一样现象训练完直接预测是对的保存再加载就错了。原因保存时用了model.save但推理时没有重建相同的预处理或者class_indices顺序变了。解决把class_indices一起保存成 json推理时读取不要依赖目录扫描顺序。6. 进阶技巧用混淆矩阵和 Top-3 准确率判断模型边界训练日志里的 accuracy 只告诉你整体对多少不告诉你错在哪。我习惯在验证集上跑一遍混淆矩阵看哪些身份容易被混。下面代码输出混淆矩阵和 Top-3 准确率这两个指标比单一 accuracy 更能反映模型在真实场景下的可用性。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 收集验证集全部预测结果 val_gen.reset() preds model.predict(val_gen, verbose0) y_pred np.argmax(preds, axis1) y_true val_gen.classes cm confusion_matrix(y_true, y_pred) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotFalse, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png) # Top-3 准确率 top3 np.mean([ y_true[i] in np.argsort(preds[i])[-3:] for i in range(len(y_true)) ]) print(fTop-3 准确率: {top3:.4f})逻辑说明val_gen.reset()必须调用否则生成器可能停在上一次遍历的位置导致预测和标签错位。np.argsort(preds[i])[-3:]取概率最高的三个类别只要真实标签在里面就算对。Top-3 在身份多、类间相似度高时特别有用比如双胞胎或同一个人不同年龄段。参数说明混淆矩阵图的大小根据类别数调整类别超过 50 时annotFalse避免数字挤在一起。Top-3 准确率一般比 Top-1 高 5 到 15 个百分点如果差距过大说明模型对某些类完全没学到需要检查那几类的数据质量。从那以后我每次训练完都强制跑一遍混淆矩阵和 Top-3不看这两个指标就不敢把模型交出去。希望帮到你。本文还有配套的精品资源点击获取