一、复习:简单的串行CNN结构

CNN网络有点_笔记

        上一节讲到的CNN结构图示,其实这是一个相对比较简单的CNN,因为它的结构也仅仅只是把各个模块相互串联起来,一串到底。以下是对这个图示的解释(摘取至第10小节内容):

        输入图像的shape是(N,1,28,28),表示单通道,宽28,高28,N个样本组成的输入。

        卷积层1:(IC, OC, KW, KH)=(1, 10, 5, 5),表示输入通道为1,准备输出10个通道,卷积核为5*5,本次操作完毕后,得到一个(N,10,24,24)的输出,24=28-5/2-5/2。

        池化层1:(KS)=(2),表示使用2*2池化,得到(N,10,12,12)的输出。12=24/2

        卷积层2:(IC, OC, KW, KH)=(10, 20, 5, 5),表示输入通道为10,准备输出20个通道,卷积核为5*5,本次操作完毕后,得到一个(N,20,8,8)的输出,8=12-5/2-5/2。

        池化层2:(KS)=(2),表示使用2*2池化,得到(N,20,4,4)的输出。4=8/2

        线性层:将(N,20,4,4)的输入展平成(N,320),通过一系列线性层将其变换为(N,10)的输出。

       

二、复杂的CNN结构能带来哪些特点?

        简单的CNN模型和复杂的CNN模型在优缺点上可以说是相反的,但对于需求高,任务复杂且硬件资源充足的情况下,更适合用复杂的CNN进行模型的训练与测试。相反地,任务简单,需求不高而且资源紧缺的情况下,简单的CNN模型能够有较多的发挥空间。

        简单的CNN模型优点:

  1. 训练速度快:由于参数数量较少,相对较简单的架构可以更快地进行训练和推理。
  2. 内存消耗低:相对较少的层数和参数量使得内存消耗较小,在资源受限的情况下更容易部署和使用。
  3. 解释性好:由于结构简单,模型的每个组成部分都比较容易解释和理解。

        简单的CNN模型缺点:

  1. 表示能力有限:相对较简单的结构可能无法处理更复杂的任务或提取更高级的特征表示。
  2. 性能可能不如复杂模型:对于某些复杂的任务,简单模型的表现可能会逊于复杂模型。

        复杂的CNN模型优点:

  1. 表示能力强:使用更深、更复杂的结构可以捕捉更多细节和抽象特征,可以在更复杂的任务中获得更好的性能。
  2. 适应不同的数据:复杂模型通常具有更大的容量,可以更好地适应各种类型和规模的数据集。

        复杂的CNN模型缺点:

  1. 训练时间长:由于层数较多、参数较多,复杂模型的训练时间相对较长。
  2. 内存消耗大:由于参数量较多,复杂模型需要更多的内存进行训练和推理。
  3. 可解释性较差:由于结构复杂,模型的解释性较差,很难理解模型内部的具体运作方式。

三、复杂CNN案例-1:GoogleNet

0、前言背景

        我们可以先看一下GoogleNet的具体模型结构:

CNN网络有点_cnn_02

        输入在左,输出在右,且有多个可能的输出路线(经过Softmax层处理后的就是输出,可以从图中看出有经过相对少量中间层处理的输出,也有经过大量中间层处理的输出,具体采用什么路线输出结果要根据实际情况选择)。

        我们可以看到中间用到了大量的分支结构,以此来共同完成对某一个中间输入的处理。而且我们可以注意到这个网络大部分时候都在重复的使用着一个相同的模块,也就是图中框选的Inception Module。

        Inception Module的结构长这个样子:

CNN网络有点_笔记_03

        解释一下:Concatenate表示【沿着通道进行拼接的组合张量】

        可以看出:从输入(下方)到输出(上方),整个流程是如此进行的:

        输入经过4个分支进行4次处理,分别是:

        (1)先进行Average Pooling池化层操作,再进行1*1卷积层操作,输出通道为24

        (2)直接进行1*1卷积层操作,输出通道为16

        (3)先进行1*1卷积层操作,输出通道为16,再进行5*5卷积层操作,输出通道为24

        (4)先进行1*1卷积层操作,输出通道16,再进行两次3*3卷积层操作,输出通道为24

        最后,再进行4个分支执行下来的输出按照Channel方向拼凑在一起,成为新的组合张量。

【注意:平均池化与最大池化的区别】


1、平均池化和最大池化的操作都是针对指定窗口进行操作,只是在计算窗口内的值时采用了不同的方法。


2、平均池化(Average Pooling)会对窗口内的数值取平均值作为输出。它的作用是保留整体特征,并且对输入特征图进行降采样,减小特征图的尺寸。平均池化能够平滑图像中的噪声,使得模型对于位置的微小变化更加鲁棒,一定程度上可以提高模型的稳定性。


3、最大池化(Max Pooling)则会对窗口内的数值取最大值作为输出。它的作用是提取窗口内的最显著特征,通过选择最大值来表示该区域的特征。最大池化可以帮助模型更加关注输入特征图中最强烈的特征。它在某种程度上能够保留更多的细节信息,适用于一些需要较强边缘检测的任务。


4、因此,平均池化和最大池化的操作方式不同,分别用于不同的应用场景。在卷积神经网络中,这两种池化操作常常被用于特征提取和降采样的过程中,以减小特征图的空间维度和计算量。具体选择使用哪种池化方法会根据实际任务需求、数据特点和模型设计进行决策。

1、什么是1*1卷积核

        在上文的叙述中,我们发现在对输入的组合张量进行分支处理的时候,总是要经过一个叫做1*1卷积层的处理层。那么这个1*1卷积核是什么?

        按照前一小节对卷积运算的描述,我们不难知道1*1的卷积运算是这样算的:

CNN网络有点_cnn_04

2、1*1卷积核有什么用

        那么这样处理有啥好处呢,为什么要进行这个1*1卷积核运算?

        其实,1*1卷积核的作用很大,最主要的就是它可以直接改变输入的通道数,到指定的输出的通道数,用于降低运算量。

        如果你有一个192通道的28*28张量,直接经过5*5卷积核处理:需要计算1亿2042万2400次。(这里考虑了对输入的padding=2的操作)

CNN网络有点_卷积_05

        如果将一个192通道的28*28输入,经过一个1*1卷积核,变成了16通道的28*28输入,再通过5*5卷积核运算,输出为32通道的28*28,仅需要进行一千多万次计算。

       

CNN网络有点_cnn_06

        可以明显看出,在携带了1*1卷积核操作处理后,计算量会大幅度下降近90%,所以为什么要使用1*1卷积核,至少在此刻我们知道了使用它可以帮助我们节省大量的时间。

        虽然是节省了很多时间,但是1*1卷积核也不是没有缺陷,由于原来的通道是192,现在经过1*1卷积核后变成了16通道,这个过程会丢失部分信息和特征,如果说这个192通道的输入各个通道都八九分甚至十分的重要,那么经过1*1卷积核操作后,会使得模型不够精确。

        以下是对上面两种处理方式的比较:

(1)直接经过5*5卷积核处理的方式:

        计算量较大,需要进行一亿多次计算,但没有丢失输入通道中的任何信息。适用于对输入的每个通道都重要且需要进行全局关联性计算的情况。例如,当输入通道表示不同颜色的通道或不同特征提取器时,保留所有通道的信息可能更有意义。


(2)先通过11卷积核将通道数从192减少到16,再经过55卷积核处理的方式:

        计算量较小,只需进行一千多万次计算,但会丢失一部分输入通道的信息。适用于输入通道中的某些通道相对不重要或相似的情况。通过减少通道数,可以降低计算复杂度并提高效率,同时仍然保留一定的特征信息。


