Day 54 Inception 网络

@浙大疏锦行

今日任务:

  1. 传统计算机视觉发展史
  2. inception 模块和网络
  3. 特征融合方法阶段性总结:逐元素相加或相册、concat通道数相加等
  4. 感受野和卷积核变体:深入理解不同模块和类的设计初衷

作业:对 inception 网络在cifar10 上观察精度;引入残差机制和cbam模块分别进行消融

Inception 网络架构

Inception 网络(GoogleNet),是Google 团队在2014年提出的经典卷积神经网络架构。

它的核心设计理念是 “并行的多尺度融合”,通过在同一层网络中使用多个不同大小的卷积核(如 1x1、3x3、5x5)以及池化操作,从不同尺度提取图像特征,然后将这些特征进行融合,从而在不增加过多计算量的情况下,获得更丰富的特征表达。

Inception 模块

Inception 模块是 Inception 网络的基本组成单元

一个典型的 Inception 模块包括4个并行分支:

  • 1x1卷积分支:实现快速降维,减少计算量;并提取局部特征
  • 3x3卷积分支:先1x1降维,后3x3提取中等尺度的特征
  • 5x5卷积分支:先1x1降维,后5x5 提取大尺度的全局特征
  • 池化分支:增强特征的平移不变性

在定义 Inception 模块时,整体结构与上面四个部分相同。注意,在前向传播时,并行计算完成不同尺度的特征提取后,要使用 concat 在通道维度拼接不同分支的特征输出

实际上,inception模块中不同的卷积核和步长最后输出同样尺寸的特征图,这是经过精心设计的,才能在空间上对齐,才能在维度上正确拼接(concat)。

import torch
import torch.nn as nn

class Inception(nn.Module):
    def __init__(self, in_channels):
        """
        Inception模块初始化,实现多尺度特征并行提取与融合
        参数:
            in_channels: 输入特征图的通道数
        """
        super(Inception, self).__init__()
        
        # 1x1卷积分支:快速降维并提取通道间特征关系
        # 减少后续卷积的计算量,同时保留局部特征信息
        self.branch1x1 = nn.Sequential(
            nn.Conv2d(in_channels, 64, kernel_size=1),  # 降维至64通道
            nn.ReLU()  # 引入非线性激活
        )
        
        # 3x3卷积分支:通过1x1卷积降维后使用3x3卷积捕捉中等尺度特征
        # 先降维减少计算量,再进行空间特征提取
        self.branch3x3 = nn.Sequential(
            nn.Conv2d(in_channels, 96, kernel_size=1),  # 先降维至96通道
            nn.ReLU(),
            nn.Conv2d(96, 128, kernel_size=3, padding=1),  # 3x3卷积,保持空间尺寸不变
            nn.ReLU()
        )
        
        # 5x5卷积分支:通过1x1卷积降维后使用5x5卷积捕捉大尺度特征
        # 较大的感受野用于提取更全局的结构信息
        self.branch5x5 = nn.Sequential(
            nn.Conv2d(in_channels, 16, kernel_size=1),  # 大幅降维至16通道
            nn.ReLU(),
            nn.Conv2d(16, 32, kernel_size=5, padding=2),  # 5x5卷积,保持空间尺寸不变
            nn.ReLU()
        )
        
        # 池化分支:通过池化操作保留全局信息并降维
        # 增强特征的平移不变性
        self.branch_pool = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),  # 3x3最大池化,保持尺寸
            nn.Conv2d(in_channels, 32, kernel_size=1),  # 降维至32通道
            nn.ReLU()
        )

    def forward(self, x):
        """
        前向传播函数,并行计算四个分支并在通道维度拼接
        参数:
            x: 输入特征图,形状为[batch_size, in_channels, height, width]
        返回:
            拼接后的特征图,形状为[batch_size, 256, height, width]
        """
        # 注意,这里是并行计算四个分支
        branch1x1 = self.branch1x1(x)  # 输出形状: [batch_size, 64, height, width]
        branch3x3 = self.branch3x3(x)  # 输出形状: [batch_size, 128, height, width]
        branch5x5 = self.branch5x5(x)  # 输出形状: [batch_size, 32, height, width]
        branch_pool = self.branch_pool(x)  # 输出形状: [batch_size, 32, height, width]
        
        # 在通道维度(dim=1)拼接四个分支的输出
        # 总通道数: 64 + 128 + 32 + 32 = 256
        outputs = [branch1x1, branch3x3, branch5x5, branch_pool]
        return torch.cat(outputs, dim=1)

验证输出维度:

model = Inception(in_channels=64)
input = torch.randn(32, 64, 28, 28)
output = model(input)
print(f"输入形状: {input.shape}") # torch.Size([32, 64, 28, 28])
print(f"输出形状: {output.shape}") # torch.Size([32, 256, 28, 28]) 

InceptionNet 网络

一个简化版的InceptionNet:

  • 初始卷积层:大步长卷积快速下采样,提取低级特征
  • 两个 inception 模块:每个Inception模块同时捕捉不同尺度的特征,实现中高级特征提取
  • 分类 :全局平均池化和全连接层

实际的GoogLeNet中会使用更多的Inception模块辅助分类器

class InceptionNet(nn.Module):
    def __init__(self, num_classes=10):
        super(InceptionNet, self).__init__()
        # 初始卷积层:快速下采样,提取低级特征(边缘、纹理)
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        ) # [batch, 3, H, W] -> [batch, 64, H/4, W/4]
        # Inception 模块堆叠
        self.inception1 = Inception(64) # 64 -> 256,中级多尺度特征
        self.inception2 = Inception(256) # 256 -> 256,高级多尺度特征
        # 分类
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 全局平均池化
        self.fc = nn.Linear(256, num_classes) # 全连接

    def forward(self, x):
        x = self.conv1(x)
        x = self.inception1(x)
        x = self.inception2(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

# 创建网络实例
model = InceptionNet()
# 创建一个随机输入张量,模拟图像数据,这里假设输入图像是3通道,尺寸为224x224
input_tensor = torch.randn(1, 3, 224, 224)
# 前向传播
output = model(input_tensor)
print(output.shape)

特征融合方法

常见方法

(1)拼接(Concatenation)

fused = torch.cat([feature1, feature2, feature3], dim=1)

将特征沿通道维度融合,输出通道数等于所有输入通道之和,可用于Inception模块

(2)逐元素相加(Element-wise Addition)

fused = feature1 + feature2 + feature3

要求所有输入特征图尺寸完全相同,输出通道与输入相同,常用于残差连接

(3)逐元素相乘(Element-wise Multiplication)

fused = feature1 * feature2  # 哈达玛积

强调共同激活的区域,可用于注意力机制、门控机制

(4)加权融合(Weighted Fusion)

# 学习每个特征的权重
weight1 = torch.sigmoid(self.weights1(pooled_feat1))
weight2 = torch.sigmoid(self.weights2(pooled_feat2))
fused = weight1 * feature1 + weight2 * feature2

小结

此外还有其它特征融合方法:

卷积核的变体

感受野

感受野:在卷积神经网络(CNN)中,神经元在原始输入图像上所对应的区域大小。通俗来说,卷积层中的每个输出特征图上的一个像素点,其信息来源于输入图像中的某个特定区域,这个区域的大小就是该像素点的感受野(视野)。

感受野的计算公式:

RFₗ = RFₗ₋₁ + (kₗ - 1) × ∏ sᵢ
  • RFₗ:第l层的感受野
  • RFₗ₋₁:前一层的感受野
  • kₗ:第l层的卷积核大小
  • sᵢ:前面所有层的步长乘积

套入公式,RF = 3 + (3 - 1) * 1 = 5

假设我们有一个 3×3 的卷积核,对一张 5×5 的图像进行步长为 1 的卷积操作: 输出特征图的每个像素点,都由输入图像中 3×3 的区域计算得到,因此该层的感受野为 3×3。 如果再叠加一层 3×3 卷积(步长 1),第二层的每个像素点会融合第一层 3×3 区域的信息,而第一层的每个区域又对应原始图像的 3×3 区域,因此第二层的感受野扩展为 5×5(即 3+3-1=5)

对上面例子的理解:

原始输入图像: 5×5
卷积核: 3×3, stride=1
输入图像坐标:
(1,1)(1,2)(1,3)(1,4)(1,5)
(2,1)(2,2)(2,3)(2,4)(2,5)
(3,1)(3,2)(3,3)(3,4)(3,5)
(4,1)(4,2)(4,3)(4,4)(4,5)
(5,1)(5,2)(5,3)(5,4)(5,5)

第一层输出特征图位置(1,1) = 由输入图像的(1,1)到(3,3)区域计算得到
感受野: 3×3

(1)第一层卷积中,比如输出特征图位置(1,1) = 由输入图像的(1,1)到(3,3)区域计算得到,因此感受野为 3×3 。

(2)第二层卷积中,每个神经元要看第一层的3×3区域,而第一层的每个神经元又对应原始图像的3×3区域(第二层某个位置 -> 第一层 -> 原始图像)。通过下面的说明,可以看到第二层(1,1)位置实际上看到了整个原始图像,因此感受野为 5×5 。

第二层位置(1,1) ← 需要第一层的哪些位置?
第一层需要: 位置(1,1)到(3,3) ← 这些位置各对应原始图像的哪些区域?

第一层位置(1,1) ← 对应原始图像(1,1)到(3,3)
第一层位置(1,2) ← 对应原始图像(1,2)到(3,4)  
第一层位置(1,3) ← 对应原始图像(1,3)到(3,5)
第一层位置(2,1) ← 对应原始图像(2,1)到(4,3)
第一层位置(2,2) ← 对应原始图像(2,2)到(4,4)
第一层位置(2,3) ← 对应原始图像(2,3)到(4,5)
第一层位置(3,1) ← 对应原始图像(3,1)到(5,3)
第一层位置(3,2) ← 对应原始图像(3,2)到(5,4)
第一层位置(3,3) ← 对应原始图像(3,3)到(5,5)

因此,可以发现,感受野的核心思想就是"视野的累积效应" ——每一层都在前一层的基础上看到更大的区域。

此外,卷积核尺寸小具有优势:减少参数、简化计算;引入更多非线性操作,提升拟合效果。故而在深度学习通过堆叠层数逐步增大感受野,也能实现从局部到全局理解图像。比如VGG 网络就用多层 3×3 卷积核替代大卷积核,平衡模型性能与复杂度

卷积的变体

除了标准卷积,还有空洞卷积、幻影卷积等变体。这里主要说明空洞卷积。

空洞(膨胀)卷积:在卷积核元素间插入空洞(间隔),用空洞率(dilation rate,d)控制间隔。

与标准卷积的比较:

(1)标准(d=1):卷积核元素紧密排列,直接覆盖输入特征图相邻区域

卷积核采样点:
● ● ●
● ● ●  
● ● ●
感受野: 3×3区域

(2)空洞(d>1):卷积核元素间插入d-1个空洞,等效扩大卷积核的“感受野范围”,但不增加参数数量。无需增大卷积核尺寸或叠加多层卷积,仅通过调整 d,就能指数级提升感受野

卷积核采样点:
● · ● · ●
· · · · ·
● · ● · ●  
· · · · ·
● · ● · ●
实际覆盖: 5×5区域
感受野: 5×5 (但参数仍是3×3=9个)

注意:当空洞率太大时,卷积核只能看到稀疏的像素点,可能丢失局部连续性信息。

空洞卷积的有效感受野:感受野 = (k - 1) × d + 1,其中k为原始卷积核大小,d为空洞率。

核心价值,适合精细定位 + 上下文信息的任务:

  • 用少量参数获得大感受野
  • 保持特征图分辨率,不丢失空间信息:池化或下采样会丢失
  • 多尺度特征提取

空洞卷积训练

整个流程与标准卷积相同,不同的是增加了 dilation 这个参数:

self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=2, dilation=2)  

这里局部替换成空洞卷积,在不显著增加计算量的情况下,增强模型对长距离特征的捕捉能力,尤其适合想在小数据集(CIFAR-10)里尝试扩大感受野的场景。可以尝试在不同层设置不同dilation(比如dilation=[1,2,1] ),让模型从多个感受野维度提取特征。

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 数据预处理
transform = transforms.Compose([
    transforms.ToTensor(),  # 转为张量
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 归一化
])

# 加载CIFAR-10数据集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=128, shuffle=True)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = DataLoader(testset, batch_size=128, shuffle=False)

# 定义含空洞卷积的CNN模型
class SimpleCNNWithDilation(nn.Module):
    def __init__(self):
        super(SimpleCNNWithDilation, self).__init__()
        # 第一层:普通3×3卷积,捕捉基础特征
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)  
        # 第二层:空洞卷积,dilation=2,感受野扩大(等效5×5普通卷积感受野)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=2, dilation=2)  
        # 第三层:普通3×3卷积,恢复特征对齐
        self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)  
        
        self.pool = nn.MaxPool2d(2, 2)  # 池化层
        self.relu = nn.ReLU()
        
        # 全连接层,根据CIFAR-10尺寸计算:32×32→池化后16×16→...→最终特征维度需匹配
        self.fc1 = nn.Linear(64 * 8 * 8, 256)  
        self.fc2 = nn.Linear(256, 10)  

    def forward(self, x):
        # 输入: [batch, 3, 32, 32]
        x = self.conv1(x)  # [batch, 16, 32, 32]
        x = self.relu(x)
        x = self.pool(x)   # [batch, 16, 16, 16]
        
        x = self.conv2(x)  # [batch, 32, 16, 16](dilation=2 + padding=2 保持尺寸)
        x = self.relu(x)
        x = self.pool(x)   # [batch, 32, 8, 8]
        
        x = self.conv3(x)  # [batch, 64, 8, 8]
        x = self.relu(x)
        
        x = x.view(-1, 64 * 8 * 8)  # 展平
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# 初始化模型、损失函数、优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNNWithDilation().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 训练函数
def train(epoch):
    model.train()
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data[0].to(device), data[1].to(device)
        
        optimizer.zero_grad()
        
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        if i % 100 == 99:  # 每100个batch打印一次
            print(f'Epoch: {epoch + 1}, Batch: {i + 1}, Loss: {running_loss / 100:.3f}')
            running_loss = 0.0

# 测试函数
def test():
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data in testloader:
            images, labels = data[0].to(device), data[1].to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print(f'Accuracy on test set: {100 * correct / total:.2f}%')

# 训练&测试流程
for epoch in range(5):  # 简单跑5个epoch示例
    train(epoch)
    test()

小结

  • 不同的设计适配不同的任务:同样是不同尺度信息捕捉,对目标检测有效,但图像分类无效
  • 理解模块和类的参数本身能力,才能减少盲目尝试的可能,更好地搭积木

作业

Inception 架构

复用之前的代码(数据预处理与加载、训练)+ 今日的InceptionNet架构

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

# 检查GPU是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 数据预处理(与原代码一致)
train_transform = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
    transforms.RandomRotation(15),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

# 加载数据集(与原代码一致)
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, transform=test_transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 训练
def train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs):
    model.train()
    all_iter_losses = []
    iter_indices = []
    train_acc_history = []
    test_acc_history = []
    train_loss_history = []
    test_loss_history = []
    
    for epoch in range(epochs):
        running_loss = 0.0
        correct = 0
        total = 0
        
        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = data.to(device), target.to(device)
            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            
            iter_loss = loss.item()
            all_iter_losses.append(iter_loss)
            iter_indices.append(epoch * len(train_loader) + batch_idx + 1)
            
            running_loss += iter_loss
            _, predicted = output.max(1)
            total += target.size(0)
            correct += predicted.eq(target).sum().item()
            
            if (batch_idx + 1) % 100 == 0:
                print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '
                      f'| 单Batch损失: {iter_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}')
        
        epoch_train_loss = running_loss / len(train_loader)
        epoch_train_acc = 100. * correct / total
        train_acc_history.append(epoch_train_acc)
        train_loss_history.append(epoch_train_loss)
        
        # 测试阶段
        model.eval()
        test_loss = 0
        correct_test = 0
        total_test = 0
        
        with torch.no_grad():
            for data, target in test_loader:
                data, target = data.to(device), target.to(device)
                output = model(data)
                test_loss += criterion(output, target).item()
                _, predicted = output.max(1)
                total_test += target.size(0)
                correct_test += predicted.eq(target).sum().item()
        
        epoch_test_loss = test_loss / len(test_loader)
        epoch_test_acc = 100. * correct_test / total_test
        test_acc_history.append(epoch_test_acc)
        test_loss_history.append(epoch_test_loss)
        
        scheduler.step(epoch_test_loss)
        
        print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%')
    
    plot_iter_losses(all_iter_losses, iter_indices)
    plot_epoch_metrics(train_acc_history, test_acc_history, train_loss_history, test_loss_history)
    
    return epoch_test_acc

