深度学习——残差网络

目录

一、深层残差模型

1. 卷积批归一化块

2. 残差块

3. 残差网络

4. 训练

 二、浅层网络模型

1. 残差块

2. 残差网络

3. 训练:

三、改进版本

四、总结


在深度学习中,一般来说,神经网络层数越多,网络结构也就越复杂,对复杂特征的表示能力就更强,对于复杂的数据集,就有更强的预测能力。但是在实际上,增加神经网络的层数后,训练误差往往不降反升。

如上图我们理论上期望的曲线是绿色的“theory”,训练误差随网络层次的加深而不断降低。然而实践表明,实际的曲线应该是蓝色的曲线“reality”,在网络层次达到一定深度后,训练误差不但不会降低,反而会逐渐上升。

这是因为单纯的提示网络的深度会导致梯度在反向传播中损失信息,也就是梯度信息在从顶层反向传播到底层时会逐渐的消失,导致顶层参数更新的快,但底层几乎不会更新,但某一层停止学习,则信号就会在这一层直接消失,导致信息无法传递到底层,从而造成梯度消失的现象。此外,在深层的神经网络中如果某一层与原模型产生了较大的偏差,预测结果反而不如原模型,后续的网络会不断加剧这个问题,模型一直往错误的方向更新,导致“负优化”,如下图。

    为了解决上面的问题,我们可以使用残差网络ResNet,ResNet最初就是为了解决“负优化”问题的,但同时它也能解决梯度信息在反向传播过程中逐渐消失的问题。

    在学习残差块之前我们需要先了解BatchNorm层,这个层是用于对输出数据进行标准化处理的,以次来保证后续网络接收的输入数据是进行标准化处理后的。

一、深层残差模型

1. 卷积批归一化块

    虽然一般我们在输入数据时都会对数据进行标准化处理,但是在神经网络中,每层的参数都是在不断更新的,即使第一层已经处理过,在不断更新参数后数据中仍然可能会出现不稳定的数值,导致模型很难收敛,因此在每一层操作过后通常都需要进行标准化处理,常用的方法是批归一化(Batch Normalization),下面我们就定义一个卷积批归一化块,其中包含卷积层和BatchNorm层。

import paddle

import paddle.nn as nn



class ConvBNLayer(paddle.nn.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 filter_size,
                 stride=1,
                 groups=1,
                 act=None):     
        """

        num_channels, 卷积层的输入通道数

        num_filters, 卷积层的输出通道数

        stride, 卷积层的步幅

        groups, 分组卷积的组数,默认groups=1不使用分组卷积

        """
        super(ConvBNLayer, self).__init__()
        # 创建卷积层
        # 根据卷积运算的特点,卷积核的输入通道数与特征矩阵输入通道数是一样的,
        # 卷积核的输出通道数就是卷积层的输出通道数filter_size是过滤器大小,
        # 实际上在这里卷积核就是过滤器,过滤器大小就是卷积核大小。
        self._conv = nn.Conv2D(
            in_channels=num_channels,
            out_channels=num_filters,
            kernel_size=filter_size,
            stride=stride,
            padding=(filter_size - 1) // 2,
            groups=groups,
            bias_attr=False)

        # 创建BatchNorm层
        self._batch_norm = paddle.nn.BatchNorm2D(num_filters)
        self.act = act

    def forward(self, inputs):
        # 卷积层的前向传播就是进行卷积操作,不过这里为了使输出的数据的数值更加稳定,
        # 我们要对输出数据进行归一化操作。
        y = self._conv(inputs)
        y = self._batch_norm(y)
        if self.act == 'leaky':
            y = F.leaky_relu(x=y, negative_slope=0.1)
        elif self.act == 'relu':
            y = F.relu(x=y)
        return y

上述代码使用nn.Conv2D类来创建卷积层,输入输入输出通道数、卷积核大小、步幅、填充等参数来创建一个二维的卷积层。而BatchNorm则使用paddle.nn.BatchNorm2D来创建,输入的参数是模型的输出通道数(实际上就是卷积核的输出通道数)。

前向传播就是使用上面定义的卷积层和BatchNorm首先进行卷积操作,然后对输出进行批量归一化操作,act是网络最后进行的操作,一般是激活函数,根据输入的act调用不同的激活函数(leaky是在relu基础上改进的激活函数,可以缓解ReLU中可能出现的神经元“死亡”问题。)

2. 残差块

接下来就是定义残差块了,在此之前,我们先了解一下什么是残差块,以及它的实现步骤。

在一般的卷积神经网络中,网络的输出是输入数据的映射,即y=F(x),也就是输入数据进行卷积和激活函数后的输出,如下图。

而在残差网络中,输出的不单单是映射输出,而是y=F(x)+x,是输入数据的映射与输入数据之和,此时网络不是直接学习输出特征y的表示,而是学习残差,残差就是F(x),也就是y-x,即输出与输入之间的残差,如下图。

这样,每一层的网络都有输入与输出直连的旁路,也就相当于每一层都有与最顶层的损失直接对话的机会,这样损失就不会在自顶层向下反向传播时逐渐消失了。

在比较深层的网络中,复杂的参数通常导致非常高的计算复杂度,我们可以先将输入数据与1*1卷积核进行卷积运算来减少数据的通道数,因为每个通道都要进行一次卷积计算,减少数据的通道数就能显著减少计算的复杂度。

上图就是在进行卷积计算前先经过1*1卷积将通道数由256降低到64,进行卷积计算后,再进行一次1*1卷积将通道数恢复到256。

根据这个步骤,我们就可以定义残差块了,不过残差块中的卷积核的输出通道数与上图不一样。

按照上图,残差块中首先进行一次1*1卷积,降低一倍通道数,然后进行3*3卷积,最后1*1卷积恢复通道数,不过这里不是直接恢复到输入的通道数,而是输入的两倍,在3*3卷积前降低了一倍的通道数,因此在最后一个1*1卷积核的通道数是输入的4倍。

import paddle
import paddle.nn.functional as F

class BottleneckBlock(paddle.nn.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 stride,
                 shortcut=True):
        super(BottleneckBlock, self).__init__()

        # 创建第一个卷积层 1x1
        self.conv0 = ConvBNLayer(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=1,
            act='relu')

        # 创建第二个卷积层 3x3
        self.conv1 = ConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters,
            filter_size=3,
            stride=stride,
            act='relu')

        # 创建第三个卷积 1x1,但输出通道数乘以4
        self.conv2 = ConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters * 4,
            filter_size=1,
            act=None)

        # 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
        # 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if not shortcut:
            self.short = ConvBNLayer(
                num_channels=num_channels,
                num_filters=num_filters * 4,
                filter_size=1,
                stride=stride)
        self.shortcut = shortcut
        self._num_channels_out = num_filters * 4

    def forward(self, inputs):
        y = self.conv0(inputs)
        conv1 = self.conv1(y)
        conv2 = self.conv2(conv1)
        # 如果shortcut=True,直接将inputs跟conv2的输出相加
        # 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
        if self.shortcut:
            short = inputs
        else:
            short = self.short(inputs)

        y = paddle.add(x=short, y=conv2)
        y = F.relu(y)
        return y

上面的代码首先定义3个我们上面定义的卷积批归一化块,包含卷积、归一化、激活三个操作,第一个是1*1的卷积操作,用于减少通道数,降低计算复杂度,第二个块是正常的3*3的卷积操作提取数据特征,最后一个块是1*1卷积恢复通道数,如果此时输出结果与输入数据形状不相同的话需要对输入数据进行一次1*1卷积操作使其与conv2形状相同,否则无法将两个数组进行相加。前向传播的过程就是依次进行三次卷积批归一化操作,如果输入与最后卷积输出结果形状不同,再修改输入数据的形状,最后将输入数据与卷积输出结果相加得到此残差块的输出。

