深度学习----------------------残差网络ResNet

ResNet

在这里插入图片描述




加更多的层总是改进精度吗?

在这里插入图片描述

虽然 F 6 F_6 F6这个模型更加复杂了,但是实际上可能学偏了,学到的可能不如小模型的最优点近。

那么应该怎么做呢?
就是说每一层增加模型的复杂度,将包含前一层模型(即: F 2 F_2 F2包含 F 1 F_1 F1)。如右边图。所以学到的模型比之前的模型更大,所以不会比之前的更差。(函数的大小代表着它的复杂程度)




残差块

串联一个层改变函数类,我们希望能扩大函数类
残差块加入快速通道(右边)来得到f(x)=x+g(x)的结构

在这里插入图片描述




ResNet块细节

在这里插入图片描述
1×1的卷积是为了变换通道,为了最后能够进行相加。

假设卷积层要对通道进行变换,那么没加1×1的卷积就加不回去了。加入了1×1的卷积来把通道数变到合适的范围能够按照原数相加。

在这里插入图片描述




不同的残差块

在这里插入图片描述




ResNet块

高宽减半ResNet块(步幅2)
后接多个高宽不变ResNet块

虚线框的叫ResNet块。
在这里插入图片描述

进入第一步将高宽减半,然后通道数增加1倍。右边的1×1卷积将输入的通道数也增加1倍。

在这里插入图片描述




ResNet架构

类似VGG和GoogleNet的总体框架,但替换成了ResNet块。

在这里插入图片描述

下面的152是指包含152个卷积层,层数越少越快。
在这里插入图片描述




总结

    ①残差块使得很深的网络更加容易训练
        甚至可以训练一千层的网络

    ②残差网络对随后的深度神经网络设计产生了深远影响,无论是卷积类网络还是全连接类网络




ResNet代码实现

残差块

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
 
 
class Residual(nn.Module):  #@save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        # 可以设置stride=2
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        # 第一个参数,这里它接收的是self.conv1的输出通道数,意味着self.conv2的输入通道数与self.conv1的输出通道数相同。
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        # 如果存在一个额外的卷积层,它主要用于调整输入X的通道数,使其与self.conv2的输出通道数相匹配
        if use_1x1conv:
            # 可以设置stride=2
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        # 定义两个批归一化层self.bn1和self.bn2,用来加速训练过程。
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True) # inplace原地操作,不创建新变量,对原变量操作,节约内存

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



输入和输出形状一致

blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape)

结果:
在这里插入图片描述




该部分总代码

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Residual(nn.Module):  # @save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        # 可以设置stride=2
        # 这里对应ResNet块的细节,有两个3×3的卷积层和批量归一化层、两个激活函数(这里使用原地操作)
        # 输入通道是3,输出通道也是3,卷积核为3×3,填充为1,步幅为1
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        # 第一个参数,这里它接收的是self.conv1的输出通道数,意味着self.conv2的输入通道数与self.conv1的输出通道数相同。
        # 这个卷积层的输入为上个卷积层的输出即为3,输出通道为3,卷积核为3×3,填充为1
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        # 如果存在一个额外的卷积层,它主要用于调整输入X的通道数,使其与self.conv2的输出通道数相匹配
        if use_1x1conv:
            # 可以设置stride=2
            # 这里的输入通道为3,输出通道也是3,卷积核为1×1,步幅为1
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        # 定义两个批归一化层self.bn1和self.bn2,用来加速训练过程。
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True)  # inplace原地操作,不创建新变量,对原变量操作,节约内存

    def forward(self, X):
        # 这里第一个3×3卷积经过批量归一化然后经过激活函数
        Y = F.relu(self.bn1(self.conv1(X)))
        # 这里是第二个3×3的卷积经过批量归一化
        Y = self.bn2(self.conv2(Y))
        # 这个判断这个ResNet块是否有1×1的卷积(1×1的卷积是为了变换通道,为了最后能够进行相加。)
        if self.conv3:
            X = self.conv3(X)
        Y += X
        # 使用激活函数引入非线性,并准备作为下一个网络层的输入。
        return F.relu(Y)


blk = Residual(3, 3)
# 代表了一个批量大小为4的数据集,每个数据项有3个通道(例如,RGB图像),且每个通道的高度和宽度都是6。
X = torch.rand(4, 3, 6, 6)
print(X)
# 将输入张量X通过定义的残差块blk进行前向传播,以提取和增强特征表示,并将这些特征表示传递给网络的后续部分。
Y = blk(X)
print(Y.shape)




增加输出通道数的同时,减半输出的高和宽

# 输入通道数为3,输出通道数为6,原先是3将它增加到6则输出的高和宽将会减半
blk = Residual(3,6, use_1x1conv=True, strides=2)
print(blk(X).shape)

结果:
在这里插入图片描述




ResNet模型

ResNet:

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

    ResNet的前两层跟之前介绍的GoogLeNet中的一样:

# 第一个卷积层接受1个通道(例如灰度图像)的输入,并输出64个特征图(或称为通道)。
# 在输出通道数为64、步幅为2的 7×7 卷积层后,接步幅为2的 3×3 的最大池化层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
                   #减少特征图的高度和宽度,并通过1的填充来稍微保持空间信息。
                   

    GoogLeNet在后面接了4个由Inception块组成的模块。
     ResNet则使用4个残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2最大池化层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。

# resnet_block函数用于生成一个阶段的残差块列表。第三个参数是残差块的数量,后面我们都使用的是2
def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []
    # 在循环中,对于每个残差块,如果它不是阶段中的第一个块且不是第一个阶段的第一个块(通过not first_block判断),则使用strides=2的Residual来减半特征图的高度和宽度。
    for i in range(num_residuals):
    	# 这里的first_block就是b2,b2作为第一阶段
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk

问题:预备阶段b1高宽减半,第一阶段b2第一块为什么不减少高宽,第二阶段b3,第三阶段b4,第四阶段b5又减少高宽了呢?
由于b1(b1通常不被视为一个单独的“阶段”,而是作为网络的预处理部分)已经完成了特征图的尺寸减少。
b2作为第一个阶段,其第一个残差块不会改变特征图的尺寸,因为前面已经减半已经很多了。
b3、b4、b5等后续阶段继续减少特征图尺寸是为了适应网络结构的深层设计、减少计算量。

    接着在ResNet加入所有残差块,这里每个模块使用2个残差块。


# b2作为第一个阶段,其第一个残差块不会改变特征图的尺寸(因为first_block=True)。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

    最后,与GoogLeNet一样,在ResNet中加入全局平均池化层,以及全连接层输出。

net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))

    每个模块有4个卷积层(不包括恒等映射的 1×1 卷积层)。加上第一个 7×7 卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。如下图:

在这里插入图片描述
    通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。 虽然ResNet的主体架构跟GoogLeNet类似,但ResNet架构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。




观察ResNet中不同模块的输入形状是如何变化的

X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

结果:
批量大小、通道数、高度、宽度

在这里插入图片描述
①第一步经过一个conv,第二步经过一个max pooling其中步幅都为2,所以224/2/2=56。

②没有进行高宽减半所以没有变化

③进行减半(strides=2)

④进行减半(strides=2)

⑤进行减半(strides=2)

⑥AdaptiveAvgPool2d设置的高宽为1×1

⑦经过展平层,只剩下参数批量大小和所有通道数的总和。

⑧固定输出为10,即:只有10种类别




该部分总代码

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Residual(nn.Module):  # @save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        # 可以设置stride=2
        # 这里对应ResNet块的细节,有两个3×3的卷积层和批量归一化层、两个激活函数(这里使用原地操作)
        # 输入通道是3,输出通道也是3,卷积核为3×3,填充为1,步幅为1
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        # 第一个参数,这里它接收的是self.conv1的输出通道数,意味着self.conv2的输入通道数与self.conv1的输出通道数相同。
        # 这个卷积层的输入为上个卷积层的输出即为3,输出通道为3,卷积核为3×3,填充为1
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        # 如果存在一个额外的卷积层,它主要用于调整输入X的通道数,使其与self.conv2的输出通道数相匹配
        if use_1x1conv:
            # 可以设置stride=2
            # 这里的输入通道为3,输出通道也是3,卷积核为1×1,步幅为1
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        # 定义两个批归一化层self.bn1和self.bn2,用来加速训练过程。
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True)  # inplace原地操作,不创建新变量,对原变量操作,节约内存

    def forward(self, X):
        # 这里第一个3×3卷积经过批量归一化然后经过激活函数
        Y = F.relu(self.bn1(self.conv1(X)))
        # 这里是第二个3×3的卷积经过批量归一化
        Y = self.bn2(self.conv2(Y))
        # 这个判断这个ResNet块是否有1×1的卷积(1×1的卷积是为了变换通道,为了最后能够进行相加。)
        if self.conv3:
            X = self.conv3(X)
        Y += X
        # 使用激活函数引入非线性,并准备作为下一个网络层的输入。
        return F.relu(Y)


# resnet_block函数用于生成一个阶段的残差块列表。第三个参数是残差块的数量,程序下面使用的都是2个数量
def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
    blk = []
    # 在循环中,对于每个残差块,如果它不是阶段中的第一个块且不是第一个阶段的第一个块(通过not first_block判断),则使用strides=2的Residual来减半特征图的高度和宽度。
    for i in range(num_residuals):
        # 这里的first_block就是b2,b2作为第一阶段
        # i == 0检查残差块是否是序列中的第一个,not first_block检查是不是整个网络的第一阶段
        # 如果当前正在处理的是序列中的第一个残差块,并且这个序列不是整个网络的第一阶段,那么执行接下来的代码块。
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            # 这里是不是有一个小问题,在第一阶段,假设b2输入通道和输出通道不相等,那么这行代码是不是就不对了
            # 因为b2的序列第一个输入通道应该等于(input_channels,num_channels),而不是(num_channels,num_channels),然后序列的第二个才是(num_channels,num_channels)
            blk.append(Residual(num_channels, num_channels))
    return blk

# 对应图片中底部的三个层(还没有用残差块)
# 第一个卷积层接受1个通道(例如灰度图像)的输入,并输出64个特征图(或称为通道)。
# 在输出通道数为64、步幅为2的 7×7 卷积层后,经过归一化后接步幅为2的 3×3 的最大池化层。
# (224-7+6)/2+1=112
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   # (112-3+2)/2+1=56
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b2作为第一个阶段,其第一个残差块不会改变特征图的尺寸,即:输入通道等于输出通道(因为first_block=True)。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1, 1)),
                    nn.Flatten(), nn.Linear(512, 10))
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__, 'output shape:\t', X.shape)

在这里插入图片描述




训练模型

lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

结果:
在这里插入图片描述




该部分总代码

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Residual(nn.Module):  # @save
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        # 可以设置stride=2
        # 这里对应ResNet块的细节,有两个3×3的卷积层和批量归一化层、两个激活函数(这里使用原地操作)
        # 输入通道是3,输出通道也是3,卷积核为3×3,填充为1,步幅为1
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        # 第一个参数,这里它接收的是self.conv1的输出通道数,意味着self.conv2的输入通道数与self.conv1的输出通道数相同。
        # 这个卷积层的输入为上个卷积层的输出即为3,输出通道为3,卷积核为3×3,填充为1
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        # 如果存在一个额外的卷积层,它主要用于调整输入X的通道数,使其与self.conv2的输出通道数相匹配
        if use_1x1conv:
            # 可以设置stride=2
            # 这里的输入通道为3,输出通道也是3,卷积核为1×1,步幅为1
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        # 定义两个批归一化层self.bn1和self.bn2,用来加速训练过程。
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True)  # inplace原地操作,不创建新变量,对原变量操作,节约内存

    def forward(self, X):
        # 这里第一个3×3卷积经过批量归一化然后经过激活函数
        Y = F.relu(self.bn1(self.conv1(X)))
        # 这里是第二个3×3的卷积经过批量归一化
        Y = self.bn2(self.conv2(Y))
        # 这个判断这个ResNet块是否有1×1的卷积(1×1的卷积是为了变换通道,为了最后能够进行相加。)
        if self.conv3:
            X = self.conv3(X)
        Y += X
        # 使用激活函数引入非线性,并准备作为下一个网络层的输入。
        return F.relu(Y)


# resnet_block函数用于生成一个阶段的残差块列表。第三个参数是残差块的数量,程序下面使用的都是2个数量
def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
    blk = []
    # 在循环中,对于每个残差块,如果它不是阶段中的第一个块且不是第一个阶段的第一个块(通过not first_block判断),则使用strides=2的Residual来减半特征图的高度和宽度。
    for i in range(num_residuals):
        # 这里的first_block就是b2,b2作为第一阶段
        # i == 0检查残差块是否是序列中的第一个,not first_block检查是不是整个网络的第一阶段
        # 如果当前正在处理的是序列中的第一个残差块,并且这个序列不是整个网络的第一阶段,那么执行接下来的代码块。
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            # 这里是不是有一个小问题,在第一阶段,假设b2输入通道和输出通道不相等,那么这行代码是不是就不对了
            # 因为b2的序列第一个输入通道应该等于(input_channels,num_channels),而不是(num_channels,num_channels),然后序列的第二个才是(num_channels,num_channels)
            blk.append(Residual(num_channels, num_channels))
    return blk

# 对应图片中底部的三个层(还没有用残差块)
# 第一个卷积层接受1个通道(例如灰度图像)的输入,并输出64个特征图(或称为通道)。
# 在输出通道数为64、步幅为2的 7×7 卷积层后,经过归一化后接步幅为2的 3×3 的最大池化层。
# (224-7+6)/2+1=112
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   # (112-3+2)/2+1=56
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b2作为第一个阶段,其第一个残差块不会改变特征图的尺寸,即:输入通道等于输出通道(因为first_block=True)。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1, 1)),
                    nn.Flatten(), nn.Linear(512, 10))
# X = torch.rand(size=(1, 1, 224, 224))
# for layer in net:
#     X = layer(X)
#     print(layer.__class__.__name__, 'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

在这里插入图片描述

在这里插入图片描述




问题

①为什么LeNet 的batch size大于1000收敛会比较慢?
batch size等于1000的时候,里面大部分图片都是很相似的图片,所以重复的图片在重复计算,所以会导致收敛进度。

②f(x)=x+g(x),这样就能保证至少不会变坏吗?如果g(x)不是变好,而是变坏呢?
训练g(x),模型发现g(x)很难训练或者训练没什么好处的话,它就拿不到梯度。加上g(x)对loss下降没什么明显的话,它在做梯度反传的时候拿不到什么梯度,所以它的权重不会被更新,很可能是一个很小的权重,所以不会有什么贡献。(有梯度下降在,最次就是0作用,不可能负作用。)

③ResNet实现里最后* ResNet_Block里 * 号是什么意思?
* 是把python中的list展开,ResNet_Block是一个list,* 号就是把list展开成一堆的输入。




ResNet为什么能训练出1000层的模型?

ResNet是怎么处理梯度消失使得训练出1000层的模型?
避免梯度消失(方法一):将乘法变加法,ResNet就是这么做的。

x是输入,y是输出,f其中的变换(例:10个卷积层的样子)。里面有个w是要进行更新的,计算梯度的话

在这里插入图片描述
在这里插入图片描述
假设在这个网络上面的一层再加一层会怎么样?
假设f为10层卷积层,这个g的意思是在这10层上面再加10层。

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

假设新加的层拟合能力比较强的话,下面这个会很快变得特别小。可以简单认为它的导数等价于你的预测值和真实值之间的差别是有一定的关系的。假设预测的比较好的情况下,则下面的值会变得比较小。如果它很小的话,一个很小的值乘之前的梯度,那么梯度会比之前小很多。梯度小很多的话,要么增大学习率(但很有可能增大也没有太多用,因为不能增的太大,因为这是对于靠近数据底部层的更新,如果增的太大的话可能会不稳定。)
在这里插入图片描述

ResNet是这样解决的

y"=本来来自于下一层网络的输入在加上g(f(x))

在这里插入图片描述

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

在这里插入图片描述
假设这个值很小的话
在这里插入图片描述
最差也等于下面这个
在这里插入图片描述

ResNet能够有效训练深层(1000层)的关键是:将乘法运算改为加法运算(残差连接),从而避免底层的梯度消失,使得最靠近数据的层的权重也能够获得较大的梯度(大数+小数=大数,大数×小数=小数),深层网络得以有效更新。




问题

学习率可不可以让靠近输出的小一些,靠近输入的大一些,这样会不会就可以缓解梯度消失的问题?
可以的,但是不是那么好设置,就是不知到靠近输入设置多大,靠近输出设置多大点。

为什么深层的网络,底层比较难训练?是因为它拿到的梯度一般比较小?
是的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值