资讯详情

资讯详情

基于机器学习的人脸发型推荐算法实现:从特征提取到在线服务

简介这是一个基于机器学习的人脸发型推荐算法研究与应用实现项目面向机器学习初学者、计算机视觉研究者和 Flask Web 开发者解决根据用户面部形状自动推荐适配发型的问题。资源按数据、模型、应用三个层次组织数据集收集了约 74 位名人的近 1500 张人脸图像并标注长形、圆形、椭圆形、心形、方形等脸型模型部分实现多层感知机、K近邻、随机森林、梯度提升、线性判别分析等多种分类器完成特征标准化、降维和性能对比应用层使用 Flask 开发支持上传照片后输出脸型分类结果和推荐发型。rar 压缩包共 2002 个文件其中 1937 张 jpg 为图像数据集7 个 Python 脚本是核心训练与预测代码另有 CSS、JavaScript、HTML 前端资源和 JSON、XML 配置文件整包约 705.67MB目录结构清晰便于按模块阅读理解。目前已有 270 人学习使用适合希望从数据处理、模型训练到 Web 应用部署完整走通一个机器学习项目的读者。1. 基于机器学习的人脸发型推荐算法先从“特征”而不是“生成”说起美发门店的导购终端、相册类 App 的换装模块、线上预约工具里的发型试戴核心场景都是同一件事用户拍一张正脸照系统在几秒内给出几款适合的发型供人对比决策。很多初版做法直接让模型把头发“换”到人脸上但真实环境里刘海边缘、颧骨弧度、发际线位置只要有一处偏差视觉上就完全不可信。更稳的落地方式是把“基于机器学习的人脸发型推荐算法研究与应用实现”拆成三个可验证环节人脸属性建模、发型编码与相似推荐、轻量服务化部署。这也是当前工程界比较通行的做法尤其适合刚入门人脸项目、或者有一定图像基础但没接触过推荐系统的开发者参考。整条链路里真正决定推荐质量的是特征怎么定义、距离怎么度量而不是那层推荐壳子。下面按数据准备、模型训练、在线实现和验证调优的顺序展开给出的命令和参数可以直接拷到项目里改动。2. 数据准备与特征表示对齐、关键点和发型属性建模发型推荐的数据问题比通用人脸识别更麻烦。通用人脸识别只要回答“这是不是同一个人”发型推荐却要回答“这副五官适合什么头发”属于主观性很强的细粒度属性问题。常见做法是组合两类数据一类是公开的人脸属性数据集像 CelebA、FFHQ 这类有大量对齐后的人脸图像和标签可用于预训练另一类是业务侧自采的标注集针对门店或线上渠道的真实用户。正式实施时可以先把公开数据做通用特征预训练再把业务标注集拿来做属性微调。2.1 发型标签的拆法不要只标“长发”“短发”一个类别单一的发型类别对深度学习模型来说太粗。两个人都是“中长发”一个适合方脸一个适合圆脸差异往往体现在刘海形态、层次感和卷曲度这些细节上。实际项目里我一般把标签拆成一组离散属性形成多标签标注任务字段大致如下属性字段取值范围说明fringe_type0无刘海, 1空气刘海, 2厚刘海, 3斜分影响额头露出的比例hair_length0超短, 1短发, 2中发, 3长发影响脸型拉长效果curl_level0直发, 1微卷, 2大卷卷度改变脸部轮廓的视觉宽度color_tone0黑色, 1棕系, 2浅色与肤色亮度有关forehead_expose0遮住, 1部分, 2全露发际线高低与脸长相关face_shape0圆, 1方, 2长, 3瓜子推荐排序的主要分组依据之一sideburn0无鬓角, 1短鬓角, 2长鬓角修饰下颚线style_attr0休闲, 1商务, 2甜美等用于千人千面的业务排序标注时要让同一张图由 3 个人分别打采用投票一致的标签进入训练集不一致的放入人工复核。这样能明显减少“标注员对发型风格的主观分歧”带来的噪声比直接让模型硬学一个统一答案更稳。2.2 预处理流水线OpenCV 检测、人脸关键点对齐、统一裁剪图像预处理的目标是把所有人脸摆到大致相同的位置和尺度避免模型把“脸偏了”当成特征。常用的方案是先用 OpenCV 的 DNN 人脸检测器或 RetinaFace 拿到 bounding box 和关键点再根据双眼位置做仿射变换。给出一段基于 face_recognition 的最小可用对齐代码底层的 dlib 模型已经在 68 点关键点任务上很成熟适合快速起步import face_recognition import cv2 import numpy as np def align_face(image_bgr, target_size112): rgb cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) face_locations face_recognition.face_locations(rgb, modelhog) landmarks_list face_recognition.face_landmarks(rgb, face_locations) if len(landmarks_list) 0: return None landmarks landmarks_list[0] left_eye np.mean(landmarks[left_eye], axis0) right_eye np.mean(landmarks[right_eye], axis0) dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] # 以双眼连线为水平基准计算旋转角 angle np.degrees(np.arctan2(dy, dx)) center (image_bgr.shape[1] / 2, image_bgr.shape[0] / 2) matrix cv2.getRotationMatrix2D(center, angle, scale1.0) aligned cv2.warpAffine(image_bgr, matrix, (image_bgr.shape[1], image_bgr.shape[0])) return cv2.resize(aligned, (target_size, target_size))逻辑说明代码先用人脸定位接口拿到所有人脸区域再取第一个人的左右眼平均坐标。双眼连线的水平倾角就是图像需要旋转的角度warpAffine负责做旋转矫正最后统一缩放到 112×112。这个尺寸是常见的人脸识别模型输入尺寸训练和推理保持一致即可。参数说明modelhog在 CPU 上速度更快适合做离线数据处理如果场景里有大量侧面或仰头照片可以改用modelcnn但显存占用和耗时都会上升。target_size建议训练时用 112入库时用 224 以上保留发丝纹理两个尺寸各存一份避免推荐阶段损失太多细节。2.3 特征表示关键点几何特征、人脸嵌入向量和发型属性向量对齐之后数据要转成特征才能被机器学习模型消费。这一步有两条路线一是只做几何特征把 68 点关键点坐标归一化后作为脸型信息二是用深度网络提取的人脸嵌入向量也就是把整张脸压成一个 512 维左右的浮点向量。单纯的关键点坐标容易受遮挡影响但语义非常直接深度嵌入向量对身份信息保持得更好却不能直接告诉你“这个人适合露出额头吗”。实际工程中我一般把这两种特征和发型属性向量一起使用人脸嵌入负责“找到长得像的人”发型属性向量负责“发型是否适合这个人”。三者对比关系如下特征类型维度对发型的相关度标注成本主要用途68 点几何特征136 维低已有模型可生成辅助脸型判断人脸嵌入向量128~512 维中无需人工标注相似人脸召回发型属性向量8~12 维高需人工标注发型适配排序建模时还有一个容易踩的坑发型特征必须从“带完整头部范围的图”里提取不能只裁人脸框。人脸检测器的框通常到发际线就停了头发会被切掉一半此时提取出来的特征根本看不出卷度和长度。操作方法是把人脸检测框按比例向上向外扩展外扩比例一般取 0.3 到 0.5再送进后续的特征提取网络。3. 模型设计与训练单分类、多属性和度量学习的前后顺序特征表示准备好之后进入模型核心环节。发型推荐这个任务适合用“分类 度量学习”的组合结构而不是一上来就上生成式模型。常见做法是把骨干网络做成共享结构输出端分成两个分支一个分支输出发型属性类别概率另一个分支输出用于相似度比较的嵌入向量。两分支协同训练推理时只保留嵌入向量这样既保持可解释性又不牺牲检索性能。3.1 先用 ResNet 做属性分类基线先跑通一个最简单但有效的属性分类模型用它确认标签质量是否足够再接更复杂的结构。基于 PyTorch 的实现起来很直接import torch.nn as nn from torchvision import models class HairAttributeModel(nn.Module): def __init__(self, num_attr8, num_classes4): super().__init__() base models.resnet18(pretrainedTrue) self.backbone nn.Sequential(*list(base.children())[:-1]) # 每个属性一个独立分类头 self.attr_heads nn.ModuleList([ nn.Linear(512, num_classes) for _ in range(num_attr) ]) def forward(self, x): feat self.backbone(x).flatten(1) return [head(feat) for head in self.attr_heads]逻辑说明代码去掉 ResNet18 自带的全局池化和分类层只保留卷积骨干输出 512 维特征然后再接 8 个独立的线性分类头每个头对应一种发型属性。损失函数分别计算 8 个交叉熵再取平均这样“长度分错了”不会拖累“卷度分类”的梯度。参数说明pretrainedTrue会让模型加载 ImageNet 预训练权重在数据量不大时能显著加快收敛。num_attr8要与第 2 章里的属性字段数量对应如果业务实际只有 6 个可标注属性就把它改成 6不要硬凑。这一步跑完要在验证集上看每个属性的准确率和混淆矩阵。如果“刘海类型”和“发际线暴露度”两个属性混淆严重说明标注定义有重叠需要回到数据标注阶段修口径而不是继续加模型复杂度。3.2 从分类到度量学习让“相似脸”找到相似发型属性分类能解释“这个人的额头适不适合刘海”但它解决不了“这个人整体气质和哪个发型匹配”的问题。审美型任务没有唯一的正确答案同一张脸配三种发型都可能不错。这时候度量学习更合适训练一个嵌入模型让同一发型标签下的样本在向量空间里距离更近不同发型标签的样本距离更远。三元组损失是度量学习里最常用的形式核心代码可以精简成下面这样import torch.nn.functional as F def batch_triplet_loss(anchor, positive, negative, margin0.3): # 每个样本都用欧氏距离衡量 pos_dist F.pairwise_distance(anchor, positive, p2) neg_dist F.pairwise_distance(anchor, negative, p2) loss torch.clamp(pos_dist - neg_dist margin, min0.0) return loss.mean()逻辑说明函数接收三组特征向量计算正样本对和负样本对的距离目标是让负样本距离至少比正样本距离大margin否则产生损失。0.3 是一个比较常用的初始值先粗后细调。参数说明margin不要设置太大太大会让训练早期梯度持续很大、难收敛也不要小于 0.1否则同类样本区分度过低。数据采样上负样本不要纯随机取训练到中期之后随机负样本大多数都离得很远损失为 0梯度消失。常见做法是每 5 个 epoch 做一次特征预计算给每个 anchor 找一批难负样本重新组 batch。3.3 联合训练时的三个实践决策训练时我会同时保留属性分类分支和三元组分支但两者损失权重不同。属性分类损失权重设为 1.0三元组损失权重设为 0.3并让两个分支从固定的第 5 个 ResNet block 之后分叉而不是从一开始就分。这个设定的原因是底层卷积特征同时服务于属性识别和相似度度量强行拆开会让两个任务学到的特征相互割裂。还要注意类别不平衡问题。“黑色直发”样本可能占到 60%“浅色大卷”可能只有 1%。直接用交叉熵会让模型产生严重的多数类偏向。缓解方法有 3 个常用手段按样本数做重采样、对少数类提高损失权重、或者在数据增强里对少数类别做过采样。实际操作上重采样的效果最稳定我一般先把每类样本数压到最大类样本数的 60% 以内再做随机增强。最后一坑是学出来的嵌入向量有“身份泄露”。同一人不同照片会被模型认为是同一个簇导致检索结果里反复出现同一个人的脸。解决办法是在组 batch 时不把同一个 ID 的样本放入同一 batch或者在损失函数中对相同 ID 对加上惩罚权重。这属于训练数据组织问题很多机器学习项目里被归为“数据 pipeline 没写对”但它对推荐结果的多样性影响非常明显。模型结构优势劣势适用阶段属性分类单模型简单、可解释性强无法表达“多种发型都合适”基线验证、冷启动纯度量学习模型检索效果好、相似语义强分类解释弱发型库数量大之后属性分支 嵌入分支联合两者兼顾线上最常用训练复杂度高正式上线4. 推荐算法落地向量召回、规则排序与一个小型推理服务模型训练完只是中点真正决定项目能否被业务用起来的是推荐链路的设计。发型推荐不适合直接拿模型输出的相似度 Top1 作为唯一结果因为审美主观性太强单一答案会让用户失去选择感。实际系统通常拆成召回、排序、后处理三层最后通过接口把结果交给前端展示。4.1 先用 NumPy 跑通 KNN 召回发型库的规模通常在几千到几万条用暴力 KNN 完全可以满足。如果只有 5000 条向量一次全量距离计算在 CPU 上也就是几毫秒。先不用直接上复杂检索数据库把逻辑跑通再优化架构。召回代码可以这样写import numpy as np def knn_recall(query_embedding, gallery_embeddings, top_k30): # 归一化不是必须的但用了余弦距离就必须做 query_norm query_embedding / (np.linalg.norm(query_embedding) 1e-9) gallery_norm gallery_embeddings / (np.linalg.norm(gallery_embeddings, axis1, keepdimsTrue) 1e-9) # 余弦相似度等价于归一化后的内积 scores gallery_norm query_norm top_indices np.argsort(-scores)[:top_k] return top_indices, scores[top_indices]逻辑说明代码先分别归一化查询向量和候选库向量再用矩阵乘法一次性算出所有候选的发型和目标脸的相似分数最后取分数最高的前 30 个作为召回候选。为什么不用欧氏距离因为不同批次提取的嵌入向量其绝对数值分布会受模型权重影响归一化后计算余弦相似度对尺度更鲁棒。参数说明top_k30是召回量不是最终展示量。召回量设太大会让后续排序压力上升太小会把正确结果挡在门外。在实际业务里30 到 50 是常见区间排序之后只保留 5 到 8 个结果。等发型库量级超过 10 万条以后可以把gallery_embeddings构建成 faiss 的 IVF 索引检索耗时能从几十毫秒降到个位数毫秒但一开始完全没必要。4.2 排序层相似度、属性和业务规则的三段式融合KNN 召回之后候选集合里可能存在大量相似风格相近的发型这不利于用户体验。排序阶段要综合三部分信号余弦相似度召回阶段的核心分数属性匹配度用户的人脸属性与发型属性之间的匹配业务得分新品权重、门店流行度、运营置顶位。我给出一套极简排序函数def rerank(cand_ids, cand_scores, user_attr, hair_attr_map, hot_scores, style_penalty0.05): results [] for hair_id, sim in zip(cand_ids, cand_scores): # 属性差异只取和脸型、长度、刘海相关的五个字段 attr_diff 0.0 for key in [face_shape, hair_length, fringe_type]: attr_diff abs(user_attr[key] - hair_attr_map[hair_id][key]) # 连续热门排序会让结果趋同这里做成可调节参数 score 0.6 * sim - 0.3 * attr_diff 0.1 * hot_scores[hair_id] # 同风格候选只保留分数最高的一个 style hair_attr_map[hair_id][style_attr] results.append((hair_id, score, style)) results.sort(keylambda x: -x[1]) deduped {} for hair_id, score, style in results: if style not in deduped: deduped[style] (hair_id, score) return [v[0] for v in deduped.values()][:8]逻辑说明排序分等于 0.6 倍的相似度分减去 0.3 倍的属性差异再加上 0.1 倍的热门分。属性差异直接取三个关键字段的绝对值相加差异越大说明发型越不适合当前脸型。最后用style_attr做同风格去重保证展示结果覆盖多种路线。参数说明0.6、0.3、0.1 这三个权重不是固定值也不需要一开始调得很精细。先跑离线评测把“相似度得分高但用户根本不点”的样本捞出来看是属性差异权重太低还是热门分干扰太大再逐项调整。某个业务里如果门店希望让新品得到更多曝光可以把热门分的来源从点击量改为“点击率 新品时间衰减”这样既能保证折损可控也不用改排序逻辑。4.3 用 FastAPI 封装一个最小可用的“应用实现”服务链路验证完毕后要用 Web 服务把功能接出去。FastAPI 是目前比较合适的轻量方案异步支持好自带参数校验和 OpenAPI 文档。一个最小可用的推理服务长这样from fastapi import FastAPI, UploadFile import numpy as np import cv2 app FastAPI() gallery_embeddings np.load(gallery.npy) hair_meta load_hair_meta() # 发型属性与业务信息字典 app.post(/api/v1/hair/recommend) async def recommend(file: UploadFile): raw await file.read() img cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) aligned align_face(img, target_size112) # 第2章的对齐函数 if aligned is None: return {code: 400, msg: no face detected} embedding extract_embedding(aligned) # 模型推理函数 cand_ids, cand_scores knn_recall(embedding, gallery_embeddings, top_k30) user_attr predict_attributes(aligned) # 属性分类分支的输出 final_ids rerank(cand_ids, cand_scores, user_attr, hair_meta) return { code: 0, data: [ {hair_id: str(i), cover: hair_meta[i][cover_url], reason: 相似度匹配, order: idx} for idx, i in enumerate(final_ids) ] }启动服务用一行命令uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2逻辑说明接口先读取上传图片做人脸对齐再同时做嵌入向量提取和属性预测最后调用排序函数返回发型 ID 列表。这里“对齐”和“推理”拆成两步是为了方便在 GPU 机器上把推理并行化而 CPU 只处理预处理。参数说明--workers 2代表启动两个进程模型会在每个进程里加载一份内存翻倍。如果显卡显存不够放两份模型可以先设成 1或改用共享内存加载权重的方式。返回数据里的cover_url是指向前端静态资源的地址一般是对象存储链接前端拿到后直接渲染图片。模型在启动阶段加载一次不要在每个请求里重新torch.load否则高并发时磁盘 I/O 会成为瓶颈。必要的时候加一层 Redis 做结果缓存以人脸的感知哈希值作为 key把同一张照片的重复请求直接打在缓存上。5. 上线后真正要盯的三个技巧遮挡兜底、A/B 分流与特征缓存版本化模型上线不代表工作结束发型推荐这类主观性强的业务最怕的不是算法精度低而是用户对推荐结果没有感知、也无从反馈。上点技巧验证流程和缓存要提前设计好。遮挡是真实场景里出现频率最高的问题。用户在地铁、门店随手一拍可能存在刘海遮挡眉眼、手部遮挡下颚这类情况。此时人脸对齐仍能成功但关键点误差变大嵌入向量会产生偏移推荐结果随之漂移。在服务里加入姿态角判断当检测到的左右眼连线超过 30 度、或关键点置信度低于阈值时不再进入个性化推荐链路而是返回运营配置的热门发型。热门榜可以按发型库自身点击率生成保证推荐流程在异常输入下不出现空结果也不出现离谱结果。线上验证采用 A/B 分流时分流键要选稳定的用户 ID而不是每次请求动态生成。常见做法是取user_id后 7 位哈希哈希值落在[0, 500)的进实验组其余进对照组实验组返回算法推荐结果对照组返回热门发型排序。核心指标不要直接看点击率发型推荐里用户点进详情不代表满意要看“点击后停留时长”和“收藏率”这两个指标能更真实反映发型和用户的匹配度。实验至少跑两周覆盖一个完整周末因为周末用户活跃形态和工作日有显著差异。最后是特征缓存的版本化问题。模型迭代时新旧版本的嵌入向量不在同一语义空间直接把新旧数据混在一起做 KNN 会让结果乱掉。推荐做法是给每个版本的模型分配一个版本号缓存 key 写成emb:{user_hash}:model_v{version}这样的结构。比如model_v3上传后先把离线发型库的向量全部重新提取一遍存入新 key线上请求进来后首先检查当前用户有没有新版本的缓存没有就现场推理并回填。这样任何时刻线上只消费同一版本的向量旧版本缓存自然淘汰也不会因为模型热更新影响到正在进行的 A/B 实验。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →