CV-CNN-2015:FaceNet(人脸特征向量提取、计算欧氏距离)【Triplet Loss(三元组损失):最大化不同人脸的距离最小化相同人脸的距离】【可使用MobileNet当特征提取网络】
1. 从零理解FaceNet的核心思想第一次接触人脸识别时我完全被各种专业术语搞晕了。直到遇到FaceNet才发现原来人脸识别可以这么直观。想象你是个班主任要记住全班50个学生的长相。你不会死记硬背每个五官细节而是会在大脑里形成每个同学的特征印象——比如小明的圆眼镜、小红的酒窝。FaceNet做的就是类似的事情只不过它用数学向量来表示这些特征。FaceNet最巧妙的地方在于它直接把整个人脸图像映射到一个128维的欧式空间。在这个空间里同一个人的不同照片会聚集在一起而不同人的照片则相互远离。这就好比把全班同学的照片贴到教室墙上让关系好的同学坐得近不熟悉的同学离得远。具体实现时它会计算三个关键距离基准照片Anchor与同一人照片Positive的距离要尽可能小基准照片与其他人照片Negative的距离要尽可能大正负样本之间要保持至少一个安全间隔Margin我最早用OpenCV做实验时发现传统方法在光线变化时就失效了。而FaceNet的Triplet Loss机制让它对角度、光照都有惊人鲁棒性。有次我故意用侧脸照片测试系统依然能准确匹配到同一人的正面照这让我意识到深度特征学习的强大之处。2. MobileNet轻量化改造实战原版FaceNet用的Inception-ResNet虽然精度高但在树莓派上跑起来像老牛拉车。后来看到谷歌的MobileNet论文我决定用它做主干网络。MobileNet的杀手锏是深度可分离卷积就像把传统卷积拆成两步深度卷积每个滤波器只处理一个输入通道逐点卷积用1x1卷积组合通道信息这种设计让参数量直降8-9倍。我在PyTorch里实现时关键代码如下def conv_dw(inp, oup, stride1): return nn.Sequential( # 深度卷积 nn.Conv2d(inp, inp, 3, stride, 1, groupsinp, biasFalse), nn.BatchNorm2d(inp), nn.ReLU6(), # 逐点卷积 nn.Conv2d(inp, oup, 1, 1, 0, biasFalse), nn.BatchNorm2d(oup), nn.ReLU6(), )实测发现三个调参技巧特别重要学习率要设为原网络的1.5倍我用的0.045配合RMSprop优化器比Adam更稳定最后一层ReLU6的阈值6不能去掉否则量化部署时会溢出在华为昇腾310芯片上测试改造后的模型推理速度从380ms降到89ms内存占用从1.2GB减到230MB完全能满足嵌入式设备需求。3. 三元组数据集的构建秘诀刚开始我直接用CASIA-WebFace原始数据训练结果惨不忍睹。后来发现构建三元组才是真正的技术活。好的三元组要满足正样本对同一人在不同光照/角度下的照片负样本对长相相似的不同人照片Hard样本占比要超过30%我总结的实用采样策略如下表样本类型采集方法增强方式建议比例Easy正样本同一视频帧序列色彩抖动随机裁剪20%Hard正样本不同时间段照片极端亮度调整30%Semi-hard负样本同种族同性别添加相似妆容40%Hard负样本双胞胎数据集姿态对齐处理10%具体实现时我用dlib先做五点对齐然后采用在线挖掘策略def get_triplets(embeddings, labels): triplets [] for i in range(len(embeddings)): # 找最难正样本 pos_idx np.where(labelslabels[i])[0] pos_dist np.sum((embeddings[pos_idx] - embeddings[i])**2, 1) hardest_pos pos_idx[np.argmax(pos_dist)] # 找最难负样本 neg_idx np.where(labels!labels[i])[0] neg_dist np.sum((embeddings[neg_idx] - embeddings[i])**2, 1) hardest_neg neg_idx[np.argmin(neg_dist)] triplets.append((i, hardest_pos, hardest_neg)) return triplets注意要控制每批次的正负样本比例我通常设batch_size45包含15个ID每人3张图。这样既能保证多样性又不会让loss震荡太大。4. 双损失函数训练技巧单纯用Triplet Loss训练就像教小孩只用对错来学习效果很差。我的解决方案是引入交叉熵损失作为辅助class CombinedLoss(nn.Module): def __init__(self, alpha0.2, lambda_ce0.5): super().__init__() self.triplet_loss TripletLoss(alpha) self.ce_loss nn.CrossEntropyLoss() self.lambda_ce lambda_ce def forward(self, y_pred, y_true, batch_size): # y_pred包含[特征向量, 分类logits] embeddings, logits y_pred triplet_loss self.triplet_loss(embeddings, batch_size) ce_loss self.ce_loss(logits, y_true) return triplet_loss self.lambda_ce * ce_loss训练过程要分三个阶段前5轮只训练分类层冻结特征提取层lr1e-36-20轮解冻全部层lr5e-4λ_ce0.520轮后只保留Triplet Losslr1e-4在LFW测试集上这种策略让准确率从98.1%提升到99.2%。关键是要监控两个loss的比值当triplet_loss/ce_loss ≈ 0.3时效果最佳。5. 边缘设备部署优化在树莓派4B上部署时我遇到了内存溢出的问题。通过以下优化最终将内存控制在200MB内模型量化方案训练后动态量化PyTorch自带model torch.quantization.quantize_dynamic( model, {nn.Linear, nn.Conv2d}, dtypetorch.qint8 )自定义算子融合# 将ConvBNReLU6合并为单个算子 torch.quantization.fuse_modules(model, [[conv1, bn1, relu1]], inplaceTrue)推理加速技巧输入尺寸从160x160降到128x128使用OpenMP多线程预处理开启ARM NEON指令集优化实测结果优化手段内存占用推理时延准确率原始模型1.2GB380ms99.1%动态量化410MB210ms98.9%算子融合230MB150ms98.7%多线程优化235MB89ms98.7%最后分享一个实际踩坑案例有次客户反馈夜间识别率骤降排查发现是MobileNet的ReLU6在低光照下产生大量零激活。解决方案是在图像预处理时加入自适应直方图均衡化同时将第一个卷积层的输出通道从32增加到48。这个小改动让夜间识别率从82%回升到96%。