3. 残差网络

我们知道卷积网络就是多个卷积层、池化层与全连接层串联起来的网络,而残差网络也是类似的,残差网络是多个残差块串联起来的,这些残差块被分在不同的模块中,一共有4个模块,各个模块中各有几个一样的残差块,如下图展示了不同层数的残差网络各个模块的残存块和数量。

以50层为例,首先对输入进行7*7卷积,然后是3*3的最大池化层,接着就是4个模块,各个模块中的残差块各有3、4、6、3,各个残差块有各有3层卷积,最后再加一层全连接层,一共就是1+3*3+4*3+6*3+3*3=49个卷积层和1个全连接层,一共是50层。如下图。

观察不同层数的ResNet网络结构,我们能发现50、101、152层在同一个模块中都是使用同样的残差块,各个模块的残差块的最后两维的形状也是相同的,只是输入输出通道不同,因此我们可以定义一个支持50、101、152层的模型,不同层数的网络只是模块中的残差块数量不同而已,其他都是一样的。

接下来我们设计一个支持50、101、152层网络的残差网络模型。

# ResNet模型代码
import numpy as np
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
 
# 定义ResNet模型
class ResNet(paddle.nn.Layer):
    def __init__(self, layers=50, class_dim=1):
        """
        layers, 网络层数,可以是50, 101或者152
        class_dim,分类标签的类别数
        """
        super(ResNet, self).__init__()
        self.layers = layers
        supported_layers = [50, 101, 152]
        assert layers in supported_layers, \
            "supported layers are {} but input layer is {}".format(supported_layers, layers)

        if layers == 50:
            #ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块
            depth = [3, 4, 6, 3]

        elif layers == 101:
            #ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块
            depth = [3, 4, 23, 3]

        elif layers == 152:
            #ResNet152包含多个模块,其中第2到第5个模块分别包含3、8、36、3个残差块
            depth = [3, 8, 36, 3]
      
        # 残差块中使用到的卷积的输出通道数
        num_filters = [64, 128, 256, 512]

        # ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层
        self.conv = ConvBNLayer(
            num_channels=3,
            num_filters=64,
            filter_size=7,
            stride=2,
            act='relu')

        self.pool2d_max = nn.MaxPool2D(
            kernel_size=3,
            stride=2,
            padding=1)

        # ResNet的第二到第五个模块c2、c3、c4、c5
        self.bottleneck_block_list = []
        num_channels = 64
        for block in range(len(depth)):
            shortcut = False
            for i in range(depth[block]):
                # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1
                bottleneck_block = self.add_sublayer(
                    'bb_%d_%d' % (block, i),
                    BottleneckBlock(
                        num_channels=num_channels,
                        num_filters=num_filters[block],
                        stride=2 if i == 0 and block != 0 else 1,
                        shortcut=shortcut))

                num_channels = bottleneck_block._num_channels_out
                self.bottleneck_block_list.append(bottleneck_block)
                shortcut = True

        # 在c5的输出特征图上使用全局池化
        self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1)
        # stdv用来作为全连接层随机初始化参数的方差
        import math
        stdv = 1.0 / math.sqrt(2048 * 1.0)

        # 创建全连接层,输出大小为类别数目,经过残差网络的卷积和全局池化后,
        # 卷积特征的维度是[B,2048,1,1],故最后一层全连接的输入维度是2048
        self.out = nn.Linear(in_features=2048, out_features=class_dim,
                      weight_attr=paddle.ParamAttr(
                          initializer=paddle.nn.initializer.Uniform(-stdv, stdv)))

    def forward(self, inputs):
        y = self.conv(inputs)
        y = self.pool2d_max(y)
        for bottleneck_block in self.bottleneck_block_list:
            y = bottleneck_block(y)
        y = self.pool2d_avg(y)
        y = paddle.reshape(y, [y.shape[0], -1])
        y = self.out(y)
        return y

