计算机视觉:让机器学会如何去“看”
最基本的子任务:
主要内容
b [ i , j ] = ∑ u , v a [ i + u , j + v ] ⋅ w [ u , v ] b[i ,j] = \sum_{u,v} a[i+u,j+v]\cdot w[u,v] b[i,j]=u,v∑a[i+u,j+v]⋅w[u,v]
在卷积神经网络示意图中 用 ⨂ \bigotimes ⨂符号表示卷积操作
假设输入图片大小为 H × \times ×W,卷积核大小为 k h × k w k_h\times k_w kh×kw,输出特征图尺寸是
H o u t = H − k h + 1 H_{out} =H- k_h + 1 Hout=H−kh+1
W o u t = H − k w + 1 W_{out}=H - k_w + 1 Wout=H−kw+1
新输出图片尺寸公式
假设输入图片大小为 H × \times ×W,卷积核大小为 k h × k w k_h\times k_w kh×kw,补零的行数和列数(padding)为p,步幅(stride)为s,输出特征图尺寸是
H o u t = H + 2 p h − k h s h + 1 H_{out} =\frac{H + 2p_h- k_h}{s_h} + 1 Hout=shH+2ph−kh+1 W o u t = W + 2 p w − k w s w + 1 W_{out} =\frac{W + 2p_w- k_w}{s_w} + 1 Wout=swW+2pw−kw+1
思考:当卷积核处在图片上不同位置输出什么样?
黑白边界处输出1
其他位置输出0
import matplotlib.pyplot as plt
import numpy as np
import paddle
from paddle.nn import Conv2D
from paddle.nn.initializer import Assign
%matplotlib inline
# 创建初始化权重参数w
w = np.array([1, 0, -1], dtype='float32')
# 将权重参数调整成维度为[cout, cin, kh, kw]的四维张量
w = w.reshape([1, 1, 1, 3])
# 创建卷积算子,设置输出通道数,卷积核大小,和初始化权重参数
# kernel_size = [1, 3]表示kh = 1, kw=3
# 创建卷积算子的时候,通过参数属性weight_attr指定参数初始化方式
# 这里的初始化方式时,从numpy.ndarray初始化卷积参数
conv = Conv2D(in_channels=1, out_channels=1, kernel_size=[1, 3],
weight_attr=paddle.ParamAttr(
initializer=Assign(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 = paddle.to_tensor(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()
池化是使用某一位置的相邻输出的总体统计特征代替网络在该位置的输出
其好处是当输入数据做出少量平移时,经过池化函数后的大多数输出还能保持不变。比如:当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时候通过池化某一片区域的像素点来得到总体统计特征会显得很有用。由于池化之后特征图会变得更小,如果后面连接的是全连接层,能有效的减小神经元的个数,节省存储空间并提高计算效率。 如 图 所示,将一个2×2的区域池化成一个像素点。通常有两种方法,平均池化和最大池化。
与卷积核类似,池化窗口在图片上滑动时,每次移动的步长称为步幅,当宽和高方向的移动大小不一样时,分别用 s w s_w sw和 s h s_h sh表示。也可以对需要进行池化的图片进行填充,填充方式与卷积类似,假设在第一行之前填充 p h 1 p_{h1} ph1行,在最后一行后面填充 p h 2 p_{h2} ph2行。在第一列之前填充 p w 1 p_{w1} pw1列,在最后一列之后填充 p w 2 p_{w2} pw2列,则池化层的输出特征图大小为:
H o u t = H + p h 1 + p h 2 − k h s h + 1 H_{out} =\frac{H + p_{h1}+p_{h2}- k_h}{s_h} + 1 Hout=shH+ph1+ph2−kh+1 H o u t = H + p w 1 + p w 2 − k w s w + 1 H_{out} =\frac{H + p_{w1}+p_{w2}- k_w}{s_w} + 1 Hout=swH+pw1+pw2−kw+1
在神经网络发展的早期,Sigmoid函数用的比较多,而目前用的较多的激活函数是ReLU。这是因为Sigmoid函数在反向传播过程中,容易造成梯度的衰减。让我们仔细观察Sigmoid函数的形式,就能发现这一问题。
Sigmoid激活函数定义如下:
y = 1 1 + e − x y= \frac{1}{1+e^{-x}} y=1+e−x1
ReLU激活函数的定义如下:
y = { 0 , ( x < 0 ) x , ( x ≥ 0 ) y=\left\{ \begin{aligned} 0 ,\quad (x<0)\\ x , \quad (x \ge 0)\\ \end{aligned} \right. y={0,(x<0)x,(x≥0)
下面的程序画出了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()
批归一化方法(Batch Normalization,BatchNorm)是由Ioffe和Szegedy于2015年提出的,已被广泛应用在深度学习中,其目的是对神经网络中间层的输出进行标准化处理,使得中间层的输出更加稳定。
通常我们会对神经网络的数据进行标准化处理,处理后的样本数据集满足均值为0,方差为1的统计分布,这是因为当输入数据的分布比较固定时,有利于算法的稳定和收敛。对于深度神经网络来说,由于参数是不断更新的,即使输入数据已经做过标准化处理,但是对于比较靠后的那些层,其接收到的输入仍然是剧烈变化的,通常会导致数值不稳定,模型很难收敛。BatchNorm能够使神经网络中间层的输出变得更加稳定,并有如下三个优点:
使学习快速进行(能够使用较大的学习率)
降低模型对初始值的敏感性
从一定程度上抑制过拟合
BatchNorm主要思路是在训练时以mini-batch为单位,对神经元的数值进行归一化,使数据的分布满足均值为0,方差为1。
具体计算过程如下:
上面列出的是BatchNorm方法的计算逻辑,下面针对两种类型的输入数据格式分别进行举例。飞桨支持输入数据的维度大小为2、3、4、5四种情况,这里给出的是维度大小为2和4的示例。
示例一: 当输入数据形状是[N,K]时,一般对应全连接层的输出,示例代码如下所示。
这种情况下会分别对K的每一个分量计算N个样本的均值和方差,数据和参数对应如下:
# 输入数据形状是 [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))
示例二: 当输入数据形状是[N,C,H,W]时, 一般对应卷积层的输出,示例代码如下所示。
# 输入数据形状是[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
在预测场景时,会向前传递所有神经元的信号,可能会引出一个新的问题:训练时由于部分神经元被随机丢弃了,输出数据的总大小会变小。比如:计算其L1范数会比不使用Dropout时变小,但是预测时却没有丢弃神经元,这将导致训练和预测时数据的分布不一样。为了解决这个问题,飞桨支持如下两种方法:
downscale_in_infer
训练时以比例rrr随机丢弃一部分神经元,不向后传递它们的信号;预测时向后传递所有神经元的信号,但是将每个神经元上的数值乘以 (1−r)。
upscale_in_train
训练时以比例rrr随机丢弃一部分神经元,不向后传递它们的信号,但是将那些被保留的神经元上的数值除以 (1−r);预测时向后传递所有神经元的信号,不做任何处理。