ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

亲子识别新思路:dlib人脸特征点与三模型集成实战

亲子识别新思路:dlib人脸特征点与三模型集成实战 简介一套基于Python机器学习的人脸亲子识别完整方案面向人脸识别方向的学习者与开发者解决“两张人脸照片是否存在亲缘关系”的分类问题。方案以亲属照片为数据通过dlib提取人脸关键点并计算欧氏距离联合父母年龄、亲子年龄构成四维特征依次完成SVM、随机森林等三个传统模型的调参并采用Voting集成学习构建最终分类器。资源包为zip格式共21个文件、108.78MB涵盖Jupyter建模代码、可执行脚本与exe程序、sav格式训练好的模型权重、pkl标准化器、dat格式的人脸关键点模型、xlsx特征数据及jpg照片样本从数据处理到模型部署均有对应文件。目前已有267人学习利用该资源可直观理解特征构造思路、模型调参与集成方法并可直接运行Face.exe对真实照片进行亲子识别测试。1. 从人脸照片到亲子距离一个可复现的机器学习基线亲子识别在安防寻人、证件核验、家庭相册自动标注这些场景里一直是个“能做但不好解释”的任务。很多人第一反应是堆深度学习模型做人脸相似度比较实际效果却常被光照、姿态和年龄变化影响。这个 Python 机器学习项目换了一个思路把“照片里是否存在亲子关系”重新定义为一个四特征分类问题先用 dlib 检测并抽取人脸特征点计算出亲子人脸间的欧氏距离再联合父母年龄与亲子年龄构成特征集交给 SVM、KNN、随机森林三个模型分别调参最后用集成投票输出分类结果。整套工程 face-master.zip 已经把原始照片、裁剪后的人脸、68/81 点特征模型、resnet 人脸编码模型、标注 Excel、训练脚本和可运行的 Face.exe 都准备好了。整个流程不需要 GPU一台普通笔记本就能从训练跑到部署很适合想完整走一遍特征构建-模型调参-集成部署流程的人。2. 人脸检测与特征点距离的计算2.1 用 dlib 完成人脸定位与特征点抽取工程包的 necessary files 里放着 shape_predictor_68_face_landmarks.dat 和 shape_predictor_81_face_landmarks.dat 两个特征点模型对应 68 点和 81 点两种标注体系。68 点模型覆盖眉毛、眼睛、鼻子、嘴巴和下颌轮廓是 dlib 生态里最常用的版本81 点在 68 点基础上补了额头和面部外轮廓控制点对脸型变化的描述更细但计算量和模型体积都会大一些。对亲子距离计算来说68 点已经足够稳定81 点适合侧脸比例较高的照片集。表两个特征点模型的使用差异模型文件特征点数量适用场景推理耗时shape_predictor_68_face_landmarks.dat68正脸为主、光照相对均匀的照片低shape_predictor_81_face_landmarks.dat81侧脸、角度变化较大的照片中先写一段代码把照片中第一张人脸取出来import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) img dlib.load_rgb_image(Kinship Pictures Cropped/volunteer_face1.jpg) faces detector(img, 1) if len(faces) 0: raise ValueError(no face detected) shape predictor(img, faces[0]) points [(p.x, p.y) for p in shape.parts()]detector 的第二个参数 1 表示对输入图像做一次金字塔上采样能提高小脸检出率代价是检测时间变长。批量跑数千张照片时建议改回 0 以节省时间。predictor 接收彩色图像和 dlib 的 rectangle 对象把人脸矩形区域投影到已训练好的形状模型上输出一个 shape 对象parts() 返回按固定顺序排列的特征点坐标。2.2 用欧氏距离量化亲子人脸差异亲子人脸之间的相似度在本项目里不作为像素比较而是转化为特征点距离。核心假设是有亲缘关系的人脸在五官比例和轮廓上有更强的空间一致性对应特征点在坐标空间里更接近。具体做法是取一对亲子照片各自提取 68 个特征点然后按顺序号一一计算欧氏距离。由于两张照片的分辨率、拍摄距离可能不同直接使用像素距离会引入尺度误差我一般会先按双眼距离做归一化再计算距离统计量。import numpy as np from scipy.spatial.distance import euclidean def landmark_embedding(shape): pts np.array([(p.x, p.y) for p in shape.parts()]) eye_dist euclidean(pts[36], pts[45]) # 左右眼外角 return (pts - np.mean(pts, axis0)) / eye_dist def pair_distance(shape1, shape2): e1 landmark_embedding(shape1) e2 landmark_embedding(shape2) dists np.linalg.norm(e1 - e2, axis1) return np.mean(dists), np.var(dists), np.max(dists)代码里先把每个形状的坐标中心平移到原点再除以双眼距离等于把位置和尺度对齐了一遍随后按特征点顺序计算逐点距离。返回的均值、方差、最大值分别描述整体差异、局部抖动和最不匹配的点位这三个统计量会进入后面的特征表。值得注意的是工程文件里的 dlib_face_recognition_resnet_model_v1.dat 提供另一条计算距离的路线用 resnet 把整张脸编码成 128 维向量再算欧氏距离。相比 68 点距离128 维向量更关注身份层面的差异受表情影响更小但对裁剪框质量要求更高需要先用 dlib.get_face_chip() 对齐。3. 四个特征维度如何落地成数据集3.1 face_data.xlsx 里的特征拼装摘要中提到的四个特征落到表格里分别是欧氏距离、亲属关系、父母年龄、亲子年龄。这里容易产生歧义亲属关系在实际建模中是监督标签而不是参与距离计算的数值特征。真正输入模型的特征组合是“欧氏距离 父母年龄 亲子年龄”亲属关系列作为 y 标签。年龄特征不能直接用字符串读入必须转成整数父母年龄统一填拍照时父母的年龄亲子年龄同理。缺失的年龄宁可整行丢弃也不要填 0否则会把决策边界整体拉偏。表face_data.xlsx 中的列结构示意列名类型说明euclidean_distancefloat68 点欧氏距离的三个统计量之一parent_ageint父母年龄child_ageint亲子年龄kinship_labelint0 表示无亲缘关系1 表示有亲子关系3.2 读取、清洗与标准化用 pandas 读入后先做两步清洗一是检查欧氏距离列是否存在 NaN二是过滤掉 parent_age child_age 的矛盾记录。然后进入标准化。SVM 和 KNN 都依赖距离计算年龄特征的量纲与欧氏距离不一致会让年龄特征主导梯度StandardScaler 能把每个特征拉回均值为 0、方差为 1 的分布。import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split df pd.read_excel(necessary files/face_data.xlsx) df df.dropna(subset[euclidean_distance, parent_age, child_age]) df df[df[parent_age] df[child_age]] X df[[euclidean_distance, parent_age, child_age]].values y df[kinship_label].values scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42, stratifyy )train_test_split 里 stratifyy 能保证训练集和测试集的正负样本比例与原数据一致。亲子识别数据集通常正样本远少于负样本如果忽略这个参数随机切分很可能让测试集里没有正样本F1 指标直接失真。scaler 用 fit_transform 得到训练集的均值与方差后测试集和预测阶段只能调用 transform不能再次 fit否则会出现信息泄露导致后续精度虚高。3.3 年龄特征为什么不能省略如果只给模型欧氏距离模型学到的本质是一个相似度阈值很难区分“兄妹”和“亲子”。年龄差的加入给模型提供了时间窗口约束亲子年龄差一般落在 1845 岁区间而相似的同辈人脸年龄差可能只有几岁。单独看这个特征不够强但与欧氏距离组合后可以降低误报率。我在实验里见过纯距离特征在负样本上误报率偏高的情况加入年龄特征后同一批负样本的误报明显减少。这说明年龄特征确实改变了决策边界模型不再只是“长得像不像”而是“长得像且年龄差合理”双重约束。4. 三个模型调参与集成投票4.1 模型选型与超参数空间选择 SVM、KNN、随机森林三个模型主要考虑特征维度低、样本量中等、需要可解释性。RBF 核的 SVM 擅长处理低维非线性边界KNN 在特征量纲统一后表现直接且稳定随机森林能给出特征重要性方便反推哪些距离统计量最有区分度。三个模型对应工程里的 Face_SVM.sav、Face_KNN.sav、Face_Random_Forest.sav。表三个模型的调参空间模型关键超参数搜索范围重点观察SVMC、gammaC ∈ [0.1, 1, 10, 100]gamma ∈ [0.001, 0.01, 0.1]小 C 避免过拟合KNNn_neighbors、weightsn ∈ [3, 5, 7, 9, 11]weights ∈ [uniform, distance]奇数近邻避免平票随机森林n_estimators、max_depthn ∈ [50, 100, 200]depth ∈ [3, 5, 7, None]depth 控制单棵树复杂度4.2 用 GridSearchCV 找出每个模型的最佳参数调参在 AI_models.ipynb 里完成。核心思路是交给 GridSearchCV 在参数网格上穷举依据交叉验证的 F1 分数选择组合。由于三个模型的搜索空间都不大训练时间可接受。这里以 SVM 为例from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV import joblib param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1], kernel: [rbf], } svm_model SVC(probabilityTrue, random_state42) grid GridSearchCV(svm_model, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_) joblib.dump(grid.best_estimator_, models/Face_SVM.sav)SVC 里加 probabilityTrue是为了让后续集成投票能拿到概率值代价是训练时间略增样本量不大时可接受。scoringf1 在这里比 accuracy 更适合因为正负样本不平衡时 accuracy 会被多数类主导。KNN 和随机森林的调参逻辑完全一致替换模型类与 param_grid 即可。调参结果用 joblib.dump 落盘模型文件按 Face_KNN.sav、Face_Random_Forest.sav 保存后面集成阶段直接加载。4.3 集成投票把三个模型的预测合成一个决策集成阶段用 VotingClassifier 把三个最优模型组合成 Face_Voting.sav。soft voting 会把三个模型各自输出的正类概率取平均再以 0.5 为阈值决定最终类别。相比 hard votingsoft voting 能保留置信度信息避免三个模型各自给出争持不下的硬标签时出现随意裁决。如果某个模型明显偏弱可以在 weights 参数里给它更低的权重。我一般先跑一次默认权重再看三个单模型在验证集上的 F1按 F1 比例折算权重效果通常比拍脑袋设权重要好。from sklearn.ensemble import VotingClassifier import joblib svm_best joblib.load(models/Face_SVM.sav) knn_best joblib.load(models/Face_KNN.sav) rf_best joblib.load(models/Face_Random_Forest.sav) voting VotingClassifier( estimators[(svm, svm_best), (knn, knn_best), (rf, rf_best)], votingsoft, ) voting.fit(X_train, y_train) joblib.dump(voting, models/Face_Voting.sav)集成模型的意义不只是提升精度。三个模型的错误模式不同SVM 对靠近边界的样本更敏感KNN 对局部密集区域更激进随机森林则对特征交互更稳健。投票之后单一模型的偶然误判会被另外两个模型抵消这也是工程里最终预测用 Face_Voting.sav 而不是单模型的原因。除了 VotingClassifierBagging 和 Stacking 也是常见集成路线但在这个四特征低维场景里 Voting 的性价比最高。Stacking 需要额外一层元模型样本量不足时反而容易增加过拟合风险。4.4 验证指标与类别不平衡处理评估时不能只看准确率。亲子识别数据集里负样本通常多于正样本模型如果全预测为负准确率也能很高但毫无使用价值。需要在测试集上同时看 precision、recall、f1-score必要时画混淆矩阵确认正样本的召回率没被牺牲。如果训练集存在严重不平衡可以给 SVC 的 class_weight 参数设 balanced或者对随机森林的 class_weight 做同样处理。最终预测阶段scaler.pkl 和 Face_Voting.sav 必须成对加载只加载模型不加载标准化器会让预测概率偏移到完全错误的区间。5. 把分类器封装成命令行工具并规避打包坑5.1 Face_exe.py 的入口设计Face_exe.py 是打包成 Face.exe 的源码文件。入口参数应包含两张照片路径、父母年龄和孩子年龄。预测前必须重新加载 scaler.pkl 做 transform不能直接把原始年龄代入模型否则训练时的标准化参数与预测时不一致输出概率会偏移。我用 argparse 组织入口再交给 joblib 加载的集成模型输出预测概率。import argparse, sys, joblib from pathlib import Path def resource_path(relative): if hasattr(sys, _MEIPASS): return str(Path(sys._MEIPASS) / relative) return str(Path(__file__).parent / relative) parser argparse.ArgumentParser() parser.add_argument(--img1, requiredTrue, help第一张人脸照片) parser.add_argument(--img2, requiredTrue, help第二张人脸照片) parser.add_argument(--parent-age, typeint, requiredTrue) parser.add_argument(--child-age, typeint, requiredTrue) args parser.parse_args() voting joblib.load(resource_path(models/Face_Voting.sav)) scaler joblib.load(resource_path(models/scaler.pkl))PyInstaller 打包时用 --add-data 把模型文件一起带进 exepyinstaller -F -w --add-data models/Face_Voting.sav;models --add-data models/scaler.pkl;models Face_exe.py-w 让程序不弹出控制台窗口如果希望运行日志可见去掉 -w 即可。Windows 下 --add-data 用分号分隔源文件与目标目录Linux 下用冒号跨平台写脚本时要单独判断。resource_path 函数会在 PyInstaller 解压临时目录时自动切换路径前缀这是打包后找不到模型的常见解法。5.2 三个容易踩的坑第一个坑是 dlib 版本与模型文件不匹配。shape_predictor_68_face_landmarks.dat 在不同 dlib 版本下序列化格式基本稳定但 resnet 模型对版本更敏感如果打包环境里的 dlib 版本与训练时相差过大加载 dlib_face_recognition_resnet_model_v1.dat 会直接报错建议训练和打包环境锁定同一个 dlib 版本。第二个坑是照片里检测不到人脸时不处理就继续算距离会产生空的特征向量我在 Face_exe.py 里遇到这种情况会直接打印 no face detected 并返回非零退出码。第三个坑是年龄参数没有校验父年龄小于子年龄的记录应在入口直接拒绝。我在 Face_exe.py 的入口做检测结果和年龄范围双重校验任何一项不通过就直接返回非零退出码并附带原因文本。这种快速失败策略放在命令行工具里比把脏数据送进模型后在概率上猜要容易定位得多。本文还有配套的精品资源点击获取
返回列表