首先根据上图分别定义50、101、152层的4个模块中的残差块数量,保存在depth中。不同层数的网络只是模块中残差块的数量不同,各个模块里的残差块是相同的,因此我们只需要保存各个模块里的残差块数量就行了,不同层数的网络在同一个模块中都是使用同样残差块(仅针对50、101、152层网络而言,18、34层是没有1*1卷积操作的,因此残差块不同),而残差块都是1*1、3*3、1*1这三个卷积批归一化块组成,只是不同模块中输入输出通道数不同,我们可以将各个模块中的卷积输出通道数保存在num_filters中。

接下来首先定义7*7卷积,和3*3池化层,7*7卷积层使用的是之前我们定义的卷积批归一化块,池化层则使用nn.MaxPool2D,然后就是接下来的4个模块,根据depth中各个模块的数量在网络中添加残差块。残差块使用之前我们定义的BottleneckBlock类进行创建,卷积层的输出通道数就在num_filters中,再将残差块添加到bottleneck_block_list中。最后就是定义一层平均池化层和全连接层。

前向计算的过程就是依次通过各个层,首先是7*7卷积层和3*3池化层,接着是4个模块,4个模块中的残差块都在bottleneck_block_list中,直接从中依次取出残差块,传入输入数据进行计算,最后再通过一层平均池化层和全连接层out,输出结果。

4. 训练

飞浆开源框架支持高层API,高层API中支持paddle.vision.models接口,实现了对常用模型的封装,包括ResNet、VGG、MobileNet、LeNet等。我们尝试使用API中的ResNet50模型进行计算。

import paddle
from paddle.vision.models import resnet50

# 调用高层API的resnet50模型
model = resnet50()
# 设置pretrained参数为True,可以加载resnet50在imagenet数据集上的预训练模型
# model = resnet50(pretrained=True)


# 随机生成一个输入
x = paddle.rand([1, 3, 224, 224])
# 得到残差50的计算结果
out = model(x)
# 打印输出的形状,由于resnet50默认的是1000分类
# 所以输出shape是[1x1000]
print(out.shape)

这里只是尝试调用一下ResNet50模型,输出结果没有意义,因此只打印输出的形状,ResNet50默认是1000分类,因此输出结果的形状是1*1000。

现在我们使用paddle.vision中的ResNet模型进行训练

# 从paddle.vision.models 模块中import 残差网络,VGG网络,LeNet网络
from paddle.vision.models import resnet50, vgg16, LeNet
from paddle.vision.datasets import Cifar10
from paddle.optimizer import Momentum
from paddle.regularizer import L2Decay
from paddle.nn import CrossEntropyLoss
from paddle.metric import Accuracy
from paddle.vision.transforms import Transpose

# 确保从paddle.vision.datasets.Cifar10中加载的图像数据是np.ndarray类型
paddle.vision.set_image_backend('cv2')
# 调用resnet50模型
model = paddle.Model(resnet50(pretrained=False, num_classes=10))

# 使用Cifar10数据集
train_dataset = Cifar10(mode='train', transform=Transpose())
val_dataset = Cifar10(mode='test', transform=Transpose())
# 定义优化器
optimizer = Momentum(learning_rate=0.01,
                     momentum=0.9,
                     weight_decay=L2Decay(1e-4),
                     parameters=model.parameters())
# 进行训练前准备
model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))
# 启动训练
model.fit(train_dataset,
          val_dataset,
          epochs=50,
          batch_size=64,
          save_dir="./output",
          num_workers=8)

训练结果:

这是在飞浆平台上训练的结果,训练的时间非常的长,快接近一个小时了。

我们也可以使用飞浆的API训练我们自己定义的模型:

model = paddle.Model(ResNetDeep(class_dim=10))
train_dataset = Cifar10(mode='train', transform=Transpose())
val_dataset = Cifar10(mode='test', transform=Transpose())
# 定义优化器
optimizer = Momentum(learning_rate=0.01,
                     momentum=0.9,
                     weight_decay=L2Decay(1e-4),
                     parameters=model.parameters())
