学习资源整理

Deep_Residual_Learning_For_Image_Recognition.pdf
Identity_Mappings_In_Deep_Residual.pdf
Reducing_the_dimensionality_of_data_with_neural_networks.pdf
kaiming 介绍 ResNet视频 - 知乎
作者源代码

内容整理

两大经典问题

经验表明,网络深度有着至关重要的影响,深层网络可以读取出图片的低层、中层和高层特征

网络深度达到一定程度时,会带来两个经典问题:

  1. (vanishing/exploding gradients)梯度消失/爆炸
  2. (degradation)退化问题

    Vanishing/Exploding Gradients

    image.png
    上图是一个四层的全连接网络,假设每一层的输出为fi(x),f表示激活函数;那么可以得到:
    image.png
    忽略偏置项,则简单记为:
    image.png
    应用基于梯度下降策略的反向传播算法对训练参数进行更新,根据链式法则,最终每一层的参数更新变成了一长串的链式求导相乘,以下图的反向传播为例:
    image.png
    可以推导出C关于b1的导数为:
    论文笔记 - ResNet - 图6
    假设我们激活函数采用sigmoid,对于sigmoid激活函数,其导数最大值为1/4.而我们初始化的权值绝对值通常都小于1,因此论文笔记 - ResNet - 图7,所以通过上面的的链式求导,层数越多,求导的结果就越小,最终导致梯度消失的情况出现
    image.png
    同理,如果我们采用的激活函数最终计算的论文笔记 - ResNet - 图9,层数越多,求导的结果就越大,最后呈指数增长,导致梯度爆炸的情况出现

    Degradation

    模型退化可以简单理解为在模型的每层中只有少量隐藏单元对不同的输入改变它们的激活值,而大部分隐藏单元对不同输入都是相同反应

模型退化后的train error 和 test error 都很高

解决方案

Vanishing/Exploding Gradient

预训练加微调

Hinton在2006年发表的论文介绍的一种方法
Reducing_the_dimensionality_of_data_with_neural_networks.pdf
Hinton为了解决梯度的问题,提出采取无监督逐层训练方法,其基本思想是每次训练一层隐节点,训练时将上一层隐节点的输出作为输入,而本层隐节点的输出作为下一层隐节点的输入,此过程就是逐层“预训练”(pre-training);在预训练完成后,再对整个网络进行“微调”(fine-tunning)

梯度剪切、正则

梯度剪切是针对梯度爆炸问题提出的,其思想是:设置一个梯度剪切阈值,然后更新梯度的时候,如果梯度超过这个阈值,就强制限制在这个范围之内,从而防止梯度爆炸

梯度正则化也是针对梯度爆炸问题提出的,首先了解下范数norm,对于向量α,他的Lp范数为:
image.png
可以看常用的三个范数:

  1. L0范数:向量中非0的元素的个数
  2. L1范数:向量中各个元素绝对值之和
  3. L2范数:向量各元素的平方和然后求平方根

在pytorch中,通过优化器的weight_decay参数可以指定权重衰减的策略,如adadelta优化器默认采用的weight_decay就是L2正则化
image.png
L2正则在神经网络中的使用,就是在损失函数后面再加上一个正则项:
论文笔记 - ResNet - 图12
论文笔记 - ResNet - 图13是样本大小 , 论文笔记 - ResNet - 图14 是正则项系数(一般取1e-5),系数1/2是为了方便求导后的结果计算;下面开始计算梯度:
论文笔记 - ResNet - 图15
w通过BP进行参数更新后得到如下公式:
论文笔记 - ResNet - 图16
论文笔记 - ResNet - 图17
学习率论文笔记 - ResNet - 图18一般取1e-3,通过公式可以看到w参数的更新取决于两个部分:

  1. 衰减的权重 论文笔记 - ResNet - 图19
  2. 梯度 论文笔记 - ResNet - 图20

通过正则化,对w的值进行衰减,从而使w变小,解决梯度爆炸问题

ReLU、LeakReLU、ElU等激活函数替代Sigmoid

以ReLU函数为例,其表达式为:
论文笔记 - ResNet - 图21
原图像及导数图像为下图:
论文笔记 - ResNet - 图22
可以看到ReLU的导数在正数部分是恒等于1的,因此可以有效的规避梯度消失和梯度爆炸的问题

ReLU的缺点在于:

  1. 负数部分恒为0,会导致一些神经元无法激活
  2. 输出不是以0为中心的

LeakReLU函数时为了解决ReLU的第二个缺点,其数学表达式为:max(k∗x,x);其中的k是leak系数,一般选择0.01或者0.02,下图为k取0.2时的图像:
论文笔记 - ResNet - 图23
ELU同样是Wie了解决ReLU的第二个缺点,其表达式为:
论文笔记 - ResNet - 图24
其原图像以及导数图像为:
论文笔记 - ResNet - 图25
ELU相对于LeakReLU函数来说, 计算耗费的时间更长

Batch Normalization

相关介绍论文:Accelerating_Deep_Network_Training_by_Reducing_Internal_Covariate_Shift.pdf
反向传播中,经过每一层的梯度都会乘以该层的权重:
image.png
那么在反向传播中,反向求导为:
image.png
因此可以看出,x的大小也会影响梯度值,Batch Normalization就是通过对每一层的输出规范为均值和方差一致的方法,消除了x带来的放大缩小的影响,进而解决梯度消失和爆炸的问题

后续深入参见文章:
深度学习:Batch Normalization 学习笔记

残差结构

在后面会提到

LSTM

长短期记忆网络:long-short term memory networks
没了解

Degradation

残差结构

在后面会提到

残差网络(Deep-Residual-Learning-For-Image-Recognition)

残差结构

残差结构内部组成(以全连接层来解释):
image.png

两类残差块

image.png

左侧残差块用于ResNet-34,右侧残差块用于ResNet-50/101/152
image.png
右侧残差块在论文中称为“Deeper Bottleneck Architectures”,分为三个层:

  1. 1*1卷积,用于降维,减少了计算
  2. 3*3卷积,用于计算
  3. 1*1卷积,用于升维,还原维度

两类残差块的代码实现(pytorch):

  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. # 用于ResNet18和34的残差块,用的是2个3x3的卷积
  5. class BasicBlock(nn.Module):
  6. expansion = 1
  7. def __init__(self, in_planes, planes, stride=1):
  8. super(BasicBlock, self).__init__()
  9. self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3,
  10. stride=stride, padding=1, bias=False)
  11. self.bn1 = nn.BatchNorm2d(planes)
  12. self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
  13. stride=1, padding=1, bias=False)
  14. self.bn2 = nn.BatchNorm2d(planes)
  15. self.shortcut = nn.Sequential()
  16. # 经过处理后的x要与x的维度相同(尺寸和深度)
  17. # 如果不相同,需要添加卷积+BN来变换为同一维度
  18. if stride != 1 or in_planes != self.expansion*planes:
  19. self.shortcut = nn.Sequential(
  20. nn.Conv2d(in_planes, self.expansion*planes,
  21. kernel_size=1, stride=stride, bias=False),
  22. nn.BatchNorm2d(self.expansion*planes)
  23. )
  24. def forward(self, x):
  25. out = F.relu(self.bn1(self.conv1(x)))
  26. out = self.bn2(self.conv2(out))
  27. out += self.shortcut(x)
  28. out = F.relu(out)
  29. return out
  30. # 用于ResNet50,101和152的残差块,用的是1x1+3x3+1x1的卷积
  31. class Bottleneck(nn.Module):
  32. # 前面1x1和3x3卷积的filter个数相等,最后1x1卷积是其expansion倍
  33. expansion = 4
  34. def __init__(self, in_planes, planes, stride=1):
  35. super(Bottleneck, self).__init__()
  36. self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
  37. self.bn1 = nn.BatchNorm2d(planes)
  38. self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
  39. stride=stride, padding=1, bias=False)
  40. self.bn2 = nn.BatchNorm2d(planes)
  41. self.conv3 = nn.Conv2d(planes, self.expansion*planes,
  42. kernel_size=1, bias=False)
  43. self.bn3 = nn.BatchNorm2d(self.expansion*planes)
  44. self.shortcut = nn.Sequential()
  45. if stride != 1 or in_planes != self.expansion*planes:
  46. self.shortcut = nn.Sequential(
  47. nn.Conv2d(in_planes, self.expansion*planes,
  48. kernel_size=1, stride=stride, bias=False),
  49. nn.BatchNorm2d(self.expansion*planes)
  50. )
  51. def forward(self, x):
  52. out = F.relu(self.bn1(self.conv1(x)))
  53. out = F.relu(self.bn2(self.conv2(out)))
  54. out = self.bn3(self.conv3(out))
  55. out += self.shortcut(x)
  56. out = F.relu(out)
  57. return out
  58. class ResNet(nn.Module):
  59. def __init__(self, block, num_blocks, num_classes=10):
  60. super(ResNet, self).__init__()
  61. self.in_planes = 64
  62. self.conv1 = nn.Conv2d(3, 64, kernel_size=3,
  63. stride=1, padding=1, bias=False)
  64. self.bn1 = nn.BatchNorm2d(64)
  65. self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
  66. self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
  67. self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
  68. self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
  69. self.linear = nn.Linear(512*block.expansion, num_classes)
  70. def _make_layer(self, block, planes, num_blocks, stride):
  71. strides = [stride] + [1]*(num_blocks-1)
  72. layers = []
  73. for stride in strides:
  74. layers.append(block(self.in_planes, planes, stride))
  75. self.in_planes = planes * block.expansion
  76. return nn.Sequential(*layers)
  77. def forward(self, x):
  78. out = F.relu(self.bn1(self.conv1(x)))
  79. out = self.layer1(out)
  80. out = self.layer2(out)
  81. out = self.layer3(out)
  82. out = self.layer4(out)
  83. out = F.avg_pool2d(out, 4)
  84. out = out.view(out.size(0), -1)
  85. out = self.linear(out)
  86. return out
  87. def ResNet18():
  88. return ResNet(BasicBlock, [2,2,2,2])
  89. def ResNet34():
  90. return ResNet(BasicBlock, [3,4,6,3])
  91. def ResNet50():
  92. return ResNet(Bottleneck, [3,4,6,3])
  93. def ResNet101():
  94. return ResNet(Bottleneck, [3,4,23,3])
  95. def ResNet152():
  96. return ResNet(Bottleneck, [3,8,36,3])
  97. def test():
  98. net = ResNet18()
  99. y = net(torch.randn(1,3,32,32))
  100. print(y.size())
  101. # test()

残差网络(Identity-Mapping-in-Deep-Residual)

残差概念

残差和误差是非常容易混淆的两个概念:

  1. 误差是衡量观测值和真实值之间的差距
  2. 残差是指预测值和观测值之间的差距

    残差块

    残差网络是由一系列残差块组成的,残差快结构如下图所示,其中weight在卷积网络中表示卷积操作,addition表示单位加操作:
    image.png
    上图残差块的表示公式为:
    论文笔记 - ResNet - 图32
    残差块分成两个部分:

  3. 直接映射:h(xl),对应上图左侧

  4. 残差:F(xl, wl),对应上图右侧,一般由两个或者三个卷积操作构成

在卷积网络中,Xl可能和X(l+1)的Feature Map数量不一样,这时候就需要使用11的卷积进行升维或者降维,如下图:
image.png
这时候,残差块的表达式就变为:
论文笔记 - ResNet - 图34
其中,h(xl) = W’l
x,其中W’l是11卷积操作(**经验表明,11的卷积对模型性能的提升有限,所以一般在升维或者降维时才会使用**)

该版本的残差块称为resnet_v1,对应的keras代码如下:

  1. def res_block_v1(x, input_filter, output_filter):
  2. res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(x)
  3. res_x = BatchNormalization()(res_x)
  4. res_x = Activation('relu')(res_x)
  5. res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(res_x)
  6. res_x = BatchNormalization()(res_x)
  7. if input_filter == output_filter:
  8. identity = x
  9. else: #需要升维或者降维
  10. identity = Conv2D(kernel_size=(1,1), filters=output_filter, strides=1, padding='same')(x)
  11. x = keras.layers.add([identity, res_x])
  12. output = Activation('relu')(x)
  13. return output

残差网络的搭建

分为两步:

  1. 使用VGG公式搭建Plain VGG网络
  2. 在Plain VGG的卷积网络之间插入 Identity Mapping

搭建代码:

  1. def resnet_v1(x):
  2. x = Conv2D(kernel_size=(3,3), filters=16, strides=1, padding='same', activation='relu')(x)
  3. x = res_block_v1(x, 16, 16)
  4. x = res_block_v1(x, 16, 32)
  5. x = Flatten()(x)
  6. outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(x)
  7. return outputs

残差网络原理

直接映射

