Pytorch从零开始实战14

Pytorch从零开始实战------DenseNet + SENet算法实战

本系列来源于365天深度学习训练营

原作者K同学

文章目录

环境准备

本文基于Jupyter notebook,使用Python3.8,Pytorch2.0.1+cu118,torchvision0.15.2,需读者自行配置好环境且有一些深度学习理论基础。本次实验的目的是使用DenseNet+SENet模型。

第一步,导入常用包

python 复制代码
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn.functional as F
import random
from time import time
import numpy as np
import pandas as pd
import datetime
import gc
import os
import copy
import warnings
os.environ['KMP_DUPLICATE_LIB_OK']='True'  # 用于避免jupyter环境突然关闭
torch.backends.cudnn.benchmark=True  # 用于加速GPU运算的代码

设置随机数种子

python 复制代码
torch.manual_seed(428)
torch.cuda.manual_seed(428)
torch.cuda.manual_seed_all(428)
random.seed(428)
np.random.seed(428)

检查设备对象

python 复制代码
torch.manual_seed(428)
torch.cuda.manual_seed(428)
torch.cuda.manual_seed_all(428)
random.seed(428)
np.random.seed(428)

检查设备对象

python 复制代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device, torch.cuda.device_count() # # (device(type='cuda'), 2)

数据集

本次实验继续使用猴痘病数据集,使用pathlib查看类别,本次类别只有0,1两种类别分别代表患病和不患病。

python 复制代码
import pathlib
data_dir = './data/ill/'
data_dir = pathlib.Path(data_dir) # 转成pathlib.Path对象
data_paths = list(data_dir.glob('*')) 
classNames = [str(path).split("/")[2] for path in data_paths]
classNames # ['Monkeypox', 'Others']

使用transforms对数据集进行统一处理,并且根据文件夹名映射对应标签

python 复制代码
all_transforms = transforms.Compose([
    transforms.Resize([224, 224]),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 标准化
])

total_data = datasets.ImageFolder("./data/ill/", transform=all_transforms)
total_data.class_to_idx # {'Monkeypox': 0, 'Others': 1}

随机查看5张图片

python 复制代码
def plotsample(data):
    fig, axs = plt.subplots(1, 5, figsize=(10, 10)) #建立子图
    for i in range(5):
        num = random.randint(0, len(data) - 1) #首先选取随机数,随机选取五次
        #抽取数据中对应的图像对象,make_grid函数可将任意格式的图像的通道数升为3,而不改变图像原始的数据
        #而展示图像用的imshow函数最常见的输入格式也是3通道
        npimg = torchvision.utils.make_grid(data[num][0]).numpy()
        nplabel = data[num][1] #提取标签 
        #将图像由(3, weight, height)转化为(weight, height, 3),并放入imshow函数中读取
        axs[i].imshow(np.transpose(npimg, (1, 2, 0))) 
        axs[i].set_title(nplabel) #给每个子图加上标签
        axs[i].axis("off") #消除每个子图的坐标轴

plotsample(total_data)

根据8比2划分数据集和测试集,并且利用DataLoader划分批次和随机打乱

python 复制代码
train_size = int(0.8 * len(total_data))
test_size  = len(total_data) - train_size
train_ds, test_ds = torch.utils.data.random_split(total_data, [train_size, test_size])

batch_size = 32
train_dl = torch.utils.data.DataLoader(train_ds,
                                        batch_size=batch_size,
                                        shuffle=True,
                                      )
test_dl = torch.utils.data.DataLoader(test_ds,
                                        batch_size=batch_size,
                                        shuffle=True,
                                     )

len(train_dl.dataset), len(test_dl.dataset) # (1713, 429)

模型选择

本次实验使用DenseNet + SENet模型,DenseNet的设计核心思想是通过密集连接来增强神经网络的信息流动,促进梯度的传播,以及提高参数的共享和重复使用。采用跨通道concat的形式来连接,会连接前面所有层作为输入。

核心公式为:

DenseNet中的基本组成单元是DenseBlock,它由多个密集连接的DenseLayer组成。每个DenseLayer都接收所有前面的DenseLayer特征作为输入,将其连接到自己的输出上,并传递给后续的层。如图所示,这是一个基本的DenseBlock模块。

整体网络架构图如下所示,借用K同学的图片

