4、深度学习检测器(上):MTCNN网络结构详解

聊到人脸检测,MTCNN 绝对是个绕不开的名字。我记得 2016 年刚接触这个算法时,就被它的精巧设计打动了——用三个小网络级联,就把检测和对齐一起搞定。说实话,直到今天,很多工业级项目里依然能看到它的身影。

这一章,咱们就把它拆开揉碎了讲。从 P-Net、R-Net 到 O-Net,每个网络负责什么、怎么训练的,我都会结合自己的踩坑经历跟你聊透。

4.1 为什么是三个网络?

你想想看,如果直接用一个超大网络去检测人脸,计算量会爆炸。尤其早期设备算力有限,根本跑不动。

MTCNN 的思路很聪明:先粗筛,再细挑,最后精修

  • P-Net(Proposal Network):快速扫描全图,找出可能有人脸的候选框。说白了就是「宁可错杀一千,不可放过一个」。
  • R-Net(Refine Network):对候选框做精细筛选,去掉大部分误检。
  • O-Net(Output Network):最终输出人脸框和关键点坐标。

这种级联架构的好处很明显:计算量逐级递减,精度逐级提升。我在项目中遇到过类似场景,如果只用单阶段检测器,小脸很容易漏掉。但 MTCNN 这种多阶段设计,对小目标的召回率确实高不少。

4.2 P-Net:全卷积的快速扫描器

P-Net 的结构非常轻量。它本质上是一个全卷积网络(FCN),输入是 12×12 的图像块。

为什么是 12×12?嗯,这是经过实验验证的——既能保留足够的人脸特征,又不会让计算量太大。

网络结构

输入: 12×12×3
  ↓
Conv: 3×3, 10 filters, stride=1, PReLU
  ↓
MaxPool: 2×2, stride=2
  ↓
Conv: 3×3, 16 filters, stride=1, PReLU
  ↓
Conv: 3×3, 32 filters, stride=1, PReLU
  ↓
Conv: 1×1, 2 filters (face classification)
Conv: 1×1, 4 filters (bounding box regression)
Conv: 1×1, 10 filters (facial landmark localization)

注意最后三层是并行的。它们分别输出:

  • 分类分支:2 个值,表示是人脸/非人脸的概率
  • 回归分支:4 个值,候选框的偏移量
  • 关键点分支:10 个值,5 个关键点的坐标

我个人习惯把 P-Net 看作一个「滑动窗口加速器」。它通过全卷积结构,一次前向就能处理整张图片的所有 12×12 窗口,效率极高。

避坑指南:我曾经在部署时发现 P-Net 输出的人脸框特别多,后来排查发现是图像金字塔的尺度没选好。建议金字塔缩放因子设为 0.709,最小人脸尺寸不低于 40×40 像素。

4.3 R-Net:精筛器的担当

P-Net 输出的候选框大概有几千个,但大部分是误检。R-Net 的任务就是把这些候选框进一步筛选

R-Net 的输入是 24×24 的图像块。相比 P-Net,它多了全连接层,所以判别能力更强。

网络结构

输入: 24×24×3
  ↓
Conv: 3×3, 28 filters, stride=1, PReLU
  ↓
MaxPool: 3×3, stride=2
  ↓
Conv: 3×3, 48 filters, stride=1, PReLU
  ↓
MaxPool: 3×3, stride=2
  ↓
Conv: 2×2, 64 filters, stride=1, PReLU
  ↓
FC: 128 units, PReLU
  ↓
FC: 2 (classification)
FC: 4 (bbox regression)
FC: 10 (landmark localization)

R-Net 的参数量大约是 P-Net 的 5 倍。它会把 P-Net 输出的候选框缩放到 24×24,然后判断「这到底是不是人脸」。

我在项目中遇到过一个问题:R-Net 的阈值设得太高,导致很多正脸被误删。后来我调低了阈值,配合 NMS(非极大值抑制)来平衡召回率和精度。具体来说,R-Net 的阈值建议设在 0.6~0.7 之间

4.4 O-Net:最后的把关人

O-Net 是三个网络中最重的。它的输入是 48×48,参数量是 R-Net 的 3 倍左右。

网络结构

输入: 48×48×3
  ↓
Conv: 3×3, 32 filters, stride=1, PReLU
  ↓
MaxPool: 3×3, stride=2
  ↓
Conv: 3×3, 64 filters, stride=1, PReLU
  ↓
MaxPool: 3×3, stride=2
  ↓
Conv: 3×3, 64 filters, stride=1, PReLU
  ↓
MaxPool: 2×2, stride=2
  ↓
