批归一化(Batch Normalization)
批归一化方法(Batch Normalization,BatchNorm)是由Ioffe和Szegedy于2015年提出的,已被广泛应用在深度学习中,其目的是对神经网络中间层的输出进行标准化处理,使得中间层的输出更加稳定。
通常我们会对神经网络的数据进行标准化处理,处理后的样本数据集满足均值为0,方差为1的统计分布,这是因为当输入数据的分布比较固定时,有利于算法的稳定和收敛。对于深度神经网络来说,由于参数是不断更新的,即使输入数据已经做过标准化处理,但是对于比较靠后的那些层,其接收到的输入仍然是剧烈变化的,通常会导致数值不稳定,模型很难收敛。BatchNorm能够使神经网络中间层的输出变得更加稳定,并有如下三个优点:
- 使学习快速进行(能够使用较大的学习率)
- 降低模型对初始值的敏感性
- 从一定程度上抑制过拟合
BatchNorm主要思路是在训练时以mini-batch为单位,对神经元的数值进行归一化,使数据的分布满足均值为0,方差为1。具体计算过程(有点像计算标准正态分布)如下:
1. 计算mini-batch内样本的均值
2. 计算mini-batch内样本的方差
3. 计算标准化之后的输出
- 读者可以自行验证由 x ^ ( 1 ) , x ^ ( 2 ) , x ^ ( 3 ) 构成的mini-batch,是否满足均值为0,方差为1的分布。
如果强行限制输出层的分布是标准化的,可能会导致某些特征模式的丢失,所以在标准化之后,BatchNorm会紧接着对数据做缩放和平移。
上面列出的是BatchNorm方法的计算逻辑,下面针对两种类型的输入数据格式分别进行举例。飞桨支持输入数据的维度大小为2、3、4、5四种情况,这里给出的是维度大小为2和4的示例。
- 示例一: 当输入数据形状是 [ N , K ]时,一般对应全连接层的输出,示例代码如下所示。
这种情况下会分别对K的每一个分量计算N个样本的均值和方差,数据和参数对应如下:
- 输入 x, [N, K]
- 输出 y, [N, K]
- 均值 μ B,[K, ]
- 方差 σ B ², [K, ]
- 缩放参数 γ, [K, ]
- 平移参数 β, [K, ]
In [ ]
# 输入数据形状是 [N, K]时的示例 import numpy as np import paddle from paddle.nn import BatchNorm1D # 创建数据 data = np.array([[1,2,3], [4,5,6], [7,8,9]]).astype('float32') # 使用BatchNorm1D计算归一化的输出 # 输入数据维度[N, K],num_features等于K bn = BatchNorm1D(num_features=3) x = paddle.to_tensor(data) y = bn(x) print('output of BatchNorm1D Layer: \n {}'.format(y.numpy())) # 使用Numpy计算均值、方差和归一化的输出 # 这里对第0个特征进行验证 a = np.array([1,4,7]) a_mean = a.mean() a_std = a.std() b = (a - a_mean) / a_std print('std {}, mean {}, \n output {}'.format(a_mean, a_std, b)) # 建议读者对第1和第2个特征进行验证,观察numpy计算结果与paddle计算结果是否一致
- 示例二: 当输入数据形状是 [ N , C , H , W ]时, 一般对应卷积层的输出,示例代码如下所示
这种情况下会沿着C这一维度进行展开,分别对每一个通道计算N个样本中总共N×H×W个像素点的均值和方差,数据和参数对应如下:
- 输入 x, [N, C, H, W]
- 输出 y, [N, C, H, W]
- 均值 μ B,[C, ]
- 方差 σ B², [C, ]
- 缩放参数 γ, [C, ]
- 平移参数 β, [C, ]
小窍门:
可能有读者会问:“BatchNorm里面不是还要对标准化之后的结果做仿射变换吗,怎么使用Numpy计算的结果与BatchNorm算子一致?” 这是因为BatchNorm算子里面自动设置初始值γ=1,β=0,这时候仿射变换相当于是恒等变换。在训练过程中这两个参数会不断的学习,这时仿射变换就会起作用。
# 输入数据形状是[N, C, H, W]时的batchnorm示例 import numpy as np import paddle from paddle.nn import BatchNorm2D # 设置随机数种子,这样可以保证每次运行结果一致 np.random.seed(100) # 创建数据 data = np.random.rand(2,3,3,3).astype('float32') # 使用BatchNorm2D计算归一化的输出 # 输入数据维度[N, C, H, W],num_features等于C bn = BatchNorm2D(num_features=3) x = paddle.to_tensor(data) y = bn(x) print('input of BatchNorm2D Layer: \n {}'.format(x.numpy())) print('output of BatchNorm2D Layer: \n {}'.format(y.numpy())) # 取出data中第0通道的数据, # 使用numpy计算均值、方差及归一化的输出 a = data[:, 0, :, :] a_mean = a.mean() a_std = a.std() b = (a - a_mean) / a_std print('channel 0 of input data: \n {}'.format(a)) print('std {}, mean {}, \n output: \n {}'.format(a_mean, a_std, b)) # 提示:这里通过numpy计算出来的输出 # 与BatchNorm2D算子的结果略有差别, # 因为在BatchNorm2D算子为了保证数值的稳定性, # 在分母里面加上了一个比较小的浮点数epsilon=1e-05
- 预测时使用BatchNorm
上面介绍了在训练过程中使用BatchNorm对一批样本进行归一化的方法,但如果使用同样的方法对需要预测的一批样本进行归一化,则预测结果会出现不确定性。
例如样本A、样本B作为一批样本计算均值和方差,与样本A、样本C和样本D作为一批样本计算均值和方差,得到的结果一般来说是不同的。那么样本A的预测结果就会变得不确定,这对预测过程来说是不合理的。
解决方法是在训练过程中将大量样本的均值和方差保存下来,预测时直接使用保存好的值而不再重新计算。
实际上,在BatchNorm的具体实现中,训练时会计算均值和方差的移动平均值。在飞桨中,默认是采用如下方式计算:
BatchNorm的变体包括:层归一化(Layer Normalization, LN)、组归一化(Group Normalization, GN)、实例归一化(Instance Normalization, IN),通过下图进行比较,
其中N知batch size、H和W分别表示特征图的高度和宽度、C表示特征图的通道数,蓝色像素表示使用相同的均值和方差进行归一化:
图14:归一化方法
- LN:对[C,W,H]维度求均值方差进行归一化,即在通道方向做归一化,与batch size大小无关,在小batch size上效果可能更好
- GN:先对通道方向进行分组,然后每个组内对[ C i C_{i} ,W,H]维度进行归一化,也与batch size大小无关
- IN:只对[H,W]维度进行归一化,图像风格化任务适合使用IN算法
图14来源于Yuxin Wu, Kaiming He,Group Normalization
丢弃法(Dropout)
丢弃法(Dropout)是深度学习中一种常用的抑制过拟合的方法,其做法是在神经网络学习过程中,随机删除一部分神经元。训练时,随机选出一部分神经元,将其输出设置为0,这些神经元将不对外传递信号。
图15是Dropout示意图,左边是完整的神经网络,右边是应用了Dropout之后的网络结构。应用Dropout之后,会将标了×的神经元从网络中删除,让它们不向后面的层传递信号。在学习过程中,丢弃哪些神经元是随机决定,因此模型不会过度依赖某些神经元,能一定程度上抑制过拟合。
图15 Dropout示意图
在预测场景时,会向前传递所有神经元的信号,可能会引出一个新的问题:训练时由于部分神经元被随机丢弃了,输出数据的总大小会变小。比如:计算其L1范数会比不使用Dropout时变小,但是预测时却没有丢弃神经元,这将导致训练和预测时数据的分布不一样。为了解决这个问题,飞桨支持如下两种方法:
- downscale_in_infer
训练时以比例r随机丢弃一部分神经元,不向后传递它们的信号;预测时向后传递所有神经元的信号,但是将每个神经元上的数值乘以(1- r)。
- upscale_in_train
训练时以比例p随机丢弃一部分神经元,不向后传递它们的信号,但是将那些被保留的神经元上的数值除以(1−p);预测时向后传递所有神经元的信号,不做任何处理。
在飞桨Dropout API中,通过mode参数来指定用哪种方式对神经元进行操作,
paddle.nn.Dropout(p=0.5, axis=None, mode="upscale_in_train”, name=None)
主要参数如下:
- p (float) :将输入节点置为0的概率,即丢弃概率,默认值:0.5。该参数对元素的丢弃概率是针对于每一个元素而言,而不是对所有的元素而言。举例说,假设矩阵内有12个数字,经过概率为0.5的dropout未必一定有6个零。
- mode(str) :丢弃法的实现方式,有'downscale_in_infer'和'upscale_in_train'两种,默认是'upscale_in_train'。
说明:
不同框架对于Dropout的默认处理方式可能不同,读者可以查看API详细了解。
下面这段程序展示了经过Dropout之后输出数据的形式。
从上述代码的输出可以发现,经过dropout之后,tensor中的某些元素变为了0,这个就是dropout实现的功能,通过随机将输入数据的元素置0,消除减弱了神经元节点间的联合适应性,增强模型的泛化能力。
Dropout的变体有很多,如:DropConnect、Standout、Gaussian Dropout、Spatial Dropout、Cutout、Max-Drop、RNNDrop、循环Drop等,下面简单介绍其中几种算法:
- DropConnect是由L. Wan等人提出,没有直接在神经元上应用dropout,而是在连接神经元的权重和偏置上应用,只能应用在全连接层
- Standout是由L. J. Ba和B. Frey提出,第 i i 层丢弃一部分神经元的概率 p p 不是恒定的,根据权重的值,是自适应的,权重越大被丢弃的概率也越大
- Spatial Dropout是由 J. Tompson等人提出,考虑相邻像素之间的高度相关性,不再进行单个像素进行dropout,而是在特征图上进行dropout
- Cutout是由T. DeVries和G. W. Taylor提出,通过对图像的隐藏区域进行泛化从而防止过拟合,提高神经网络的鲁棒性(指系统或者器件在不同的环境或者条件下,能够保持其功能或者性能的特性******)******和整体性能
小结
学习完这些概念,您就具备了搭建卷积神经网络的基础。下一节,我们将应用这些基础模块,一起完成图像分类中的典型应用 — 医疗图像中的眼疾筛查任务的模型搭建。
作业
1 计算卷积中一共有多少次乘法和加法操作
输入数据形状是[10,3,224,224],卷积核k_h = k_w = 3,输出通道数为6464,步幅stride=1,填充p_h = p_w = 1。
则完成这样一个卷积,一共需要做多少次乘法和加法操作?
- 提示
先看输出一个像素点需要做多少次乘法和加法操作,然后再计算总共需要的操作次数。
- 提交方式
请回复乘法和加法操作的次数,例如:乘法1000,加法1000
2 计算网络层的输出数据和参数的形状
网络结构定义如下面的代码所示,输入数据形状是[10,3,224,224],
请分别计算每一层的输出数据形状,以及各层包含的参数形状
In [ ]
# 定义 SimpleNet 网络结构 import paddle from paddle.nn import Conv2D, MaxPool2D, Linear import paddle.nn.functional as F class SimpleNet(paddle.nn.Layer): def __init__(self, num_classes=1): #super(SimpleNet, self).__init__(name_scope) self.conv1 = Conv2D(in_channels=3, out_channels=6, kernel_size=5, stride=1, padding=2) self.max_pool1 = MaxPool2D(kernel_size=2, tride=2) self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=2) self.max_pool2 = MaxPool2D(kernel_size=2, tride=2) self.fc1 = Linear(in_features=50176, out_features=64) self.fc2 = Linear(in_features=64, out_features=num_classes) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.sigmoid(x) x = self.fc2(x) return x
- 提示,第一层卷积 c o n v 1 conv1 ,各项参数如下:
输出特征图的形状是[N,Cout,Hout,Wout]=[10,6,224,224]
请将下面的表格补充完整:
名称 |
w形状 |
w参数个数 |
b形状 |
b参数个数 |
输出形状 |
conv1 |
[6,3,5,5] |
450 |
[6] |
6 |
[10, 6, 224, 224] |
pool1 |
无 |
无 |
无 |
无 |
[10, 6, 112, 112] |
conv2 |
|||||
pool2 |
|||||
fc1 |
|||||
fc2 |
- 提交方式:将表格截图发到讨论区
靠《填充》那章下面的公式:Hout=H+ph1+ph2−kh+1、Wout=W+pw1+pw2−kw+1
pool和fc都不会做
本节以上一节介绍的ResNet来完成眼疾识别任务为例,介绍一个基本的计算机视觉任务研发全流程,主要涵盖如下内容:
- 基本的计算机视觉任务研发全流程:介绍基本的计算机视觉任务研发全流程。
- 眼疾数据集:介绍数据集结构及数据预处理方法。
- ResNet网络:如何应用眼疾数据集进行模型训练和测试。