残差块的一个更通用的表达方式为:
论文笔记 - ResNet - 图35
论文笔记 - ResNet - 图36
现在不考虑升维或降维的情况,那么在这里,h(.)是直接映射,f(.)是激活函数,一般采用ReLU;首先,提出两个假设:

  1. h(.)是直接映射
  2. f(.)是直接映射

这时候残差块就可以表示为:
论文笔记 - ResNet - 图37
对于更深的层L,其与l层的关系可以表示为:
论文笔记 - ResNet - 图38
公式6反映了残差网络的两个属性:

  1. L层可以表示为任意一个比它浅的l层和他们之间的残差部分之和;
  2. 论文笔记 - ResNet - 图39,L是各个残差块特征的单位累和,MLP是特征矩阵的累积

根据BP的链式求导法则,损失函数论文笔记 - ResNet - 图40关于xl的梯度可以表示为:
论文笔记 - ResNet - 图41
公式7又反映了残差网络的两个属性:

  1. 在整个训练过程中,论文笔记 - ResNet - 图42不可能一直为-1,因此可以说在残差网络中不会出现梯度消失的问题
  2. 论文笔记 - ResNet - 图43表示L层的梯度可以直接传递到任何一个比它浅的层

最终得出结论:当残差块满足上面两个假设时,信息可以非常畅通的在高层和底层之间相互传导,说明这两个假设是让残差网络可以训练深度模型的充分条件

对于假设1,采用反证法,假设论文笔记 - ResNet - 图44,那么这时候,残差块可以表示为:
论文笔记 - ResNet - 图45
对于更深的L层,表示为:
论文笔记 - ResNet - 图46
现在只考虑公式的左半部分论文笔记 - ResNet - 图47,损失函数对xl求偏微分得:
论文笔记 - ResNet - 图48
公式10得到两个结论:

  1. 论文笔记 - ResNet - 图49时,很有可能发生梯度爆炸
  2. 论文笔记 - ResNet - 图50时,很有可能发生梯度消失问题

因此,想要规避梯度消失/爆炸问题,就需要将其值设置为1

而对于其他不影响梯度的h(.),例如LSTM中的门机制(图中c和d)、Dropout(图中f)以及用于升降维的1*1卷积(图中e)也许会有效果:
image.png

各模型在CiFar10数据集上的表现如图:
image.png
可以看出,直接映射的效果是最好的。从而可以得出结论,假设1成立,即:
论文笔记 - ResNet - 图53
论文笔记 - ResNet - 图54

激活函数的位置

将最开始的残差块详细展开为下图a,其余几个是在此基础上的变异:
image.png
在前述已经得知了“直接映射是最好的选择”,所以这里我们希望构造一种结构能够满足直接映射,即定义一个新的残差结构论文笔记 - ResNet - 图56,满足公式:
论文笔记 - ResNet - 图57
该公式反映到网络里即将激活函数移到残差部分去使用,即上图c。这种在卷积之后使用激活函数的方法叫:post-activation,通过调整ReLU和BN的位置得到了几个变种,即d和e,在Cifar10数据集上的实验结构如下图:
image.png
实验结果表明:将激活函数移动到残差部分可以提高模型的精度

该网络称为:resnet_v2,keras实现代码如下:

  1. def res_block_v2(x, input_filter, output_filter):
  2. res_x = BatchNormalization()(x)
  3. res_x = Activation('relu')(res_x)
  4. res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(res_x)
  5. res_x = BatchNormalization()(res_x)
  6. res_x = Activation('relu')(res_x)
  7. res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(res_x)
  8. if input_filter == output_filter:
  9. identity = x
  10. else: #需要升维或者降维
  11. identity = Conv2D(kernel_size=(1,1), filters=output_filter, strides=1, padding='same')(x)
  12. output= keras.layers.add([identity, res_x])
  13. return output
  14. def resnet_v2(x):
  15. x = Conv2D(kernel_size=(3,3), filters=16 , strides=1, padding='same', activation='relu')(x)
  16. x = res_block_v2(x, 16, 16)
  17. x = res_block_v2(x, 16, 32)
  18. x = BatchNormalization()(x)
  19. y = Flatten()(x)
  20. outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(y)
  21. return outputs

代码相关下载

ResNet-50

ResNet-50网络模型
ResNet-50模型代码

ResNet-101

ResNet-101网络模型
ResNet-101模型代码

ResNet-152

ResNet-152网络模型
ResNet-152模型代码

resnet-1k-layers.lua

resnet-1k-layers.txt