卷积神经网络
卷积神经网络是目前计算机视觉中使用最普遍的模型结构。本章节主要为读者介绍卷积神经网络的一些基础模块,包括:
- 卷积(Convolution)
- 池化(Pooling)
- ReLU激活函数
- 批归一化(Batch Normalization)
- 丢弃法(Dropout)
回顾一下,在上一章“一个案例带你吃透深度学习”中,我们介绍了手写数字识别任务,应用的是全连接层的特征提取,即将一张图片上的所有像素点展开成一个1维向量输入网络,存在如下两个问题:
1. 输入数据的空间信息被丢失。 空间上相邻的像素点往往具有相似的RGB值,RGB的各个通道之间的数据通常密切相关,但是转化成1维向量时,这些信息被丢失。同时,图像数据的形状信息中,可能隐藏着某种本质的模式,但是转变成1维向量输入全连接神经网络时,这些模式也会被忽略。
2. 模型参数过多,容易发生过拟合。 在手写数字识别案例中,每个像素点都要跟所有输出的神经元相连接。当图片尺寸变大时,输入神经元的个数会按图片尺寸的平方增大,导致模型参数过多,容易发生过拟合。
为了解决上述问题,我们引入卷积神经网络进行特征提取,既能提取到像相邻素点之间的特征模式,又能保证参数的个数不随图片尺寸变化。图4 是一个典型的卷积神经网络结构,多层卷积和池化层组合作用在输入图片上,在网络的最后通常会加入一系列全连接层,ReLU激活函数一般加在卷积或者全连接层的输出上,网络中通常还会加入Dropout来防止过拟合。
图4:卷积神经网络经典结构
说明:
在卷积神经网络中,计算范围是在像素点的空间邻域内进行的,卷积核参数的数目也远小于全连接层。卷积核本身与输入图片大小无关,它代表了对空间临域内某种特征模式的提取。比如,有些卷积核提取物体边缘特征,有些卷积核提取物体拐角处的特征,图像上不同区域共享同一个卷积核。当输入图片大小不一样时,仍然可以使用同一个卷积核进行操作。
卷积(Convolution)
这一小节将为读者介绍卷积算法的原理和实现方案,并通过具体的案例展示如何使用卷积对图片进行操作,主要涵盖如下内容:
卷积计算
填充(padding)
步幅(stride)
感受野(Receptive Field)
多输入通道、多输出通道和批量操作
飞桨卷积API介绍
卷积算子应用举例
卷积计算
卷积是数学分析中的一种积分变化的方法,在图像处理中采用的是卷积的离散形式。这里需要说明的是,在卷积神经网络中,卷积层的实现方式实际上是数学中定义的互相关 (cross-correlation)运算,与数学分析中的卷积定义有所不同,这里跟其他框架和卷积神经网络的教程保持一致,都使用互相关运算作为卷积的定义,具体的计算过程如 图5 所示。
图5:卷积计算过程
说明:
卷积核(kernel)也被叫做滤波器(filter),假设卷积核的高和宽分别为
和 ,则将称为 卷积,比如 卷积,就是指卷积核的高为3, 宽为5。
- 如图5(a)所示:左边的图大小是 ,表示输入数据是一个维度为 的二维数组;中间的图大小是 ,表示一个维度为 的二维数组,我们将这个二维数组称为卷积核。先将卷积核的左上角与输入数据的左上角(即:输入数据的(0, 0)位置)对齐,把卷积核的每个元素跟其位置对应的输入数据中的元素相乘,再把所有乘积相加,得到卷积输出的第一个结果
- 如图5(b)所示:将卷积核向右滑动,让卷积核左上角与输入数据中的(0,1)位置对齐,同样将卷积核的每个元素跟其位置对应的输入数据中的元素相乘,再把这4个乘积相加,得到卷积输出的第二个结果,
- 如图5(c)所示:将卷积核向下滑动,让卷积核左上角与输入数据中的(1, 0)位置对齐,可以计算得到卷积输出的第三个结果,
- 如图5(d)所示:将卷积核向右滑动,让卷积核左上角与输入数据中的(1, 1)位置对齐,可以计算得到卷积输出的第四个结果,
卷积核的计算过程可以用下面的数学公式表示,其中
代表输入图片, 代表输出特征图, 是卷积核参数,它们都是二维数组, 表示对卷积核参数进行遍历并求和。
举例说明,假如上图中卷积核大小是
,则 可以取0和1, 也可以取0和1,也就是说:
读者可以自行验证,当
取不同值时,根据此公式计算的结果与上图中的例子是否一致。
- 【思考】 当卷积核大小为 时,b和a之间的对应关系应该是怎样的?
其它说明:
在卷积神经网络中,一个卷积算子除了上面描述的卷积过程之外,还包括加上偏置项的操作。例如假设偏置为1,则上面卷积计算的结果为:
填充(padding)
在上面的例子中,输入图片尺寸为
,输出图片尺寸为 ,经过一次卷积之后,图片尺寸变小。卷积输出特征图的尺寸计算方法如下:
如果输入尺寸为4,卷积核大小为3时,输出尺寸为
。读者可以自行检查当输入图片和卷积核为其他尺寸时,上述计算式是否成立。通过多次计算我们发现,当卷积核尺寸大于1时,输出特征图的尺寸会小于输入图片尺寸。说明经过多次卷积之后尺寸会不断减小。为了避免卷积之后图片尺寸变小,通常会在图片的外围进行填充(padding),如 图6 所示。
图6:图形填充
- 如图6(a)所示:填充的大小为1,填充值为0。填充之后,输入图片尺寸从
变成了 ,使用3x3的卷积核,输出图片尺寸为 。
- 如图6(b)所示:填充的大小为2,填充值为0。填充之后,输入图片尺寸从
变成了 ,使用3x3的卷积核,输出图片尺寸为 。
如果在图片高度方向,在第一行之前填充
行,在最后一行之后填充 行;在图片的宽度方向,在第1列之前填充 列,在最后1列之后填充 列;则填充之后的图片尺寸为 。经过大小为 的卷积核操作之后,输出图片的尺寸为:
在卷积计算过程中,通常会在高度或者宽度的两侧采取等量填充,即
,上面计算公式也就变为:
卷积核大小通常使用1,3,5,7这样的奇数,如果使用的填充大小为
,则卷积之后图像尺寸不变。例如当卷积核大小为3时,padding大小为1,卷积之后图像尺寸不变;同理,如果卷积核大小为5,使用padding的大小为2,也能保持图像尺寸不变。
步幅(stride)
图6 中卷积核每次滑动一个像素点,这是步幅为1的特殊情况。图7 是步幅为2的卷积过程,卷积核在图片上移动时,每次移动大小为2个像素点。
图7:步幅为2的卷积过程
当宽和高方向的步幅分别为
和 时,输出特征图尺寸的计算公式是:
假设输入图片尺寸是
,卷积核大小 ,填充 ,步幅为 ,则输出特征图的尺寸为:
感受野(Receptive Field)
输出特征图上每个点的数值,是由输入图片上大小为
的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上 区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如 卷积对应的感受野大小就是 。
多输入通道、多输出通道和批量操作
前面介绍的卷积计算过程比较简单,实际应用时,处理的问题要复杂的多。例如:对于彩色图片有RGB三个通道,需要处理多输入通道的场景。输出特征图往往也会具有多个通道,而且在神经网络的计算中常常是把一个批次的样本放在一起计算,所以卷积算子需要具有批量处理多输入和多输出通道数据的功能,下面将分别介绍这几种场景的操作方式。
- 多输入通道场景
上面的例子中,卷积层的数据是一个2维数组,但实际上一张图片往往含有RGB三个通道,要计算卷积的输出结果,卷积核的形式也会发生变化。假设输入图片的通道数为
,输入数据的形状是 ,计算过程如 图8 所示。
- 对每个通道分别设计一个2维数组作为卷积核,卷积核数组的形状是
。
- 对任一通道
,分别用大小为 的卷积核在大小为 的二维数组上做卷积。
- 将这
个通道的计算结果相加,得到的是一个形状为 的二维数组。
图8:多输入通道计算过程
- 多输出通道场景
一般来说,卷积操作的输出特征图也会具有多个通道
,这时我们需要设计 个维度为 的卷积核,卷积核数组的维度是 ,如 图9 所示。
- 对任一输出通道 ,分别使用上面描述的形状为 的卷积核对输入图片做卷积。
- 将这 个形状为 的二维数组拼接在一起,形成维度为 的三维数组。
说明:
通常将卷积核的输出通道数叫做卷积核的个数。
图9:多输出通道计算过程
- 批量操作
在卷积神经网络的计算中,通常将多个样本放在一起形成一个mini-batch进行批量操作,即输入数据的维度是
。由于会对每张图片使用同样的卷积核进行卷积操作,卷积核的维度与上面多输出通道的情况一样,仍然是 ,输出特征图的维度是 ,如 图10 所示。
图10:批量操作
飞桨卷积API介绍
飞桨卷积算子对应的API是paddle.fluid.dygraph.nn.Conv2D,用户可以直接调用API进行计算,也可以在此基础上修改。常用的参数如下:
- name_scope, 卷积层的名字,数据类型是字符串,可以是"conv1"或者"conv2"等形式。
- num_filters, 输出通道数目,相当于上文中的 。
- filter_size, 卷积核大小,可以是整数,比如3;或者是两个整数的list,例如[3, 3]。
- stride, 步幅,可以是整数,比如2;或者是两个整数的list,例如[2, 2]。
- padding, 填充大小,可以是整数,比如1;或者是两个整数的list,例如[1, 1]。
- act, 激活函数,卷积操作完成之后使用此激活函数作用在神经元上。
输入数据维度
,输出数据维度 ,权重参数 的维度 ,偏置参数 的维度是 。
卷积算子应用举例
下面介绍卷积算子在图片中应用的三个案例,并观察其计算结果。
案例1——简单的黑白边界检测
下面是使用Conv2D算子完成一个图像边界检测的任务。图像左边为光亮部分,右边为黑暗部分,需要检测出光亮跟黑暗的分界处。 可以设置宽度方向的卷积核为
,此卷积核会将宽度方向间隔为1的两个像素点的数值相减。当卷积核在图片上滑动的时候,如果它所覆盖的像素点位于亮度相同的区域,则左右间隔为1的两个像素点数值的差为0。只有当卷积核覆盖的像素点有的处于光亮区域,有的处在黑暗区域时,左右间隔为1的两个点像素值的差才不为0。将此卷积核作用到图片上,输出特征图上只有对应黑白分界线的地方像素值才不为0。具体代码如下所示,结果输出在下方的图案中。
import matplotlib.pyplot as plt
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
%matplotlib inline
with fluid.dygraph.guard():
# 创建初始化权重参数w
w = np.array([1, 0, -1], dtype='float32')
# 将权重参数调整成维度为[cout, cin, kh, kw]的四维张量
w = w.reshape([1, 1, 1, 3])
# 创建卷积算子,设置输出通道数,卷积核大小,和初始化权重参数
# filter_size = [1, 3]表示kh = 1, kw=3
# 创建卷积算子的时候,通过参数属性param_attr,指定参数初始化方式
# 这里的初始化方式时,从numpy.ndarray初始化卷积参数
conv = Conv2D(num_channels=1, num_filters=1, filter_size=[1, 3],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)))
# 创建输入图片,图片左边的像素点取值为1,右边的像素点取值为0
img = np.ones([50,50], dtype='float32')
img[:, 30:] = 0.
# 将图片形状调整为[N, C, H, W]的形式
x = img.reshape([1,1,50,50])
# 将numpy.ndarray转化成paddle中的tensor
x = fluid.dygraph.to_variable(x)
# 使用卷积算子作用在输入图片上
y = conv(x)
# 将输出tensor转化为numpy.ndarray
out = y.numpy()
f = plt.subplot(121)
f.set_title('input image', fontsize=15)
plt.imshow(img, cmap='gray')
f = plt.subplot(122)
f.set_title('output featuremap', fontsize=15)
# 卷积算子Conv2D输出数据形状为[N, C, H, W]形式
# 此处N, C=1,输出数据形状为[1, 1, H, W],是4维数组
# 但是画图函数plt.imshow画灰度图时,只接受2维数组
# 通过numpy.squeeze函数将大小为1的维度消除
plt.imshow(out.squeeze(), cmap='gray')
plt.show()
- <Figure size 432x288 with 2 Axes>
# 查看卷积层的参数
with fluid.dygraph.guard():
# 通过 conv.parameters()查看卷积层的参数,返回值是list,包含两个元素
print(conv.parameters())
# 查看卷积层的权重参数名字和数值
print(conv.parameters()[0].name, conv.parameters()[0].numpy())
# 参看卷积层的偏置参数名字和数值
print(conv.parameters()[1].name, conv.parameters()[1].numpy())
- [name conv2d_0.w_0, dtype: VarType.FP32 shape: [1, 1, 1, 3] lod: {}
- dim: 1, 1, 1, 3
- layout: NCHW
- dtype: float
- data: [1 0 -1]
- , name conv2d_0.b_0, dtype: VarType.FP32 shape: [1] lod: {}
- dim: 1
- layout: NCHW
- dtype: float
- data: [0]
- ]
- conv2d_0.w_0 [[[[ 1. 0. -1.]]]]
- conv2d_0.b_0 [0.]
案例2——图像中物体边缘检测
上面展示的是一个人为构造出来的简单图片使用卷积检测明暗分界处的例子,对于真实的图片,也可以使用合适的卷积核对它进行操作,用来检测物体的外形轮廓,观察输出特征图跟原图之间的对应关系,如下代码所示:
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
img = Image.open('./work/images/section1/000000098520.jpg')
with fluid.dygraph.guard():
# 设置卷积核参数
w = np.array([[-1,-1,-1], [-1,8,-1], [-1,-1,-1]], dtype='float32')/8
w = w.reshape([1, 1, 3, 3])
# 由于输入通道数是3,将卷积核的形状从[1,1,3,3]调整为[1,3,3,3]
w = np.repeat(w, 3, axis=1)
# 创建卷积算子,输出通道数为1,卷积核大小为3x3,
# 并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(num_channels=3, num_filters=1, filter_size=[3, 3],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)))
# 将读入的图片转化为float32类型的numpy.ndarray
x = np.array(img).astype('float32')
# 图片读入成ndarry时,形状是[H, W, 3],
# 将通道这一维度调整到最前面
x = np.transpose(x, (2,0,1))
# 将数据形状调整为[N, C, H, W]格式
x = x.reshape(1, 3, img.height, img.width)
x = fluid.dygraph.to_variable(x)
y = conv(x)
out = y.numpy()
plt.figure(figsize=(20, 10))
f = plt.subplot(121)
f.set_title('input image', fontsize=15)
plt.imshow(img)
f = plt.subplot(122)
f.set_title('output feature map', fontsize=15)
plt.imshow(out.squeeze(), cmap='gray')
plt.show()
- <Figure size 1440x720 with 2 Axes>
案例3——图像均值模糊
另外一种比较常见的卷积核是用当前像素跟它邻域内的像素取平均,这样可以使图像上噪声比较大的点变得更平滑,如下代码所示:
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
# 读入图片并转成numpy.ndarray
#img = Image.open('./images/section1/000000001584.jpg')
img = Image.open('./work/images/section1/000000355610.jpg').convert('L')
img = np.array(img)
# 换成灰度图
with fluid.dygraph.guard():
# 创建初始化参数
w = np.ones([1, 1, 5, 5], dtype = 'float32')/25
conv = Conv2D(num_channels=1, num_filters=1, filter_size=[5, 5],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)))
x = img.astype('float32')
x = x.reshape(1,1,img.shape[0], img.shape[1])
x = fluid.dygraph.to_variable(x)
y = conv(x)
out = y.numpy()
plt.figure(figsize=(20, 12))
f = plt.subplot(121)
f.set_title('input image')
plt.imshow(img, cmap='gray')
f = plt.subplot(122)
f.set_title('output feature map')
out = out.squeeze()
plt.imshow(out, cmap='gray')
plt.show()
- <Figure size 1440x864 with 2 Axes>
池化(Pooling)
池化是使用某一位置的相邻输出的总体统计特征代替网络在该位置的输出,其好处是当输入数据做出少量平移时,经过池化函数后的大多数输出还能保持不变。比如:当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时候通过约化某一片区域的像素点来得到总体统计特征会显得很有用。由于池化之后特征图会变得更小,如果后面连接的是全连接层,能有效的减小神经元的个数,节省存储空间并提高计算效率。 如 图11 所示,将一个
的区域池化成一个像素点。通常有两种方法,平均池化和最大池化。
图11:池化
- 如图11(a):平均池化。这里使用大小为 的池化窗口,每次移动的步长也为2,对池化窗口内的元素数值取平均,得到相应的输出特征图的像素值。
- 如图11(b):最大池化。对池化窗口覆盖区域内的元素取最大值,得到输出特征图的像素值。当池化窗口在图片上滑动时,会得到整张输出特征图。池化窗口的大小称为池化大小,用 表示。在卷积神经网络中用的比较多的是窗口大小为 ,步长也为2的池化。
与卷积核类似,池化窗口在图片上滑动时,每次移动的步长称为步幅,当宽和高方向的移动大小不一样时,分别用
和 表示。也可以对需要进行池化的图片进行填充,填充方式与卷积类似,假设在第一行之前填充 行,在最后一行后面填充 行。在第一列之前填充 列,在最后一列之后填充 列,则池化层的输出特征图大小为:
在卷积神经网络中,通常使用
大小的池化窗口,步幅也使用2,填充为0,则输出特征图的尺寸为:
通过这种方式的池化,输出特征图的高和宽都减半,但通道数不会改变。
ReLU激活函数
前面介绍的网络结构中,普遍使用Sigmoid函数做激活函数。在神经网络发展的早期,Sigmoid函数用的比较多,而目前用的较多的激活函数是ReLU。这是因为Sigmoid函数在反向传播过程中,容易造成梯度的衰减。让我们仔细观察Sigmoid函数的形式,就能发现这一问题。
Sigmoid激活函数定义如下:
ReLU激活函数的定义如下:
下面的程序画出了Sigmoid和ReLU函数的曲线图:
# ReLU和Sigmoid激活函数示意图
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
plt.figure(figsize=(10, 5))
# 创建数据x
x = np.arange(-10, 10, 0.1)
# 计算Sigmoid函数
s = 1.0 / (1 + np.exp(0. - x))
# 计算ReLU函数
y = np.clip(x, a_min=0., a_max=None)
#####################################
# 以下部分为画图代码
f = plt.subplot(121)
plt.plot(x, s, color='r')
currentAxis=plt.gca()
plt.text(-9.0, 0.9, r'$y=Sigmoid(x)$', fontsize=13)
currentAxis.xaxis.set_label_text('x', fontsize=15)
currentAxis.yaxis.set_label_text('y', fontsize=15)
f = plt.subplot(122)
plt.plot(x, y, color='g')
plt.text(-3.0, 9, r'$y=ReLU(x)$', fontsize=13)
currentAxis=plt.gca()
currentAxis.xaxis.set_label_text('x', fontsize=15)
currentAxis.yaxis.set_label_text('y', fontsize=15)
plt.show()
- <Figure size 720x360 with 2 Axes>
- 梯度消失现象
在神经网络里面,将经过反向传播之后,梯度值衰减到接近于零的现象称作梯度消失现象。
从上面的函数曲线可以看出,当x为较大的正数的时候,Sigmoid函数数值非常接近于1,函数曲线变得很平滑,在这些区域Sigmoid函数的导数接近于零。当x为较小的负数的时候,Sigmoid函数值非常接近于0,函数曲线也很平滑,在这些区域Sigmoid函数的导数也接近于0。只有当x的取值在0附近时,Sigmoid函数的导数才比较大。可以对Sigmoid函数求导数,结果如下所示:
从上面的式子可以看出,Sigmoid函数的导数
最大值为 。前向传播时, ;而在反向传播过程中,x的梯度等于y的梯度乘以Sigmoid函数的导数,如下所示:
使得x的梯度数值最大也不会超过y的梯度的
。
由于最开始是将神经网络的参数随机初始化的,x很有可能取值在数值很大或者很小的区域,这些地方都可能造成Sigmoid函数的导数接近于0,导致x的梯度接近于0;即使x取值在接近于0的地方,按上面的分析,经过Sigmoid函数反向传播之后,x的梯度不超过y的梯度的
,如果有多层网络使用了Sigmoid激活函数,则比较靠前的那些层梯度将衰减到非常小的值。
ReLU函数则不同,虽然在
的地方,ReLU函数的导数为0。但是在 的地方,ReLU函数的导数为1,能够将y的梯度完整的传递给x,而不会引起梯度消失。
批归一化(Batch Normalization)
批归一化方法方法(Batch Normalization,BatchNorm)是由Ioffe和Szegedy于2015年提出的,已被广泛应用在深度学习中,其目的是对神经网络中间层的输出进行标准化处理,使得中间层的输出更加稳定。
通常我们会对神经网络的数据进行标准化处理,处理后的样本数据集满足均值为0,方差为1的统计分布,这是因为当输入数据的分布比较固定时,有利于算法的稳定和收敛。对于深度神经网络来说,由于参数是不断更新的,即使输入数据已经做过标准化处理,但是对于比较靠后的那些层,其接收到的输入仍然是剧烈变化的,通常会导致数值不稳定,模型很难收敛。BatchNorm能够使神经网络中间层的输出变得更加稳定,并有如下三个优点:
使学习快速进行(能够使用较大的学习率)
降低模型对初始值的敏感性
从一定程度上抑制过拟合
BatchNorm主要思路是在训练时按mini-batch为单位,对神经元的数值进行归一化,使数据的分布满足均值为0,方差为1。具体计算过程如下:
1. 计算mini-batch内样本的均值
其中
表示mini-batch中的第 个样本。
例如输入mini-batch包含3个样本,每个样本有2个特征,分别是:
对每个特征分别计算mini-batch内样本的均值:
则样本均值是:
2. 计算mini-batch内样本的方差
上面的计算公式先计算一个批次内样本的均值
和方差 ,然后再对输入数据做归一化,将其调整成均值为0,方差为1的分布。
对于上述给定的输入数据
,可以计算出每个特征对应的方差:
则样本方差是:
3. 计算标准化之后的输出
其中
是一个微小值(例如 ),其主要作用是为了防止分母为0。
对于上述给定的输入数据
,可以计算出标准化之后的输出:
- 读者可以自行验证由 构成的mini-batch,是否满足均值为0,方差为1的分布。
如果强行限制输出层的分布是标准化的,可能会导致某些特征模式的丢失,所以在标准化之后,BatchNorm会紧接着对数据做缩放和平移。
其中
和 是可学习的参数,可以赋初始值 ,在训练过程中不断学习调整。
上面列出的是BatchNorm方法的计算逻辑,下面针对两种类型的输入数据格式分别进行举例。飞桨支持输入数据的维度大小为2、3、4、5四种情况,这里给出的是维度大小为2和4的示例。
- 示例一: 当输入数据形状是 时,一般对应全连接层的输出,示例代码如下所示。
这种情况下会分别对K的每一个分量计算N个样本的均值和方差,数据和参数对应如下:
- 输入 x, [N, K]
- 输出 y, [N, K]
- 均值 ,[K, ]
- 方差 , [K, ]
- 缩放参数 , [K, ]
- 平移参数 , [K, ]
# 输入数据形状是 [N, K]时的示例
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import BatchNorm
# 创建数据
data = np.array([[1,2,3], [4,5,6], [7,8,9]]).astype('float32')
# 使用BatchNorm计算归一化的输出
with fluid.dygraph.guard():
# 输入数据维度[N, K],num_channels等于K
bn = BatchNorm(num_channels=3)
x = fluid.dygraph.to_variable(data)
y = bn(x)
print('output of BatchNorm 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计算结果是否一致
- output of BatchNorm Layer:
- [[-1.2247438 -1.2247438 -1.2247438]
- [ 0. 0. 0. ]
- [ 1.2247438 1.2247438 1.2247438]]
- std 4.0, mean 2.449489742783178,
- output [-1.22474487 0. 1.22474487]
- 示例二: 当输入数据形状是 时, 一般对应卷积层的输出,示例代码如下所示。
这种情况下会沿着C这一维度进行展开,分别对每一个通道计算N个样本中总共
个像素点的均值和方差,数据和参数对应如下:
- 输入 x, [N, C, H, W]
- 输出 y, [N, C, H, W]
- 均值 ,[C, ]
- 方差 , [C, ]
- 缩放参数 , [C, ]
- 平移参数 , [C, ]
小窍门:
可能有读者会问:“BatchNorm里面不是还要对标准化之后的结果做仿射变换吗,怎么使用Numpy计算的结果与BatchNorm算子一致?” 这是因为BatchNorm算子里面自动设置初始值
,这时候仿射变换相当于是恒等变换。在训练过程中这两个参数会不断的学习,这时仿射变换就会起作用。
# 输入数据形状是[N, C, H, W]时的batchnorm示例
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import BatchNorm
# 设置随机数种子,这样可以保证每次运行结果一致
np.random.seed(100)
# 创建数据
data = np.random.rand(2,3,3,3).astype('float32')
# 使用BatchNorm计算归一化的输出
with fluid.dygraph.guard():
# 输入数据维度[N, C, H, W],num_channels等于C
bn = BatchNorm(num_channels=3)
x = fluid.dygraph.to_variable(data)
y = bn(x)
print('input of BatchNorm Layer: \n {}'.format(x.numpy()))
print('output of BatchNorm 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计算出来的输出
# 与BatchNorm算子的结果略有差别,
# 因为在BatchNorm算子为了保证数值的稳定性,
# 在分母里面加上了一个比较小的浮点数epsilon=1e-05
- input of BatchNorm Layer:
- [[[[0.54340494 0.2783694 0.4245176 ]
- [0.84477615 0.00471886 0.12156912]
- [0.67074907 0.82585275 0.13670659]]
- [[0.5750933 0.89132196 0.20920213]
- [0.18532822 0.10837689 0.21969749]
- [0.9786238 0.8116832 0.17194101]]
- [[0.81622475 0.27407375 0.4317042 ]
- [0.9400298 0.81764936 0.33611196]
- [0.17541045 0.37283206 0.00568851]]]
- [[[0.25242636 0.7956625 0.01525497]
- [0.5988434 0.6038045 0.10514768]
- [0.38194343 0.03647606 0.89041156]]
- [[0.98092085 0.05994199 0.89054596]
- [0.5769015 0.7424797 0.63018394]
- [0.5818422 0.02043913 0.21002658]]
- [[0.5446849 0.76911515 0.25069523]
- [0.2858957 0.8523951 0.9750065 ]
- [0.8848533 0.35950786 0.59885895]]]]
- output of BatchNorm Layer:
- [[[[ 0.4126078 -0.46198368 0.02029109]
- [ 1.4071034 -1.3650038 -0.97940934]
- [ 0.832831 1.344658 -0.9294571 ]]
- [[ 0.2520175 1.2038351 -0.84927964]
- [-0.9211378 -1.1527538 -0.8176896 ]
- [ 1.4666051 0.96413004 -0.961432 ]]
- [[ 0.9541142 -0.9075856 -0.36629617]
- [ 1.37925 0.9590063 -0.6945517 ]
- [-1.2463869 -0.5684581 -1.8291974 ]]]
- [[[-0.5475932 1.2450331 -1.3302356 ]
- [ 0.5955492 0.6119205 -1.0335984 ]
- [-0.12019944 -1.2602081 1.5576957 ]]
- [[ 1.473519 -1.2985382 1.2014993 ]
- [ 0.25745988 0.7558342 0.41783488]
- [ 0.27233088 -1.4174379 -0.8467981 ]]
- [[ 0.02166975 0.79234385 -0.98786545]
- [-0.86699003 1.0783203 1.4993572 ]
- [ 1.1897788 -0.6142123 0.20769882]]]]
- channel 0 of input data:
- [[[0.54340494 0.2783694 0.4245176 ]
- [0.84477615 0.00471886 0.12156912]
- [0.67074907 0.82585275 0.13670659]]
- [[0.25242636 0.7956625 0.01525497]
- [0.5988434 0.6038045 0.10514768]
- [0.38194343 0.03647606 0.89041156]]]
- std 0.4183686077594757, mean 0.3030227720737457,
- output:
- [[[ 0.41263014 -0.46200886 0.02029219]
- [ 1.4071798 -1.3650781 -0.9794626 ]
- [ 0.8328762 1.3447311 -0.92950773]]
- [[-0.54762304 1.2451009 -1.3303081 ]
- [ 0.5955816 0.61195374 -1.0336547 ]
- [-0.12020606 -1.2602768 1.5577804 ]]]
- 预测时使用BatchNorm
上面介绍了在训练过程中使用BatchNorm对一批样本进行归一化的方法,但如果使用同样的方法对需要预测的一批样本进行归一化,则预测结果会出现不确定性。
例如样本A、样本B作为一批样本计算均值和方差,与样本A、样本C和样本D作为一批样本计算均值和方差,得到的结果一般来说是不同的。那么样本A的预测结果就会变得不确定,这对预测过程来说是不合理的。解决方法是在训练过程中将大量样本的均值和方差保存下来,预测时直接使用保存好的值而不再重新计算。实际上,在BatchNorm的具体实现中,训练时会计算均值和方差的移动平均值。在飞桨中,默认是采用如下方式计算:
在训练过程的最开始将
和 设置为0,每次输入一批新的样本,计算出 和 ,然后通过上面的公式更新 和 ,在训练的过程中不断的更新它们的值,并作为BatchNorm层的参数保存下来。预测的时候将会加载参数 和 ,用他们来代替 和 。
丢弃法(Dropout)
丢弃法(Dropout)是深度学习中一种常用的抑制过拟合的方法,其做法是在神经网络学习过程中,随机删除一部分神经元。训练时,随机选出一部分神经元,将其输出设置为0,这些神经元将不对外传递信号。
图12 是Dropout示意图,左边是完整的神经网络,右边是应用了Dropout之后的网络结构。应用Dropout之后,会将标了
的神经元从网络中删除,让它们不向后面的层传递信号。在学习过程中,丢弃哪些神经元是随机决定,因此模型不会过度依赖某些神经元,能一定程度上抑制过拟合。
图12 Dropout示意图
在预测场景时,会向前传递所有神经元的信号,可能会引出一个新的问题:训练时由于部分神经元被随机丢弃了,输出数据的总大小会变小了。比如:计算其
范数会比不使用Dropout时变小,但是预测时却没有丢弃神经元,这将导致训练和预测时数据的分布不一样。为了解决这个问题,飞桨支持如下两种方法:
- 1 downgrade_in_infer
训练时以比例
随机丢弃一部分神经元,不向后传递它们的信号;预测时向后传递所有神经元的信号,但是将每个神经元上的数值乘以 。
- 2 upscale_in_train
训练时以比例
随机丢弃一部分神经元,不向后传递它们的信号,但是将那些被保留的神经元上的数值除以 ;预测时向后传递所有神经元的信号,不做任何处理。
在飞桨dropout API中,paddle.fluid.layers.dropout通过dropout_implementation参数来指定用哪种方式对神经元进行操作,dropout_implementation参数的可选值是’downgrade_in_infer’或’upscale_in_train’,缺省值是’downgrade_in_infer’。
说明:
不同框架中dropout的默认处理方式可能不一样,读者可以查看其API以确认用的是哪种方式。
飞桨dropout API包含的主要参数如下:
- x,数据类型是Tensor,需要采用丢弃法进行操作的对象。
- dropout_prob,对x中元素进行丢弃的概率,即输入单元设置为0的概率,该参数对元素的丢弃概率是针对于每一个元素而言而不是对所有的元素而言。举例说,假设矩阵内有12个数字,经过概率为0.5的dropout未必一定有6个零。
- is_test,是否运行在测试阶段,由于dropout在训练和测试阶段表现不一样,通过此参数控制其表现,默认值为False。
- dropout_implementation,丢弃法的实现方式,有’downgrade_in_infer’和’upscale_in_train’两种,具体情况请见上面的说明,默认是’downgrade_in_infer’。
下面这段程序展示了经过dropout之后输出数据的形式。
# dropout操作
import numpy as np
import paddle
import paddle.fluid as fluid
# 设置随机数种子,这样可以保证每次运行结果一致
np.random.seed(100)
# 创建数据[N, C, H, W],一般对应卷积层的输出
data1 = np.random.rand(2,3,3,3).astype('float32')
# 创建数据[N, K],一般对应全连接层的输出
data2 = np.arange(1,13).reshape([-1, 3]).astype('float32')
# 使用dropout作用在输入数据上
with fluid.dygraph.guard():
x1 = fluid.dygraph.to_variable(data1)
out1_1 = fluid.layers.dropout(x1, dropout_prob=0.5, is_test=False)
out1_2 = fluid.layers.dropout(x1, dropout_prob=0.5, is_test=True)
x2 = fluid.dygraph.to_variable(data2)
out2_1 = fluid.layers.dropout(x2, dropout_prob=0.5, \
dropout_implementation='upscale_in_train')
out2_2 = fluid.layers.dropout(x2, dropout_prob=0.5, \
dropout_implementation='upscale_in_train', is_test=True)
print('x1 {}, \n out1_1 \n {}, \n out1_2 \n {}'.format(data1, out1_1.numpy(), out1_2.numpy()))
print('x2 {}, \n out2_1 \n {}, \n out2_2 \n {}'.format(data2, out2_1.numpy(), out2_2.numpy()))
- x1 [[[[0.54340494 0.2783694 0.4245176 ]
- [0.84477615 0.00471886 0.12156912]
- [0.67074907 0.82585275 0.13670659]]
- [[0.5750933 0.89132196 0.20920213]
- [0.18532822 0.10837689 0.21969749]
- [0.9786238 0.8116832 0.17194101]]
- [[0.81622475 0.27407375 0.4317042 ]
- [0.9400298 0.81764936 0.33611196]
- [0.17541045 0.37283206 0.00568851]]]
- [[[0.25242636 0.7956625 0.01525497]
- [0.5988434 0.6038045 0.10514768]
- [0.38194343 0.03647606 0.89041156]]
- [[0.98092085 0.05994199 0.89054596]
- [0.5769015 0.7424797 0.63018394]
- [0.5818422 0.02043913 0.21002658]]
- [[0.5446849 0.76911515 0.25069523]
- [0.2858957 0.8523951 0.9750065 ]
- [0.8848533 0.35950786 0.59885895]]]],
- out1_1
- [[[[0.54340494 0.2783694 0.4245176 ]
- [0. 0.00471886 0.12156912]
- [0.67074907 0. 0.13670659]]
- [[0. 0.89132196 0.20920213]
- [0.18532822 0.10837689 0.21969749]
- [0. 0.8116832 0.17194101]]
- [[0.81622475 0. 0.4317042 ]
- [0. 0.81764936 0.33611196]
- [0. 0. 0.00568851]]]
- [[[0. 0. 0. ]
- [0. 0.6038045 0.10514768]
- [0. 0. 0.89041156]]
- [[0.98092085 0. 0. ]
- [0. 0.7424797 0. ]
- [0.5818422 0. 0. ]]
- [[0.5446849 0.76911515 0.25069523]
- [0. 0. 0.9750065 ]
- [0.8848533 0. 0.59885895]]]],
- out1_2
- [[[[0. 0. 0.4245176 ]
- [0.84477615 0. 0.12156912]
- [0.67074907 0.82585275 0.13670659]]
- [[0.5750933 0. 0. ]
- [0. 0.10837689 0. ]
- [0. 0. 0. ]]
- [[0.81622475 0.27407375 0. ]
- [0.9400298 0.81764936 0. ]
- [0. 0. 0. ]]]
- [[[0. 0. 0.01525497]
- [0.5988434 0. 0.10514768]
- [0. 0.03647606 0. ]]
- [[0.98092085 0. 0. ]
- [0.5769015 0. 0. ]
- [0. 0.02043913 0.21002658]]
- [[0.5446849 0. 0. ]
- [0. 0.8523951 0.9750065 ]
- [0. 0. 0.59885895]]]]
- x2 [[ 1. 2. 3.]
- [ 4. 5. 6.]
- [ 7. 8. 9.]
- [10. 11. 12.]],
- out2_1
- [[ 2. 4. 6.]
- [ 8. 0. 12.]
- [14. 0. 18.]
- [ 0. 0. 24.]],
- out2_2
- [[ 2. 4. 6.]
- [ 8. 0. 0.]
- [14. 16. 18.]
- [20. 22. 0.]]