Pytorch搭建ResNet(附完整代码)

Pytorch搭建ResNet

1. 问题描述

1.1 ResNet

卷积神经网络逐渐取代传统算法,成为处理计算机视觉任务的核心,ResNet[1]的提出更是CNN图像史上一件里程碑事件。当网络深度增加时,网络准确度出现饱和甚至下降,残差学习的提出解决了网络退化的问题。ResNet通过shortcut的方法让信息跨层传播,被跨越的层拟合的就是shortcut连接的两层之间的残差。
Shortcut的具体方法如下:
存在 H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x
F ( x ) = 0 F(x)=0 F(x)=0时, H ( x ) H(x) H(x)就是恒等映射;
F ( x ) ≠ 0 F(x)\neq 0 F(x)=0时,就能在恒等映射的基础上补充学习残差。
本文的改进包括:

  • 调整残差块内部顺序为,先经过ReLU后再进行加和
  • 通过方法res_layer_maker动态地生成含有多个连续卷积核的残差块
  • 在网络中尝试加入Bottlenck结构节省计算开销,先压缩通道数,经过3x3卷积核后再提升通道数
  • 先使用Adam算法进行快速下降,再用SGD调优[5]

2. 数据集描述

2.1 CIFAR-10

CIFAR-10数据集由6万张尺寸大小为32x32的图片组成,其中图片分为10类,每类有6千张图片。这些图片中有5万张被划分为训练集,1万张被划分为测试集。
CIFAR-10

3. 实验结果与分析

3.1 ResNet

3.1.1 残差块

ResNet继承了VGG的经典网络结构,将其中细节部分替换为一个个残差块。其中,我设计的残差块分为图示两种:

残差块设计

这两种残差块相较原论文更改了与shortcut相加的位置,他们的区别是shortcut有无一个1x1的卷积核,该1x1卷积核主要用于跨层时匹配输入特征和输出特征的通道数。

3.1.2 Bottleneck

Bottleneck一般使用在较深的神经网络中,比如ResNet101。其主要目的是为了减少参数的数量,从而减少计算量。我搭建的Bottleneck功能为先压缩通道数,再提升通道数,具体架构如下图所示:
bottleneck设计

其中,首尾的两个1x1卷积核分别用于降低和升高特征通道数,使特征通道数经过Bottleneck后不发生改变。中间是一个常规3x3卷积核,每次通过卷积核后都进行数据归一化与ReLu激活处理。

3.1.3 架构图与描述

详细的网络架构图见文末附录,以下是使用基于LaTeX的PlotNeuralNet[4]绘制的可视化架构图:
架构图可视化

其中,特征先进入一个7x7的卷积核,步长取2,填充取3,经过BatchNorm与ReLU激活后,通过一个3x3的最大池化层,这是经典的VGG网络结构。
然后,特征通过8个设计结构相同的残差块,残差块的两种设计结构已经在上文中给出。当经过了6个残差块后,接上了一个Bottleneck,后续连接剩下的残差块。从我绘制的简化架构图中可以看出,特征在部分残差块中宽高减半,通道数加倍;在通过Bottleneck时,通道数发生压缩与提高,图片宽高不发生改变。
最后,特征通过一个自适应平均池化层将宽高尺寸压缩到1x1,经过Flatten后变成长度为512的向量,通过一个全连接层将输出降至10维,以对应10种分类。

3.1.4 损失函数变化

在模型训练过程中,我选取交叉熵作为损失函数。以下是训练100epochs的损失下降曲线:
下降曲线

第一次训练全程使用Adam优化器,第二次训练中先使用Adam训练50epochs至损失下降平缓处,后切换至SGD优化器训练50epochs。训练在RTX3060上进行,Adam训练50epochs耗时93分钟,SGD训练耗时68分钟,总耗时161分钟。使用Adam优化器比SGD在相同网络结构与损失函数下耗时增加30%左右。

3.1.5 可视化卷积层输出

模型完成训练后,选取数据集中的一架飞机图片作为输入,除shortcut外所有卷积核的输出可视化呈现如下:

卷积核可视化

从图中可以看出,conv1提取的是背景特征,conv2提取了飞机的上边缘轮廓,conv3提取了机身的大致轮廓。随着网络层的深入,后续卷积核提取的特征逐渐变得抽象。

