人工智能-python-深度学习-神经网络-GoogLeNet

发布于:2025-09-09 ⋅ 阅读:(24) ⋅ 点赞:(0)

GoogLeNet(Inception v1)(详解)——从原理到 PyTorch 实现(含训练示例)

简介(历史与意义)
GoogLeNet(又称 Inception v1)由 Szegedy et al. 于 2014 年提出,是一次以“在有限计算资源下大幅提升深度与宽度、同时控制参数量”为目标的架构创新。它通过 Inception 模块(多尺度并行卷积 + 1×1 降维)实现了高效的特征抽取,并在 ILSVRC2014 上取得了显著成绩。([Computer Science][1])


文章目标

  • 讲清 GoogLeNet 的核心思想与关键设计点(为什么要这样设计)。
  • 逐层结构与逐层计算举例(输入 → 输出维度变化、卷积参数与运算量公式演示)。
  • 给出可直接运行的 PyTorch(torchvision) 使用/训练示例代码。
  • 给出可以做的扩展/对比实验(BN、池化、激活、数据增强等)。

一、核心思想(简洁概述)

  1. 多尺度并行处理:在每个模块里并行使用 1×1、3×3、5×5 卷积与池化,从而在同一层捕捉不同感受野的特征(细粒度到粗粒度)。([Computer Science][1])
  2. 稀疏连接的密集近似:受“现实中稀疏连接更高效”的启发,用多分支并联近似稀疏结构,但在实现上仍用稠密卷积。([Computer Science][1])
  3. 1×1 卷积用于“降维 + 非线性”:用来先对通道数进行压缩(降低计算量),同时引入额外的非线性映射。1×1 卷积的概念可追溯到 Network-in-Network(NIN)。([arXiv][2], [Computer Science][1])
  4. 辅助分类器(auxiliary classifiers):在中间某些位置接入额外的分类器作为辅助损失,有助于缓解较深网络的梯度传播与做正则化。([Computer Science][1])
  5. 参数/计算效率优先:在不牺牲准确率的前提下,通过降维、分支设计和全局平均池化等大幅减少参数量(比早期大网络参数少很多)。([Computer Science][1])

二、GoogLeNet 结构(逐层概览)

下面是对原文表格的简化与说明,表示“层次 / 输出大小 / 备注(卷积核/步长/输出通道)”。来源:Szegedy 等(2014)。([Computer Science][1])

层 / 模块 输出尺寸 (HxWxc) 说明(核 / 步长 / 通道)
conv1 112 × 112 × 64 7×7, s=2, 64
pool1 56 × 56 × 64 3×3 maxpool, s=2
conv2 56 × 56 × 192 3×3, pad=1, 192
pool2 28 × 28 × 192 3×3 maxpool, s=2
Inception(3a) 28 × 28 × 256 (多个并行分支,见下)
Inception(3b) 28 × 28 × 480 ...
pool3 14 × 14 × 480 s=2
(后续若干 Inception 模块) ...
最终 avg pool 1 × 1 × 1024 global average pooling
dropout → linear 1 × 1 × 1000 softmax 分类层(ImageNet:1000)

注:上表为简化版,原文对每个 Inception 模块中的通道划分有详细表格,请参见原论文和实现。([Computer Science][1])


三、Inception 模块(单元)与 1×1 卷积的由来及作用

原始 Inception 模块(并联四条路径)

  • Path A:1×1 卷积 → 输出 a 个通道
  • Path B:1×1 降维(r1),接 3×3 卷积(b 个输出)
  • Path C:1×1 降维(r2),接 5×5 卷积(c 个输出)
  • Path D:3×3 池化后接 1×1 投影(p 个输出)
    最终将四路的输出在 channel 维度上拼接(concatenate)。

为什么要 1×1 降维?

  • 直接对高通道输入做 3×3/5×5 的卷积,计算量和参数量会非常大;先用 1×1 将通道数降下来可以显著降低后续核的计算负担,而且 1×1 自身可增加非线性(有激活函数),提升表示能力。
  • 1×1 的思想和全局平均池化被早期的 Network-in-Network(NIN)提出并验证。([arXiv][2], [Computer Science][1])