为了控制模型的复杂度并减少特征图的大小,DenseNet引入了Transition Block。过渡块包括批归一化、ReLU激活和 1x1 卷积,以减小特征图的通道数,并通过池化操作降低空间维度。

首先对DenseLayer类定义,本次实验使用add_module函数,默认是用于向类中添加一个子模块,第一个参数为模块名,第二个参数为模块实例,其实相当于加到父类的nn.Sequential里面,所以调用的时候使用super().forward(x),这段的核心是将输入 x 与新特征 t 进行通道维度上的连接,完成密集连接。

python 复制代码
class DenseLayer(nn.Sequential):
    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):
        super().__init__()
        self.add_module("norm1", nn.BatchNorm2d(num_input_features))
        self.add_module("relu1", nn.ReLU(inplace=True))
        self.add_module("conv1", nn.Conv2d(num_input_features, bn_size * growth_rate, kernel_size=1, stride=1, bias=False))
        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, bias=False))
        self.drop_rate = drop_rate

    def forward(self, x):
        t = super().forward(x)
        if self.drop_rate > 0:
            t = F.dropout(t, p=self.drop_rate, training=self.training)
        return torch.cat([x, t], 1)

下面是DenseBlock的实现,通过循环创建了多个DenseLayer。其中的 num_input_features + i * growth_rate 用于指定输入通道的数量,确保每个DenseLayer的输入通道数逐渐增加。将新创建的DenseLayer添加为 DenseBlock 的子模块。循环结束后,DenseBlock 就包含了多个DenseLayer,每个DenseLayer都具有逐渐增加的输入通道数量。

python 复制代码
class DenseBlock(nn.Sequential):
    def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate):
        super().__init__()
        for i in range(num_layers):
            layer = DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
            self.add_module("denselayer%d" % (i + 1), layer)

下面是Transition,实现过渡的功能,是在块之间降低通道数量和空间维度。

python 复制代码
class Transition(nn.Sequential):
    def __init__(self, num_input_feature, num_output_features):
        super().__init__()
        self.add_module("norm", nn.BatchNorm2d(num_input_feature))
        self.add_module("relu", nn.ReLU(inplace=True))
        self.add_module("conv", nn.Conv2d(num_input_feature, num_output_features, kernel_size=1, stride=1, bias=False))
        self.add_module("pool", nn.AvgPool2d(2, stride=2))

SENet是一种深度神经网络结构,它的核心思想是允许网络在训练期间对每个通道进行自适应的加权,以使网络能够更加关注对任务有用的通道,并抑制对任务无关的通道。这有助于提高网络对输入数据的敏感性,并提升网络性能。SENet的结构包括两个主要组件:Squeeze 操作和 Excitation 操作。

Squeeze 操作(Global Average Pooling):通过全局平均池化,将每个通道的空间维度降为1。这样,对于每个通道,都得到一个单一的数值,反映了该通道对整个特征图的重要性。

Excitation 操作(通道注意力):在 Squeeze 操作后,通过一个小型的多层感知机(MLP)来学习通道之间的关系。这个小型MLP包含一个压缩操作和一个激励操作)。最后,利用学到的权重对每个通道的特征图进行加权,得到加权后的特征表示。

下面是SENet的实现,首先,通过全局平均池化层对输入特征图进行平均池化,将每个通道的空间维度降为1。然后,通过全连接层序列 fc 对降维后的特征进行处理,得到每个通道的注意力权重。最后,将得到的注意力权重通过 view 操作还原为与输入特征图相同的形状,并将其与输入特征图相乘,得到应用了注意力机制的特征图。

python 复制代码
from torch.nn import init
class SEAttention(nn.Module):

    def __init__(self, channel=512, reduction=16):
        super().__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channel // reduction, channel, bias=False),
            nn.Sigmoid()
        )

    def init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                init.kaiming_normal_(m.weight, mode='fan_out')
                if m.bias is not None:
                    init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                init.constant_(m.weight, 1)
                init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                init.normal_(m.weight, std=0.001)
                if m.bias is not None:
                    init.constant_(m.bias, 0)

    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.expand_as(x)

整体模型实现,self.features 是一个包含多个层的序列,包括初始卷积块、多个DenseBlock和Transition,以及最后的全局平均池化和分类器。遍历 block_config 中的配置,创建DenseBlock和Transition。参数初始化部分使用了 Kaiming 初始化和常数初始化。