综上所述,选择哪种方式更有意义取决于具体应用场景和对输入通道的重要性。如果所有通道都对最终结果至关重要,那么直接经过5*5卷积核处理的方式可能更合适。如果有一些通道相对不重要,可以通过先减少通道数再进行卷积操作的方式来降低计算量。

        以下是对1*1卷积核的优缺点的阐述:

(1)降维和增加非线性:

        通过使用1*1卷积核,可以减少输入通道的数量(降维),从而降低计算复杂度和内存需求。同时,在1*1卷积中引入非线性激活函数,可以引入非线性变换,为模型提供更强的表示能力。


(2)特征提取和组合:

        1*1卷积核可以在通道维度上进行特征提取和组合,帮助模型学习更具表达力的特征表示。它可以通过调整通道之间的权重来控制不同通道的重要性,并提供一种有效的方式来融合不同通道的信息。


(3)网络设计和模型压缩:

        使用1*1卷积核可以灵活地设计网络架构,例如在深度残差网络中应用密集连接块(DenseNet)。通过1*1卷积核的适当应用,可以减少网络的参数量和计算成本,实现模型的压缩和加速。


        因此,使用1*1卷积核可以在减少计算负担的同时,引入非线性变换、扩展感受野、提取和组合特征,以及灵活设计网络架构。这些优点使得11卷积核在深度学习中得到广泛应用,并且在一些情况下能够取得更好的性能。

3、Inception Module的实现与封装

(1)第一分支

CNN网络有点_CNN网络有点_07

        

CNN网络有点_CNN网络有点_08

        1、上面一行写在模型类定义的init方法中,主要是构造对象,下面的写在forward方法中,主要是使用对象进行数据处理。

        2、需要注意的是:AveragePooling的调用方式有2种;

        一种是如上图所示的通过torch.nn.Functional as F这个模块中,直接在forward中调用F.avg_pool2d(),无需创建实例对象(所以不必在init中定义平均池化的实例对象);

        另外一种就是传统的调用,在torch.nn as nn这个模块中,通过nn.AvgPool2d()创建一个平均池化的实例对象(这个定义要写在init中),然后在forward中利用这个实例对象进行平均池化的处理操作。

  • self.branch_pool=nn.Conv2d(in_channels, 24, kernel_size=1)
  • 在模型的__init__方法中,创建了一个名为branch_pool的卷积层对象。这个卷积层是一个1x1的卷积核,将输入特征图的通道数变换为24。in_channels是输入特征图的通道数。
  • branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
  • 在模型的forward方法中,使用了F.avg_pool2d函数对输入x进行平均池化操作。kernel_size=3表示池化窗口大小为3x3,stride=1表示步幅为1,padding=1表示填充大小为1。
  • branch_pool = self.branch_pool(branch_pool)
  • 将上一步得到的branch_pool作为输入,通过之前定义的self.branch_pool卷积层对象进行卷积操作。这一步将对池化后的特征图进行1x1的卷积操作,以改变通道数。

(2)第二分支

CNN网络有点_笔记_09

CNN网络有点_深度学习_10

        第二分支比较简单,就是在init先定义一个卷积层,输出通道16,1*1卷积核;

        在forward中,直接使用这个卷积层对x进行处理即可。

(3)第三分支

CNN网络有点_深度学习_11

        

CNN网络有点_卷积_12

        有了上面两个数据处理分支的分析经验,第三分支也很容易理解,就是在init中定义两个卷积层,第一个输出通道为16,1*1卷积核,第二个输出通道为24,5*5卷积核,由于5*5卷积核会使得输入缩减2圈数据,因此要在卷积之前通过padding=2补足,保证输入输出的宽高一致。

        forward方法中,先用1*1卷积层对输入进行处理,然后再把这个输出作为5*5卷积层的输入进行处理即可。

(4)第四分支

CNN网络有点_cnn_13