四、逐层计算举例(详细演示)—— 以 Inception(3a) 为例

假设 Inception(3a) 的输入为 28 × 28 × 192(HxWxCin),模块输出 28 × 28 × 256(这个示例来自原论文表格)。具体通道分解(与原文一致的一个示例):

  • 1×1 路径:64 个 1×1 输出 → 输出通道 64
  • 3×3 路径:先 96 个 1×1 降维,再 128 个 3×3 → 输出通道 128
  • 5×5 路径:先 16 个 1×1 降维,再 32 个 5×5 → 输出通道 32
  • 池化投影:池化后 1×1 投影 32 → 输出通道 32
    合计输出通道 = 64 + 128 + 32 + 32 = 256(拼接而成)。

卷积输出尺寸公式(通用):

H o u t = ⌊ H i n + 2 ⋅ p a d − k e r n e l s t r i d e ⌋ + 1 H_{out} = \left\lfloor\frac{H_{in} + 2\cdot pad - kernel}{stride}\right\rfloor + 1 Hout=strideHin+2padkernel+1

若使用“same” 卷积(padding = (kernel-1)/2,stride=1),空间尺寸和输入相同。

参数量、算力估算(只计卷积核参数,不计偏置)

对于一个 k×k 卷积,输入通道 Cin,输出通道 Cout:

  • 参数数量(不含偏置) = Cin * Cout * k * k
  • 每次前向的乘加次数(multiply-adds)≈ H_out * W_out * Cout * (k*k*Cin)

我们对 Inception(3a) 各分支逐项计算(数值来源于上面给出的通道分配):

  • 输入:H=W=28,Cin=192

  • 1×1 路径参数 = 192 × 64 = 12,288

  • 3×3 路径(降维 + 卷积):

    • 1×1 降维参数 = 192 × 96 = 18,432
    • 3×3 卷积参数 = 96 × 128 × 3 × 3 = 110,592
  • 5×5 路径(降维 + 卷积):

    • 1×1 降维参数 = 192 × 16 = 3,072
    • 5×5 卷积参数 = 16 × 32 × 5 × 5 = 12,800
  • 池化投影(1×1)参数 = 192 × 32 = 6,144

参数总和(不含偏置) = 12,288 + 18,432 + 110,592 + 3,072 + 12,800 + 6,144 = 163,328(约 0.16M)。
对应的乘加量(大约) = ~128,049,152 次 multiply-add(即约 1.28e8 次)。(上面所有乘法 / 累加均按每个输出元素上 k×k×Cin 的乘加计算得到,步骤见上面公式。)

(注:不同实现对偏置、BN、激活的计数略有差别,本文计算以说明规模与比例为主,来源与示例参数来自原论文表格与常见实现,逐步算式在文中已展示。)


五、关键设计点解析(更深入)

  • 多尺度思想:同一位置并行使用不同大小的感受野(1×1 / 3×3 / 5×5 / pooling)使网络能同时关注不同尺度的特征。([Computer Science][1])
  • 稀疏连接近似:理论上稀疏结构更高效但难以直接实现;Inception 用多路径并行的稠密实现去近似稀疏结构。([Computer Science][1])
  • 辅助分类器:在中间插入小型分类器,计算中间交叉熵并按一定权重加入总损失,帮助训练早期层。([Computer Science][1])
  • 用 1×1 做降维 + 非线性:降低后续大核的计算量,同时增加层间非线性,来源与 NIN 工作相关。([arXiv][2])
  • 训练技巧:原文使用 ReLU、dropout、local response normalization(LRN)等;BN 是随后被引入到 Inception 家族(Inception-v2/v3)来稳定和加速训练的改进。([arXiv][3], [Computer Science][1])

六、PyTorch 实现(两种方式:1. 直接用 torchvision;2. 自定义 Inception 模块示例)

下面给出可直接复制粘贴到 CSDN 的代码片段(可作为文章中的“可运行示例”)。

