摘要:在深度神经网络训练的过程中,由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称为内部协变量偏移(Internal Covariate Shift),而 BN 可以解决这个问题。
本文分享自华为云社区《神经网络基础部件-BN层详解》,作者:嵌入式视觉 。
随机变量(random variable)是可以随机地取不同值的变量。随机变量可以是离散的或者连续的。简单起见,本文用大写字母 XX 表示随机变量,小写字母 xx 表示随机变量能够取到的值。例如,x1x1 和 x2x2 都是随机变量 XX 可能的取值。随机变量必须伴随着一个概率分布来指定每个状态的可能性。
概率分布(probability distribution)用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。我们描述概率分布的方式取决于随机变量是离散的还是连续的。
当我们研究的对象是连续型随机变量时,我们用概率密度函数(probability density function, PDF)而不是概率质量函数来描述它的概率分布。
更多内容请阅读《花书》第三章-概率与信息论,或者我的文章-深度学习数学基础-概率与信息论。
当我们不知道数据真实分布时使用正态分布的原因之一是,正态分布拥有最大的熵,我们通过这个假设来施加尽可能少的结构。
实数上最常用的分布就是正态分布(normal distribution),也称为高斯分布 (Gaussian distribution)。
如果随机变量 XX ,服从位置参数为 μμ、尺度参数为 σσ 的概率分布,且其概率密度函数为:
则这个随机变量就称为正态随机变量,正态随机变量服从的概率分布就称为正态分布,记作:
如果位置参数 μ=0μ=0,尺度参数 σ=1σ=1 时,则称为标准正态分布,记作:
此时,概率密度函数公式简化为:
正太分布的数学期望值或期望值 μμ 等于位置参数,决定了分布的位置;其方差 σ2σ2 的开平方或标准差 σσ 等于尺度参数,决定了分布的幅度。正太分布的概率密度函数曲线呈钟形,常称之为钟形曲线,如下图所示:
可视化正态分布,可直接通过 np.random.normal 函数生成指定均值和标准差的正态分布随机数,然后基于 matplotlib + seaborn 库 kdeplot函数绘制概率密度曲线。示例代码如下所示:
import seaborn as sns x1 = np.random.normal(0, 1, 100) x2 = np.random.normal(0, 1.5, 100) x3 = np.random.normal(2, 1.5, 100) plt.figure(dpi = 200) sns.kdeplot(x1, label="μ=0, σ=1") sns.kdeplot(x2, label="μ=0, σ=1.5") sns.kdeplot(x3, label="μ=2, σ=2.5") #显示图例 plt.legend() #添加标题 plt.title("Normal distribution") plt.show()
以上代码直接运行后,输出结果如下图:
当然也可以自己实现正太分布的概率密度函数,代码和程序输出结果如下:
import numpy as np import matplotlib.pyplot as plt plt.figure(dpi = 200) plt.style.use('seaborn-darkgrid') # 主题设置 def nd_func(x, sigma, mu): """自定义实现正太分布的概率密度函数 """ a = - (x-mu)**2 / (2*sigma*sigma) f = np.exp(a) / (sigma * np.sqrt(2*np.pi)) return f if __name__ == '__main__': x = np.linspace(-5, 5) f = nd_fun(x, 1, 0) p1, = plt.plot(x, f) f = nd_fun(x, 1.5, 0) p2, = plt.plot(x, f) f = nd_fun(x, 1.5, 2) p3, = plt.plot(x, f) plt.legend([p1 ,p2, p3], ["μ=0,σ=1", "μ=0,σ=1.5", "μ=2,σ=1.5"]) plt.show()
训练深度神经网络的复杂性在于,因为前面的层的参数会发生变化导致每层输入的分布在训练过程中会发生变化。这又导致模型需要需要较低的学习率和非常谨慎的参数初始化策略,从而减慢了训练速度,并且具有饱和非线性的模型训练起来也非常困难。
网络层输入数据分布发生变化的这种现象称为内部协变量转移,BN 就是来解决这个问题。
在深度神经网络训练的过程中,由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称在论文中称之为内部协变量偏移(Internal Covariate Shift)。
那么,为什么网络中间层数据分布会发生变化呢?
在深度神经网络中,我们可以将每一层视为对输入的信号做了一次变换(暂时不考虑激活,因为激活函数不会改变输入数据的分布):
其中 WW 和 BB 是模型学习的参数,这个公式涵盖了全连接层和卷积层。
随着 SGD 算法更新参数,和网络的每一层的输入数据经过公式5的运算后,其 ZZ 的分布一直在变化,因此网络的每一层都需要不断适应新的分布,这一过程就被叫做 Internal Covariate Shift。
而深度神经网络训练的复杂性在于每层的输入受到前面所有层的参数的影响—因此当网络变得更深时,网络参数的微小变化就会被放大。
随着网络层的加深,后面网络输入 xx 越来越大,而如果我们又采用 Sigmoid 型激活函数,那么每层的输入很容易移动到非线性饱和区域,此时梯度会变得很小甚至接近于 00,导致参数的更新速度就会减慢,进而又会放慢网络的收敛速度。
饱和问题和由此产生的梯度消失通常通过使用修正线性单元激活(ReLU(x)=max(x,0)ReLU(x)=max(x,0)),更好的参数初始化方法和小的学习率来解决。然而,如果我们能保证非线性输入的分布在网络训练时保持更稳定,那么优化器将不太可能陷入饱和状态,进而训练也将加速。
Normalization 操作虽然缓解了 ICS 问题,让每一层网络的输入数据分布都变得稳定,但却导致了数据表达能力的缺失。
论文中给出的 Batch Normalizing Transform 算法计算过程如下图所示。其中输入是一个考虑一个大小为 mm 的小批量数据 BB。
论文中的公式不太清晰,下面我给出更为清晰的 Batch Normalizing Transform 算法计算过程。
设 mm 表示 batch_size 的大小,nn 表示 features 数量,即样本特征值数量。在训练过程中,针对每一个 batch 数据,BN 过程进行的操作是,将这组数据 normalization,之后对其进行线性变换,具体算法步骤如下:
以上公式乘法都为元素乘,即 element wise 的乘法。其中,参数 γ,βγ,β 是训练出来的, ϵϵ 是为零防止 σB2σB2 为 00 ,加的一个很小的数值,通常为1e-5。公式各个符号解释如下:
其中:
可以看出 BN 本质上是做线性变换。
在论文中,训练一个带 BN 层的网络, BN 算法步骤如下图所示:
在训练期间,我们一次向网络提供一小批数据。在前向传播过程中,网络的每一层都处理该小批量数据。 BN 网络层按如下方式执行前向传播计算:
图片来源这里。
注意,图中计算均值与方差的无偏估计方法是吴恩达在 Coursera 上的 Deep Learning 课程上提出的方法:对 train 阶段每个 batch 计算的 mean/variance 采用指数加权平均来得到 test 阶段 mean/variance 的估计。
在训练期间,它只是计算此 EMA,但不对其执行任何操作。在训练结束时,它只是将该值保存为层状态的一部分,以供在推理阶段使用。
如下图可以展示BN 层的前向传播计算过程数据的 shape ,红色框出来的单个样本都指代单个矩阵,即运算都是在单个矩阵运算中计算的。
图片来源 这里。
BN 的反向传播过程中,会更新 BN 层中的所有 ββ 和 γγ 参数。
批量归一化(batch normalization)的“批量”两个字,表示在模型的迭代训练过程中,BN 首先计算小批量( mini-batch,如 32)的均值和方差。但是,在推理过程中,我们只有一个样本,而不是一个小批量。在这种情况下,我们该如何获得均值和方差呢?
第一种方法是,使用的均值和方差数据是在训练过程中样本值的平均,即:
这种做法会把所有训练批次的 μμ 和 σσ 都保存下来,然后在最后训练完成时(或做测试时)做下平均。
第二种方法是使用类似动量的方法,训练时,加权平均每个批次的值,权值 αα 可以为0.9:
推理或测试时,直接使用模型文件中保存的 μmoviμmovi 和 σmoviσmovi 的值即可。
BN 在 ImageNet 分类数据集上实验结果是 SOTA 的,如下表所示: