今日任务:
- 传统计算机视觉发展史
- inception 模块和网络
- 特征融合方法阶段性总结:逐元素相加或相册、concat通道数相加等
- 感受野和卷积核变体:深入理解不同模块和类的设计初衷
作业:对 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
结果如下:


981

被折叠的 条评论
为什么被折叠?