CNN网络有点_卷积_14

        请根据以上三个分支的例子,自行分析第四分支的含义。

(5)将4个分支的子结果拼凑成一个新的Concatenate

CNN网络有点_深度学习_15

CNN网络有点_卷积_16

        具体操作为:

        (1)将4个子结果共同放在一个列表中,按顺序放置

        (2)然后通过torch.cat(),按照第一维度进行拼凑即可

        【注意1:第一维度其实就是Channel维度】

        【注意2:我们不难发现,输入经过这4个分支的处理后,输出变成了88通道】

        分支1:输出24通道

        分支2:输出16通道

        分支3:输出24通道

        分支4:输出24通道

        torch.cat按照通道维度拼凑,就是24+16+24+24=88维度

(6)对Inception Module的封装

CNN网络有点_笔记_17

4、代码实验与结论记录

        我们进行如下的模拟实验:

(输入)——(卷积层1)——(最大池化层1)——(激活层1)——(Inception1)——

(卷积层2)——(最大池化层2)——(激活层2)——(Inception2)

(展平操作)——(全连接层,记得每一线性层要进行一次激活层)——(输出)

import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.nn as nn
import torch.optim as optim

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
batch_size = 64

train_dataset = datasets.MNIST(root="dataset/mnist",
                               train=True,
                               download=False,
                               transform=transform)

train_loader = DataLoader(train_dataset,
                          shuffle=True,
                          batch_size=batch_size)

test_dataset = datasets.MNIST(root="dataset/mnist",
                              train=False,
                              download=False,
                              transform=transform)

test_loader = DataLoader(test_dataset,
                         shuffle=False,
                         batch_size=batch_size)


class Inception(nn.Module):
    def __init__(self, input_channels):
        super(Inception, self).__init__()

        # 第A分支:avgPool + 1*1卷积,输出24通道,其中avgPool后续直接用Functional调用
        self.branch_A_1x1 = nn.Conv2d(input_channels, 24, kernel_size=1)

        # 第B分支:1*1卷积,输出16通道
        self.branch_B_1x1 = nn.Conv2d(input_channels, 16, kernel_size=1)

        # 第C分支:1*1卷积 + 5*5卷积,输出16/24通道
        self.branch_C_1x1 = nn.Conv2d(input_channels, 16, kernel_size=1)
        self.branch_C_5x5 = nn.Conv2d(16, 24, kernel_size=5, padding=2)

        # 第D分支:1*1卷积 + 3*3卷积 + 3*3卷积,输出16/24/24通道
        self.branch_D_1x1 = nn.Conv2d(input_channels, 16, kernel_size=1)
        self.branch_D_3x3_1 = nn.Conv2d(16, 24, kernel_size=3, padding=1)
        self.branch_D_3x3_2 = nn.Conv2d(24, 24, kernel_size=3, padding=1)

    def forward(self, x):
        # A分支:先对输入进行平均池化,再进行1*1卷积
        branch_A = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
        branch_A = self.branch_A_1x1(branch_A)

        # B分支:直接对输入进行1*1卷积
        branch_B = self.branch_B_1x1(x)

        # C分支:先进行1*1卷积,再进行5*5卷积
        branch_C = self.branch_C_1x1(x)
        branch_C = self.branch_C_5x5(branch_C)

        # D分支:先进行1*1卷积,再进行2次3*3卷积
        branch_D = self.branch_D_1x1(x)
        branch_D = self.branch_D_3x3_1(branch_D)
        branch_D = self.branch_D_3x3_2(branch_D)

        outputs_list = [branch_A, branch_B, branch_C, branch_D]
        outputs_tensor = torch.cat(outputs_list, dim=1)
        return outputs_tensor


