从零跑通人脸关键点检测:Jupyter Notebook 实战入门 简介面向计算机视觉与深度学习入门者的面部关键点检测项目实践包对应 CVND 首个项目围绕人脸检测与眼、鼻、嘴周围关键点定位展开可用于面部跟踪、姿态识别、滤镜与情绪识别等场景的自学与课程作业。包内收录约2000个文件以 jpg 人脸图像样本为主配合 ipynb 分步笔记本、png 示意图、xml 标注、csv 关键点索引及少量 Python 脚本与说明文档压缩包约330.12MB便于按数据、建模、检测流程检索。已有201人学习内容将任务拆为数据加载与可视化、CNN定义与训练、Haar级联结合CNN的完整检测管线、趣味滤镜与关键点应用四部分并附模型代码可帮助读者理解关键点回归的训练细节、评估思路和从单图预测到端到端检测的衔接。全流程采用 Jupyter Notebook 组织适合按编号顺序复现与二次修改也能为课程展示和项目复盘提供参考。1. 从一个困惑说起手机相机里那圈跟着你动的小点是什么你有没有盯着手机前置摄像头看过一张脸凑过去屏幕上浮出一圈小点眼角、鼻尖、嘴角一共十几个你歪头它也跟着歪。这圈东西就是人脸关键点英文叫 Facial Keypoints。这份叫 P1_Facial_Keypoints 的资源就是一个用 Jupyter Notebook 带你亲手把这件事做一遍的入门项目输入一张人脸照片让模型吐出 15 组坐标再把这些点画回脸上。它不需要你先啃完一本深度学习教材也不用先学完一整门课只要你能打开一个笔记本页面、照着敲几行 Python就能看到第一张被标出五官的脸。适合完全没跑过模型、但想真正动手一次的新手。2. 拆开这份资源一个 Jupyter Notebook 里装着怎样一条流水线2.1 它到底在解决什么问题先把任务说清楚后面才不会糊。这个项目做的事本质上就是「填表」给一张人脸图片填出一张 15 行的表每行两个数字分别代表某个五官位置在图片里的横坐标和纵坐标。左眼中心一行、右眼中心一行、鼻尖一行、嘴角左右各一行……凑齐 15 行也就是 30 个数字。为什么是 30 个数字而不是别的因为每个点要两个数才能定位一个管左右一个管上下。这 30 个数字按固定顺序排成一串就是一个长度为 30 的向量。模型要学的就是从「一张图」到「这串数字」的映射关系。这里有个新手常混淆的地方值得多说两句。常见的图像任务分两类一类叫分类回答「这张图是猫还是狗」输出是几个类别里挑一个另一类叫回归回答「这个数值是多少」输出是连续的数字。人脸关键点属于后者——它不是在猜「这是不是一张脸」而是在算「这张脸上鼻尖大概在第 137 像素、第 92 像素的位置」。既然是算数字误差就可以用「预测值和真实值差了多少」来衡量这也是为什么这类任务常用均方误差当损失函数差得越远惩罚越大模型就往差得少的方向调。那这东西学了有什么用最直接的是给相机做贴纸和特效知道眼睛在哪才能把猫耳朵戴到头顶而不是戴到下巴上。往深一点走可以做表情驱动——把关键点的位移翻译成表情参数可以做视线追踪判断你在看屏幕的哪个角落也可以做图像对齐换脸、修图、人脸变形都靠先找到这些基准点。这些应用听着远但它们的第一步完全一样先把 15 个点稳稳地找出来。这个项目就是那第一步。再说数据长什么样。资源里的数据通常由两部分组成一堆人脸图片文件和一张表格。表格每行对应一张图第一列是图片文件名后面若干列是这张图上那 15 个点的坐标。所以你打开表格看到的就是一行一行的数字没有图片——图片是分开存的文件名是它们之间的钥匙。这个「图片 表格」的组合是后面所有步骤的地基先把这层关系记住看代码时就不会晕。2.2 打开一个 Notebook 你会看到什么Jupyter Notebook 最反直觉的地方是它不像普通程序那样从头跑到尾。它是一格一格cell的每一格可以是文字也可以是代码你可以只跑其中一格跑完结果就留在页面上。你可以把它想成一本实验记录本上面写着说明下面是能直接执行的小实验做完一个看一个结果。这份资源的结构大致是这样的顺序你可能第一眼看到会觉得格数很多其实就六段第一段是读数据。把图片路径、关键点表格、训练集和验证集的划分准备好。这一段跑完你能看到表格的形状比如有多少行多少列。第二段是看数据。随机挑几张图把图画出来再把关键点叠上去。这一段是给人看的不是给模型看的——你要亲自确认「表格里的坐标和图片对得上」否则后面训练半天全是错的方向。第三段是预处理。把人脸区域裁出来、把图片统一成同一个尺寸常见是 96×96 或 224×224、把坐标从「像素值」换算成「相对比例」。这一步是所有新手最容易出错的地方第 5 章会专门讲。第四段是定义模型。通常是一个小型卷积网络几层卷积加池化负责看最后接几层全连接负责算输出长度是 30。第五段是训练。设定损失函数、优化器、迭代轮数然后循环前向算预测、算损失、反向传梯度、更新参数。每一轮打印一次损失你能看到它从大变小。第六段是预测和可视化。拿验证集里没见过的图预测出 15 个点画在图上用眼睛判断准不准。提示第一次打开时不要急着「全部运行」。一格一格往下跑每跑完一格就看输出出问题你能立刻知道是哪一步出的。还有一点要提醒Notebook 里的变量是「记住」的。你在第一格定义了变量跑到第十格它还活着。这是它的方便之处也是它的陷阱——如果中间某格你改过又没重跑后面的结果可能是旧的变量算出来的看起来就像「代码明明改了却没生效」。遇到这种玄学情况第一反应应该是「从第一格重新跑一遍」而不是盯着代码找 bug。2.3 为什么它适合零基础而不是先学三个月理论很多新手会问我是不是得先看完线代、概率、反向传播推导才有资格碰这种项目答案是反过来的。先跑通再回头补理论效率高得多。第一反馈极其直观。分类任务你只能看到一个准确率数字90% 还是 92%你其实没什么感觉。关键点任务不一样预测完直接画在脸上准不准你一眼就能看出眼睛的点是落在眼珠上还是歪到鼻梁上。这种「肉眼可见对错」的反馈对新手建立感觉特别重要。第二数据量小、训练快。这种入门数据集通常只有几千张脸用一张普通显卡甚至只用 CPU跑几轮也就几分钟。你可以在一个下午里试五六种改动而不是等一晚上才看到一次结果。第三脚手架已经搭好了。你不需要从「怎么读取一张图片」写起读数据、画图、训练循环这些重复劳动资源里大半已经给你写好了。你要做的是在已有的格子里改几个数字然后看结果怎么变。这种「改一点、看一点」的节奏比从零搭整个项目轻松太多。第四它把深度学习的完整流程走了一遍一个都没漏数据、预处理、模型、损失、训练、评估、可视化。走完一遍之后你再看别的项目会发现它们只是把其中某一段换掉了骨架是同一副。走完这个项目你能带走的东西很具体你知道一张图片在程序里其实是一个数字矩阵你知道坐标要先归一化再喂给模型你知道损失不降的时候该先怀疑学习率和数据你能独立把一个 Notebook 从第一格跑到最后一格并解释每一步在干什么。这些听起来朴素但它们是往后所有计算机视觉任务的地基。3. 动手前的一小时把运行环境配成「能跑的样子」3.1 先分清三样东西Python、Notebook、框架新手最容易在装环境这一步翻车原因是把三个不同层级的东西混为一谈。Python 是语言本身是地基。你写的每一行代码最终都由它执行。Jupyter Notebook 是一个「运行界面」它让你在浏览器里一格一格地写和跑 Python。它本身不是语言也不是框架它是壳。你可以用命令行跑 Python也可以用这个壳跑壳的好处是结果能直接留在页面上图也能直接显示。深度学习框架是「工具箱」比如 PyTorch 这类。它把卷积、梯度、张量运算这些底层活封装成一行调用你不用自己算导数。这份资源可能用其中某一种跟着它的代码走就行不用为了「哪个更好」纠结。三者的关系是Notebook 里写 PythonPython 调用框架。所以顺序是——先有 Python再有 Notebook再装框架。顺序反了就会出现「明明装了却 import 报错」这类问题。3.2 装依赖按顺序执行的三条命令假设你用的是 conda 这类环境管理工具新手强烈建议用别直接在系统 Python 里装东西装崩了很难收拾可以按下面的顺序走。先建一个独立环境把它想成一个专用的抽屉里面的包互不干扰# 建一个独立环境指定 Python 版本避免和系统里的包打架 conda create -n keypoints python3.10 -y # 进入这个环境之后所有安装都发生在抽屉里 conda activate keypoints # 安装 Notebook 本体用来打开 .ipynb 文件 pip install notebook接着装计算和画图要用的包。这一步建议一次装完避免后面一格一格报错、一格一格补# 数值计算、表格处理、画图、读图片 pip install numpy pandas matplotlib pillow # 深度学习框架按资源里 import 的那个名字选一个装 pip install torch torchvision装完之后在项目目录下执行启动命令# 启动 Notebook浏览器会自动打开一个带文件列表的页面 jupyter notebook执行完你会看到一个网页里面列出当前目录的所有文件。点开那个后缀是 .ipynb 的文件就是这份资源的主体。如果浏览器没自动弹出来终端里会打印一个带端口的地址复制到浏览器里打开即可。注意启动命令要在「资源所在的那个目录」下执行。在别的目录启动页面里就看不到文件很多新手会以为文件丢了。3.3 检查环境是否真的通了装完不代表能用一定要验证。新建一格把下面这段贴进去跑import sys import numpy as np import pandas as pd import matplotlib.pyplot as plt import torch # 打印 Python 版本确认跑的是刚建的那个环境而不是系统自带的 print(python:, sys.version.split()[0]) print(numpy:, np.__version__) print(torch:, torch.__version__) # 有独立显卡的话这里会打印 True没有就是 FalseCPU 也能跑这个项目 print(cuda 可用:, torch.cuda.is_available()) # 造一个小张量做一次真实运算确认框架不是装了个空壳 x torch.randn(2, 3) print(张量形状:, x.shape, 均值:, round(x.mean().item(), 4))这段的作用是逐一确认四件事Python 版本对不对、几个包能不能 import、框架有没有真的装好、显卡能不能用。四行输出全打出来且不报错环境就算通了。如果torch.cuda.is_available()打印 False 而你确实有显卡别慌先按 CPU 跑。这个项目的数据量小CPU 跑几轮也能出结果只是慢一点。环境问题留到后面再修先让自己看到第一个能跑的模型比卡在装环境上三天要值。再补一个新手常忽略的检查确认当前 Notebook 的工作目录。跑import os; print(os.getcwd())看看打印出来的路径是不是资源所在的目录。后面所有读文件的相对路径都是相对这个目录算的。路径不对读数据那一格一定报「找不到文件」而这跟代码写得对不对毫无关系。4. 跑通第一个例子一张人脸图变成 15 个坐标点的三步4.1 先把数据读进来看看一张脸长什么样这一步的目标很朴素确认你能拿到图片也能拿到对应的坐标。下面这段是通用写法具体文件名以你解压后看到的为准import numpy as np import pandas as pd import matplotlib.pyplot as plt from PIL import Image # 读关键点表格每行一张图第一列是图片文件名后面 30 列是 15 个点的坐标 keypoints pd.read_csv(data/training_frames_keypoints.csv) # 看看整体规模多少张图、多少列 print(表格形状:, keypoints.shape) # 前几列的名字确认第一列确实是文件名 print(前几列:, keypoints.columns[:4].tolist()) # 取第一张图的前 6 个数字也就是前 3 个点的 (x, y) print(第一张图前三个点:, keypoints.iloc[0, 1:7].values)读完之后要做一件事确认列的顺序。很多关键点表格的坐标不是按「x1, y1, x2, y2」这样成对排列而是「所有 x 排一起所有 y 排一起」。这两种排法在画图时结果完全不一样一个画得对一个画出一堆乱七八糟的点。所以别急着往下走先打印几列看看数值范围如果某一列的值普遍在 0 到 200 之间那它大概率是像素坐标如果出现负数或者超过图片宽度很多的值说明数据本身有问题或者列理解错了。再把图片读进来看看# 按表格里的文件名去图片目录找图转成 RGB 三通道 img np.asarray(Image.open(data/training/000001.jpg).convert(RGB)) # 打印形状(高, 宽, 3)三个数字分别对应行数、列数、通道数 print(图片形状:, img.shape, 数据类型:, img.dtype) # 画出来看看 plt.imshow(img) plt.axis(off) plt.show()图片在程序里就是一个三维数组高、宽、通道。img.shape打印出(96, 96, 3)这样的结果意思是 96 行像素、96 列像素、3 个颜色通道。新手看到「图片变成数字」经常觉得抽象其实不抽象——每一个像素就是三个 0 到 255 的数字分别代表红绿蓝的强度。整张图就是这些数字排成的一张表。提示读进来的图片尺寸往往不统一。有的 96×96有的 250×250。后面喂给模型前必须统一否则网络层会直接报维度不匹配。4.2 把关键点画上去用眼睛验证数据这是整个流程里最值得花时间的一步也是最容易被跳过的一步。画一次图能省掉后面几小时的排查。# 从表格里取第一张图的 30 个坐标重排成 15 行 2 列每行是一个点 pts keypoints.iloc[0, 1:].values.astype(float32).reshape(-1, 2) fig, axes plt.subplots(1, 2, figsize(8, 4)) # 左图原图 axes[0].imshow(img) axes[0].set_title(original) axes[0].axis(off) # 右图原图 关键点。x 是横坐标列y 是纵坐标行 axes[1].imshow(img) axes[1].scatter(pts[:, 0], pts[:, 1], s14, cred, markerx) axes[1].set_title(with keypoints) axes[1].axis(off) plt.show()几个参数值得说清楚。reshape(-1, 2)里的 -1 是「这一维让程序自己算」30 个数字自然排成 15 行 2 列。s14是点的显示大小图小的时候调大一点才看得清。cred是颜色markerx是标记形状用叉号比圆点更容易看清点落在哪个位置。画出来之后怎么判断对不对看三件事。第一点的数量是不是 15 个。第二点是不是集中在脸部区域如果有一半的点飘到背景里说明坐标和图片没对上很可能是文件名匹配错了行。第三左右眼的两个点是不是水平对称的如果明显一高一低可能是坐标的列顺序被搞混了。这一步的真正价值在于它是你唯一能用眼睛验证「数据和标签是配套的」的机会。跳过它直接训练模型也能跑损失也能降但你可能训练了一个小时才发现自己一直在学一堆错位的标签。这种血泪经验几乎每个入门的人都交过一次学费。4.3 搭一个最小模型让损失先降下来现在到了最有仪式感的一步。先别追求精度先追求「能跑通、损失在降」。下面这个网络很小三层卷积加两层全连接输入 96×96 的图输出 30 个数字import torch import torch.nn as nn class TinyKeypointNet(nn.Module): def __init__(self): super().__init__() # 三层卷积每层后接 ReLU 和池化把 96x96 一路缩到 12x12 self.features nn.Sequential( nn.Conv2d(3, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 96 - 48 nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 48 - 24 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 24 - 12 ) # 全连接头把特征图拉平最后回归出 30 个坐标值 self.head nn.Sequential( nn.Flatten(), nn.Linear(64 * 12 * 12, 128), nn.ReLU(), nn.Linear(128, 30), ) def forward(self, x): return self.head(self.features(x)) # 造一个假输入验证输出形状这是养成习惯的好动作 model TinyKeypointNet() print(model(torch.zeros(1, 3, 96, 96)).shape) # 期望 torch.Size([1, 30])Conv2d(3, 16, 3)三个参数分别是输入通道、输出通道、卷积核大小。第一层的输入通道必须是 3因为图片是 RGB 三通道写错了会直接报错。MaxPool2d(2)是每次把长宽各缩一半所以 96 经过三次池化变成 12全连接层的输入就是64 * 12 * 12。这个数字必须和前面算出来的一致差一个数就报维度错误——新手最常卡的地方就在这。再往下是训练循环import torch # 坐标回归用均方误差预测偏离真实值越远惩罚越大 loss_fn nn.MSELoss() # 学习率 1e-3 是这类小项目的稳妥起点太大容易震荡太小降得慢 optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(5): model.train() total 0.0 for images, targets in train_loader: # images: [N,3,96,96] targets: [N,30] pred model(images) # 前向算出一批预测坐标 loss loss_fn(pred, targets) # 算这批预测和真实值差多少 optimizer.zero_grad() # 清掉上一轮的梯度不清会累加 loss.backward() # 反向算出每个参数该往哪调 optimizer.step() # 按梯度真的调一次参数 total loss.item() print(fepoch {epoch}, loss {total / len(train_loader):.4f})train_loader需要你自己封装一下把图片统一缩放、转成张量把坐标归一化到 -1 到 1 之间然后按批次打包。如果资源里已经写好了这部分直接用别重复造。跑起来之后盯着 loss 看。正常情况是从一个较大的数比如 0.1 上下一路往下降前几轮降得明显后面变缓。如果五轮下来几乎没动先别怀疑模型结构去看第 5 章的第一条和第二条——八成是数据归一化或者学习率的问题。5. 新手最容易翻车的四个地方现象、原因和修法5.1 画出来的图是黑的或者颜色很怪现象plt.imshow显示出来的脸要么一片黑要么颜色像反相红脸变青脸。原因有两个。一是通道顺序。有些读图库默认按「蓝绿红」的顺序给出通道而画图和预训练模型都期望「红绿蓝」顺序反了颜色就错乱。二是数值范围。有的流程会把像素除以 255 变成 0 到 1 的小数如果这里被做了两次归一化或者被减了均值显示出来自然一片灰黑。解决读图之后立刻打印img.shape和img.min()、img.max()。正常像素值应该落在 0 到 255 之间。如果通道顺序可疑用img[:, :, ::-1]反转一下通道再画颜色对了就说明是这个问题。显示和训练是两件事训练前该归一化就归一化但画图给人看的时候记得还原成原始像素。5.2 损失一直不降甚至越跑越大现象训练五轮、十轮loss 在同一个数字附近晃或者干脆往上窜。原因通常是学习率不合适或标签没归一化。坐标是像素值动辄几十上百而模型输出的初始值在 0 附近两边的量级差太远梯度会非常大参数一更新就跳飞。另外学习率设成 0.1 这种大值在这种小网络上很容易震荡。解决先把坐标归一化到 -1 到 1 之间再算损失预测时再乘回去学习率从 1e-3 开始试不降就降到 1e-4。还有一个隐蔽原因忘记清梯度。如果optimizer.zero_grad()写在了backward()后面或者干脆没写梯度会一轮轮累加表现为损失突然爆掉。这几个都是新手常踩的坑按顺序排查八成能定位。5.3 点全部挤在图片的一个角落现象训练完了预测出来的 15 个点全部堆在左上角或者正中间一小团看起来像模型「摆烂」。原因归一化和反归一化没配对。训练时你把坐标压到了 -1 到 1预测时忘了乘回图片尺寸那 30 个 0 附近的小数画在几百像素的图上自然全挤在原点附近。反过来也有训练时没归一化预测时却乘了一次点会飞到图外面去。解决把「归一化」和「反归一化」写成两个小函数训练和预测时严格用同一对。归一化用「减最小值除范围」反归一化用「乘范围加最小值」两边参数必须一致。写完先拿一张训练集里的图自测用真实标签归一化再反归一化看能不能还原成原来的坐标还原得回来说明这对函数是对的。5.4 明明有显卡跑起来却像在爬现象torch.cuda.is_available()是 True但训练一轮要好几分钟风扇也不怎么转。原因张量和模型没有搬到同一块设备上。框架默认把新创建的张量放在 CPU 上模型如果也留在 CPU那显卡就一直闲着。只有模型和数据都在显卡上计算才会真正走显卡。解决在定义模型之后加一句model model.to(device)在每一批数据取出之后加一句images, targets images.to(device), targets.to(device)其中device提前定义为torch.device(cuda if torch.cuda.is_available() else cpu)。注意预测和画图时要把张量搬回 CPUpred.cpu().numpy()否则和画图库配合时会报设备不匹配。6. 走完这一站往哪去从 15 个点到完整项目的学习路线跑通这个项目之后最怕的是「跑完了然后呢」。下面这条路径是我带新手时最常用的一条从近到远排每一步都能独立看到结果不用等学完才敢动手。顺序下一步做什么具体学什么大概投入1把当前模型调好一点加数据增强随机翻转、轻微旋转、换成更深的骨干网络、加早停一到两个晚上2学会看训练过程画损失曲线、留出验证集、区分过拟合与欠拟合一个晚上3换一个相近任务人体姿态关键点、手部关键点流程几乎一样只是点的数量变了两到三天4补一层理论卷积在算什么、感受野是什么、批归一化为什么有用边做边补5做成能演示的东西读摄像头画面、实时画出关键点、做成一个小界面一周左右第 1 步最值得先做因为收益最直观。加一个随机水平翻转标签里的横坐标要跟着镜像这一步能让你的验证误差明显下降也能让你真正理解「数据增强不只是把图转一下标签也得跟着动」。第 4 步要提醒一句理论不用等学完再做项目但也别一直不补。我的建议是「遇到再补」——模型不收敛的时候去查学习率和归一化精度上不去的时候去查过拟合和正则化。带着问题去看比从头啃教材记得牢得多。还有一个小技巧是我自己一直在用的每开一个新项目先在 Notebook 最上面写三行注释——输入是什么、输出是什么、成功标准是什么。这个项目就是「输入一张人脸图 / 输出 15 个点的坐标 / 成功标准是点能画在五官上」。别小看这三行它能挡住很多「写了一堆代码但不知道自己要做啥」的时刻。我刚开始学的时候最大的教训是急着调模型结构。第一次跑这个项目损失不降我第一反应是「网络太浅了」于是加卷积层、加通道数折腾了一整晚最后发现问题出在坐标忘了归一化——数据这一层错了模型再深也没用。从那以后我每次跑新流程都强制按「先看一张图、再看一组标签、再跑一个批次、最后才看损失」的顺序走一遍任何一步的输出不符合预期就停下来。这个习惯帮我省下的时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取