其中,OrderedDict是Python中的一种有序字典数据结构,它保留了元素添加的顺序。在神经网络中,我们可以使用OrderedDict来指定模型的层次结构。

在进行平均池化之前,进入到SENet进行学习通道注意力权重从而提高网络的表征能力。

python 复制代码
from collections import OrderedDict

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().__init__()
        self.features = nn.Sequential(OrderedDict([
            ("conv0", nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),
            ("norm0", nn.BatchNorm2d(num_init_features)),
            ("relu0", nn.ReLU(inplace=True)),
            ("pool0", nn.MaxPool2d(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, growth_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)

        self.features.add_module("norm5", nn.BatchNorm2d(num_features))
        self.features.add_module("relu5", nn.ReLU(inplace=True))
        self.se = SEAttention(channel=1024, reduction=8)
        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):
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
    def forward(self, x):
        features = self.features(x)
        out = self.se(features)
        out = F.avg_pool2d(features, 7, stride=1)
        out = out.view(features.size(0), -1)
        out = self.classifier(out)
        return out

使用summary查看网络

python 复制代码
from torchsummary import summary
model = DenseNet().to(device)
summary(model, input_size=(3, 224, 224))

开始训练

定义训练函数

python 复制代码
def train(dataloader, model, loss_fn, opt):
    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)

        opt.zero_grad()
        loss.backward()
        opt.step()

        train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
        train_loss += loss.item()

    train_acc /= size
    train_loss /= num_batches
    return train_acc, train_loss

定义测试函数

python 复制代码
def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    test_acc, test_loss = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            loss = loss_fn(pred, y)
    
            test_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
            test_loss += loss.item()

    test_acc /= size
    test_loss /= num_batches
    return test_acc, test_loss

定义学习率、损失函数、优化算法

python 复制代码
loss_fn = nn.CrossEntropyLoss()
learn_rate = 0.0001
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)

开始训练,epoch设置为20

python 复制代码
import time
epochs = 20
train_loss = []
train_acc = []
test_loss = []
test_acc = []

T1 = time.time()

best_acc = 0
best_model = 0

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)
        
    train_acc.append(epoch_train_acc)
    train_loss.append(epoch_train_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)
    
    print("epoch:%d, train_acc:%.1f%%, train_loss:%.3f, test_acc:%.1f%%, test_loss:%.3f"
          % (epoch + 1, epoch_train_acc * 100, epoch_train_loss, epoch_test_acc * 100, epoch_test_loss))

T2 = time.time()
print('程序运行时间:%s秒' % (T2 - T1))

PATH = './best_model.pth'  # 保存的参数文件名
if best_model is not None:
    torch.save(best_model.state_dict(), PATH)
    print('保存最佳模型')
print("Done")

可视化

可视化训练过程与测试过程

python 复制代码
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(epochs)

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()

总结

SE模块引入了通道注意力机制,使得网络在学习过程中能够更加自适应地关注对任务有用的通道,抑制对任务无关的通道。这有助于提高网络的特征表达能力。当前也可以与各种其他的深度神经网络结构集成。因此,可以在不改变整体网络架构的情况下,通过引入通道注意力机制来增强网络性能。

相关推荐
程序员小远3 分钟前
软件测试常见Bug清单
自动化测试·软件测试·python·功能测试·测试工具·测试用例·bug
guoji778817 分钟前
安全与对齐的深层博弈:Gemini 3.1 Pro 安全护栏与对抗测试深度拆解
人工智能·安全
实在智能RPA25 分钟前
实在 Agent 和通用大模型有什么不一样?深度拆解 AI Agent 的感知、决策与执行逻辑
人工智能·ai
独隅30 分钟前
PyTorch 模型部署的 Docker 配置与性能调优深入指南
人工智能·pytorch·docker
lihuayong37 分钟前
OpenClaw 系统提示词
人工智能·prompt·提示词·openclaw
黑客说1 小时前
AI驱动剧情,解锁无限可能——AI游戏发展解析
人工智能·游戏
踩着两条虫1 小时前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
小仙女的小稀罕1 小时前
听不清重要会议录音急疯?这款常见AI工具听脑AI精准转译
开发语言·人工智能·python
书到用时方恨少!1 小时前
Python random 模块使用指南:从入门到精通
开发语言·python