方式 A:用 torchvision 直接载入 GoogLeNet(推荐用于快速实验)

# pip install torch torchvision
import torch
import torch.nn as nn
from torchvision import models, transforms, datasets
from torch.utils.data import DataLoader
import torch.optim as optim

# 1. 创建模型(可选加载预训练权重)
num_classes = 100  # 示例:换成你数据集的类别数
model = models.googlenet(weights=None, aux_logits=True)  # 或 weights=models.GoogLeNet_Weights.IMAGENET1K_V1
# 如果你要微调最后一层:
model.fc = nn.Linear(in_features=1024, out_features=num_classes)
# 注意 torchvision 的 GoogLeNet 在 aux_logits=True 时会返回两个额外输出(训练时使用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

# 2. 数据(这里只示例文件夹结构 Data/train, Data/val)
train_tfms = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])
val_tfms = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])

train_ds = datasets.ImageFolder("Data/train", transform=train_tfms)
val_ds = datasets.ImageFolder("Data/val", transform=val_tfms)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)

# 3. 损失与优化器(原论文用 SGD + momentum)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)

# 4. 训练 / 验证循环(支持 aux_logits)
def train_one_epoch(model, loader, optimizer, device):
    model.train()
    total_loss = 0
    total_correct = 0
    total_samples = 0
    for imgs, labels in loader:
        imgs = imgs.to(device); labels = labels.to(device)
        optimizer.zero_grad()
        outputs = model(imgs)  # 如果 aux_logits=True,返回 (main_logits, aux1, aux2)
        if isinstance(outputs, tuple):
            main_out, aux1, aux2 = outputs
            loss = criterion(main_out, labels) + 0.3*criterion(aux1, labels) + 0.3*criterion(aux2, labels)
            logits = main_out
        else:
            logits = outputs
            loss = criterion(logits, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * imgs.size(0)
        preds = logits.argmax(dim=1)
        total_correct += (preds == labels).sum().item()
        total_samples += imgs.size(0)
    return total_loss / total_samples, total_correct / total_samples

def evaluate(model, loader, device):
    model.eval()
    total_loss = 0
    total_correct = 0
    total_samples = 0
    with torch.no_grad():
        for imgs, labels in loader:
            imgs = imgs.to(device); labels = labels.to(device)
            outputs = model(imgs)
            # evaluate only main output
            if isinstance(outputs, tuple):
                logits = outputs[0]
            else:
                logits = outputs
            loss = criterion(logits, labels)
            total_loss += loss.item() * imgs.size(0)
            preds = logits.argmax(dim=1)
            total_correct += (preds == labels).sum().item()
            total_samples += imgs.size(0)
    return total_loss / total_samples, total_correct / total_samples

# 5. 主循环(示例)
epochs = 50
best_val_acc = 0.0
for epoch in range(epochs):
    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, device)
    val_loss, val_acc = evaluate(model, val_loader, device)
    scheduler.step()
    print(f"[{epoch+1}/{epochs}] train_loss={train_loss:.4f} train_acc={train_acc:.4f} val_loss={val_loss:.4f} val_acc={val_acc:.4f}")
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        torch.save(model.state_dict(), "best_googlenet.pth")

注:torchvision.models.googlenet 的权重与文档参见官方 docs(torchvision)。([PyTorch Docs][4])


方式 B(教学用)—— 自定义简化 Inception 模块(示例)
下面给出一个“教学版”的 Inception 模块实现,便于理解结构(并不是完整 GoogLeNet 的全套配置,但可用来搭建自己的网络):

import torch
import torch.nn as nn
import torch.nn.functional as F

class InceptionBlock(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3_reduce, ch3x3, ch5x5_reduce, ch5x5, pool_proj):
        super().__init__()
        # 1x1 path
        self.branch1 = nn.Sequential(
            nn.Conv2d(in_channels, ch1x1, kernel_size=1),
            nn.ReLU(inplace=True)
        )
        # 3x3 path
        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)
        )
        # 5x5 path
        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)
        )
        # pool proj
        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):
        b1 = self.branch1(x)
        b2 = self.branch2(x)
        b3 = self.branch3(x)
        b4 = self.branch4(x)
        return torch.cat([b1, b2, b3, b4], dim=1)