class InceptionNet(nn.Module):
    def __init__(self):
        super(InceptionNet, self).__init__()
        self.convo1 = nn.Conv2d(1, 10, kernel_size=5)
        self.convo2 = nn.Conv2d(88, 20, kernel_size=5)

        self.maxPool = nn.MaxPool2d(2)

        self.inception1 = Inception(input_channels=10)
        self.inception2 = Inception(input_channels=20)

        self.linear1 = torch.nn.Linear(1408, 1024)
        self.linear2 = torch.nn.Linear(1024, 512)
        self.linear3 = torch.nn.Linear(512, 256)
        self.linear4 = torch.nn.Linear(256, 128)
        self.linear5 = torch.nn.Linear(128, 64)
        self.linear6 = torch.nn.Linear(64, 10)

    def forward(self, x):
        batch_size = x.size(0)

        x = F.relu(self.maxPool(self.convo1(x)))
        x = self.inception1(x)

        x = F.relu(self.maxPool(self.convo2(x)))
        x = self.inception2(x)

        x = x.view(batch_size, -1)

        x = self.linear1(x)
        x = F.relu(x)

        x = self.linear2(x)
        x = F.relu(x)

        x = self.linear3(x)
        x = F.relu(x)

        x = self.linear4(x)
        x = F.relu(x)

        x = self.linear5(x)
        x = F.relu(x)

        y_pred = self.linear6(x)
        return y_pred


Inception_Net = InceptionNet()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
Inception_Net.to(device)

CELoss = torch.nn.CrossEntropyLoss()
SGD_optim = optim.SGD(Inception_Net.parameters(), lr=0.03, momentum=0.7)


def train(epoch):
    running_loss = 0.0

    for batch_index, data in enumerate(train_loader, 1):
        train_inputs, train_label = data
        train_inputs = train_inputs.to(device)
        train_label = train_label.to(device)

        SGD_optim.zero_grad()

        train_outputs = Inception_Net(train_inputs)
        loss = CELoss(train_outputs, train_label)

        loss.backward()
        SGD_optim.step()

        running_loss += loss.item()

        if batch_index % 300 == 0:
            print(f"第{epoch + 1}轮训练,批次{batch_index}, 平均损失值Loss = {running_loss / 300 :.7f} ")
            running_loss = 0.0


def test():
    test_correct_number = 0
    test_label_number = 0

    with torch.no_grad():
        for data in test_loader:
            test_inputs, test_label = data
            test_inputs = test_inputs.to(device)
            test_label = test_label.to(device)

            test_outputs = Inception_Net(test_inputs)

            _, predict_index = torch.max(test_outputs.data, dim=1)

            test_label_number += test_label.size(0)

            test_correct_number += (predict_index == test_label).sum().item()

    print(f"测试准确度:{test_correct_number / test_label_number * 100 :.4f} %")


if __name__ == '__main__':

    for epoch in range(10):
        train(epoch)
        test()

【实验数据记录】