# 绘图函数
def plot_iter_losses(losses, indices):
    plt.figure(figsize=(10, 4))
    plt.plot(indices, losses, 'b-', alpha=0.7, label='Iteration Loss')
    plt.xlabel('Iteration')
    plt.ylabel('Loss')
    plt.title('The Loss of Each Iteration')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()

# 7. 绘制每个 epoch 的准确率和损失曲线
def plot_epoch_metrics(train_acc, test_acc, train_loss, test_loss):
    epochs = range(1, len(train_acc) + 1)
    
    plt.figure(figsize=(12, 4))
    
    # 绘制准确率曲线
    plt.subplot(1, 2, 1)
    plt.plot(epochs, train_acc, 'b-', label='Train Accuracy')
    plt.plot(epochs, test_acc, 'r-', label='Test Accuracy')
    plt.xlabel('Epoch')
    plt.ylabel('Accuracy (%)')
    plt.title('Accuracy Curve')
    plt.legend()
    plt.grid(True)
    
    # 绘制损失曲线
    plt.subplot(1, 2, 2)
    plt.plot(epochs, train_loss, 'b-', label='Train Loss')
    plt.plot(epochs, test_loss, 'r-', label='Test Loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Loss Curve')
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 执行训练

model = InceptionNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=3, factor=0.5)

epochs = 50
print("开始使用带CBAM的CNN训练模型...")
final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")

# # 保存模型
# torch.save(model.state_dict(), 'cifar10_cbam_cnn_model.pth')
# print("模型已保存为: cifar10_cbam_cnn_model.pth")

结果如下:

消融实验

消融实验的核心思想:有意识地“移除”或“替换”模型某一部分,然后观察性能的变化。

Inception + CBAM

复用之前定义的CBAM

