- 本文为《深度学习入门 – 基于 Python 的理论与实现》的读书笔记
目录
加深层的动机
感受野 (receptive field)
- 理解感受野是理解卷积神经网络工作的基础,尤其是对于使用 Anchor 作为强先验区域的物体检测算法,如 Faster RCNN 和 SSD。如何设置 Anchor 的大小,Anchor 应该对应特征图的哪一层,都应该考虑感受野。通常来讲,Anchor 的大小应该与感受野相匹配,尤其是有效感受野,过大或过小都不好
定义
- 感受野(Receptive Field)的定义是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。再通俗点的解释是,特征图上的一个点对应输入图上的区域
由上图可以看出,经过几个卷积层之后,特征图的大小逐渐变小,一个特征所表示的信息量越来越多
感受野的计算
- 注意上图中 Layer1 到 Layer2 的过程中,padding = 1,这里的 1 是指特征所占的区域,即一个特征所占的感受野,所以 Conv2 这张图上才会在外面加上三个格。stride = 2 也是同样的道理,2 表示跨过两个特征
- 从上图可以整理出以下公式:
j l = j l − 1 ∗ s l \begin{aligned} j_{l} = j_{l-1} * s_{l} \end{aligned} jl=jl−1∗sl r l + 1 = r l + ( k l + 1 − 1 ) ∗ j l \begin{aligned} r_{l+1} = r_l + (k_{l+1} - 1) *j_{l} \end{aligned} rl+1=rl+(kl+1−1)∗jl其中 r l r_l rl 为第 l l l 层的感受野大小, k k k 为卷积核大小, j l j_l jl 为第 l l l 层中两个特征相隔的距离 - 设
R
F
l
+
1
RF_{l+1}
RFl+1 为第
l
l
l 层的感受野大小,
k
k
k 为卷积核大小,
S
t
r
i
d
e
l
Stride_l
Stridel 为第
l
l
l 层的卷积核步长, 整理一下就可以得到感受野
R
F
RF
RF 的计算公式:
S l = ∏ i = 1 l S t r i d e i \begin{aligned} S_{l} = \prod_{i=1}^l Stride_i \end{aligned} Sl=i=1∏lStridei R F l + 1 = R F l + ( k − 1 ) ∗ S l \begin{aligned} RF_{l+1} =RF_l + (k - 1) *S_{l} \end{aligned} RFl+1=RFl+(k−1)∗Sl由上述公式计算的感受野通常很大,而因为输入层中边缘点的使用次数明显比中间点少,因此做出的贡献不同。经过多层的卷积堆叠之后,输入层对于特征图点做出的贡献分布呈高斯分布形状。因此,实际的有效感受野 Effective Receptive Field 往往小于理论感受野
减少网络的参数数量
- 与没有加深层的网络相比,加深了层的网络可以用更少的参数达到同等水平(或者更强)的表现力。这一点结合卷积运算中的滤波器大小来思考就好理解了:
使用 5 × 5 5 \times 5 5×5 的滤波器每个输出节点都是从输入数据的某个 5 × 5 5 \times 5 5×5 的区域算出来的
由上图可以看出,重复两次 3 × 3 3 \times 3 3×3 的卷积层后,输出数据同样是观察了输入数据的某个 5 × 5 5 \times 5 5×5 的区域后计算出来的 - 也就是说,一次 5 × 5 5 \times 5 5×5 的卷积运算的区域可以看作由两次 3 × 3 3 \times 3 3×3 的卷积运算抵充,而且参数数量更少了。这个参数数量之差会随着层的加深而变大。比如,重复三次 3 × 3 3 \times 3 3×3 的卷积运算时,参数的数量总共是 27。而为了用一次卷积运算“观察”与之相同的区域,需要一个 7 × 7 7 \times 7 7×7 的滤波器,此时的参数数量是 49
- 由此可以看出叠加小型滤波器来加深网络的好处:减少参数的数量,扩大感受野(receptive field,给神经元施加变化的某个局部空间区域)。并且,通过叠加层,将
ReLU
等激活函数夹在卷积层的中间,进一步提高了网络的表现力。这是因为向网络添加了基于激活函数的“非线性”表现力,通过非线性函数的叠加,可以表现更加复杂的东西
使学习更加高效
- 卷积层中,神经元会对边缘等简单的形状有响应,随着层的加深,开始对纹理、物体部件等更加复杂的东西有响应。CNN 的卷积层会分层次地提取信息,从而高效地进行学习
- 例如,考虑一下 “狗”的识别问题。要用浅层网络解决这个问题的话,卷积层需要一下子理解很多“狗”的特征。“狗”有各种各样的种类,根据拍摄环境的不同,外观变化也很大。因此,要理解“狗”的特征,需要大量富有差异性的学习数据,而这会导致学习需要花费很多时间。不过,通过加深网络,就可以分层次地分解需要学习的问题。因此,各层需要学习的问题就变成了更简单的问题。比如,最开始的层只要专注于学习边缘就好,这样一来,只需用较少的学习数据就可以高效地进行学习。这是为什么呢?因为和印有“狗”的照片相比,包含边缘的图像数量众多,并且边缘的模式比“狗”的模式结构更简单
深度学习的高速化
基于 GPU 的高速化
- GPU (Graphic Processing Unit) 原本是作为图像专用的显卡使用的,但由于GPU 可以高速地进行并行数值计算,最近不仅用于图像处理,也用于通用的数值计算
- 深度学习中需要进行大量的乘积累加运算(或者大型矩阵的乘积运算)。这种大量的并行运算正是 GPU 所擅长的(反过来说,CPU 比较擅长连续的、复杂的计算)。因此,与使用单个 CPU 相比,使用 GPU 进行深度学习的运算可以达到惊人的高速化
- GPU 主要由 NVIDIA 和 AMD 两家公司提供。虽然两家的 GPU 都可以用于通用的数值计算,但与深度学习比较“亲近”的是 NVIDIA 的 GPU。实际上,大多数深度学习框架只受益于 NVIDIA 的 GPU。这是因为深度学习的框架中使用了 NVIDIA 提供的 CUDA 这个 面向 GPU 计算的综合开发环境
cuDNN 是在 CUDA 上运行的库,它里面实现了为深度学习最优化过的函数等
CuPy
- 同时要注意 Numpy 这个库是不会主动检测并使用 GPU 的,如果要使用 GPU 进行运算,可以使用它的替代品 CuPy 或者直接使用其他的深度学习框架
- CuPy 是基于 GPU 进行并行计算的库。要使用 CuPy,需要使用安装有 NVIDIA 的 GPU 的机器,并且需要安装 CUDA 这个面向 GPU 的通用并行计算平台
虽然 CuPy 和 NumPy 并不完全兼容,但是它们有许多共同的 API
>>> import cupy as cp
>>> x = cp.arange(6).reshape(2, 3).astype('f')
>>>> x
array([[ 0., 1., 2.],
[ 3., 4., 5.]], dtype=float32)
>>> x.sum(axis=1)
array([ 3., 12.], dtype=float32)
分布式学习
- 为了进一步提高深度学习所需的计算的速度,可以考虑在多个 GPU 或者多台机器上进行分布式计算
“如何进行分布式计算”是一个非常难的课题。它包含了机器间的通信、数据的同步等多个无法轻易解决的问题。可以将这些难题都交给 TensorFlow 等优秀的框架
运算精度的位数缩减
- 在深度学习的高速化中,除了计算量之外,内存容量、总线带宽等也有可能成为瓶颈。关于内存容量,需要考虑将大量的权重参数或中间数据放在内存中。关于总线带宽,当流经G PU(或者CPU)总线的数据超过某个限制时,就会成为瓶颈。考虑到这些情况,我们希望尽可能减少流经网络的数据的位数, 特别是在面向嵌入式应用程序中使用深度学习时,位数缩减非常重要
- 深度学习并不那么需要数值精度的位数。这是神经网络的一个重要性质。这个性质是基于神经网络的健壮性而产生的。这里所说的健壮性是指,比如,即便输入图像附有一些小的噪声,输出结果也仍然保持不变。可以认为,正是因为有了这个健壮性,流经网络的数据即便有所“劣化”,对输出结果的影响也较小
- 根据以往的实验结果,在深度学习中,即便是16 位的半精度浮点数(half float),也可以顺利地进行学习。实际上,NVIDIA 的下一代 GPU 框架 Pascal 也支持半精度浮点数的运算,由此可以认为今后半精度浮点数将被作为标准使用
- 以往的深度学习的实现中并没有注意数值的精度,不过 Python 中一般使用64 位的浮点数。NumPy 中提供了 16 位的半精度浮点数类型(不过,只有 16 位类型的存储,运算本身不用 16 位进行),即便使用 NumPy 的半精度浮点数,识别精度也不会下降
实现深层 CNN
- 这个网络结构参考了 VGG。卷积层全部使用
3
×
3
3 \times 3
3×3 的小型滤波器,
p
a
d
=
1
,
s
t
r
i
d
e
=
1
pad=1,stride=1
pad=1,stride=1 (有一层的卷积层滤波器
p
a
d
=
2
pad=2
pad=2 以保证输入数据在经过最后一层池化层之前长宽均为偶数),使输入数据每经过一个卷积层,长宽不变而通道数变大 (通道数从前面的层开始按顺序以 16、16、32、32、64、64 的方式增加)。池化层用于逐渐减小中间数据的空间大小,使用
2
×
2
2 \times 2
2×2 的滤波器,
p
a
d
=
0
,
s
t
r
i
d
e
=
2
pad=0,stride=2
pad=0,stride=2,使输入数据每经过一个池化层,通道数不变而长宽均减半。同时也加入了 Dropout 层,用于抑制过拟合
import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 当前文件夹的路径
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上级目录到python模块搜索路径
import numpy as np
from func.gradient import numerical_gradient, gradient_check
from layer.common import *
from collections import OrderedDict
import os
import pickle
class DeepConvNet:
"""
网络结构如下所示
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
affine - relu - dropout - affine - dropout - softmax
输入数据(1,28,28)的形状变化
(16, 28, 28) - (16, 28, 28) - (16, 14, 14) -
(32, 14, 14) - (32, 16, 16) - (32, 8, 8) -
(64, 8, 8) - (64, 8, 8) - (64, 4, 4) -
(50) - (10)
如果要改变卷积核的参数或者全连接层中的神经元个数的话,则不仅需要调整初始化方法中的参数,还要在代码中手动更改初始化权重的标准差以及第一个隐藏层的输入数据形状
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10, dropout_ratio=0.5,
pretrain_flag=True, pkl_file_name=None):
self.pkl_file_name = pkl_file_name
if pretrain_flag == 1 and os.path.exists(self.pkl_file_name):
self.load_pretrain_model()
else:
# 初始化权重===========
# 各层的神经元平均与前一层的几个神经元有连接
# conv1 conv2 conv3 conv4 conv5 conv6 affine1 affine2
pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
wight_init_scales = np.sqrt(2.0 / pre_node_nums) # 使用ReLU的情况下推荐的初始值
self.params = {}
pre_channel_num = input_dim[0]
for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
self.params['W' + str(idx+1)] = wight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
pre_channel_num = conv_param['filter_num']
self.params['W7'] = wight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
self.params['b7'] = np.zeros(hidden_size)
self.params['W8'] = wight_init_scales[7] * np.random.randn(hidden_size, output_size)
self.params['b8'] = np.zeros(output_size)
# 生成层===========
self.layers = []
self.layers.append(Convolution(self.params['W1'], self.params['b1'],
conv_param_1['stride'], conv_param_1['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W2'], self.params['b2'],
conv_param_2['stride'], conv_param_2['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W3'], self.params['b3'],
conv_param_3['stride'], conv_param_3['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W4'], self.params['b4'],
conv_param_4['stride'], conv_param_4['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W5'], self.params['b5'],
conv_param_5['stride'], conv_param_5['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W6'], self.params['b6'],
conv_param_6['stride'], conv_param_6['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Affine(self.params['W7'], self.params['b7']))
self.layers.append(Relu())
self.layers.append(Dropout(dropout_ratio))
self.layers.append(Affine(self.params['W8'], self.params['b8']))
self.layers.append(Dropout(dropout_ratio))
self.last_layer = SoftmaxWithLoss()
def load_pretrain_model(self):
with open(self.pkl_file_name, 'rb') as f:
model = pickle.load(f)
for key in ('params', 'layers', 'last_layer'):
exec('self.' + key + '=model.' + key)
print('params loaded!')
def predict(self, x, train_flg=False):
for layer in self.layers:
if isinstance(layer, Dropout):
x = layer.forward(x, train_flg)
else:
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x, train_flg=True)
return self.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1:
t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx, train_flg=False)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
# 设定
grads = {}
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
grads['W' + str(i+1)] = self.layers[layer_idx].dW
grads['b' + str(i+1)] = self.layers[layer_idx].db
return grads
if __name__ == '__main__':
from dataset.mnist import load_mnist
from trainer.trainer import Trainer
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=False, one_hot_label=True, shuffle_data=True)
# setting
train_flag = 1 # 进行训练还是预测
gradcheck_flag = 0 # 对已训练的网络进行梯度检验
pkl_file_name = dir_path + '/deep_convnet.pkl'
fig_name = dir_path + '/deep_convnet.png'
net = DeepConvNet(input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10, dropout_ratio=0.5,
pretrain_flag=True, pkl_file_name=pkl_file_name)
trainer = Trainer(net, x_train, t_train, x_test, t_test,
epochs=5, mini_batch_size=128,
optimizer='Adam', optimizer_param={},
save_model_flag=True, pkl_file_name=pkl_file_name, plot_flag=True, fig_name=fig_name,
evaluate_sample_num_per_epoch=1000, verbose=True)
if gradcheck_flag == 1:
gradient_check(net, x_train[:2], t_train[:2])
if train_flag:
trainer.train()
else:
acc = net.accuracy(x_train, t_train)
print('accuracy:', acc)
- 训练了 3 个 epoch 之后,精度成功突破了 99%,没有过拟合现象发生
=============== Final Test Accuracy ===============
test acc:0.9917
深度学习的应用案例 (简介)
物体识别
物体检测
图像分割
- 图像分割是指在像素水平上对图像进行分类。如下图所示,使用以像素为单位对各个对象分别着色的监督数据进行学习。然后,在推理时,对输入图像的所有像素进行分类
- 要基于神经网络进行图像分割,最简单的方法是以所有像素为对象,对每个像素执行推理处理
- 比如,准备一个对某个矩形区域中心的像素进行分类的网络,以所有像素为对象执行推理处理
- 正如大家能想到的,这样的方法需要按照像素数量进行相应次 forward 处理,因而需要耗费大量的时间(正确地说,卷积运算中会发生重复计算很多区域的无意义的计算)。为了解决这个无意义的计算问题,有人提出了一个名为 FCN(Fully Convolutional Network)的方法。该方法通过一次 forward 处理,对所有像素进行分类
FCN
- 相对于一般的 CNN 包含全连接层,FCN 将全连接层替换成发挥相同作用的卷积层。在物体识别中使用的网络的全连接层中,中间数据的空间容量被作为排成一列的节点进行处理,而只由卷积层构成的网络中,空间容量可以保持原样直到最后的输出
- 全连接层中,输出和全部的输入相连。使用卷积层也可以实现与此结构完全相同的连接。比如,针对输入大小是 32 × 10 × 10 32×10×10 32×10×10(通道数 32、高 10、长 10)的数据的全连接层可以替换成滤波器大小为 32 × 10 × 10 32×10×10 32×10×10 的卷积层。如果全连接层的输出节点数是 100,那么在卷积层准备 100 个 32 × 10 × 10 32×10×10 32×10×10 的滤波器就可以实现完全相同的处理。像这样,全连接层可以替换成进行相同处理的卷积层
- 如下图所示,FCN 的特征在于最后导入了扩大空间大小的处理。基于这个处理,变小了的中间数据可以一下子扩大到和输入图像一样的大小。FCN 最后进行的扩大处理是基于双线性插值法的扩大(双线性插值扩大)。FCN 中,这个双线性插值扩大是通过去卷积(逆卷积运算)来实现的