今日目标 :理解Inception模块的设计哲学,从零实现GoogLeNet,掌握多分支并行卷积的设计思想
预计阅读 :10分钟 | 动手操作:40分钟
一、Inception的核心思想:多尺度并行
python
"""
VGG的问题:每一层只有一个卷积核尺寸(3×3)
GoogLeNet的回答:为什么不同时用多个尺寸?
Inception模块 = 在同一层同时做 1×1, 3×3, 5×5 卷积 + 池化
然后把结果拼接起来!
朴素想法 → 计算量爆炸 → 用1×1卷积降维 → 高效Inception
"""
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
torch.manual_seed(42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
二、核心知识点
2.1 Inception模块演进
python
# ===== 朴素Inception(计算量大)=====
class NaiveInception(nn.Module):
"""
最原始的Inception:并行多个卷积
问题:计算量巨大!
假设输入192通道,输出256通道:
- 1×1分支: 192×64×1×1 = 12K
- 3×3分支: 192×128×3×3 = 221K
- 5×5分支: 192×32×5×5 = 154K
总计: ~387K 参数
解决方案:用1×1卷积先降维!
"""
def __init__(self, in_channels, out_1x1, out_3x3, out_5x5, out_pool):
super().__init__()
self.branch1 = nn.Conv2d(in_channels, out_1x1, kernel_size=1)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, out_3x3, kernel_size=3, padding=1),
)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, out_5x5, kernel_size=5, padding=2),
)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, out_pool, kernel_size=1),
)
def forward(self, x):
b1 = F.relu(self.branch1(x))
b2 = F.relu(self.branch2(x))
b3 = F.relu(self.branch3(x))
b4 = F.relu(self.branch4(x))
return torch.cat([b1, b2, b3, b4], dim=1)
# ===== 降维Inception(1×1 bottleneck)=====
class InceptionV1(nn.Module):
"""
Inception v1:用1×1卷积降维,大幅减少计算量
1×1降维的优势:
- 3×3分支: 先1×1降维到96 → 192×96×1×1 + 96×128×3×3 = 18K + 110K = 128K
- 对比朴素版: 221K → 128K,减少了42%
- 5×5分支: 先1×1降维到16 → 192×16×1×1 + 16×32×5×5 = 3K + 13K = 16K
- 对比朴素版: 154K → 16K,减少了90%!
这就是1×1卷积的魔力!
"""
def __init__(self, in_channels, ch1x1, ch3x3_reduce, ch3x3, ch5x5_reduce, ch5x5, pool_proj):
super().__init__()
# 分支1: 1×1卷积
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, kernel_size=1),
nn.ReLU(inplace=True),
)
# 分支2: 1×1降维 → 3×3卷积
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3_reduce, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(ch3x3_reduce, ch3x3, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
)
# 分支3: 1×1降维 → 5×5卷积
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5_reduce, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(ch5x5_reduce, ch5x5, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
)
# 分支4: 3×3最大池化 → 1×1卷积
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, kernel_size=1),
nn.ReLU(inplace=True),
)
def forward(self, x):
return torch.cat([
self.branch1(x),
self.branch2(x),
self.branch3(x),
self.branch4(x),
], dim=1)
2.2 Inception v2/v3 改进
python
class InceptionV2A(nn.Module):
"""
Inception v2 改进: 大卷积核分解
5×5 → 两个3×3堆叠
好处:
- 参数量: 5×5=25C² → 2×3×3=18C² (减少28%)
- 更多非线性: 2个ReLU vs 1个ReLU
- 和VGG一样的思路!
"""
def __init__(self, in_channels, pool_proj):
super().__init__()
mid_channels = 64 # 简化
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, 64, 1),
nn.ReLU(inplace=True),
)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, 48, 1),
nn.ReLU(inplace=True),
nn.Conv2d(48, 64, 3, padding=1),
nn.ReLU(inplace=True),
)
# 5×5 分解为两个 3×3
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, 64, 1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 96, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(96, 96, 3, padding=1),
nn.ReLU(inplace=True),
)
self.branch4 = nn.Sequential(
nn.AvgPool2d(3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, 1),
nn.ReLU(inplace=True),
)
def forward(self, x):
return torch.cat([
self.branch1(x),
self.branch2(x),
self.branch3(x),
self.branch4(x),
], dim=1)
class InceptionV2B(nn.Module):
"""
Inception v2 进一步分解: n×n → 1×n + n×1
3×3 → 1×3 + 3×1
好处:
- 参数量: 3×3=9C² → 1×3+3×1=6C² (减少33%)
- 等价于空间可分离卷积
注意:这种分解在中等大小特征图上效果好(12-20之间)
太小的特征图不适用
"""
def __init__(self, in_channels):
super().__init__()
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, 128, 1),
nn.ReLU(inplace=True),
)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, 128, 1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, (1, 3), padding=(0, 1)),
nn.ReLU(inplace=True),
nn.Conv2d(128, 192, (3, 1), padding=(1, 0)),
nn.ReLU(inplace=True),
)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, 128, 1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, (1, 3), padding=(0, 1)),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, (3, 1), padding=(1, 0)),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, (1, 3), padding=(0, 1)),
nn.ReLU(inplace=True),
nn.Conv2d(128, 192, (3, 1), padding=(1, 0)),
nn.ReLU(inplace=True),
)
self.branch4 = nn.Sequential(
nn.AvgPool2d(3, stride=1, padding=1),
nn.Conv2d(in_channels, 192, 1),
nn.ReLU(inplace=True),
)
def forward(self, x):
return torch.cat([
self.branch1(x),
self.branch2(x),
self.branch3(x),
self.branch4(x),
], dim=1)
2.3 完整GoogLeNet(Inception v1)
python
class GoogLeNet(nn.Module):
"""
完整GoogLeNet (Inception v1)
架构要点:
1. 22层(含池化),但只有~7M参数(AlexNet是60M!)
2. 两个辅助分类器(训练时帮助梯度流动)
3. 全局平均池化替代全连接层
4. 9个Inception模块堆叠
关键设计决策:
- 前面的卷积层降低分辨率,后面的Inception模块处理特征
- 辅助分类器在训练时提供额外的梯度信号
- 推理时只用主分类器
"""
def __init__(self, num_classes=1000):
super().__init__()
# 前置卷积层
self.pre_layers = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, stride=2, padding=1),
nn.LocalResponseNorm(5), # 原版用LRN,现在可以用BN
nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.LocalResponseNorm(5),
nn.MaxPool2d(3, stride=2, padding=1),
)
# Inception模块序列
# 格式: (in_channels, ch1x1, ch3x3_r, ch3x3, ch5x5_r, ch5x5, pool_proj)
self.inception3a = InceptionV1(192, 64, 96, 128, 16, 32, 32)
self.inception3b = InceptionV1(256, 128, 128, 192, 32, 96, 64)
self.maxpool3 = nn.MaxPool2d(3, stride=2, padding=1)
self.inception4a = InceptionV1(480, 192, 96, 208, 16, 48, 64)
self.inception4b = InceptionV1(512, 160, 112, 224, 24, 64, 64)
self.inception4c = InceptionV1(512, 128, 128, 256, 24, 64, 64)
self.inception4d = InceptionV1(512, 112, 144, 288, 32, 64, 64)
self.inception4e = InceptionV1(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = nn.MaxPool2d(3, stride=2, padding=1)
self.inception5a = InceptionV1(832, 256, 160, 320, 32, 128, 128)
self.inception5b = InceptionV1(832, 384, 192, 384, 48, 128, 128)
# 辅助分类器1(在inception4a之后)
self.aux1 = nn.Sequential(
nn.AvgPool2d(5, stride=3),
nn.Conv2d(512, 128, kernel_size=1),
nn.ReLU(inplace=True),
nn.Flatten(),
nn.Linear(128 * 4 * 4, 1024),
nn.ReLU(inplace=True),
nn.Dropout(0.7),
nn.Linear(1024, num_classes),
)
# 辅助分类器2(在inception4d之后)
self.aux2 = nn.Sequential(
nn.AvgPool2d(5, stride=3),
nn.Conv2d(528, 128, kernel_size=1),
nn.ReLU(inplace=True),
nn.Flatten(),
nn.Linear(128 * 4 * 4, 1024),
nn.ReLU(inplace=True),
nn.Dropout(0.7),
nn.Linear(1024, num_classes),
)
# 最后分类
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.dropout = nn.Dropout(0.4)
self.fc = nn.Linear(1024, num_classes)
def forward(self, x, return_aux=False):
x = self.pre_layers(x)
x = self.inception3a(x)
x = self.inception3b(x)
x = self.maxpool3(x)
x = self.inception4a(x)
aux1 = self.aux1(x) if return_aux else None
x = self.inception4b(x)
x = self.inception4c(x)
x = self.inception4d(x)
aux2 = self.aux2(x) if return_aux else None
x = self.inception4e(x)
x = self.maxpool4(x)
x = self.inception5a(x)
x = self.inception5b(x)
x = self.avgpool(x)
x = self.dropout(x)
x = torch.flatten(x, 1)
x = self.fc(x)
if return_aux:
return x, aux1, aux2
return x
# 对比
model = GoogLeNet(num_classes=1000)
print(f"GoogLeNet参数量: {sum(p.numel() for p in model.parameters()):,}")
print(f"对比AlexNet: 60M参数 → GoogLeNet: 7M参数 (减少88%)")
print(f"对比VGG16: 138M参数 → GoogLeNet: 7M参数 (减少95%)")
2.4 Inception设计智慧总结
python
"""
Inception系列的设计哲学:
Inception v1 (2014):
→ 多尺度并行卷积 + 1×1降维
→ 辅助分类器帮助梯度传播
→ 全局平均池化替代全连接
Inception v2 (2015):
→ 5×5 → 两个3×3堆叠
→ 加入BatchNorm
→ 这其实就是VGG的思路!
Inception v3 (2015):
→ n×n → 1×n + n×1 分解
→ 进一步减少参数
→ Label Smoothing正则化
Inception v4 + Inception-ResNet (2016):
→ 统一简化网络结构
→ 融入残差连接
→ ResNet + Inception = 最强组合
核心思想总结:
1. 多尺度:不同尺寸的卷积核并行
2. 降维:1×1卷积减少计算量
3. 分解:大卷积核分解为小卷积核
4. 融合:最后和残差连接结合
"""
三、动手实践
3.1 实战:Inception模块的多尺度特性
python
def demonstrate_multiscale():
"""
演示Inception模块的多尺度特征提取能力
不同分支对不同尺寸的物体有不同响应
"""
# 创建一个简单的测试图像
x = torch.randn(1, 64, 32, 32)
# 创建Inception模块
inception = InceptionV1(64, 32, 48, 64, 8, 16, 16)
# 查看各分支输出
with torch.no_grad():
b1 = inception.branch1(x)
b2 = inception.branch2(x)
b3 = inception.branch3(x)
b4 = inception.branch4(x)
out = inception(x)
# 关键:不同分支关注不同尺度的特征
print("Inception多尺度分析:")
print(f" 输入: {x.shape}")
print(f" 分支1 (1×1, 细节): {b1.shape}")
print(f" 分支2 (1×1→3×3, 中等尺度): {b2.shape}")
print(f" 分支3 (1×1→5×5, 大尺度): {b3.shape}")
print(f" 分支4 (pool→1×1, 全局上下文): {b4.shape}")
print(f" 拼接输出: {out.shape}")
print(f"\n 1×1卷积感受野: 1×1 → 检测像素级细节")
print(f" 3×3卷积感受野: 3×3 → 检测局部纹理")
print(f" 5×5卷积感受野: 5×5 → 检测中等结构")
print(f" 池化分支: 全局上下文 → 检测整体布局")
demonstrate_multiscale()
3.2 实战:1×1卷积降维效果验证
python
def verify_1x1_bottleneck():
"""验证1×1卷积降维的效果"""
in_channels = 192
# 朴素版:3×3卷积
naive_conv = nn.Conv2d(in_channels, 128, 3, padding=1)
naive_params = sum(p.numel() for p in naive_conv.parameters())
# 降维版:1×1 → 3×3
bottleneck = nn.Sequential(
nn.Conv2d(in_channels, 64, 1), # 降维到64
nn.Conv2d(64, 128, 3, padding=1),
)
bottleneck_params = sum(p.numel() for p in bottleneck.parameters())
print(f"1×1降维对比:")
print(f" 朴素3×3: {naive_params:,} 参数")
print(f" 1×1降维: {bottleneck_params:,} 参数")
print(f" 减少: {(1-bottleneck_params/naive_params)*100:.1f}%")
# 不同降维比例的效果
print(f"\n不同降维比例:")
for reduce_ratio in [0.125, 0.25, 0.375, 0.5, 0.75]:
reduced = int(in_channels * reduce_ratio)
b_params = in_channels * reduced + reduced * 128 * 9
n_params = in_channels * 128 * 9
print(f" 降维到{reduced:3d} ({reduce_ratio:.0%}): "
f"{b_params:>8,} 参数, 减少{(1-b_params/n_params)*100:5.1f}%")
verify_1x1_bottleneck()
3.3 实战:GoogLeNet vs ResNet 设计哲学对比
python
"""
GoogLeNet vs ResNet 设计哲学对比:
┌──────────────┬─────────────────────┬─────────────────────┐
│ 维度 │ GoogLeNet │ ResNet │
├──────────────┼─────────────────────┼─────────────────────┤
│ 核心思想 │ 宽度(多分支并行) │ 深度(残差连接) │
│ 模块结构 │ 4个分支并行拼接 │ 单分支+skip连接 │
│ 计算效率 │ 1×1降维 │ Bottleneck降维 │
│ 梯度流动 │ 辅助分类器 │ 残差连接 │
│ 最终分类 │ 全局平均池化 │ 全局平均池化 │
│ 参数量(2014) │ ~7M │ ~25M(ResNet-50) │
│ 影响 │ 启发多分支设计 │ 成为最流行backbone │
└──────────────┴─────────────────────┴─────────────────────┘
端侧AI视角:
- GoogLeNet: 参数少但分支多,GPU并行友好但移动端不友好
- ResNet: 结构简单,容易优化,更适合端侧部署
- 现代端侧网络(MobileNet/EfficientNet)吸取了Inception的宽度思想
和ResNet的深度思想,走得更远
"""
四、常见坑点
坑1:Inception输出通道数计算
python
# Inception模块的输出通道 = 所有分支输出通道之和
# 设计时必须提前算好,否则下一层输入通道对不上
# ✅ 正确
inception = InceptionV1(192, 64, 96, 128, 16, 32, 32)
# 输出通道 = 64 + 128 + 32 + 32 = 256
x = torch.randn(1, 192, 28, 28)
out = inception(x)
print(f"输出通道: {out.shape[1]}") # 256 ✓
坑2:辅助分类器的权重
python
# 训练时 loss = main_loss + 0.3 * aux1_loss + 0.3 * aux2_loss
# 辅助分类器权重一般为0.3,太小没效果,太大会干扰主分类器
# 推理时 auxiliary classifiers are discarded
# model.eval() 时不需要计算辅助分类器
坑3:Inception模块的池化分支
python
# 池化分支必须 stride=1, padding=1 保持尺寸不变
# 这样才能和其他分支拼接
# ✅ 正确
nn.MaxPool2d(3, stride=1, padding=1) # 尺寸不变
# ❌ 错误
nn.MaxPool2d(3, stride=2) # 尺寸减半,无法拼接
坑4:Inception v2/v3中的分解不适用于所有层
python
# n×n → 1×n + n×1 分解在中等特征图(12-20)上效果好
# 在很小的特征图(如7×7或更小)上不适用
# 在很大的特征图(如35×35)上效果也不明显
五、今日作业
- 手写Inception:实现InceptionV1模块,验证四个分支的拼接结果
- 降维对比:跑通1×1降维效果验证,对比不同降维比例的参数量
- 端侧思考:分析GoogLeNet为什么参数少但不适合端侧部署(提示:分支多=并行度高但分支调度开销大)
- 打卡 :评论区发你的分析,格式:"Day 24/100 打卡:Inception多尺度设计已掌握!"
今日小结
今天你学会了:
✅ Inception核心思想:多尺度并行卷积
✅ 朴素Inception → 1×1降维Inception → 参数减少90%的秘密
✅ 辅助分类器:帮助梯度传播
✅ 全局平均池化:替代全连接层
✅ Inception v2: 5×5 → 两个3×3
✅ Inception v3: n×n → 1×n + n×1
✅ Inception v4 + Inception-ResNet
✅ GoogLeNet vs ResNet 设计哲学对比
✅ 4个经典坑点
明日预告
Day 25:深度学习训练技巧全攻略
学习率调度、优化器选择、混合精度训练、梯度累积、分布式训练概述
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:GoogLeNet的Inception模块 | 一个模块同时用4种卷积 🎯
P1 --- 封面
标题:GoogLeNet与Inception
副标题:多尺度 / 1×1降维 / 拼接
关键词:Inception / GoogLeNet / 多尺度
P2 --- 多尺度并行
同一层同时做1×1/3×3/5×5卷积+池化
不同分支检测不同尺度的特征
拼接起来 = 丰富的特征表达
P3 --- 1×1降维魔法
朴素3×3: 192×128×9 = 221K参数
先1×1降到64: 192×64 + 64×128×9 = 86K
减少61%!5×5分支减少90%!
P4 --- Inception进化
v1: 多尺度+1×1降维 (2014)
v2: 5×5→两个3×3+BN (2015)
v3: 3×3→1×3+3×1 (2015)
v4: 融入ResNet残差 (2016)
P5 --- GoogLeNet vs ResNet
GoogLeNet: 宽度优先,7M参数
ResNet: 深度优先,残差连接
GoogLeNet参数少但不适合端侧
ResNet结构简单更易优化
P6 --- 今日作业
手写Inception + 1×1降维对比
评论区打卡 Day 24/100
标签:#GoogLeNet #Inception #多尺度 #深度学习
CSDN发布提示:CSDN版本建议在Inception模块放一张结构图(四个分支+拼接),在1×1降维部分放对比表,在进化史部分放时间线图。