- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
一、实验目的:
- 在DenseNet系列算法中插入SE-Net通道注意力机制,并完成猴痘病识别
- 改进思路是否可以迁移到其他地方
- 测试集accuracy到达89%(拔高)
二、实验环境:
- 语言环境:python 3.8
- 编译器:Jupyter notebook
- 深度学习环境:Pytorch
- torch==2.4.0+cu124
- torchvision==0.19.0+cu124
三、SE-NET介绍
SE-Net 是 ImageNet 2017(ImageNet 收官赛)的冠军模型,是由WMW团队发布。具有复杂度低,参数少和计算量小的优点。且SENet 思路很简单,很容易扩展到已有网络结构如 Inception 和 ResNet 中。
已经有很多工作在空间维度上来提升网络的性能,如 Inception 等,而 SENet 将关注点放在了特征通道之间的关系上。其具体策略为:通过学习的方式来自动获取到每个特征通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征,这又叫做“特征重标定”策略。具体的 SE 模块如下图所示:
SE-NET 的核心思想是通过 Squeeze-and-Excitation(SE)模块的机制来增强网络的表示能力。具体来说,SE模块对每一个卷积层的特征图进行重新标定,通过显式建模通道之间的相互依赖关系来提升网络的表现力。以下是SE模块的两个主要步骤:
- Squeeze(压缩):
- 通过全局平均池化(Global Average Pooling)将特征图的空间维度压缩到一个单一的值,从而获得全局的通道描述。
- 输出的每一个值代表该通道在整个图像上的全局信息。
- Excitation(激发):
- 通过两个全连接层(Fully Connected Layers)和一个非线性激活函数(通常是ReLU和Sigmoid)生成每个通道的权重。
- 这些权重反映了每个通道的重要性,并用来重新标定(Recalibrate)原始的特征图。
SE模块的灵活性在于它可以直接应用现有的网络结构中。以 Inception 和 ResNet 为例,我们只需要在 Inception 模块或 Residual 模块后添加一个 SE 模块即可。具体如下图所示:
上图分别是将 SE 模块嵌入到 Inception 结构与 ResNet 中的示例,方框旁边的维度信息代表该层的输出,c 表示 Excitation 操作中的降维系数。
四、使用Pytorch实现DenseNet+SE-Net
设置GPU、导入数据、划分数据集等步骤同前。
1. 构建模型
import torch.nn.functional as F
from collections import OrderedDict
class DenseLayer(nn.Sequential):
def __init__(self,in_channel,growth_rate,bn_size,drop_rate):
super(DenseLayer,self).__init__()
self.add_module('norm1',nn.BatchNorm2d(in_channel))
self.add_module('relu1',nn.ReLU(inplace=True))
self.add_module('conv1',nn.Conv2d(in_channel,bn_size*growth_rate,kernel_size=1,stride=1))
self.add_module('norm2',nn.BatchNorm2d(bn_size*growth_rate))
self.add_module('relu2',nn.ReLU(inplace=True))
self.add_module('conv2',nn.Conv2d(bn_size*growth_rate,growth_rate,kernel_size=3,stride=1,padding=1))
self.drop_rate = drop_rate
def forward(self,x):
new_feature = super(DenseLayer,self).forward(x)
if self.drop_rate > 0:
new_feature = F.dropout(new_feature,p=self.drop_rate,training=self.training)
return torch.cat([x,new_feature],1)
class DenseBlock(nn.Sequential):
def __init__(self,num_layers,in_channel,bn_size,growth_rate,drop_rate):
super(DenseBlock,self).__init__()
for i in range(num_layers):
layer = DenseLayer(in_channel+i*growth_rate,growth_rate,bn_size,drop_rate)
self.add_module('denselayer%d'%(i+1,),layer)
class Transition(nn.Sequential):
def __init__(self,in_channel,out_channel):
super(Transition,self).__init__()
self.add_module('norm',nn.BatchNorm2d(in_channel))
self.add_module('relu',nn.ReLU(inplace=True))
self.add_module('conv',nn.Conv2d(in_channel,out_channel,kernel_size=1,stride=1))
self.add_module('pool',nn.AvgPool2d(kernel_size=2,stride=2))
class SELayer(nn.Module):
def __init__(self,channel,reduction=16):
super(SELayer,self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel,channel//reduction),
nn.ReLU(inplace=True),
nn.Linear(channel//reduction,channel),
nn.Sigmoid()
)
def forward(self,x):
b,c,_,_ = x.size()
y = self.avg_pool(x).view(b,c) # 只保留批次和通道,抛弃长宽维度
y = self.fc(y).view(b,c,1,1) # 增加长宽维度(从平面变立体)
return x * y
class DenseNet(nn.Module):
def __init__(self,growth_rate=32,block_config=(6,12,24,16),num_init_features=64,bn_size=4,compression_rate=0.5,drop_rate=0,num_classes=1000):
super(DenseNet,self).__init__()
self.features = nn.Sequential(OrderedDict([
('conv0',nn.Conv2d(3,num_init_features,kernel_size=7,stride=2,padding=3)),
('norm0',nn.BatchNorm2d(num_init_features)),
('relu0',nn.ReLU(inplace=True)),
('pool0',nn.MaxPool2d(kernel_size=3,stride=2,padding=1))
]))
num_features = num_init_features
for i,num_layers in enumerate(block_config):
block = DenseBlock(num_layers,num_features,bn_size=bn_size,growth_rate=growth_rate,drop_rate=drop_rate)
self.features.add_module('denseblock%d'%(i+1),block)
num_features += num_layers*growth_rate
if i!= len(block_config)-1:
transition = Transition(num_features,int(num_features*compression_rate))
self.features.add_module('transition%d'%(i+1),transition)
num_features = int(num_features*compression_rate)
SeLayer = SELayer(channel=num_features)
self.features.add_module('SE',SeLayer)
self.features.add_module('norm5',nn.BatchNorm2d(num_features))
self.features.add_module('relu5',nn.ReLU(inplace=True))
self.classifier = nn.Linear(num_features,num_classes)
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.bias,0)
nn.init.constant_(m.weight,1)
elif isinstance(m,nn.Linear):
nn.init.constant_(m.bias,0)
def forward(self,x):
x = self.features(x)
x = F.avg_pool2d(x,7,stride=1).view(x.size(0),-1)
x = self.classifier(x)
return x
DenseLayer
- BatchNorm2d:对输入特征进行批归一化。
- ReLU:非线性激活函数。
- Conv2d:第一层卷积为1x1卷积,减少参数量;第二层卷积为3x3卷积,提取特征。
- Dropout:在训练过程中随机丢弃部分特征,防止过拟合。
- forward方法:计算新特征,并将其与输入特征连接(concatenate),形成密集连接。
DenseBlock
DenseBlock 类包含多个 DenseLayer。每个 DenseLayer的输出特征会与输入特征连接,传递给下一个 DenseLayer。
- num_layers:DenseBlock 中 DenseLayer 的数量。
- num_input_features:输入特征数。
- growth_rate:每个 DenseLayer 增加的特征数。
- bn_size:瓶颈层的缩减系数。
- drop_rate:dropout的概率。
Transition
Transition 类用于在 DenseBlock 之间进行过渡,主要是通过卷积层和池化层来减少特征图的尺寸和数量。
- BatchNorm2d:对输入特征进行批归一化。
- ReLU:非线性激活函数。
- Conv2d:1x1卷积,减少特征图的通道数。
- AvgPool2d:2x2平均池化,减小特征图的尺寸。
总结
DenseLayer:DenseBlock 的基本单元,通过两层卷积和密集连接生成新特征。
DenseBlock:由多 DenseLayer 组成,每个 DenseLayer 的输出与输入特征连接。
Transition:用于 DenseBlock 之间的过渡,减少特征图的尺寸和通道数。
这些模块共同构成了 DenseNet 的基本架构,通过密集连接提高了特征重用和梯度传播效率,显著提升了网络的性能。
- Squeeze(压缩):通过全局平均池化将每个通道的特征压缩为一个全局描述。
- Excitation(激发):通过全连接层序列生成每个通道的权重,用于重新标定输入特征图的每个通道。
- 权重调整:通过逐元素乘法,将通道的重要性信息应用于原始特征图,增强有用的特征,抑制无关特征。
该DenseNet_SE模型相比DenseNet的区别是,在最后一个DenseBlock后增加SELayer。
import re
import torch.utils.model_zoo as model_zoo
from torchvision.models.densenet import model_urls
def densenet121(pretrained=False,**kwargs):
model = DenseNet(num_init_features=64,growth_rate=32,block_config=(6,12,24,16),num_classes=len(classeNames),**kwargs)
if pretrained:
pattern = re.compile(r'^(.*denselayer\d+\.(?:norm|relu|conv))\.((?:[12])\.(?:weight|bias|running_mean|running_var))$')
state_dict = model_zoo.load_url(model_urls['densenet121'])
for key in list(state_dict.keys()):
res = pattern.match(key)
if res:
new_key = res.group(1) + res.group(2)
state_dict[new_key] = state_dict[key]
del state_dict[key]
state_dict.pop('classifier.weight')
state_dict.pop('classifier.bias')
model.load_state_dict(state_dict,strict=False)
nn.init.kaiming_normal_(model.classifier.weight)
nn.init.zeros_(model.classifier.bias)
return model
model = densenet121(pretrained=False).to(device)
model
# 统计模型参数量以及其他指标
import torchsummary as summary
summary.summary(model,(3,224,224))
代码输出部分截图:
2. 编写训练与测试函数
# 编写训练函数
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
num_batches = len(dataloader)
train_acc, train_loss = 0, 0
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss /= num_batches
train_acc /= size
return train_acc, train_loss
# 编写测试函数
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
3. 设置损失函数和学习率
import copy
loss_fn = nn.CrossEntropyLoss()
learn_rate = 1e-4
# SGD与Adam优化器,选择其中一个
# opt = torch.optim.SGD(model.parameters(),lr=learn_rate)
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)
scheduler = torch.optim.lr_scheduler.StepLR(opt, step_size=1, gamma=0.9) # 定义学习率高度器
epochs = 100 # 设置训练模型的最大轮数为100,但可能到不了100
patience = 10 # 早停的耐心值,即如果模型连续10个周期没有准确率提升,则跳出训练
train_loss = []
train_acc = []
test_loss = []
test_acc = []
best_acc = 0 # 设置一个最佳的准确率,作为最佳模型的判别指标
no_improve_epoch = 0 # 用于跟踪准确率是否提升的计数器
epoch = 0 # 用于统计最终的训练模型的轮数,这里设置初始值为0;为绘图作准备,这里的绘图范围不是epochs = 100
4. 正式训练
# 开始训练
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
if epoch_test_acc > best_acc:
best_acc = epoch_test_acc
best_model = copy.deepcopy(model)
no_improve_epoch = 0 # 重置计数器
# 保存最佳模型的检查点
PATH = 'J5_best_model.pth'
torch.save({
'epoch': epoch,
'model_state_dict': best_model.state_dict(),
'optimizer_state_dict': opt.state_dict(),
'loss': epoch_test_loss,
}, PATH)
else:
no_improve_epoch += 1
if no_improve_epoch >= patience:
print(f"Early stop triggered at epoch {epoch + 1}")
break # 早停
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
scheduler.step() # 更新学习率
lr = opt.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(
template.format(epoch + 1, epoch_train_acc * 100, epoch_train_loss, epoch_test_acc * 100, epoch_test_loss, lr))
代码输出部分截图:
5. 结果可视化
# 结果可视化
# Loss与Accuracy图
import matplotlib.pyplot as plt
# 隐藏警告
import warnings
warnings.filterwarnings("ignore") # 忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 # 分辨率
epochs_range = range(epoch)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
6. 预测
from PIL import Image
classes = list(total_data.class_to_idx)
def predict_one_image(image_path, model, transform, classes):
test_img = Image.open(image_path).convert('RGB')
plt.imshow(test_img) # 展示预测的图片
test_img = transform(test_img)
img = test_img.to(device).unsqueeze(0)
model.eval()
output = model(img)
_, pred = torch.max(output, 1)
pred_class = classes[pred]
print(f'预测结果是:{pred_class}')
import os
from pathlib import Path
import random
#从所有的图片的随机选择一张图片
image=[]
def image_path(data_dir):
file_list=os.listdir(data_dir) #列出四个分类标签
data_file_dir=file_list #从四个分类标签中随机选择一个
data_dir=Path(data_dir)
for i in data_file_dir:
i=Path(i)
image_file_path=data_dir.joinpath(i) #拼接路径
data_file_paths=image_file_path.iterdir() #罗列文件夹的内容
data_file_paths=list(data_file_paths) #要转换为列表
image.append(data_file_paths)
file=random.choice(image) #从所有的图像中随机选择一类
file=random.choice(file) #从选择的类中随机选择一张图片
return file
data_dir='./monkeypox_photos'
image_path=image_path(data_dir)
# 预测训练集中的某张照片
predict_one_image(image_path=image_path,
model=model,
transform=train_transforms,
classes=classes)
# 模型评估
# 将参数加载到model当中
best_model.load_state_dict(torch.load(PATH,map_location=device))
epoch_test_acc,epoch_test_loss=test(test_dl,best_model,loss_fn)
epoch_test_acc,epoch_test_loss
(0.9020979020979021, 0.28417748904653956)
总结
增加注意力模块SE-Net后,最终准确率达到了91.1%。本次实验对比J3和J4,测试集的准确率和loss均得到了不错的改善。
SE-Net 源于论文:Squeeze-and-Excitation Networks,介绍了一种基于自适应特征重标定的网络结构,用于提高卷积神经网络(CNN)的性能。这个网络结构被称为“Squeeze-and-Excitation(SE)网络”。
SE网络的核心思想是利用自适应的特征重标定来增强网络的表达能力,使其能够更好地处理不同类别之间的差异。具体来说,SE网络在每个通道上引入一个Squeeze操作和一个Excitation操作。
Squeeze操作将每个通道的特征图压缩成一个数值,并且将其作为该通道的全局特征描述符。这可以通过使用全局平均池化来实现。然后,Excitation操作将该描述符作为输入,并生成一个权重向量,该向量可以动态地调整每个通道的权重,以强化重要的特征并抑制不重要的特征。这可以通过使用一系列全连接层和非线性激活函数来实现。
SE网络可以通过简单地添加SE块来嵌入到任何卷积神经网络中,而不需要对网络架构进行大规模修改。这使得SE网络非常易于实现,并且在多个视觉任务上都可以提高性能。
在论文中,作者通过在ImageNet和CIFAR-10数据集上进行实验,证明了SE网络在各种任务上都可以提高CNN的性能,包括图像分类、目标检测和语义分割等。作者还展示了SE网络对于不同类别之间的差异建模能力强于其他网络结构。
总的来说,SE网络通过引入自适应的特征重标定来增强CNN的表达能力,并在各种视觉任务上取得了显著的性能提升。