# 定义通道注意力
class ChannelAttention(nn.Module):
    def __init__(self, in_channels, ratio=16):
        """
        通道注意力机制初始化
        参数:
            in_channels: 输入特征图的通道数
            ratio: 降维比例,用于减少参数量,默认为16
        """
        super().__init__()
        # 全局平均池化,将每个通道的特征图压缩为1x1,保留通道间的平均值信息
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        # 全局最大池化,将每个通道的特征图压缩为1x1,保留通道间的最显著特征
        self.max_pool = nn.AdaptiveMaxPool2d(1)
        # 共享全连接层,用于学习通道间的关系
        # 先降维(除以ratio),再通过ReLU激活,最后升维回原始通道数
        self.fc = nn.Sequential(
            nn.Linear(in_channels, in_channels // ratio, bias=False),  # 降维层
            nn.ReLU(),  # 非线性激活函数
            nn.Linear(in_channels // ratio, in_channels, bias=False)   # 升维层
        )
        # Sigmoid函数将输出映射到0-1之间,作为各通道的权重
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        """
        前向传播函数
        参数:
            x: 输入特征图,形状为 [batch_size, channels, height, width]
        返回:
            调整后的特征图,通道权重已应用
        """
        # 获取输入特征图的维度信息,这是一种元组的解包写法
        b, c, h, w = x.shape
        # 对平均池化结果进行处理:展平后通过全连接网络
        avg_out = self.fc(self.avg_pool(x).view(b, c))
        # 对最大池化结果进行处理:展平后通过全连接网络
        max_out = self.fc(self.max_pool(x).view(b, c))
        # 将平均池化和最大池化的结果相加并通过sigmoid函数得到通道权重
        attention = self.sigmoid(avg_out + max_out).view(b, c, 1, 1)
        # 将注意力权重与原始特征相乘,增强重要通道,抑制不重要通道
        return x * attention #这个运算是pytorch的广播机制

## 空间注意力模块
class SpatialAttention(nn.Module):
    def __init__(self, kernel_size=7):
        super().__init__()
        self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # 通道维度池化
        avg_out = torch.mean(x, dim=1, keepdim=True)  # 平均池化:(B,1,H,W)
        max_out, _ = torch.max(x, dim=1, keepdim=True)  # 最大池化:(B,1,H,W)
        pool_out = torch.cat([avg_out, max_out], dim=1)  # 拼接:(B,2,H,W)
        attention = self.conv(pool_out)  # 卷积提取空间特征
        return x * self.sigmoid(attention)  # 特征与空间权重相乘

## CBAM模块
class CBAM(nn.Module):
    def __init__(self, in_channels, ratio=16, kernel_size=7):
        super().__init__()
        self.channel_attn = ChannelAttention(in_channels, ratio)
        self.spatial_attn = SpatialAttention(kernel_size)

    def forward(self, x):
        x = self.channel_attn(x)
        x = self.spatial_attn(x)
        return x

并修改InceptionNet架构,在每一个Inception模块后加入CBAM:

# 定义InceptionNet网络
class InceptionNet(nn.Module):
    def __init__(self, num_classes=10):
        super(InceptionNet, self).__init__()
        # 初始卷积层:快速下采样,提取低级特征(边缘、纹理)
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        ) # [batch, 3, H, W] -> [batch, 64, H/4, W/4]

        # Inception 模块堆叠
        self.inception1 = Inception(64) # 64 -> 256,中级多尺度特征
        self.cbam1 = CBAM(256) # 加入CBAM
        self.inception2 = Inception(256) # 256 -> 256,高级多尺度特征
        self.cbam2 = CBAM(256) # 加入CBAM

        # 分类
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 全局平均池化
        self.fc = nn.Linear(256, num_classes) # 全连接

    def forward(self, x):
        x = self.conv1(x)

        x = self.inception1(x)
        x = self.cbam1(x) # 增强中级特征

        x = self.inception2(x)
        x = self.cbam2(x) # 增强高级特征

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

结果如下:

Inception + 残差连接

将残差连接加入InceptionNet 架构:

# 残差连接的Inception
class InceptionNet(nn.Module):
    def __init__(self, num_classes=10):
        super(InceptionNet, self).__init__()
        # 初始卷积层:快速下采样,提取低级特征(边缘、纹理)
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        ) # [batch, 3, H, W] -> [batch, 64, H/4, W/4]

        # Inception 模块堆叠
        self.inception1 = Inception(64) # 64 -> 256,中级多尺度特征
        self.inception2 = Inception(256) # 256 -> 256,高级多尺度特征

        # 用于残差连接的1x1卷积(调整通道数,保证匹配)
        self.residual_conv = nn.Conv2d(64,256,kernel_size=1,stride=1)

        # 分类
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 全局平均池化
        self.fc = nn.Linear(256, num_classes) # 全连接

    def forward(self, x):
        x = self.conv1(x)

        residual = x # 保存残差连接的输入,[B,64,H/4, W/4]

        x = self.inception1(x) # [B,256,H/4, W/4]
        x = self.inception2(x) # [B,256,H/4, W/4]

        # 添加残差连接
        if residual.shape[1] != 256:  # 如果通道数不匹配,使用1x1卷积调整
            residual = self.residual_conv(residual)
        x = x + residual  # 残差连接,通道数要相同

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

结果如下:

<think>好的,用户想了解Inception网络的架构、原理和实现方式。我之前已经收集了一些相关的引用资料,现在需要整合这些信息,给出一个清晰的回答。首先,根据引用[2],Inception网络是2014年ILSVRC的冠军,主要创新是模块化结构和多尺寸卷积组合。需要详细说明它的架构特点,比如Inception模块的结构,以及不同版本的发展。 接下来,原理部分,引用[3]提到Inception模块使用不同大小的滤波器并行处理,然后合并结果。这增加了网络的宽度和对尺度的适应性。同时,1x1卷积用于降维,减少计算量。要解释清楚为什么这样的设计有效,比如多尺度特征提取和参数效率。 实现方面,引用[1]提到了基于PyTorch的实现,包括各种Inception块如Stem、Inception-A到C,以及Reduction模块的结构。可能需要简要说明代码实现的关键点,比如模块的组合方式。另外,引用[4]是InceptionV1的原始论文,可以引用其核心思想。 需要注意用户要求的结构清晰,分点回答,使用正确的中文和LaTeX格式。还要生成相关问题,比如不同版本的区别、1x1卷积的作用、应用场景等。同时,引用标记要自然添加,比如在提到降维时引用[3],提到架构时引用[2]。 最后,检查是否所有数学公式都用了正确的格式,比如$1\times1$卷积,以及独立公式用$$...$$。确保回答覆盖架构、原理、实现三个部分,并符合用户的需求。</think>### Inception网络解析 #### 一、架构特点 Inception网络的核心是**模块化结构**和**多尺度特征融合**,其架构演进包含多个版本: 1. **InceptionV1(GoogLeNet)** 由9个Inception模块构成,共22层网络,首次引入并行卷积结构。每个模块包含$1\times1$、$3\times3$、$5\times5$卷积和$3\times3$最大池化操作,输出通道级联[^2][^3]。 $$ \text{输出} = \text{Concat}(1\times1\text{Conv}, 3\times3\text{Conv}, 5\times5\text{Conv}, \text{MaxPool}) $$ 2. **InceptionV2/V3** 改进卷积分解(如将$5\times5$卷积拆分为两个$3\times3$卷积)和引入批量归一化(BatchNorm)[^3]。 3. **Inception-ResNet系列** 结合残差连接(Residual Connection),如Inception-ResNet-v2中残差结构公式: $$ x_{out} = x_{in} + \mathcal{F}(x_{in}) $$ 其中$\mathcal{F}$为Inception模块的输出[^1]。 #### 二、核心原理 1. **多尺度特征提取** 并行使用不同尺寸的卷积核($1\times1$、$3\times3$、$5\times5$),捕捉图像局部到全局的特征[^3]。 2. **参数效率优化** - **降维设计**:在$3\times3$和$5\times5$卷积前添加$1\times1$卷积,减少通道数。例如输入通道为256时,先用$1\times1$卷积压缩至64通道,再执行$5\times5$卷积,计算量减少约$75\%$[^3]。 - **稀疏连接**:通过并行分支实现类似稀疏矩阵的计算效果。 3. **梯度辅助** InceptionV1引入两个辅助分类器,缓解深层网络梯度消失问题[^4]。 #### 三、实现方式(以PyTorch为例) 关键模块代码实现要点: 1. **Stem模块** 初始特征提取层,包含多个卷积和池化操作: ```python class Stem(nn.Module): def __init__(self, in_channels=3): super().__init__() self.conv1 = nn.Conv2d(in_channels, 32, kernel_size=3, stride=2) self.conv2 = nn.Conv2d(32, 32, kernel_size=3) self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # ...后续层定义 ``` 2. **Inception-A模块** 典型的多分支结构: ```python class InceptionA(nn.Module): def __init__(self, in_channels): super().__init__() self.branch1 = nn.Conv2d(in_channels, 64, kernel_size=1) self.branch2 = nn.Sequential( nn.Conv2d(in_channels, 48, kernel_size=1), nn.Conv2d(48, 64, kernel_size=5, padding=2) ) # ...其他分支定义 ``` 3. **Reduction模块** 用于下采样特征图尺寸,例如Reduction-B: ```python class ReductionB(nn.Module): def __init__(self, in_channels): super().__init__() self.branch_pool = nn.MaxPool2d(3, stride=2) self.branch3x3 = nn.Conv2d(in_channels, 96, kernel_size=3, stride=2) # ...其他分支定义 ``` 完整网络通过堆叠多个Inception模块和Reduction模块构建,最终输出分类结果[^1]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值