3.1.6 测试正确率与计算耗时

从以下输出可以看出模型对船的分类准确率最高,对猫的分类准确率最低,对测试集的平均分类准确率为77.5%。

Accuracy for class: plane is 82.1 %
Accuracy for class: car   is 83.7 %
Accuracy for class: bird  is 72.8 %
Accuracy for class: cat   is 59.1 %
Accuracy for class: deer  is 73.0 %
Accuracy for class: dog   is 65.6 %
Accuracy for class: frog  is 85.0 %
Accuracy for class: horse is 80.5 %
Accuracy for class: ship  is 89.7 %
Accuracy for class: truck is 83.9 %
Accuracy for test data is 77.5 %

在网络中增加bottleneck结构,与之前采取相同的训练方案后,在测试集上的分类正确率几乎无变化,训练时间增加了10%。我认为bottleneck结构用在更深的网络中才能发挥出减少参数量、减少计算量的优点,本网络中计算耗时增加是因为网络较浅,新增卷积核需要的计算量多于其结构节省的计算量。
与网络上其它resnet相比,准确率过低,分析认为是对图片进行的预处理与图像变换过于简单(transforms)。

4. 引用

[1] He, Kaiming, et al. “Deep Residual Learning for Image Recognition.” [1512.03385] Deep Residual Learning for Image Recognition, 10 Dec. 2015, http://export.arxiv.org/abs/1512.03385.
[2] Goodfellow, Ian J., et al. “Generative Adversarial Networks.” ArXiv.org, 10 June 2014, https://arxiv.org/abs/1406.2661.
[3] Radford, Alec, et al. “Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks.” ArXiv.org, 7 Jan. 2016, https://arxiv.org/abs/1511.06434.
[4] Iqbal, Haris. “Harisiqbal88/PlotNeuralNet v1.0.0.” Zenodo, 25 Dec. 2018, https://zenodo.org/record/2526396.
[5] Bottou, Léon. “Stochastic Gradient Descent Tricks.” Lecture Notes in Computer Science, 2012, pp. 421–436., https://doi.org/10.1007/978-3-642-35289-8_25.

5. 附录

5.1 ResNet总架构图

ResNet总架构图

5.2 完整代码

import pandas as pd
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
import seaborn as sns
import time


