🧠 MTCNN网络结构记忆卡片
�� 基础概念速查
🔤 库引入:import torch
和 import torch.nn as nn
python
import torch # PyTorch深度学习框架
import torch.nn as nn # nn = Neural Networks (神经网络)
🏗️ 网络基类:class PNet(nn.Module)
继承关系
python
class PNet(nn.Module): # 继承nn.Module基类
def __init__(self):
super(PNet, self).__init__() # 调用父类初始化
🔍 全卷积网络 vs 混合网络概念澄清 :P-Net全卷积
+ R-Net/O-Net混合网络
P-Net: 全卷积网络 (无全连接层)
├── 可以处理任意尺寸输入
├── 输出尺寸与输入相关
└── 真正的全卷积网络
R-Net: 混合网络 (卷积+全连接)
├── 固定输入尺寸 (24×24)
├── 有全连接层
└── 不是全卷积网络
O-Net: 混合网络 (卷积+全连接)
├── 固定输入尺寸 (48×48)
├── 有全连接层
└── 不是全卷积网络
🔴 重要概念澄清:
- 全卷积网络: 不包含全连接层的网络,可以处理任意尺寸输入
- 混合网络: 包含卷积层和全连接层的网络,通常需要固定输入尺寸
- 老师说的"全卷积": 可能指1×1卷积或特定语境下的理解
🎯 P-Net 网络结构:12×12输入
→ 1×1×32特征
→ 分支输出1通道+4通道
📊 完整网络流程图:3→10→16→32通道变化
输入图像 (12×12×3)
↓
┌─────────────────────────────────────┐
│ pre_layer 流水线 │
├─────────────────────────────────────┤
│ 卷积层1: 3→10通道 (10×10×10) │
│ 批归一化: 10通道 │
│ PReLU激活: 10通道 │
│ 最大池化: 2×2 → (5×5×10) │
├─────────────────────────────────────┤
│ 卷积层2: 10→16通道 (3×3×16) │
│ 批归一化: 16通道 │
│ PReLU激活: 16通道 │
├─────────────────────────────────────┤
│ 卷积层3: 16→32通道 (1×1×32) │
│ 批归一化: 32通道 │
│ PReLU激活: 32通道 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 分支输出层 │
├─────────────────────────────────────┤
│ 分类分支: 32→1通道 (1×1×1) │
│ 偏移分支: 32→4通道 (1×1×4) │
└─────────────────────────────────────┘
↓
输出: [置信度, 偏移量]
🔍 关键参数解释:nn.Conv2d(3, 10, kernel_size=3, stride=1, padding=0)
python
nn.Conv2d(3, 10, kernel_size=3, stride=1, padding=0)
# ↑ ↑ ↑ ↑ ↑
# 输入 输出 卷积核大小 步长 填充
# 通道 通道 3×3像素 1像素 0像素
# 数量 数量
# (决定 (决定
# 卷积核 卷积核
# 的深度) 的数量)
🚨 重要公式说明:输出特征图尺寸 = [(H+2P-F)/S]+1 × [(H+2P-F)/S]+1 × 通道数
输出特征图尺寸 = `[(H + 2P - F) / S] + 1` × `[(H + 2P - F) / S] + 1` × 通道数
其中:
H = 输入特征图的高度/宽度
P = 填充大小 (padding)
F = 卷积核大小 (kernel_size)
S = 步长 (stride)
例如:12×12输入,3×3卷积核,步长1,无填充
输出尺寸 = [(12 + 2×0 - 3) / 1] + 1 = 10×10
🔴 关键理解:
- 输入通道数 = 输入特征图的通道数
- 输出通道数 = 卷积核的数量 = 提取的特征数量
🚀 R-Net 网络结构:24×24输入
→ 3×3×64特征
→ 全连接576→128
→ 分支输出1通道+4通道
📊 与P-Net的区别:新增全连接层
+ 无BatchNorm
+ 3×3池化核
输入图像 (24×24×3)
↓
┌─────────────────────────────────────┐
│ pre_layer 流水线 │
├─────────────────────────────────────┤
│ 卷积层1: 3→28通道 (22×22×28) │
│ PReLU激活: 28通道 │
│ 最大池化: 3×3, 步长2 → (11×11×28) │
├─────────────────────────────────────┤
│ 卷积层2: 28→48通道 (9×9×48) │
│ PReLU激活: 48通道 │
│ 最大池化: 3×3, 步长2 → (4×4×48) │
├─────────────────────────────────────┤
│ 卷积层3: 48→64通道 (3×3×64) │
│ PReLU激活: 64通道 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 全连接层 (新增) │
├─────────────────────────────────────┤
│ 展平: (3×3×64) → 576 │
│ 全连接: 576 → 128 │
│ PReLU激活: 128 │
├─────────────────────────────────────┤
│ 分类分支: 128 → 1 │
│ 偏移分支: 128 → 4 │
└─────────────────────────────────────┘
↓
输出: [置信度, 偏移量]
🔍 R-Net与P-Net的主要差异:输入尺寸
+ 通道数变化
+ 新增全连接层
- 输入尺寸: 24×24 (vs P-Net的12×12)
- 通道数变化: 3→28→48→64 (vs P-Net的3→10→16→32)
- 新增全连接层: 576→128维特征
- 无BatchNorm: 注释掉了批归一化层
- 输出层: 从卷积层改为全连接层
🔍 576→128的详细过程 :3×3×64展平
→ 全连接降维
python
# 最后一层卷积输出: 3×3×64 = 576维
nn.Conv2d(48, 64, kernel_size=2, stride=1) # 4×4×48 → 3×3×64
# 展平操作
x = x.view(x.size(0), -1) # (batch_size, 3, 3, 64) → (batch_size, 576)
# 全连接层降维
self.conv4 = nn.Linear(64 * 3 * 3, 128) # 576 → 128
🔴 关键差异:两次下采样 :22×22→11×11→4×4
感受野增大
P-Net: 1次下采样 (10×10 → 5×5)
R-Net: 2次下采样 (22×22 → 11×11 → 4×4)
影响:
├── 感受野更大:能看到更多上下文信息
├── 精度更高:但速度更慢
└── 特征更丰富:适合中等精度检测
🔴 关键原理:下采样越狠,感受野越大
🌟 O-Net 网络结构:48×48输入
→ 3×3×128特征
→ 全连接1152→256
→ 分支输出1通道+4通道
📊 与P-Net的区别:最多下采样
+ 最大全连接层
+ 最高精度
输入图像 (48×48×3)
↓
┌─────────────────────────────────────┐
│ pre_layer 流水线 │
├─────────────────────────────────────┤
│ 卷积层1: 3→32通道 (46×46×32) │
│ PReLU激活: 32通道 │
│ 最大池化: 3×3, 步长2 → (23×23×32) │
├─────────────────────────────────────┤
│ 卷积层2: 32→64通道 (21×21×64) │
│ PReLU激活: 64通道 │
│ 最大池化: 3×3, 步长2 → (10×10×64) │
├─────────────────────────────────────┤
│ 卷积层3: 64→64通道 (8×8×64) │
│ PReLU激活: 64通道 │
│ 最大池化: 2×2, 步长2 → (4×4×64) │
├─────────────────────────────────────┤
│ 卷积层4: 64→128通道 (3×3×128) │
│ PReLU激活: 128通道 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 全连接层 (新增) │
├─────────────────────────────────────┤
│ 展平: (3×3×128) → 1152 │
│ 全连接: 1152 → 256 │
│ PReLU激活: 256 │
├─────────────────────────────────────┤
│ 分类分支: 256 → 1 │
│ 偏移分支: 256 → 4 │
└─────────────────────────────────────┘
↓
输出: [置信度, 偏移量]
🔍 O-Net与P-Net的主要差异:最大输入尺寸
+ 最多卷积层
+ 最大全连接层
- 输入尺寸: 48×48 (vs P-Net的12×12)
- 通道数变化: 3→32→64→64→128 (vs P-Net的3→10→16→32)
- 新增全连接层: 1152→256维特征
- 无BatchNorm: 注释掉了批归一化层
- 输出层: 从卷积层改为全连接层
🔍 1152→256的详细过程 :3×3×128展平
→ 全连接降维
python
# 最后一层卷积输出: 3×3×128 = 1152维
nn.Conv2d(64, 128, kernel_size=2, stride=1) # 4×4×64 → 3×3×128
# 展平操作
x = x.view(x.size(0), -1) # (batch_size, 3, 3, 128) → (batch_size, 1152)
# 全连接层降维
self.conv5 = nn.Linear(128 * 3 * 3, 256) # 1152 → 256
为什么这样设计?
- 维度压缩: 1152维过于冗余,256维是精华特征
- 计算效率: 256维特征计算速度快,内存占用合理
- 特征融合: 全连接层组合不同位置的信息
🔴 关键差异:三次下采样 :最多下采样次数
→ 最高精度定位
P-Net: 1次下采样
R-Net: 2次下采样
O-Net: 3次下采样
规律:下采样次数越多,精度越高,速度越慢
🔴 关键原理:下采样越狠,感受野越大,精度越高
🔍 池化核大小变化规律 :输入尺寸越大
→ 池化核越大
→ 下采样越激进
P-Net: 2×2池化核 (温和下采样)
R-Net: 3×3池化核 (中等下采样)
O-Net: 3×3池化核 + 2×2池化核 (激进下采样)
为什么后面用3×3池化核?
├── 输入尺寸越大,越需要激进下采样
├── 3×3池化核感受野更大,下采样更快
└── 符合网络设计规律:输入越大,下采样越激进
🔴 核心原理:下采样越狠,感受野越大,精度越高,速度越慢
🔍 池化核代码对比 :不同网络
→ 不同池化策略
python
# P-Net: 2×2池化核
MaxPool2d(kernel_size=2, stride=2) # 10×10 → 5×5
# R-Net: 3×3池化核
MaxPool2d(kernel_size=3, stride=2, padding=1) # 22×22 → 11×11
MaxPool2d(kernel_size=3, stride=2) # 9×9 → 4×4
# O-Net: 3×3池化核 + 2×2池化核
MaxPool2d(kernel_size=3, stride=2, padding=1) # 46×46 → 23×23
MaxPool2d(kernel_size=3, stride=2) # 21×21 → 10×10
MaxPool2d(kernel_size=2, stride=2) # 8×8 → 4×4
🔧 关键概念解释
1. nn.Sequential 流水线 :按顺序依次通过
→ 工厂流水线模式
python
self.pre_layer = nn.Sequential(
层1, 层2, 层3, ... # 数据按顺序依次通过
)
# 就像工厂流水线,产品依次经过每个工序
2. Conv2d 卷积层 :提取图像特征
→ 不同滤镜看图片
python
nn.Conv2d(输入通道, 输出通道, 卷积核大小, 步长, 填充)
# 作用:提取图像特征,就像用不同滤镜看图片
# 输入通道:RGB图像是3,灰度图像是1
# 输出通道:提取的特征数量,越多特征越丰富
3. BatchNorm2d 批量归一化 :稳定训练过程
→ 加速收敛
python
nn.BatchNorm2d(通道数)
# 作用:稳定训练过程,加速收敛
# 2D:专门用于2D图像数据
# 为什么手动填写:PyTorch不知道您的数据形状
4. 通道数变化规律 :通道数逐渐增加
→ 提取更多特征
P-Net: 3 → 10 → 16 → 32 → 1/4
R-Net: 3 → 28 → 48 → 64 → 128 → 1/4
O-Net: 3 → 32 → 64 → 64 → 128 → 256 → 1/4
🔴 规律:通道数逐渐增加,提取更多特征
🎯 分支输出层详解
🔍 为什么32变成1和4?:任务需求决定通道数
+ 人为设计
分类分支 (32→1):二分类任务
→ 1通道足够
python
self.conv4_1 = nn.Conv2d(32, 1, kernel_size=1, stride=1)
# 输入:32通道特征图
# 输出:1通道 (二分类:是人脸的概率)
# 作用:预测每个位置包含人脸的概率
# 注意:1通道对应sigmoid激活,非人脸概率 = 1 - 人脸概率
偏移分支 (32→4):4个坐标偏移
→ 4通道对应
python
self.conv4_2 = nn.Conv2d(32, 4, kernel_size=1, stride=1)
# 输入:32通道特征图
# 输出:4通道 [x1偏移, y1偏移, x2偏移, y2偏移]
# 作用:预测人脸框相对于当前位置的偏移量
🎭 解耦图 (Decoupled Output) 设计:共享特征提取
→ 分支任务分离
┌─────────────────────────────────────────────────────────────┐
│ 解耦图设计原理 │
├─────────────────────────────────────────────────────────────┤
│ 共享特征提取层 (pre_layer) │
│ ↓ │
│ 32通道特征图 (高维特征表示) │
│ ↓ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 分类分支 │ │ 偏移分支 │ │
│ │ 32→1通道 │ │ 32→4通道 │ │
│ │ Sigmoid │ │ 无激活函数 │ │
│ └─────────────┘ └─────────────┘ │
│ ↓ ↓ │
│ 置信度输出 位置偏移输出 │
└─────────────────────────────────────────────────────────────┘
🔍 解耦图的优势:任务分离
+ 特征共享
+ 训练稳定
- 任务分离: 分类和回归任务独立训练
- 特征共享: 32通道特征被两个分支共享使用
- 训练稳定: 不同任务不会相互干扰
- 推理高效: 可以单独使用某个分支的输出
💡 置信度 (Confidence) 解释:32通道特征
→ 1通道概率
→ [0,1]范围
┌─────────────────────────────────────────────────────────────┐
│ 置信度机制 │
├─────────────────────────────────────────────────────────────┤
│ 32通道特征图 → 1×1卷积 → 1通道 → Sigmoid → [0,1]概率 │
│ │
│ 输出含义: │
│ - 0.0: 绝对不是人脸 │
│ - 0.5: 50%可能是人脸 │
│ - 1.0: 100%是人脸 │
│ │
│ 阈值判断: │
│ - 置信度 > 0.5: 认为是人脸 │
│ - 置信度 < 0.5: 认为不是人脸 │
└─────────────────────────────────────────────────────────────┘
🔍 偏移分支 (无激活函数):4通道输出
→ 任意实数范围
→ 正负偏移量
┌─────────────────────────────────────────────────────────────┐
│ 偏移分支机制 │
├─────────────────────────────────────────────────────────────┤
│ 32通道特征图 → 1×1卷积 → 4通道 → 无激活函数 → 任意实数 │
│ │
│ 输出含义: │
│ - 4个通道分别对应 [x1偏移, y1偏移, x2偏移, y2偏移] │
│ - 输出范围: 任意实数 (正数或负数) │
│ - 作用: 预测人脸框相对于当前位置的精确偏移量 │
│ │
│ 为什么不用激活函数? │
│ - 偏移量可以是正数(向右/向下)或负数(向左/向上) │
│ - 激活函数会限制输出范围,不适合回归任务 │
└─────────────────────────────────────────────────────────────┘
🎯 为什么32通道表示置信度?:32通道≠置信度
→ 32通道特征→1通道置信度
32通道 ≠ 置信度!
┌─────────────────────────────────────────────────────────────┐
│ 正确理解 │
├─────────────────────────────────────────────────────────────┤
│ 32通道: 提取的高级特征 (形状、纹理、结构等) │
│ ↓ │
│ 1×1卷积: 将32维特征压缩到1维 │
│ ↓ │
│ 1通道: 经过sigmoid激活后,输出[0,1]的置信度 │
│ │
│ 关系: 32通道特征 → 1通道置信度 │
│ 不是: 32通道 = 置信度 │
└─────────────────────────────────────────────────────────────┘
📊 1×1卷积核的作用:点卷积
→ 降维
+ 非线性
+ 特征融合
1×1卷积核 = 点卷积
作用:
1. 降维:减少通道数
2. 非线性:增加激活函数
3. 特征融合:组合不同通道的信息
🎯 记忆口诀
📝 P-Net (12×12):3→10→16→32
→ 分支1、4通道
"3→10→16→32,层层提取特征强
最后分支1、4,置信度偏移双任务"
📝 R-Net (24×24):3→28→48→64
→ 全连接576→128
→ 分支1、4通道
"3→28→48→64,特征越来越丰富
全连接层来帮忙,128维特征强
输出1、4,置信度偏移双任务"
📝 O-Net (48×48):3→32→64→64→128
→ 全连接1152→256
→ 分支1、4通道
"3→32→64→64→128,特征提取最全面
256维全连接,最终输出最精确
1、4通道,人脸定位最准确"
🚀 为什么这样设计?
1. 三阶段级联 :P-Net快速检测
→ R-Net中等精度
→ O-Net高精度定位
- P-Net: 快速粗检测,生成候选区域
- R-Net: 中等精度,过滤候选区域
- O-Net: 高精度,最终人脸定位
2. 通道数递增 :浅层基础特征
→ 深层高级特征
- 浅层:提取基础特征(边缘、纹理)
- 深层:提取高级特征(形状、结构)
3. 多尺度输入 :12×12快速
→ 24×24中等
→ 48×48高精度
- 12×12:快速检测
- 24×24:中等精度
- 48×48:高精度定位
4. 双任务输出设计 :分类分支1通道
+ 偏移分支4通道
- 分类分支: 判断是否是人脸 (1通道,sigmoid置信度)
- 偏移分支: 预测人脸框位置 (4通道,x1,y1,x2,y2)
5. 解耦图设计优势 :任务分离
+ 特征共享
+ 训练稳定
- 任务分离: 分类和回归任务独立训练,互不干扰
- 特征共享: 32通道特征被两个分支共享使用,提高效率
- 训练稳定: 不同任务不会相互影响,收敛更快
- 推理灵活: 可以单独使用置信度或偏移量输出
🔴 核心总结
下采样次数对比 :次数越多
→ 感受野越大
→ 精度越高
→ 速度越慢
P-Net: 1次下采样 → 小感受野 → 快速检测
R-Net: 2次下采样 → 中等感受野 → 中等精度
O-Net: 3次下采样 → 大感受野 → 高精度定位
🔴 核心原理:下采样越狠,感受野越大,精度越高,速度越慢
设计原则 :通道数=任务需求
+ 激活函数=输出特性
+ 下采样次数=精度vs速度权衡
通道数 = 任务需求
激活函数 = 输出特性
下采样次数 = 精度vs速度权衡
池化核大小 = 输入尺寸 (越大输入,越大池化核)
全连接层 = 特征压缩 (高维→低维,提取精华特征)
关键记忆点 :分类1通道+sigmoid
+ 偏移4通道+无激活
+ 下采样感受野关系
1. 分类:1通道 + sigmoid
2. 偏移:4通道 + 无激活
3. 下采样:次数越多,感受野越大,精度越高,速度越慢
4. 通道数:人为设计,对应任务需求
5. 池化核:输入越大,池化核越大,下采样越激进
6. 全连接:高维压缩,提取精华特征
7. 感受野:下采样越狠,感受野越大,精度越高
这就是MTCNN网络的完整结构!感谢老师的正确教导!🎯