# 进行训练前准备
model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))
# 启动训练
model.fit(train_dataset,
          val_dataset,
          epochs=5,
          batch_size=64,
          save_dir="./output",
          num_workers=8)

由于训练时间实在太长,这里只截取5轮的训练结果,这是我在自己的电脑上训练的结果

第5轮训练结果:

 二、浅层网络模型

上面只实现了深层网络的瓶颈架构,浅层的18和34层的模型没有实现,现在我们来实现一下这两个模型。

适用于浅层网络的基础版的ResNet,支持18层和34层,这两种模型的ResNet中残差块都是由两个3*3的卷积层构成。在实现这个模型前,首先需要先实现18层和34层所需的残差块,这两种模型的残差块都是一样的,只需要实现一个就好。

卷积批归一化层还是用之前的。

1. 残差块

根据18、34层所用残差块可以定义残差块如下:

import paddle
import paddle.nn.functional as F
# 残差块(浅层基础版)
class ResidualBlock(paddle.nn.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 stride,
                 shortcut=True):
        super(ResidualBlock, self).__init__()
        # 创建第一个卷积层 3x3
        self.conv0 = ConvBNLayer(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=3,
            act='relu')
        # 创建第二个卷积层 3x3
        self.conv1 = ConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters,
            filter_size=3,
            stride=stride,
            act='None')

        # 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
        # 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if not shortcut:
            self.short = ConvBNLayer(
                num_channels=num_channels,
                num_filters=num_filters,
                filter_size=1,
                stride=stride)

        self.shortcut = shortcut

        self._num_channels_out = num_filters

    def forward(self, inputs):
        y = self.conv0(inputs)
        conv1 = self.conv1(y)

        # 如果shortcut=True,直接将inputs跟conv2的输出相加
        # 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
        if self.shortcut:
            short = inputs
        else:
            short = self.short(inputs)

        y = paddle.add(x=short, y=conv1)
        y = F.relu(y)
        return y

该类中定义了两层3*3卷积批归一化化块,第一层卷积批归一化块需要激活,但第二层不需要,因为激活是在与输出相加后进行的。为了防止最终计算的输出与输入不相同,导致无法相加,因此需要判断输入和输出的形状是否相同,如果不相同则需要使用1*1卷积改变输入数据的通道数使其与输出数据的通道数相同。前向计算就是依次经过两次卷积,最后再与输入相加,再激活得到最终结果。

2. 残差网络

接着是残差网络模型,根据下图定义18、34层的残差网络

# ResNet模型代码
import numpy as np
import paddle
import paddle.nn as nn
import paddle.nn.functional as F

           
# 定义ResNet模型(基础浅层网络)
class ResNetBaseline(paddle.nn.Layer):
    def __init__(self, layers=18, class_dim=1):
        """
        layers, 网络层数,可以是50, 101或者152
        class_dim,分类标签的类别数
        """
        super(ResNetBaseline, self).__init__()
        self.layers = layers
        supported_layers = [18, 34]
        assert layers in supported_layers, \
            "supported layers are {} but input layer is {}".format(supported_layers, layers)

        if layers == 18:
            #ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块
            depth = [2, 2, 2, 2]
        elif layers == 34:
            #ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块
            depth = [3, 4, 6, 3]
        
        # 残差块中使用到的卷积的输出通道数
        num_filters = [64, 128, 256, 512]

        # ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层
        self.conv = ConvBNLayer(
            num_channels=3,
            num_filters=64,
            filter_size=7,
            stride=2,
            act='relu')
        self.pool2d_max = nn.MaxPool2D(
            kernel_size=3,
            stride=2,
            padding=1)

        # ResNet的第二到第五个模块c2、c3、c4、c5
        self.bottleneck_block_list = []
        num_channels = 64
        for block in range(len(depth)):
            shortcut = False
            for i in range(depth[block]):
                # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1
                bottleneck_block = self.add_sublayer(
                    'bb_%d_%d' % (block, i),
                    ResidualBlock(
                        num_channels=num_channels,
                        num_filters=num_filters[block],
                        stride=2 if i == 0 and block != 0 else 1, 
                        shortcut=shortcut))
                num_channels = bottleneck_block._num_channels_out
                self.bottleneck_block_list.append(bottleneck_block)
                shortcut = True

        # 在c5的输出特征图上使用全局池化
        self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1)

        # stdv用来作为全连接层随机初始化参数的方差
        import math
        stdv = 1.0 / math.sqrt(512 * 1.0)
        
        # 创建全连接层,输出大小为类别数目,经过残差网络的卷积和全局池化后,
        # 卷积特征的维度是[B,2048,1,1],故最后一层全连接的输入维度是2048
        self.out = nn.Linear(in_features=512, out_features=class_dim,
                      weight_attr=paddle.ParamAttr(
                          initializer=paddle.nn.initializer.Uniform(-stdv, stdv)))

    def forward(self, inputs):
        y = self.conv(inputs)
        y = self.pool2d_max(y)
        for bottleneck_block in self.bottleneck_block_list:
            y = bottleneck_block(y)
        y = self.pool2d_avg(y)
        y = paddle.reshape(y, [y.shape[0], -1])
        y = self.out(y)
        return y

