简介:面向希望借助卷积神经网络完成人脸识别的深度学习者,这份配套代码来自CSDN一篇CNN实战教程,完整覆盖图像预处理、网络搭建、模型训练、参数保存与复用等关键环节。包内共4个文件,包括两个Python脚本,分别负责CNN训练和测试识别任务;另有一个已训练好的pkl模型参数文件,以及一张用于观察数据集样本的gif预览图,整体仅14.64MB,轻量易用,适合本地复现。目前已有17746人学习浏览,属于热度较高的入门实战资料。读者可对照博文流程理解CNN结构设计与参数调优思路,也可以直接加载pkl参数运行识别,省去重新训练的时间;还能在现有代码基础上替换数据集、调整网络层次,快速用于课程设计或毕业设计,同时可参考pkl参数文件观察训练后的模型状态,为后续调优提供基线。
1. 从“认出一张脸”到“CNN 实战”:这篇笔记能带你走到哪一步
如果你搜过“CNN 人脸识别”,大概率见过两类东西:一类是高校课件,把卷积、池化、全连接讲得云里雾里;另一类是论文复现,直接丢给你一个几百层的 ResNet 训练脚本,跑一次卡两小时,最后准确率还上不去。这篇笔记想做的,是中间那条路——用 CNN 卷积神经网络把“人脸识别”这件事从头到尾拆开:从图像怎么变成张量,到卷积层在干什么,再到怎么用 Python 搭一个能跑通训练和推理的完整流程,最后给出实际调参时最容易翻车的几个细节。
适合谁读?你有 Python 基础,用过 TensorFlow 或 PyTorch 其中之一,但没正经跑过一个人脸识别项目;或者你已经跑过分类模型,但发现人脸识别和猫狗分类根本不是一回事——类内差异大、样本不平衡、光照和姿态干扰严重。读完这篇,你能自己搭出一个带数据预处理、CNN 训练、特征提取和相似度比对的完整工程,而不是只会调别人写好的接口。下面所有代码都基于 Keras/TensorFlow 2.x,用 LFW 或你自己手机拍的照片集都能跑。
2. 先把 CNN 和人脸识别的关系捋清楚:为什么不用传统方法,也不用直接上 ResNet
2.1 传统人脸识别为什么被 CNN 取代:手工特征的天花板
在 CNN 普及之前,人脸识别的主流做法是“特征工程”:先用 Haar、LBP、HOG 之类的算子把脸部的纹理、边缘、梯度提取出来,再喂给 SVM 或贝叶斯分类器。这套方案在受限场景下能用,但有个致命问题——光照一变、角度一偏、脸上多副眼镜,特征分布就乱套。你写再多的规则,也覆盖不了真实世界里的组合爆炸。
CNN 解决的是“特征自动学习”。它会自己从像素里逐层抽象:浅层卷积核学会检测边缘和斑点,中层学会组合出眼睛、鼻子、嘴巴的部件,深层学会把这些部件组合成“脸的全局结构”。而且 CNN 对平移、轻微遮挡和光照变化有天然鲁棒性,因为卷积操作本身就是局部连接 + 权值共享,相当于把“人脸先验”直接编码进网络结构里。这也是为什么现在门禁机、手机 Face ID 背后的算法全是 CNN 或其变体,而不是你十年前课本上的 PCA 特征脸。
2.2 人脸识别里的 CNN 不是“分类网络”,而是“度量学习网络”
这是新手最容易搞混的地方。你拿 VGG16 或 ResNet 跑 ImageNet 分类,输出层是 softmax,训练目标是“把一张图归到 1000 个类别之一”。但人脸识别不一样——训练集里出现的张三,测试时不会再出现,你需要的是“判断两张脸是不是同一个人”。
所以必须把 CNN 分成两段看:前面所有卷积层和池化层负责把图像压缩成一个特征向量(embedding),这个向量体现了这张脸在“语义空间”里的位置;后面的全连接层和 softmax 只是训练时用来辅助收敛的“脚手架”。真正部署推理时,我们丢掉 softmax,只保留特征向量,然后用欧氏距离或余弦相似度判断两张脸之间的距离。这个设计思路就是 FaceNet 论文里说的“三元组损失”和“端到端度量学习”——比直接训练 softmax 分类器再取倒数第二层特征可靠得多。
提示:对一个人脸识别项目来说,CNN 的“最后一层”不是网络的终点,而是“把图像变成 128 维或 512 维向量”的出口。理解这一点,后面所有代码才不会看晕。
2.3 选型:从零训练 vs 迁移学习,你该走哪条路
我的建议非常明确:除非你有十万级以上的同分布人脸数据,否则不要从零训练。当训练量不足时,从零训练的 CNN 会出现严重的过拟合——在训练集上准确率 99%,到真实场景里连你同事都认不出来。常见做法是先用 VGG16、ResNet50 或 MobileNetV2 的 ImageNet 预训练权重做迁移学习,冻结前面的卷积层,只微调后面几层。对新手,我推荐 MobileNetV2 打底,权重文件小、推理速度快,在 CPU 上也能跑出可接受的速度,适合先跑通流程。
3. 跑通最小闭环:从人脸检测到训练数据准备
3.1 数据从哪来:公开数据集与自采照片的两种路线
理论上你需要正样本(同一人的多张不同照片)和负样本(不同人的照片)。我常用这三个来源:
- LFW(Labeled Faces in the Wild):13000 多张网络人脸照片,5749 个人,适合做模型评估。
- CelebA:20 万张明星人脸,带 40 个属性标注,适合做预训练或属性分析。
- 自采数据集:用手机或摄像头给每个人拍 20~30 张,覆盖正面、左右侧脸、不同光照、戴不戴眼镜,总共收集 5~10 个人,就能训练一个“公司门禁”级别的小模型。
自采数据集注意一点:不要只拍正脸。我见过有人图省事,一个角度拍 30 张,结果模型把“角度”当成了“身份”,换个角度就认不出。每类样本尽量覆盖 4~5 种姿态和 3 种以上光照条件,这比你多拍 2 倍的正脸照有用得多。
3.2 人脸对齐:从图像中裁剪出标准化人脸
原始的检测框并不是理想的 CNN 输入——里面包含背景、头发、脖子,而且每张脸的大小和倾斜程度不一致。必须做对齐:检测到人脸关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),然后通过仿射变换把两只眼睛拉平到同一水平线,统一缩放到 160x160(FaceNet 的原生输入尺寸)或 224x224(ResNet 系列的标准输入)。
下面用 MTCNN 做人脸检测和对齐,这是目前开源社区最常用的人脸检测器:
import cv2 import numpy as np from mtcnn import MTCNN detector = MTCNN() def align_face(image_path, output_size=(160, 160)): img = cv2.imread(image_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # MTCNN 返回人脸框和五个关键点 results = detector.detect_faces(img_rgb) if not results: return None # 取最大的人脸框(通常就是主体) result = max(results, key=lambda x: x['confidence']) keypoints = result['keypoints'] left_eye = np.array(keypoints['left_eye']) right_eye = np.array(keypoints['right_eye']) # 计算两眼连线与水平线的夹角 dy = right_eye[1] - left_eye[1] dx = right_eye[0] - left_eye[0] angle = np.degrees(np.arctan2(dy, dx)) # 以左眼为锚点,旋转到水平 M = cv2.getRotationMatrix2D(tuple(left_eye), angle, 1.0) rotated = cv2.warpAffine(img_rgb, M, (img_rgb.shape[1], img_rgb.shape[0])) # 旋转后,按两眼位置和比例裁剪人脸区域 rotated_keypoints = {} for name, point in keypoints.items(): rotated_keypoints[name] = cv2.transform(np.array([point]), M)[0] eye_center = (rotated_keypoints['left_eye'] + rotated_keypoints['right_eye']) / 2 eye_distance = np.linalg.norm(rotated_keypoints['right_eye'] - rotated_keypoints['left_eye']) # 裁剪尺寸基于眼距:人的脸部宽度约为眼距的2.5倍 crop_size = int(eye_distance * 2.5) x = int(eye_center[0] - crop_size / 2) y = int(eye_center[1] - crop_size / 2.5) cropped = rotated[max(0, y):y+crop_size, max(0, x):x+crop_size] aligned = cv2.resize(cropped, output_size) return aligned这段代码的逻辑拆开看:先是MTCNN.detect_faces拿到人脸框和眼睛、鼻子、嘴角的坐标;随后计算两眼连线相对水平线的夹角,用getRotationMatrix2D做旋转,保证不管原始照片里人脸是歪的还是侧着的,输出的人脸都是正脸姿态;最后按“眼距决定裁剪框大小”的经验比例把脸裁出来并缩放到统一尺寸。
需要注意:MTCNN 的results里可能检测到多张脸,max(results, key=lambda x: x['confidence'])是取置信度最高的一张——如果你的场景是多人合影中识别指定人,这里要改成按坐标位置或身份 ID 选择目标脸。另外,MTCNN 在极端侧脸和遮挡场景下会漏检,处理这种数据时用 RetinaFace 替换会稳不少,但它需要安装额外的模型权重。
3.3 构建训练集目录结构:让数据加载不再玄学
我建议把处理后的对齐人脸直接按以下目录结构组织,配合ImageDataGenerator或tf.data流水线可以免写一坨自定义加载代码:
dataset/ ├── train/ │ ├── person_01/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ ├── person_02/ │ └── ... └── val/ ├── person_01/ ├── person_02/ └── ...按人物姓名建文件夹,类名自动成为标签,这是 Kerasflow_from_directory的默认约定。如果你采集的是同一个人在不同时间的照片,务必把同一个人的照片按拍摄时间切分到 train 和 val 而不是随机打乱,否则验证集里会混进“同一张照片的轻微变化版”,导致验证准确率虚高。
注意:一个常见翻车点——不同人拍的视频抽帧后画面分辨率不一致,直接喂给 CNN 会让模型学到“分辨率”这个伪特征。必须统一
cv2.resize到相同尺寸,且先转 RGB 再转 float 归一化。
4. 搭建 CNN 并完成训练:用 MobileNetV2 + 度量学习跑通全流程
4.1 为什么用 MobileNetV2 做骨干网络:性能与精度的平衡点
人脸识别的骨干网络可以是 ResNet50、SENet、EfficientNet 等,但 MobileNetV2 对“第一次跑通全流程”是性价比最高的:它用深度可分离卷积(depthwise separable convolution)把参数量压到极小,在 CPU 上推理一张 160x160 的人脸只需要几十毫秒;同时它的中间特征图语义信息足够丰富,微调后能在小型自采数据集上达到工程可用的准确率。
另外需要注意:MobileNetV2 的默认输入尺寸是 224x224,如果你使用 160x160 输入,必须在加载预训练权重时显式指定input_shape,否则尺寸不匹配会报错。对于人脸识别任务,我建议直接沿用 FaceNet 的 160x160 设置,因为它的骨干网络(Inception-ResNet-v1)专为人脸设计,特征输出维度是 128;但你如果选择 MobileNetV2,就按 224 或 160 都行,只是微调后要统一所有训练/推理图片的尺寸。
4.2 训练脚本:三元组损失 vs Softmax 微调,我推荐先跑通后者
纯三元组损失训练收敛慢、容易崩,对新手极不友好。更稳的做法是:先加一个全连接层做 K 类分类,用 Softmax + 交叉熵训练;训练收敛后,把全连接层去掉,输出层改成特征向量层(例如 128 维),再通过计算向量距离做人脸比对。这个方案能复用现成的分类预训练权重,训练稳定,而且最后的特征向量质量完全够用。
以下是完整的搭建与训练代码:
import tensorflow as tf from tensorflow.keras import layers, Model from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.preprocessing.image import ImageDataGenerator # 1. 构建模型:骨干网络 + 全局平均池化 + 特征向量层 + 分类层 def build_face_model(num_classes, input_size=(160, 160)): base_model = MobileNetV2( include_top=False, weights='imagenet', input_shape=(input_size[0], input_size[1], 3) ) base_model.trainable = False # 先冻结骨干网络 inputs = tf.keras.Input(shape=(input_size[0], input_size[1], 3)) x = tf.keras.applications.mobilenet_v2.preprocess_input(inputs) x = base_model(x, training=False) x = layers.GlobalAveragePooling2D()(x) x = layers.Dense(128, activation=None, name='embedding')(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) outputs = layers.Dense(num_classes, activation='softmax', name='classifier')(x) model = Model(inputs, outputs) return model # 2. 数据增强:真实场景里,增强比堆层数更重要 train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=15, width_shift_range=0.1, height_shift_range=0.1, brightness_range=[0.8, 1.2], horizontal_flip=True ) val_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( 'dataset/train', target_size=(160, 160), batch_size=32, class_mode='categorical' ) val_generator = val_datagen.flow_from_directory( 'dataset/val', target_size=(160, 160), batch_size=32, class_mode='categorical' ) # 3. 训练:先训分类头 model = build_face_model(num_classes=train_generator.num_classes) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'] ) model.fit( train_generator, validation_data=val_generator, epochs=10, callbacks=[ tf.keras.callbacks.ModelCheckpoint('face_model.h5', save_best_only=True), tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3) ] ) # 4. 解冻部分层继续微调:让卷积层适应人脸数据 base_model.trainable = True # 冻结前 100 层,只微调后面的高层语义层 for layer in base_model.layers[:100]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), loss='categorical_crossentropy', metrics=['accuracy'] ) model.fit( train_generator, validation_data=val_generator, epochs=10, callbacks=[ tf.keras.callbacks.ModelCheckpoint('face_model_finetuned.h5', save_best_only=True) ] )这里有两个关键设计:第一,GlobalAveragePooling2D把骨干网络输出的特征图压缩成一个向量,替代传统的 Flatten,能极大减少参数量并抑制过拟合;第二,embedding层用线性激活(activation=None)输出 128 维特征向量,分类层从这个向量再映射到具体身份。训练结束后,真正用于识别的不是分类层,而是embedding层的输出。
参数调整建议:learning_rate第一阶段用1e-3,第二阶段微调调低到1e-5,如果损失震荡,说明学习率偏高,降到1e-6;batch_size在 16~64 之间尝试,显存或内存不够时报错就减半。EarlyStopping 的patience设 3~5,防止过拟合的同时给训练留足余量;如果验证集准确率在前几个 epoch 就一直不涨,优先检查数据增强里的brightness_range是否过强,把增强调到“肉眼几乎看不出变化”的程度。
4.3 从分类模型到特征提取:部署阶段只取 embedding 层
模型训练完成后,你需要导出一个“预处理 + 骨干网络 + embedding 层”的推理模型:
# 加载训练好的权重 model = build_face_model(num_classes=NUM_CLASSES) model.load_weights('face_model_finetuned.h5') # 构建只输出 embedding 的模型 embedding_model = Model( inputs=model.input, outputs=model.get_layer('embedding').output ) # 保存推理模型 embedding_model.save('face_embedding_model.h5')保存以后,任何一张新的人脸图,经过相同对齐流程后,都能得到 128 维向量。注意这里不需要保存分类层,因为它在部署时没有任何用处——甚至可以说它是“训练时的脚手架”。如果你直接拿model.predict的 softmax 输出去判断身份,那你做的是“人脸分类”,不是“人脸识别”,换一个没训练过的新人就直接崩。
5. 人脸比对与阈值判定:从特征向量到“他是谁”
5.1 欧氏距离和余弦相似度怎么选
拿到两张脸的 embedding 后,常见比对方式是计算欧氏距离和余弦相似度。欧氏距离适合在 embedding 维度已经做了 L2 归一化时使用;余弦相似度对向量的模长不敏感,更适合人脸这种受光照影响会导致特征向量整体缩放的情况。实际操作中,对 embedding 先做 L2 归一化,再计算欧氏距离,等价于使用余弦相似度,但阈值更直观——阈值一般取 0.7~1.2,越小越严格。
5.2 注册库构建与识别流程代码
下面是一个可直接改用的推理脚本:
import numpy as np from scipy.spatial.distance import cosine, euclidean class FaceRecognizer: def __init__(self, embedding_model_path): from tensorflow.keras.models import load_model self.model = load_model(embedding_model_path) self.known_faces = {} # {person_id: embedding_array} def register(self, person_id, face_image): embedding = self.get_embedding(face_image) self.known_faces[person_id] = embedding def get_embedding(self, face_image): img = cv2.resize(face_image, (160, 160)) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_norm = img_rgb.astype('float32') / 255.0 img_batch = np.expand_dims(img_norm, axis=0) embedding = self.model.predict(img_batch)[0] # L2 归一化 embedding = embedding / np.linalg.norm(embedding) return embedding def recognize(self, face_image, threshold=0.8): query_embedding = self.get_embedding(face_image) min_dist = float('inf') best_id = None for person_id, known_embedding in self.known_faces.items(): dist = euclidean(query_embedding, known_embedding) if dist < min_dist: min_dist = dist best_id = person_id if min_dist < threshold: return best_id, min_dist else: return None, min_dist # 使用示例:注册两个人,各用 3 张照片做平均 embedding 更稳 recognizer = FaceRecognizer('face_embedding_model.h5') aligned_faces = [] # 通过 align_face 得到 avg_embedding = np.mean([recognizer.get_embedding(face) for face in aligned_faces], axis=0) recognizer.register('Alice', avg_embedding / np.linalg.norm(avg_embedding))注册时用多张照片取平均 embedding 是关键操作。单张照片受表情、光照影响波动太大,3~5 张取平均能把噪声抵消掉大半。识别时threshold是允许的最大欧氏距离,太大容易误认(把 A 当成 B),太小容易拒识(熟人也不认识)。先在验证集上统计“同一人的距离分布”和“不同人的距离分布”,把阈值设在两类分布的交叉点附近。
5.3 门禁场景的阈值设定经验
我有一个比较省事的调阈值方法:拿 20 对“同一个人不同照片”算平均距离 d_same,再拿 20 对“不同人照片”算平均距离 d_diff,阈值设成(d_same + d_diff) / 2,然后人工跑 100 次识别,根据误识率和拒识率微调。如果你期望“宁可拒绝也不能认错”,把阈值往 d_same 方向压;如果你期望“别总拦着人”,阈值往 d_diff 方向放。
注意:不同骨干网络输出的 embedding 分布范围差异很大,MobileNetV2 的 128 维向量在 L2 归一化后距离通常集中在 0.5~1.5,而 Facenet 的 embedding 距离集中在 0.2~1.0。所以从网上抄来一个阈值前,先打印你自己数据的距离分布,这是最可靠的避坑方式。
6. 避坑指南:CNN 人脸识别最常见的 6 个翻车点
6.1 翻车点一:训练集和测试集存在“同源照片”泄漏
现象:训练准确率 99%,验证准确率 98%,一上真实摄像头就掉到 60%。
原因:同一人的照片是从同一段视频抽帧得到的,相邻帧几乎一样,随机切分到 train 和 val 后,验证集等于开卷考试。
解决:按拍摄时间或视频片段切分数据,保证 train 里出现的“那一组照片”和 val 里出现的“那一组照片”不是同一批拍摄。最稳妥的做法是“每个人先分 70% 的拍摄时段给训练,30% 给测试”。
6.2 翻车点二:把分类准确率当成识别率
现象:模型在训练集上分类准确率 99%,但换了没训练过的新人后完全不能用。
原因:分类层的输出神经元对应固定身份,新人不在这 K 类里,softmax 永远不会输出他的 ID。
解决:永远用 embedding 距离做识别,分类层只当训练辅助。评估时不要只看accuracy,要看 ROC 曲线下的 AUC,以及特定误识率下的真正识别率。
6.3 翻车点三:灰度图丢失纹理信息
现象:同一个模型,用灰度图训练比用 RGB 训练低了 2~3 个百分点。
原因:人脸识别非常依赖肤色、唇色等颜色特征,灰度化把这些信息全丢了;而且预训练权重(ImageNet 的 MobileNetV2)是按 RGB 三通道训练的,输入单通道图时权重语义不匹配。
解决:始终用 RGB 三通道输入,不要为了省内存做灰度化。数据增强时可以用brightness_range模拟光照变化,但不要只留亮度通道。
6.4 翻车点四:人脸对齐做成了“裁个框”而不是“对齐关键点”
现象:Rotate 角度大一点,识别就失败。
原因:很多人直接用检测框裁剪,忽略了两眼连线夹角;模型没学到“正脸特征”,却学到了“这张图的倾斜方向”。
解决:严格使用关键点仿射变换,至少让两眼连线水平。如果检测到的鼻子或嘴角坐标质量很差,可以只取两只眼睛算旋转矩阵——因为眼距相对稳定。
6.5 翻车点五:图片归一化方式不对
现象:loss 不下降,前几个 epoch 准确率一直在 1/K 附近。
原因:把像素值 0~255 直接喂进模型,或者用(x - mean) / std但 mean 用的是整张图像素均值,导致分布和预训练模型期望的输入分布不一致。
解决:使用和骨干网络配套的预处理函数。MobileNetV2 用mobilenet_v2.preprocess_input,ResNet50 用resnet50.preprocess_input,不要混用。自建预处理时,用“逐通道均值”而不是“全局均值”做标准化。
6.6 翻车点六:冻结所有卷积层直接训分类头,然后抱怨效果差
现象:迁移学习后验证准确率只有 80%,怎么调都不再涨。
原因:人脸数据和 ImageNet 数据分布差异很大时,前几层卷积核虽然通用,但高层语义特征完全不对位。全冻结等于让分类头去适应一个“没见过人脸”的特征提取器。
解决:训练完成后解冻base_model的后 1/3 层,用更小学习率微调。注意要先把基础训练跑完再解冻,直接解冻从头训容易把预训练权重毁掉。
7. 进阶:把单模型升级成可用系统的三个技巧
7.1 用多人图片注册时的 “平均脸” 和 “多模板” 策略
前面代码里用的是单模板平均 embedding,但实际更稳的是多模板:每个人存 3~5 个原始 embedding(不平均),识别时取“和查询 embedding 最接近的那一个”做距离。这比平均 embedding 更能适配一个人在不同时期的外观变化(比如胖了、留了胡子)。代价是存储量从 1 条向量变成 5 条,对百人级门禁完全不是问题;对万人级系统,则需要先用 Faiss 做向量索引,这里不展开。
7.2 验证系统是否真的有效:自己构建 “同人/异人” 测试集
不要只盯着验证集准确率。我给你一个可复现的验证方案:从每个测试人物里抽出 2 张照片,一张注册,一张查询。然后计算三个数字——真正率(同人能否被识别)、误识率(异人是否被当成同人)、拒识率(同人是否被拒绝)。把查询照片换成从未见过的另一批照片,看看三个数字的波动幅度,这个波动幅度反映了模型的真实泛化能力。如果波动达到 10 个百分点以上,说明训练集过拟合或者数据采集覆盖不够。
7.3 用 t-SNE 可视化 embedding 分布,定位模型缺陷
一维准确率数字掩盖了很多信息。我会在模型训练完后做一步额外操作:从验证集每个类里抽 5 张图,把所有 embedding 喂进 t-SNE 降维到 2D 画散点图。如果同一身份的点聚成几坨(距离很远),说明该身份的照片里有你没覆盖的“姿态或光照簇”;如果不同身份的点混在一起,说明该模型的面部区分度不足,需要换更强的骨干或者加数据。这个可视化成本只有 20 行代码,但能帮你少走两个星期弯路。
from sklearn.manifold import TSNE import matplotlib.pyplot as plt # faces 是 (N, 128) 的 embedding 矩阵,labels 是 (N,) 的 id 列表 tsne = TSNE(n_components=2, perplexity=30, random_state=42) coords = tsne.fit_transform(faces) plt.figure(figsize=(10, 8)) for label in set(labels): mask = labels == label plt.scatter(coords[mask, 0], coords[mask, 1], s=10, alpha=0.6) plt.title('Face Embedding Distribution') plt.show()跑完看两点:第一,同一人的点是否紧凑;第二,不同人的簇间距是否明显大于簇内距离。如果簇内距离大,去查那个人的原始照片是不是有大量侧脸或遮挡;如果簇间距离小,去加不同人的样本身量或换骨干网络。
做这类项目,我现在的习惯是“先建验证集,再调模型”,而不是“先训模型,再找测试图”。人脸识别最贵的时间不在 GPU 训练,而在数据清洗和对齐——90% 的识别失败不是模型的问题,是你喂给模型的图本身就没对齐、没标准化。希望这篇笔记能帮你把这些坑都提前绕过去。
本文还有配套的精品资源,点击获取