资讯动态

基于卷积神经网络的人脸颜值分类:从数据到部署全流程实践

发布时间:2026/10/21 23:22:37 来源:尧图企业网站定制
1. 项目缘起与整体设计思路人脸颜值打分这个题目乍一听像是那种“娱乐向”的玩具项目但真正动手做一遍就会发现它其实是一个相当典型的图像回归/分类任务涉及数据采集、人脸检测对齐、卷积神经网络搭建、训练调参、模型评估与部署推理的完整链路。我第一次在AI Studio上看到类似项目的时候心里想的是“这不就是个二分类或者回归问题吗”结果真正踩进去才发现人脸数据的特殊性让这个任务比想象中要复杂得多。先说说这个项目到底在做什么。简单讲就是给一张人脸照片让模型输出一个“颜值分数”。这个分数可以是0到10之间的连续值回归任务也可以拆成若干档位做分类比如1到5分五档。标题里写的是“基于卷积神经网络分类方法”所以核心路线是分类而非回归。分类的好处是训练更稳定坏处是分数粒度粗需要合理设计档位划分策略。为什么选卷积神经网络而不是传统机器学习方法因为人脸颜值这个东西它的特征太抽象了。你很难用HOG、LBP或者SIFT这种手工特征去描述“好看”这件事。颜值高低涉及五官比例、皮肤质感、对称性、轮廓线条等高度非线性的组合模式传统特征工程基本无能为力。CNN的卷积核可以自动从低级边缘、纹理逐步组合到高级语义特征天然适合这种任务。那为什么选百度飞桨AI Studio平台几个很实际的原因第一它提供免费的GPU算力额度对于学生党或者个人开发者来说非常友好第二平台内置了PaddlePaddle框架和大量预训练模型可以直接调用第三数据集管理、Notebook环境、模型训练和部署都在一个平台里完成省去了自己搭环境的麻烦。我实测下来在AI Studio上用V100或者V100级别的算力跑一个中等规模的CNN训练时间完全可以接受。整体设计思路可以拆成这么几步数据获取与清洗 → 人脸检测与对齐 → 标签体系设计 → 数据增强 → CNN模型搭建 → 训练与调参 → 模型评估 → 推理部署。每一步都有坑后面我会逐个拆开讲。注意颜值打分本身带有强烈的主观性不同人对同一张脸的评分可能差异很大。所以这个项目的定位应该是“技术练手”和“完整链路实践”而不是追求绝对准确的评分。理解这一点后面的调参和评估心态会好很多。2. 数据准备与标签体系设计2.1 数据来源与采集策略做颜值打分第一个卡脖子的问题就是数据。公开的人脸数据集不少比如CelebA、FFHQ、LFW这些但它们大多只有身份标注或属性标注没有颜值分数。那分数从哪来常见的做法有这么几种一是用公开的颜值评分数据集比如SCUT-FBP5500这个数据集包含5500张人脸每张有60个人工评分最终取平均分作为标签。二是自己爬取带评分的数据比如某些评分网站上的数据但这种方式涉及版权和合规问题需要谨慎。三是用预训练模型生成伪标签比如用一个已经训练好的颜值模型去给大量无标签人脸打分然后拿这些伪标签来训练自己的模型。这种方式适合做数据蒸馏或者半监督学习。我在AI Studio上做这个项目的时候优先推荐用SCUT-FBP5500或者类似公开数据集。如果找不到可以退而求其次用CelebA的人脸图片然后通过众包或者小规模人工标注的方式生成分数。但人工标注成本高而且一致性难保证所以更实际的做法是先用小规模高质量标注数据训练一个基线模型再用这个模型去给大规模无标签数据打伪标签最后用伪标签数据做微调。2.2 人脸检测与对齐的必要性拿到原始图片之后千万不要直接resize就丢进网络。人脸在图片中的位置、角度、尺度差异极大如果不做检测和对齐模型会浪费大量容量去学习“人脸在哪”这种低级问题而不是“脸好不好看”这种高级语义。人脸检测可以用MTCNN或者RetinaFace这两个在PaddlePaddle生态里都有现成的实现。检测到人脸框之后做关键点对齐通常是用5点或68点关键点做仿射变换把眼睛、鼻子、嘴角对齐到一个标准位置。对齐之后的人脸再裁剪成固定尺寸比如224x224或者112x112。这一步的实操心得是对齐的质量直接决定模型的上限。如果对齐歪了眼睛不在同一水平线上模型学到的特征就是错的。我试过不做对齐直接训练准确率比做了对齐低了将近8个百分点。所以这一步千万别省。2.3 标签体系设计与分数离散化前面说了这个项目走的是分类路线。那怎么把连续的颜值分数变成类别最直接的方式是等宽分箱。比如分数范围是1到5分成5档每0.8分一档。但这样分有个问题中间分数段的人脸特别多两端特别少导致类别极度不平衡。另一种方式是等频分箱按分位数切分保证每个类别样本数大致相等。这种方式训练更稳定但档位的物理意义会变得模糊。我个人的建议是如果数据量在1万张以下用等频分箱分5档如果数据量在5万张以上可以尝试等宽分箱分10档。分5档的好处是每档样本充足模型容易收敛分10档的好处是分数粒度细但需要更多数据支撑。还有一个细节标签平滑。因为颜值评分本身有噪声相邻档位之间的边界很模糊。用label smoothing比如0.1的平滑系数可以防止模型对边界样本过度自信提升泛化能力。这个技巧在图像分类里很常用颜值打分任务尤其适合。2.4 数据增强策略人脸数据的增强和普通图像分类不太一样有些增强会破坏人脸的语义。比如随机裁剪可能把眼睛裁掉颜色抖动太大会让肤色失真。所以增强策略要克制。我常用的增强组合是随机水平翻转概率0.5、小角度旋转±10度、轻微颜色抖动亮度、对比度、饱和度各±0.1、随机擦除概率0.2擦除面积不超过10%。不做垂直翻转因为倒过来的人脸没有意义不做大角度旋转因为对齐之后人脸基本是正的。提示数据增强只在训练时做验证和测试时只做resize和归一化。另外增强后的图片要检查一下确保人脸没有被裁掉或者严重变形。3. 卷积神经网络模型搭建与选型3.1 backbone选择从轻量到重量在AI Studio上搭CNNbackbone的选择很关键。不是越深越好要看数据量和算力。如果数据量在1万张以下建议用轻量级网络比如MobileNetV3、ShuffleNetV2或者PaddlePaddle自带的PP-LCNet。这些网络参数量小不容易过拟合训练也快。我实测过在5000张数据上用ResNet50验证集准确率卡在60%上不去换成MobileNetV3之后直接到了68%。如果数据量在5万到10万之间可以用ResNet50或者ResNet101。如果数据量超过10万可以尝试EfficientNet系列或者Vision Transformer。但ViT对数据量要求极高小数据上基本训不动。还有一个很实用的技巧用预训练权重。在ImageNet上预训练的模型底层卷积核已经学会了边缘、纹理、颜色等通用特征这些对人脸任务同样有效。用预训练权重初始化然后fine-tune比从头训练收敛快得多最终精度也更高。AI Studio上可以直接加载PaddlePaddle的预训练模型非常方便。3.2 自定义分类头设计backbone之后要接分类头。最简单的做法是全局平均池化GAP之后接一个全连接层输出类别数。但颜值打分任务有个特点类别之间是有序的。1分和2分的差距小于1分和5分的差距。普通分类损失交叉熵不考虑这种序关系所以可以做一些改进。一种改进是序数回归Ordinal Regression把分类问题转化成多个二分类问题。比如5个类别就训练4个二分类器分别判断“分数是否大于1”、“是否大于2”……最后累加得到最终分数。这种方式保留了序信息效果通常比普通交叉熵好。另一种改进是混合损失把交叉熵和回归损失比如MSE结合起来。分类头输出类别概率同时输出一个回归值两个损失加权求和。这样既有分类的稳定性又有回归的细粒度。我在项目里用的是序数回归的思路实现起来也不复杂把全连接层的输出维度设为1然后用sigmoid激活标签做归一化处理。损失函数用MSE或者Huber损失。实测下来这种方式比普通5分类的准确率高了3到5个百分点。3.3 模型结构代码示例下面是一个基于PaddlePaddle的模型定义示例backbone用MobileNetV3分类头用序数回归import paddle import paddle.nn as nn from paddle.vision.models import mobilenet_v3_small class FaceBeautyModel(nn.Layer): def __init__(self, num_classes5, pretrainedTrue): super().__init__() self.backbone mobilenet_v3_small(pretrainedpretrained) # 去掉原来的分类头 in_features self.backbone.classifier[0].in_features self.backbone.classifier nn.Identity() # 自定义分类头 self.head nn.Sequential( nn.Linear(in_features, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 1) # 序数回归输出1维 ) self.num_classes num_classes def forward(self, x): features self.backbone(x) out self.head(features) return out这个结构很简洁但效果不错。关键点在于Dropout放在全连接层之后而不是卷积层之后。卷积层本身有正则化效果再加Dropout反而会拖慢收敛。全连接层参数量大容易过拟合加Dropout很有必要。3.4 损失函数与优化器选择序数回归的损失函数我推荐用Huber损失而不是MSE。Huber损失对异常值更鲁棒颜值评分里难免有一些标注噪声很大的样本MSE会被这些样本带偏Huber则稳定得多。优化器方面AdamW是首选。学习率设1e-3到1e-4之间weight decay设1e-4到1e-2之间。如果用了预训练权重backbone的学习率可以设小一点比如1e-4分类头的学习率设大一点比如1e-3这种分层学习率的策略在fine-tune时很有效。学习率调度用余弦退火CosineAnnealing或者带热重启的余弦退火。我一般用余弦退火训练30到50个epoch前5个epoch做warmup学习率从1e-6线性升到初始值然后再余弦下降。这样训练曲线很平滑不容易震荡。4. 训练流程与调参实操4.1 数据划分与批处理数据划分不能随便随机分。因为颜值打分数据里同一个人的多张照片可能同时存在如果随机划分同一个人可能出现在训练集和验证集里导致验证集精度虚高。正确的做法是按人划分同一个人只出现在一个集合里。划分比例一般是训练集70%、验证集15%、测试集15%。如果数据量小可以用5折交叉验证但训练成本会翻倍。批大小batch size根据显存来定。AI Studio上V100的显存是32GMobileNetV3这种轻量网络batch size可以设128甚至256。如果显存不够用梯度累积gradient accumulation来模拟大batch。大batch的好处是梯度估计更准训练更稳定但学习率要相应调大。4.2 训练过程监控与早停训练过程中要监控几个指标训练损失、验证损失、验证准确率或者MAE。如果训练损失持续下降但验证损失开始上升说明过拟合了要早停或者加正则化。早停的patience一般设5到10个epoch。比如验证损失连续10个epoch没有下降就停止训练取验证损失最低的那个checkpoint。还有一个细节保存最佳模型而不是最后一个模型。因为最后一个epoch的模型可能已经过拟合了最佳checkpoint才是泛化最好的。4.3 超参数调优经验超参数调优是个体力活但有几个经验可以分享超参数推荐范围调优优先级备注学习率1e-4 ~ 1e-3最高先用大学习率找大致范围再细化Weight decay1e-4 ~ 1e-2高过拟合严重时调大Dropout率0.2 ~ 0.5中数据量小则调大Batch size64 ~ 256中受显存限制数据增强强度弱 ~ 中中人脸数据不宜过强序数回归vs分类序数回归优先高序数回归通常更好调参的顺序建议是先调学习率再调weight decay然后调Dropout最后微调数据增强。每次只调一个参数固定其他参数否则无法判断哪个参数起了作用。4.4 训练代码框架下面是一个简化的训练循环示例import paddle from paddle.io import DataLoader from paddle.optimizer import AdamW from paddle.optimizer.lr import CosineAnnealingDecay # 数据加载 train_loader DataLoader(train_dataset, batch_size128, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size128, shuffleFalse, num_workers4) # 模型、损失、优化器 model FaceBeautyModel(num_classes5, pretrainedTrue) criterion paddle.nn.HuberLoss(delta1.0) scheduler CosineAnnealingDecay(learning_rate1e-3, T_max50) optimizer AdamW(learning_ratescheduler, parametersmodel.parameters(), weight_decay1e-4) # 训练循环 best_val_loss float(inf) patience 10 counter 0 for epoch in range(50): model.train() for batch in train_loader: images, labels batch preds model(images) loss criterion(preds, labels) loss.backward() optimizer.step() optimizer.clear_grad() scheduler.step() # 验证 model.eval() val_loss 0 with paddle.no_grad(): for batch in val_loader: images, labels batch preds model(images) val_loss criterion(preds, labels).item() val_loss / len(val_loader) if val_loss best_val_loss: best_val_loss val_loss paddle.save(model.state_dict(), best_model.pdparams) counter 0 else: counter 1 if counter patience: print(fEarly stopping at epoch {epoch}) break这个框架很基础但涵盖了训练的核心要素数据加载、模型定义、损失函数、优化器、学习率调度、早停、模型保存。你可以根据实际情况调整。5. 模型评估与常见问题排查5.1 评估指标选择分类任务常用的指标是准确率Accuracy但颜值打分任务里准确率有点苛刻。因为相邻档位之间的差异很小模型把3分预测成4分其实不算大错。所以除了准确率还要看平均绝对误差MAE和相邻准确率Adjacent Accuracy。MAE是预测分数和真实分数的平均绝对差越小越好。相邻准确率是预测档位和真实档位相差不超过1的比例这个指标更宽松也更符合实际感受。还有一个指标是Spearman相关系数衡量预测分数和真实分数的排序一致性。颜值打分本质上是个排序问题Spearman相关系数能反映模型的排序能力。5.2 常见问题与排查表问题现象可能原因排查方法解决方案训练损失不下降学习率太小或太大打印梯度范数调整学习率加warmup验证损失震荡batch size太小增大batch size用梯度累积验证准确率远低于训练过拟合对比训练/验证曲线加Dropout、weight decay、数据增强所有样本预测同一类类别不平衡统计类别分布用加权损失或重采样人脸检测失败图片质量差可视化检测结果换检测模型或手动筛选对齐后人脸变形关键点检测错误可视化对齐结果换关键点模型或过滤异常样本推理速度慢模型太大测FLOPs换轻量backbone或量化5.3 独家避坑技巧第一个坑不要用准确率作为唯一指标。我一开始只看准确率结果模型把所有样本都预测成中间档准确率也有40%多但MAE很高实际体验很差。后来加了MAE和相邻准确率才真正反映模型好坏。第二个坑验证集要按人划分。这个前面提过但真的很重要。我试过随机划分验证准确率比按人划分高了10个百分点但实际部署时效果很差因为模型“见过”验证集里的人了。第三个坑数据增强不要过度。人脸数据很敏感颜色抖动太大会让肤色失真随机裁剪可能把五官裁掉。我试过用AutoAugment这种强增强策略结果验证准确率反而下降了。后来改成手动设计的弱增强效果才好起来。第四个坑序数回归的标签归一化。如果用序数回归标签要归一化到0到1之间否则sigmoid输出和标签范围不匹配损失会很大。这个细节很容易忽略。第五个坑模型保存要保存state_dict而不是整个模型。保存整个模型虽然方便但兼容性差换一个PaddlePaddle版本可能就加载不了。保存state_dict更稳妥。5.4 模型可解释性分析颜值打分模型到底学到了什么可以用Grad-CAM可视化一下。把模型关注的区域热力图叠加到原图上看看模型是不是真的在看五官区域而不是背景或者头发。我做过这个分析发现模型主要关注眼睛、鼻子、嘴巴和下巴轮廓区域这和人类判断颜值的关注点基本一致。但也有少数样本模型关注了背景里的某些纹理说明模型还是学到了一些虚假相关。这时候可以通过数据增强或者增加数据多样性来缓解。6. 推理部署与项目扩展6.1 模型导出与推理训练完之后要把模型导出成推理格式。PaddlePaddle支持动态图和静态图两种模式。动态图方便调试静态图推理速度快。导出的时候用paddle.jit.to_static把动态图模型转成静态图然后保存成model.pdmodel和model.pdiparams两个文件。推理的时候加载模型预处理输入图片检测、对齐、resize、归一化然后前向计算得到分数。如果是序数回归输出是一个0到1之间的值乘以分数范围比如1到5就得到最终分数。推理代码示例import paddle import cv2 import numpy as np # 加载模型 model FaceBeautyModel(num_classes5, pretrainedFalse) model.set_state_dict(paddle.load(best_model.pdparams)) model.eval() # 预处理 def preprocess(image_path): img cv2.imread(image_path) # 人脸检测和对齐省略具体实现 face detect_and_align(img) face cv2.resize(face, (224, 224)) face face.astype(float32) / 255.0 face (face - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] face face.transpose(2, 0, 1) return paddle.to_tensor(face).unsqueeze(0) # 推理 with paddle.no_grad(): pred model(preprocess(test.jpg)) score pred.item() * 4 1 # 映射到1到5 print(f颜值分数: {score:.2f})6.2 部署到AI Studio在线服务AI Studio支持把模型部署成在线服务。部署之后会得到一个API接口可以通过HTTP请求调用。这种方式适合做demo或者小规模应用。部署的时候要注意输入图片的尺寸和格式要和训练时一致预处理逻辑要完全复现。我见过很多人部署之后效果变差就是因为预处理不一致比如训练时用了归一化推理时忘了。6.3 项目扩展方向这个项目做完之后可以往几个方向扩展一是多任务学习。除了颜值分数还可以同时预测性别、年龄、表情等属性。多任务学习可以共享底层特征提升泛化能力。二是个性化颜值模型。不同人对颜值的偏好不同可以收集用户的评分数据做个性化微调。这个方向比较前沿但数据收集是个难点。三是视频颜值打分。把单张图片扩展到视频做实时颜值打分。这个对推理速度要求高需要模型量化或者剪枝。四是对抗样本鲁棒性。研究颜值打分模型对对抗扰动的鲁棒性这个偏学术但很有意思。6.4 最后的经验分享这个项目我从头到尾做了大概三周踩了不少坑也学到了很多东西。最大的体会是数据质量比模型结构重要得多。我试过用很复杂的模型但数据没清洗干净效果还不如简单模型加干净数据。另外颜值打分这个任务本身有很强的主观性不要追求绝对准确。把它当成一个完整的机器学习项目练手把数据采集、清洗、增强、模型搭建、训练、评估、部署这一整套流程走通比追求几个百分点的准确率提升更有价值。最后分享一个小技巧用预训练模型的特征做可视化。把backbone输出的特征用t-SNE降维画出来看看不同颜值档位的样本在特征空间里是不是分开了。如果分开了说明模型学到了有判别力的特征如果混在一起说明模型还没学好需要继续调。这个分析比看损失曲线直观得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