目录
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代码实现
残差块
python
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)
输入和输出形状一致
python
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape)
结果:
增加输出通道数的同时,减半输出的高和宽
python
# 输入通道数为3,输出通道数为6,原先是3将它增加到6则输出的高和宽将会减半
blk = Residual(3,6, use_1x1conv=True, strides=2)
print(blk(X).shape)
结果:
ResNet模型
ResNet:
GoogLeNet:
ResNet的前两层跟之前介绍的GoogLeNet中的一样:
python
# 第一个卷积层接受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 的最大池化层 ,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
python
# 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个残差块。
python
# 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中加入全局平均池化层,以及全连接层输出。
python
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中不同模块的输入形状是如何变化的
python
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种类别
训练模型
python
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())
结果:
问题
①为什么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层)的关键是:将乘法运算改为加法运算(残差连接),从而避免底层的梯度消失,使得最靠近数据的层的权重也能够获得较大的梯度(大数+小数=大数,大数×小数=小数),深层网络得以有效更新。
问题
学习率可不可以让靠近输出的小一些,靠近输入的大一些,这样会不会就可以缓解梯度消失的问题?
可以的,但是不是那么好设置,就是不知到靠近输入设置多大,靠近输出设置多大点。
为什么深层的网络,底层比较难训练?是因为它拿到的梯度一般比较小?
是的