3. 训练:


# 从paddle.vision.models 模块中import 残差网络,VGG网络,LeNet网络
from paddle.vision.models import resnet50, vgg16, LeNet
from paddle.vision.datasets import Cifar10
from paddle.optimizer import Momentum
from paddle.regularizer import L2Decay
from paddle.nn import CrossEntropyLoss
from paddle.metric import Accuracy
from paddle.vision.transforms import Transpose
import paddle
from paddle.vision.models import resnet50
# 确保从paddle.vision.datasets.Cifar10中加载的图像数据是np.ndarray类型
paddle.vision.set_image_backend('cv2')
# 调用resnet50模型
model = paddle.Model(ResNetBaseline(class_dim=10))

# 使用Cifar10数据集
train_dataset = Cifar10(mode='train', transform=Transpose())
val_dataset = Cifar10(mode='test', transform=Transpose())
# 定义优化器
optimizer = Momentum(learning_rate=0.01,
                     momentum=0.9,
                     weight_decay=L2Decay(1e-4),
                     parameters=model.parameters())
# 进行训练前准备
model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5)))
# 启动训练
model.fit(train_dataset,
          val_dataset,
          epochs=5,
          batch_size=64,
          save_dir="./output",
          num_workers=8)

 这里还是只训练了5个周期

浅层网络学习的难度比较低,因此精度会比深层的好一些。

三、改进版本

ResNet后续版本中作者将残差块里的“卷积、批量归一化和激活”结构改成了“批量归一化,激活和卷积”,这个改进很容易实现,只需要在卷积批归一化块中更改卷积、批量归一化和激活为批量归一化,激活和卷积就行了。


import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.vision.models import resnet50, vgg16, LeNet
class NewConvBNLayer(paddle.nn.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 filter_size,
                 stride=1,
                 groups=1,
                 act=None):
       
        """
        num_channels, 卷积层的输入通道数
        num_filters, 卷积层的输出通道数
        stride, 卷积层的步幅
        groups, 分组卷积的组数,默认groups=1不使用分组卷积
        """
        super(NewConvBNLayer, self).__init__()

        # 创建卷积层
        # 根据卷积运算的特点,卷积核的输入通道数与特征矩阵输入通道数是一样的,
        # 卷积核的输出通道数就是卷积层的输出通道数filter_size是过滤器大小,
        # 实际上在这里卷积核就是过滤器,过滤器大小就是卷积核大小。
        self._conv = nn.Conv2D(
            in_channels=num_channels,
            out_channels=num_filters,
            kernel_size=filter_size,
            stride=stride,
            padding=(filter_size - 1) // 2,
            groups=groups,
            bias_attr=False)

        # 创建BatchNorm层
        self._batch_norm = paddle.nn.BatchNorm2D(num_filters)
        
        self.act = act

    def forward(self, inputs):
        y = self._conv(inputs)
        y = self._batch_norm(y)        
        if self.act == 'leaky':
            y = F.leaky_relu(x=y, negative_slope=0.1)
        elif self.act == 'relu':
            y = F.relu(x=y)
        y = self._conv(inputs)
        return y
    