第1轮训练,批次300, 平均损失值Loss = 2.3023215
第1轮训练,批次600, 平均损失值Loss = 2.1911459
第1轮训练,批次900, 平均损失值Loss = 0.3493767
测试准确度:95.3700 %
第2轮训练,批次300, 平均损失值Loss = 0.1271485
第2轮训练,批次600, 平均损失值Loss = 0.0972420
第2轮训练,批次900, 平均损失值Loss = 0.0826602
测试准确度:98.0000 %
第3轮训练,批次300, 平均损失值Loss = 0.0593733
第3轮训练,批次600, 平均损失值Loss = 0.0615803
第3轮训练,批次900, 平均损失值Loss = 0.0591467
测试准确度:98.5200 %
第4轮训练,批次300, 平均损失值Loss = 0.0425352
第4轮训练,批次600, 平均损失值Loss = 0.0384604
第4轮训练,批次900, 平均损失值Loss = 0.0414113
测试准确度:98.8800 %
第5轮训练,批次300, 平均损失值Loss = 0.0256039
第5轮训练,批次600, 平均损失值Loss = 0.0333928
第5轮训练,批次900, 平均损失值Loss = 0.0372288
测试准确度:98.7700 %
第6轮训练,批次300, 平均损失值Loss = 0.0273604
第6轮训练,批次600, 平均损失值Loss = 0.0262920
第6轮训练,批次900, 平均损失值Loss = 0.0283279
测试准确度:98.7300 %
第7轮训练,批次300, 平均损失值Loss = 0.0178627
第7轮训练,批次600, 平均损失值Loss = 0.0198222
第7轮训练,批次900, 平均损失值Loss = 0.0232860
测试准确度:98.9400 %
第8轮训练,批次300, 平均损失值Loss = 0.0178606
第8轮训练,批次600, 平均损失值Loss = 0.0183749
第8轮训练,批次900, 平均损失值Loss = 0.0169249
测试准确度:98.7600 %
第9轮训练,批次300, 平均损失值Loss = 0.0161164
第9轮训练,批次600, 平均损失值Loss = 0.0132412
第9轮训练,批次900, 平均损失值Loss = 0.0154626
测试准确度:98.8400 %
第10轮训练,批次300, 平均损失值Loss = 0.0148986
第10轮训练,批次600, 平均损失值Loss = 0.0112762
第10轮训练,批次900, 平均损失值Loss = 0.0178975
测试准确度:99.1700 %

【注意】如果在训练中途已经出现了最高点,之后的测试准确度却在下降时,可以先将训练停止住,避免过拟合,然后把本次实验的参数记录下来,如果后续调整参数后又出现了更高的准确率,那么继续记录,确保有一个准确度成长的数据备份。

四、复杂CNN案例-2:Residual Learning Network

0、前言背景

        我们利用很多所谓的卷积层,池化层,线性层,激活层去训练我们的模型,那么是不是这些层数越多越密,性能就越好呢?事实上并不是这样。

        有研究人员发现:对于一个模型而言,卷积层越多反而不一定性能越好。

CNN网络有点_CNN网络有点_18

        上图展示的就是对于CIFAR-10数据集,卷积层为20层的时候反而比卷积层为56层的情况性能好。原因可能是因为:梯度消失。

        所谓梯度消失,就是根据梯度公式:

        

CNN网络有点_卷积_19

        其中的grad随着层数的增多,在反向传播的时候,由于链式法则,这些grad可能会乘上大量的介于(0~1)之间的梯度,因此梯度在反向传播的时候越来越小,造成减号右边的值趋近于0,相当于梯度没有更新,或者说梯度和更新前的梯度基本上差不了多少。

        这就是Residual Learning Net想要解决的问题。

1、Residual Learning的原理

        如何解决梯度消失的问题?

        Residual Learning Net给出的以下解决方案,别称为跳分支。

CNN网络有点_笔记_20

        原理就是在经过权重层计算后,在输出结果之前追加一个输入x项,然后再进行输出,这样做的好处就是在反向传播对x求导的时候,追加项对x求导会变成1,即便F(x)对x求导后的值再小(哪怕小到趋近于0),也能保证整个H(x)对x的导数的结果也能趋近于1,这样就保证了grad(H(x))不再小于0,改善了梯度消失的问题。

        【注意】Residual Net的构建中也可以添加一两项卷积层和激活层,为的是让Residual Net具备非线性的表达能力,提高网络性能。

CNN网络有点_深度学习_21

        可以从上述的示意图看出,蓝色+黄色+绿色构成了一个基础的CNN层,在基础层输出到下一个基础层之前,进行了一个红色的Residual Block操作,以改善梯度消失问题。

2、Residual Learning Net的实现

CNN网络有点_笔记_22

这段代码定义了一个残差块(Residual Block),用于构建深度残差网络(Residual Learning Network)的基本组件。


【注意】残差块中加入两个卷积操作的目的是引入非线性变换,增加网络的表达能力。

 