# 测试 block 输出通道数
blk = InceptionBlock(in_channels=192, ch1x1=64, ch3x3_reduce=96, ch3x3=128, ch5x5_reduce=16, ch5x5=32, pool_proj=32)
print(blk)

这个 Block 与上面 Inception(3a) 的参数是一一对应的;把多个这样的模块堆叠并加上初始的 conv/pool 与最终的 avgpool + fc,就能构建完整的 GoogLeNet(实现细节可参考 torchvision 的源代码)。([PyTorch Docs][4])


七、训练与评估(超参建议 + 画训练曲线)

原论文训练要点(摘录):原文采用 ReLU、dropout、辅助分类器、多 GPU 并行训练等技巧,并在 ImageNet 上给出了最终结果(ILSVRC2014 成绩、top-5 等)。([Computer Science][1])

推荐的现代超参(可作起点)

  • 优化器:SGD(momentum=0.9, weight_decay=1e-4)
  • 初始学习率:0.01(batch 128–256 下可尝试 0.01–0.1,注意调整)
  • batch_size:64/128(受显存限制)
  • epoch:50–120(或按 lr decay 策略)
  • lr scheduler:StepLR(step=30, gamma=0.1)或 CosineAnnealing
  • 数据增强:RandomCrop, RandomResizedCrop, RandomHorizontalFlip, ColorJitter
  • 正则:dropout(最后 FC 前),auxiliary loss (原作给 aux loss 比例约 0.3)。([Computer Science][1])

如何画训练曲线:记录每 epoch 的 train_loss, val_loss, train_acc, val_acc,用 matplotlib 画出两张曲线图(loss 随 epoch 变化,accuracy 随 epoch 变化)。示例代码可在训练循环中保存这些值并在训练后绘图。


八、实验扩展(对比实验的建议)

以下都是常见且有意义的 ablation/扩展实验(每项都可写成独立小章节):

  1. 加入 BatchNorm(Inception-v2 的方向) vs 不加:BatchNorm 通常加速收敛并提高稳定性。参考 Ioffe & Szegedy (2015)。([arXiv][3])
  2. 激活函数比较:ReLU / LeakyReLU / ELU / tanh:原文使用 ReLU;可通过对比训练速度和最终准确率评估差异。([Computer Science][1])
  3. 池化策略:MaxPool 与 AvgPool 的局部选择:Inception 最终使用了 global average pooling 替代大规模全连接层以减少参数。([Computer Science][1])
  4. 用 3×3 两次替换 5×5(或更细粒度的核分解):这是 Inception 后续版本(v3 等)中的做法,能进一步降低参数并加深网络深度。([Computer Science][1])
  5. 数据增强 / 正则化对比:Random Erasing、Mixup、CutMix 等现代增强技术通常能带来进一步提升。

九、实践小贴士与常见问题

  • 显存与并行:GoogLeNet 本身比一些早期网络参数更少,但 Inception 模块内有许多并行分支,对实现和显存调度要注意(可用 aux_logits=False 减少开销)。([Computer Science][1])
  • 预训练权重:若数据集较小,优先使用 ImageNet 预训练权重微调(torchvision.models.googlenet(weights=...))。([PyTorch Docs][4])
  • 辅助分类器训练策略:训练初期加 auxiliary loss(例如权重 0.3),收敛后可只使用主分支评估。([Computer Science][1])

十、总结

GoogLeNet(Inception v1)在 2014 年通过 Inception 模块实现在有限计算资源下“加深且加宽”模型而不暴涨参数量,带来了非常实用的设计思想:并行多尺度 + 1×1 降维 + 辅助分类器。这些思想不仅影响了后续的 Inception 系列(v2/v3/v4),也启发了后续网络在“高效性 vs 表示能力”上的权衡与设计。([Computer Science][1], [arXiv][2])



网站公告

今日签到

点亮在社区的每一天
去签到