class ResBlock(nn.Module):
    """
    残差块
    """
    def __init__(self, input_channels, output_channels, use_1x1conv=False, strides=1):
        """
        :param input_channels: 输入的通道数
        :param output_channels: 输出的通道数
        :param use_1x1conv: 是否使用1x1的卷积核[默认不使用]
        :param strides: 滑动步长
        """
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, output_channels, kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(output_channels, output_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, output_channels, kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(output_channels)
        self.bn2 = nn.BatchNorm2d(output_channels)

    def forward(self, x):
        y = F.relu(self.bn1(self.conv1(x)))
        y = F.relu(self.bn2(self.conv2(y)))
        if self.conv3:
            x = self.conv3(x)
        y = y + x
        return y


class BottleNeck(nn.Module):
    """
    Bottleneck瓶颈层
    """
    def __init__(self, input_channels, output_channels, use_1x1conv=False, strides=1):
        """
        :param input_channels: 输入的通道数
        :param output_channels: 输出的通道数
        :param use_1x1conv: 是否使用1x1的卷积核[默认不使用]
        :param strides: 滑动步长
        """
        super().__init__()
        mid_channels = input_channels
        if input_channels / 4 != 0:
            mid_channels = int(mid_channels / 4)
        self.conv1 = nn.Conv2d(input_channels, mid_channels, kernel_size=1, padding=0, stride=strides)
        self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(mid_channels, output_channels, kernel_size=1, padding=0, stride=strides)
        if use_1x1conv:
            self.conv4 = nn.Conv2d(input_channels, output_channels, kernel_size=1, stride=strides)
        else:
            self.conv4 = None
        self.bn1 = nn.BatchNorm2d(mid_channels)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.bn3 = nn.BatchNorm2d(output_channels)

    def forward(self, x):
        y = F.relu(self.bn1(self.conv1(x)))
        y = F.relu(self.bn2(self.conv2(y)))
        y = F.relu(self.bn3(self.conv3(y)))
        if self.conv4:
            x = self.conv4(x)
        y = y + x
        return y


class MyResNet(nn.Module):
    """
    残差网络
    """
    _loss = []
    _loss4epochs = []
    model_weights = []
    conv_layers = []

    def __init__(self):
        super().__init__()
        self._loss.clear()
        self._loss4epochs.clear()
        self.bl1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
                                 nn.BatchNorm2d(64), nn.ReLU(),
                                 nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        self.bl2 = nn.Sequential(*self.res_layer_maker(64, 64, 2, first_block=True))
        self.bl3 = nn.Sequential(*self.res_layer_maker(64, 128, 2))
        self.bl4 = nn.Sequential(*self.res_layer_maker(128, 256, 2))
        self.bottleneck = BottleNeck(256, 256)
        self.bl5 = nn.Sequential(*self.res_layer_maker(256, 512, 2))
        self.pool1 = nn.AdaptiveAvgPool2d((1, 1))
        self.flatten = nn.Flatten()
        self.linear = nn.Linear(512, 10)

    def forward(self, x):
        x = self.bl1(x)
        x = self.bl2(x)
        x = self.bl3(x)
        x = self.bl4(x)
        x = self.bottleneck(x)
        x = self.bl5(x)
        x = self.linear(self.flatten(self.pool1(x)))
        return x

    @staticmethod
    def res_layer_maker(input_channels, output_channels, num_residuals,
                        first_block=False):
        """
        调用残差块,对第一个残差块进行处理(1x1卷积核),并连接重复的残差块

        :param input_channels: 输入的通道数
        :param output_channels: 输出的通道数
        :param num_residuals: 残差块重复的次数
        :param first_block: 是否是第一个残差块
        :return: 处理好的残差块
        """
        blk = []
        for i in range(num_residuals):
            if i == 0 and not first_block:
                blk.append(ResBlock(input_channels, output_channels,
                                    use_1x1conv=True, strides=2))
            else:
                blk.append(ResBlock(output_channels, output_channels))
        return blk

    def train_net(self, dataloader, epochs=2, device=None, criterion='CrossEntropyLoss',
                  optimizer='SGD'):
        """
        训练网络

        :param dataloader: 加载数据集
        :param epochs: 训练的epochs数
        :param device: 运行在cpu或gpu
        :param criterion: 损失函数种类
        :param optimizer: 优化算法种类
        """
        self.train()  # 模型设置为训练模式
        if optimizer == 'SGD':
            optimizer = optim.SGD(self.parameters(), lr=0.001, momentum=0.9)
        elif optimizer == 'Adam':
            optimizer = optim.Adam(self.parameters(), lr=0.001)
        if criterion == 'CrossEntropyLoss':
            criterion = nn.CrossEntropyLoss()
        if not device:
            # 如果未给出device,就将第一个网络层所在位置设成device
            device = next(iter(self.parameters())).device
        self.to(device)
        start_time = time.perf_counter()
        for epoch in range(epochs):  # 对数据集循环多次
            running_loss = 0.0
            loss4epoch = []
            for i, data in enumerate(dataloader, 0):
                # 获取输入; data是[inputs, labels]的列表
                inputs, labels = data[0].to(device), data[1].to(device)

                # 清空梯度,否则梯度会累加
                optimizer.zero_grad()

                # 前向传播 + 反向传播 + 优化
                outputs = self(inputs)
                loss = criterion(outputs, labels)
                loss.backward()
                optimizer.step()
                # 打印统计
                running_loss += loss.item()
                loss4epoch.append(loss.item())
                if i % 2000 == 1999:  # 每2000 mini-batches打印损失
                    print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
                    self._loss.append(running_loss / 2000)
                    running_loss = 0.0
            self._loss4epochs.append(sum(loss4epoch) / len(loss4epoch))
            loss4epoch.clear()
        end_time = time.perf_counter()
        print('Finished Training')
        print('Training time: %s Seconds' % (end_time - start_time))

    def test_net(self, testloader, classes, device=None):
        """
        在测试集上测试网络

        :param testloader: 加载测试集
        :param classes: 类别的标签
        :param device: 训练在cpu或是gpu
        """
        self.eval()  # 设置为评估模式,节省资源开销
        dataiter = iter(testloader)
        if not device:
            # 如果未给出device,就将第一个网络层所在位置设成device
            device = next(iter(self.parameters())).device
        self.to(device)
        x, labels = dataiter.next()
        x, labels = x.to(device), labels.to(device)
        output = self(x)
        # 准备计算总正确率和各类正确率
        correct_pred = {classname: 0 for classname in classes}
        total_pred = {classname: 0 for classname in classes}
        correct = 0
        total = 0
        # 不需要计算梯度,节省资源
        with torch.no_grad():
            for data in testloader:
                images, labels = data
                images, labels = images.to(device), labels.to(device)
                outputs = net(images)
                energy, predictions = torch.max(outputs, 1)
                # 为每个类统计正确的预测数
                for label, prediction in zip(labels, predictions):
                    if label == prediction:
                        correct += 1
                        correct_pred[classes[label]] += 1
                    total += 1
                    total_pred[classes[label]] += 1

        # 打印每个类别的预测准确率
        for classname, correct_count in correct_pred.items():
            accuracy = 100 * float(correct_count) / total_pred[classname]
            print(f'Accuracy for class: {classname:5s} is {accuracy:.1f} %')
        total_accuracy = 100 * float(correct) / total
        print(f'Accuracy for test data is {total_accuracy:.1f} %')

    def save_net(self, path='./cifar_net.pth'):
        """
        保存网络训练结果

        :param path: 保存的路径
        """
        torch.save(self.state_dict(), path)

    def load_net(self, path=None):
        """
        加载已经训练好的网络

        :param path: 网络路径
        """
        if path:
            self.load_state_dict(torch.load(path))
        else:
            print("Path not given!")

    def draw_loss(self):
        """
        绘制损失函数下降曲线
        """
        # 按每2000 mini-batches绘制损失曲线
        s1 = pd.Series(self._loss, name="Loss")
        sns.set_theme(style='darkgrid')
        sns.lineplot(data=s1)
        plt.xlabel("Every 2000 mini-batches")
        plt.title("Loss Function Change Curve")
        plt.show()
        # 按epochs绘制Loss曲线
        s2 = pd.Series(self._loss4epochs, name="Loss")
        sns.set_theme(style='darkgrid')
        sns.lineplot(data=s2)
        plt.xlabel("Epochs")
        plt.title("Loss Function Change Curve")
        plt.savefig('Adam_loss.png')
        plt.show()

    def count_cov(self, device=None):
        """
        统计卷积核个数及信息

        :param device: 计算设备为CPU或GPU
        """
        if not device:
            # 如果未给出device,就将第一个网络层所在位置设成device
            device = next(iter(self.parameters())).device
        # 保存模型权重
        self.model_weights.clear()
        # 保存卷积层
        self.conv_layers.clear()
        # 把model children转为list
        model_children = list(self.children())
        # print(model_children)

        cnt = 0
        # 把所有卷积层及对应权重加入list中
        for i in range(len(model_children)):
            # 遍历最外层网络寻找卷积层
            if type(model_children[i]) == nn.Conv2d:
                cnt += 1
                self.model_weights.append(model_children[i].weight)
                self.conv_layers.append(model_children[i])
            # 遍历Sequential寻找卷积层
            elif type(model_children[i]) == nn.Sequential:
                for child in model_children[i].children():
                    if type(child) == nn.Conv2d:
                        cnt += 1
                        self.model_weights.append(child.weight)
                        self.conv_layers.append(child)
                    # 遍历ResBlock残差块寻找卷积层
                    elif type(child) == ResBlock:
                        for sub_child in child.children():
                            if type(sub_child) == nn.Conv2d:
                                # 如果是1x1卷积核(一般是shortcut)则跳过
                                if sub_child.kernel_size == (1, 1):
                                    continue
                                cnt += 1
                                self.model_weights.append(sub_child.weight)
                                self.conv_layers.append(sub_child)
            # 遍历Bottleneck寻找卷积层,代码仅在shortcut上无卷积核情况下有效
            elif type(model_children[i]) == BottleNeck:
                for child in model_children[i].children():
                    if type(child) == nn.Conv2d:
                        cnt += 1
                        self.model_weights.append(child.weight)
                        self.conv_layers.append(child)

        print(f"Total convolution layers: {cnt} (ignore conv on shortcut)")

    def draw_feature_map(self, input_path, output_path, test_transform, device=None):
        """
        卷积层输出可视化

        :param input_path: 输入的图片的路径
        :param device: 在cpu或是gpu上运行网络
        """
        if not device:
            # 如果未给出device,就将第一个网络层所在位置设成device
            device = next(iter(self.parameters())).device
        self.to(device)
        if len(self.conv_layers) == 0:
            self.count_cov()
        from PIL import Image
        image = Image.open(input_path)
        image = test_transform(image)
        # print(f"Image shape before: {image.shape}")
        image = image.unsqueeze(0)
        # print(f"Image shape after: {image.shape}")
        image = image.to(device)
        outputs = []
        names = []

        # print(self.conv_layers[0:])
        cnt = 0
        # 将数据输入卷积核,并获取输出
        for layer in self.conv_layers[0:]:
            cnt += 1
            image = layer(image)
            outputs.append(image)
            names.append(str(layer))

        processed = []
        # 压缩并去除颜色维度
        for feature_map in outputs:
            feature_map = feature_map.squeeze(0)
            gray_scale = torch.sum(feature_map, 0)
            gray_scale = gray_scale / feature_map.shape[0]
            processed.append(gray_scale.data.cpu().numpy())

        fig = plt.figure(figsize=(30, 50))
        for i in range(len(processed)):
            a = fig.add_subplot(5, 4, i + 1)
            # 将数组的值以图片的形式展示出来
            plt.imshow(processed[i])
            a.axis("off")
            a.set_title(names[i].split('(')[0] + '(' + str(i + 1) + ')', fontsize=30)
        plt.savefig(output_path + str('feature_maps.jpg'), bbox_inches='tight')


if __name__ == '__main__':
    # 串联多个图片变换的操作,对RGB图像先转为张量,后做归一化
    transform = transforms.Compose(
        [transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    batch_size = 4
    # 如果cuda加速可用,则使用cuda加速,否则使用CPU
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

    # 加载训练集
    train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0)
    # 加载测试集
    test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0)
    # 设置分类标签
    classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # 实例化网络
    net = MyResNet()
    # 用Adam训练网络
    net.train_net(train_loader, device=device, epochs=50, optimizer='Adam')
    # 保存训练好的参数
    net.save_net(path='./cifar_ResNet_Adam_100epochs.pth')
    net.draw_loss()
    # 加载网络
    net.load_net(path='./cifar10_ResNet_bottleneck_Adam-SGD_100epochs.pth')
    # 用SGD训练网络
    net.train_net(train_loader, device=device, epochs=50, optimizer='SGD')
    net.save_net(path='./cifar10_ResNet_bottleneck_Adam-SGD_100epochs.pth')
    # 画损失图
    net.draw_loss()
    # 在测试集上测试网络
    net.test_net(test_loader, classes, device=device)
    # 卷积核输出可视化
    net.draw_feature_map(input_path='./airplane7.png', output_path='',
                         test_transform=transform, device=device)

5.3 演示PPT

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 21
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
PyTorchResNet是一种深度学习模型,用于图像分类任务。它基于残差网络结构,通过使用残差块(BasicBlock)来解决深度神经网络中的梯度消失和模型退化问题。 在编写ResNet的前向传播函数时,我们需要定义模型的输入和输出流程。通常,我们会先进行一些预处理,例如卷积、归一化和激活函数等操作,然后通过多个残差块进行特征提取和降维,最后使用全局平均池化层和全连接层进行分类。 为了使用ResNet模型,我们需要首先初始化网络模型并载入预训练参数。通常情况下,我们可以使用迁移学习的方法,即使用在大规模数据集上预训练好的权重,然后在自己的任务上微调模型。我们可以从网络上下载预训练好的权重,并使用`load_state_dict`函数来加载权重。在此过程中,我们一般会把模型的参数设置为不需要梯度更新,这样可以加速模型的运行。 综上所述,PyTorchResNet模型使用残差块来解决深度神经网络的问题,定义了前向传播函数来指定数据流程,同时可以通过迁移学习的方法来加载预训练好的权重进行模型初始化。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Pytorch搭建ResNet](https://blog.csdn.net/x1027105273/article/details/123466082)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值