具体分析:

  1. __init__ 方法中,初始化了 ResidualBlock 类,并定义了 input_channels(输入通道数)和两个卷积层 convo1convo2。这里使用了相同的参数 input_channels 来表示输入和输出的通道数,确保通道数一致。
  2. forward 方法中,实现了残差块的前向传播。首先,通过 F.relu 函数对第一个卷积层 convo1 的输出进行非线性激活,得到 y。然后,将 y 输入到第二个卷积层 convo2,得到输出 y。接下来,将输入 x 和输出 y 相加,并通过 F.relu 函数进行非线性激活,得到最终的输出。

这种设计的主要目的是引入残差连接(residual connection)。残差连接的思想是通过将输入直接与输出相加,可以帮助网络更好地传递梯度并缓解梯度消失的问题。在这个残差块中,通过两个卷积层和激活函数的组合,实现了对输入特征的非线性变换,并与原始输入进行相加操作,从而得到输出特征。

CNN网络有点_笔记_23

构建两个Residual Block对象,一个输入通道是16,一个输入通道是32,以匹配Conv1 / Conv2的输出 / 输入参数。接下来的操作与之前的基本一致。

 

在图中的示例,全连接层只用了一层(512到10),我们在实际的处理中,可以多设置几个线性层。

3、代码实验与结论记录

import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.nn as nn
import torch.optim as optim

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
batch_size = 64

train_dataset = datasets.MNIST(root="dataset/mnist",
                               train=True,
                               download=False,
                               transform=transform)

train_loader = DataLoader(train_dataset,
                          shuffle=True,
                          batch_size=batch_size)

test_dataset = datasets.MNIST(root="dataset/mnist",
                              train=False,
                              download=False,
                              transform=transform)

test_loader = DataLoader(test_dataset,
                         shuffle=False,
                         batch_size=batch_size)

# 定义残差块,用于构建残差深度神经网络,改善梯度消失问题
class ResidualBlock(nn.Module):
    def __init__(self, input_channels):
        super(ResidualBlock, self).__init__()

        self.channels = input_channels
        self.convo1 = nn.Conv2d(input_channels, input_channels, kernel_size=3, padding=1)
        self.convo2 = nn.Conv2d(input_channels, input_channels, kernel_size=3, padding=1)

    def forward(self, x):
        y = F.relu(self.convo1(x))
        y = self.convo2(y)
        y = F.relu(y + x)
        return y


class ResidualNet(nn.Module):

    def __init__(self):
        super(ResidualNet, self).__init__()
        self.convo1 = nn.Conv2d(1, 16, kernel_size=5)
        self.convo2 = nn.Conv2d(16, 32, kernel_size=5)

        self.maxPool = nn.MaxPool2d(2)

        self.residual_block_1 = ResidualBlock(input_channels=16)
        self.residual_block_2 = ResidualBlock(input_channels=32)

        self.linear1 = torch.nn.Linear(512, 256)
        self.linear2 = torch.nn.Linear(256, 128)
        self.linear3 = torch.nn.Linear(128, 64)
        self.linear4 = torch.nn.Linear(64, 10)

    def forward(self, x):
        batch_size = x.size(0)

        x = F.relu(self.maxPool(self.convo1(x)))
        x = self.residual_block_1(x)

        x = F.relu(self.maxPool(self.convo2(x)))
        x = self.residual_block_2(x)

        x = x.view(batch_size, -1)

        x = self.linear1(x)
        x = F.relu(x)

        x = self.linear2(x)
        x = F.relu(x)

        x = self.linear3(x)
        x = F.relu(x)

        y_pred = self.linear4(x)
        return y_pred


ResidualNet = ResidualNet()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
ResidualNet.to(device)

CELoss = torch.nn.CrossEntropyLoss()
SGD_optim = optim.SGD(ResidualNet.parameters(), lr=0.03, momentum=0.7)


def train(epoch):
    running_loss = 0.0

    for batch_index, data in enumerate(train_loader, 1):
        train_inputs, train_label = data
        train_inputs = train_inputs.to(device)
        train_label = train_label.to(device)

        SGD_optim.zero_grad()

        train_outputs = ResidualNet(train_inputs)
        loss = CELoss(train_outputs, train_label)

        loss.backward()
        SGD_optim.step()

        running_loss += loss.item()

        if batch_index % 300 == 0:
            print(f"第{epoch + 1}轮训练,批次{batch_index}, 平均损失值Loss = {running_loss / 300 :.7f} ")
            running_loss = 0.0


def test():
    test_correct_number = 0
    test_label_number = 0

    with torch.no_grad():
        for data in test_loader:
            test_inputs, test_label = data
            test_inputs = test_inputs.to(device)
            test_label = test_label.to(device)

            test_outputs = ResidualNet(test_inputs)

            _, predict_index = torch.max(test_outputs.data, dim=1)

            test_label_number += test_label.size(0)

            test_correct_number += (predict_index == test_label).sum().item()

    print(f"测试准确度:{test_correct_number / test_label_number * 100 :.4f} %")


if __name__ == '__main__':

    for epoch in range(10):
        train(epoch)
        test()

【数据记录】


第1轮训练,批次300, 平均损失值Loss = 0.8037440
第1轮训练,批次600, 平均损失值Loss = 0.1250832
第1轮训练,批次900, 平均损失值Loss = 0.0881807
测试准确度:97.7900 %
第2轮训练,批次300, 平均损失值Loss = 0.0586649
第2轮训练,批次600, 平均损失值Loss = 0.0545517
第2轮训练,批次900, 平均损失值Loss = 0.0521937
测试准确度:98.8000 %
第3轮训练,批次300, 平均损失值Loss = 0.0353681
第3轮训练,批次600, 平均损失值Loss = 0.0377920
第3轮训练,批次900, 平均损失值Loss = 0.0379578
测试准确度:98.7400 %
第4轮训练,批次300, 平均损失值Loss = 0.0240181
第4轮训练,批次600, 平均损失值Loss = 0.0299793
第4轮训练,批次900, 平均损失值Loss = 0.0317338
测试准确度:98.8600 %
第5轮训练,批次300, 平均损失值Loss = 0.0185465
第5轮训练,批次600, 平均损失值Loss = 0.0218752
第5轮训练,批次900, 平均损失值Loss = 0.0216743
测试准确度:99.1700 %
第6轮训练,批次300, 平均损失值Loss = 0.0138732
第6轮训练,批次600, 平均损失值Loss = 0.0182502
第6轮训练,批次900, 平均损失值Loss = 0.0171397
测试准确度:99.2400 %
第7轮训练,批次300, 平均损失值Loss = 0.0110176
第7轮训练,批次600, 平均损失值Loss = 0.0127779
第7轮训练,批次900, 平均损失值Loss = 0.0139847
测试准确度:99.2200 %
第8轮训练,批次300, 平均损失值Loss = 0.0121915
第8轮训练,批次600, 平均损失值Loss = 0.0122349
第8轮训练,批次900, 平均损失值Loss = 0.0114512
测试准确度:99.2300 %
第9轮训练,批次300, 平均损失值Loss = 0.0075874
第9轮训练,批次600, 平均损失值Loss = 0.0084685
第9轮训练,批次900, 平均损失值Loss = 0.0085938
测试准确度:99.3500 %
第10轮训练,批次300, 平均损失值Loss = 0.0061495
第10轮训练,批次600, 平均损失值Loss = 0.0075756
第10轮训练,批次900, 平均损失值Loss = 0.0074363
测试准确度:99.1200 %

 

可见:在近三次实验中,带残差块的CNN网络性能,明显比基础的CNN网络以及带Inception模块的CNN网络性能更好。