Conv: 2×2, 128 filters, stride=1, PReLU
  ↓
FC: 256 units, PReLU
  ↓
FC: 2 (classification)
FC: 4 (bbox regression)
FC: 10 (landmark localization)

O-Net 不仅输出人脸框,还会输出5 个关键点:左眼、右眼、鼻子、左嘴角、右嘴角。这些关键点在做人脸对齐时非常有用。

你想想看,如果只有人脸框,你很难判断人脸是正脸还是侧脸。但有了关键点,你就能做仿射变换,把人脸摆正。这在后续的人脸识别中至关重要。

注意:O-Net 的训练数据必须包含关键点标注。如果只用人脸框数据训练,关键点分支会学不到东西。我见过有人直接拿 WIDER Face 训练 O-Net,结果关键点输出全是 0——因为 WIDER Face 没有关键点标注。

4.5 训练数据准备与标注格式

聊完网络结构,咱们看看训练数据怎么准备。MTCNN 的训练需要三类数据:

数据类型 标注内容 数据来源
人脸/非人脸 0 或 1 WIDER Face + 随机裁剪
边界框回归 4 个偏移量 (dx, dy, dw, dh) WIDER Face
关键点定位 10 个坐标值 (x1,y1,...,x5,y5) CelebA / 私有数据集

标注格式详解

MTCNN 的标注格式其实很简单。每个样本对应一个文本行:

# 格式:图片路径 类别 框坐标 关键点坐标
/path/to/image.jpg 1 100 120 80 90 45.2 48.1 50.3 52.0 48.5 50.1 45.8 48.9 46.2 49.5

其中:

  • 类别:1 表示人脸,0 表示非人脸
  • 框坐标:x1, y1, w, h(左上角坐标 + 宽高)
  • 关键点坐标:依次为左眼、右眼、鼻子、左嘴角、右嘴角的 x,y 坐标

这里有个细节:坐标都是归一化后的值。归一化到 [0,1] 区间,这样训练更稳定。

数据增强策略

我在项目中总结了一套数据增强流程,效果不错:

  1. 随机裁剪:从原图中随机裁剪出不同大小的块,与真实人脸框计算 IoU。IoU > 0.65 的算正样本,IoU < 0.3 的算负样本。
  2. 颜色抖动:调整亮度、对比度、饱和度。模拟不同光照条件。
  3. 水平翻转:注意关键点也要对应翻转。左眼变右眼,右眼变左眼。
  4. 旋转:±20° 以内的随机旋转。旋转后人脸框需要重新计算。
关键经验:正负样本比例控制在 1:3 左右效果最好。负样本太多,模型会偏向于把所有东西都判为非人脸;正样本太多,模型又容易过拟合。

4.6 训练策略:多任务学习

MTCNN 的训练是典型的多任务学习。三个分支共享底层特征,但各自有独立的损失函数。

总损失函数为:

L = α * L_cls + β * L_box + γ * L_landmark

其中:

  • L_cls:交叉熵损失,用于人脸分类
  • L_box:欧氏距离损失,用于边界框回归
  • L_landmark:欧氏距离损失,用于关键点定位

权重 α、β、γ 的取值很关键。原论文建议:

  • P-Net:α=1.0, β=0.5, γ=0.5
  • R-Net:α=1.0, β=0.5, γ=0.5
  • O-Net:α=1.0, β=0.5, γ=1.0

我个人习惯把 γ 调高一些,因为关键点定位的精度直接影响后续人脸对齐的效果。尤其是 O-Net,γ 设为 1.5 时,关键点误差能降低 10% 左右。

训练技巧:先单独训练 P-Net,冻结后再训练 R-Net,最后训练 O-Net。这样逐级训练,收敛速度更快。我试过端到端联合训练,效果反而不如逐级训练。

4.7 小结

这一章咱们把 MTCNN 的三个网络结构、训练数据和标注格式都聊透了。总结几个要点:

  • P-Net:全卷积,快速生成候选框。输入 12×12,输出人脸概率、框偏移和关键点。
  • R-Net:带全连接层,精筛候选框。输入 24×24,参数量适中。
  • O-Net:最重的网络,输出最终结果。输入 48×48,关键点精度最高。
  • 训练数据:需要三类标注,正负样本比例 1:3,数据增强必不可少。
  • 多任务学习:三个分支共享特征,损失函数加权求和。

下一章,咱们会深入 MTCNN 的推理流程,包括图像金字塔构建、NMS 策略和关键点对齐。到时候我会分享一个我在实际部署中遇到的性能优化案例,很有意思。