class NewBottleneckBlock(paddle.nn.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 stride,
                 shortcut=True):
        super(NewBottleneckBlock, self).__init__()
        # 创建第一个卷积层 1x1
        self.conv0 = NewConvBNLayer(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=1,
            act='relu')
        # 创建第二个卷积层 3x3
        self.conv1 = NewConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters,
            filter_size=3,
            stride=stride,
            act='relu')
        # 创建第三个卷积 1x1,但输出通道数乘以4
        self.conv2 = NewConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters * 4,
            filter_size=1,
            act=None)

        # 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
        # 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if not shortcut:
            self.short = NewConvBNLayer(
                num_channels=num_channels,
                num_filters=num_filters * 4,
                filter_size=1,
                stride=stride)

        self.shortcut = shortcut

        self._num_channels_out = num_filters * 4

    def forward(self, inputs):
        y = self.conv0(inputs)
        conv1 = self.conv1(y)
        conv2 = self.conv2(conv1)
        # 如果shortcut=True,直接将inputs跟conv2的输出相加
        # 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
        if self.shortcut:
            short = inputs
        else:
            short = self.short(inputs)

        y = paddle.add(x=short, y=conv2)
        y = F.relu(y)
        return y
    

# ResNet模型代码
import numpy as np
import paddle
import paddle.nn as nn
import paddle.nn.functional as F

           
# 定义ResNet模型
class NewResNetDeep(paddle.nn.Layer):
    def __init__(self, layers=50, class_dim=1):
        """
        layers, 网络层数,可以是50, 101或者152
        class_dim,分类标签的类别数
        """
        super(NewResNetDeep, self).__init__()
        self.layers = layers
        supported_layers = [50, 101, 152]
        assert layers in supported_layers, \
            "supported layers are {} but input layer is {}".format(supported_layers, layers)

        if layers == 50:
            #ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块
            depth = [3, 4, 6, 3]
        elif layers == 101:
            #ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块
            depth = [3, 4, 23, 3]
        elif layers == 152:
            #ResNet152包含多个模块,其中第2到第5个模块分别包含3、8、36、3个残差块
            depth = [3, 8, 36, 3]
        
        # 残差块中使用到的卷积的输出通道数
        num_filters = [64, 128, 256, 512]

        # ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层
        self.conv = NewConvBNLayer(
            num_channels=3,
            num_filters=64,
            filter_size=7,
            stride=2,
            act='relu')
        self.pool2d_max = nn.MaxPool2D(
            kernel_size=3,
            stride=2,
            padding=1)

        # ResNet的第二到第五个模块c2、c3、c4、c5
        self.bottleneck_block_list = []
        num_channels = 64
        for block in range(len(depth)):
            shortcut = False
            for i in range(depth[block]):
                # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1
                bottleneck_block = self.add_sublayer(
                    'bb_%d_%d' % (block, i),
                    NewBottleneckBlock(
                        num_channels=num_channels,
                        num_filters=num_filters[block],
                        stride=2 if i == 0 and block != 0 else 1, 
                        shortcut=shortcut))
                num_channels = bottleneck_block._num_channels_out
                self.bottleneck_block_list.append(bottleneck_block)
                shortcut = True

        # 在c5的输出特征图上使用全局池化
        self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1)

        # stdv用来作为全连接层随机初始化参数的方差
        import math
        stdv = 1.0 / math.sqrt(2048 * 1.0)
        
        # 创建全连接层,输出大小为类别数目,经过残差网络的卷积和全局池化后,
        # 卷积特征的维度是[B,2048,1,1],故最后一层全连接的输入维度是2048
        self.out = nn.Linear(in_features=2048, out_features=class_dim,
                      weight_attr=paddle.ParamAttr(
                          initializer=paddle.nn.initializer.Uniform(-stdv, stdv)))

    def forward(self, inputs):
        y = self.conv(inputs)
        y = self.pool2d_max(y)
        for bottleneck_block in self.bottleneck_block_list:
            y = bottleneck_block(y)
        y = self.pool2d_avg(y)
        y = paddle.reshape(y, [y.shape[0], -1])
        y = self.out(y)
        return y

训练结果(5轮):

 

四、总结

        残差网络是通过将输出从输入的映射改为输入数据加上输入的映射,这样就从学习输出的特征y表示变为学习残差,这样就有一条旁路连接输入和输出,每一层都能直接与顶层的损失进行“对话”,以此解决了梯度在反向传播的过程中逐渐消失的问题。残差块是残差网络中的主要组成部分,残差块中的操作就是首先进行卷积再批归一化再激活,各种版本的残差网络都是分成几个模块,其中第2、3、4、5个模块中是多个残差块,根据层数的不同,各个模块中的残差块数量也有所不同,基础版的有18,34层的网络,每个残差块只进行两次3*3卷积,没有使用1*1卷积减少通道数,而深层网络中50,101,152层的网络,每个残差块进行三次卷积操作,第一层是1*1减少通道数,第二层是3*3卷积,第三层是1*1恢复通道数。

        由于网络模型层数很多,训练的时间也是非常的长,50层的训练就要耗费接近一个小时的时间进行训练,多个版本训练过去时间要消耗非常多,为了节省时间,后面几个版本我只训练了5个周期。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
残差块(Residual Block)是深度学习中的一种重要的网络结构,用于解决深层神经网络训练过程中的梯度消失和梯度爆炸问题。在TensorFlow中,可以通过自定义层或使用现有的库来实现残差块。 一个典型的残差块由两个主要部分组成:主路径(Main Path)和跳跃连接(Skip Connection)。主路径是指一系列的卷积层、批归一化层和激活函数层,用于提取输入数据的特征。跳跃连接是指将输入数据直接添加到主路径的输出上,以便在后续层中传递原始输入的信息。 以下是一个简单的残差块的TensorFlow实现示例: ```python import tensorflow as tf class ResidualBlock(tf.keras.layers.Layer): def __init__(self, filters, strides=1): super(ResidualBlock, self).__init__() self.conv1 = tf.keras.layers.Conv2D(filters, kernel_size=3, strides=strides, padding='same') self.bn1 = tf.keras.layers.BatchNormalization() self.relu = tf.keras.layers.ReLU() self.conv2 = tf.keras.layers.Conv2D(filters, kernel_size=3, strides=1, padding='same') self.bn2 = tf.keras.layers.BatchNormalization() self.downsample = tf.keras.Sequential([ tf.keras.layers.Conv2D(filters, kernel_size=1, strides=strides), tf.keras.layers.BatchNormalization() ]) def call(self, inputs, training=False): residual = inputs x = self.conv1(inputs) x = self.bn1(x, training=training) x = self.relu(x) x = self.conv2(x) x = self.bn2(x, training=training) if inputs.shape[-1] != x.shape[-1]: residual = self.downsample(inputs) x += residual x = self.relu(x) return x ``` 在这个示例中,我们定义了一个继承自`tf.keras.layers.Layer`的`ResidualBlock`类。在`__init__`方法中,我们定义了残差块的各个层,包括卷积层、批归一化层和激活函数层。在`call`方法中,我们实现了残差块的前向传播逻辑,包括主路径和跳跃连接的计算。 使用残差块时,可以将多个残差块堆叠在一起构成深层网络。这样可以有效地解决梯度消失和梯度爆炸问题,并提高网络的性能和训练效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值