正文
7.2 卷积层
7.2.1 全连接层存在的问题
全连接层存在的问题:数据的形状被“忽视”。
卷积神经网络可以保持形状不变。
卷积层的输入数据称为输入特征图(input feature map)
输出数据称为输出特征图(output feature map)
7.2.2 卷积运算
卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波器运算”。
将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。
7.2.3 填充
在卷积层的处理之前,向输入数据的周围填入固定的数据,称为填充(padding)。
7.2.4 步幅
应用滤波器的位置间隔称为步幅(stride)
假设输入大小为 ,滤波器大小为 ,输出大小为 ,填充为 ,步幅为 。
7.2.5 3 维数据的卷积运算
7.2.6 结合方块思考
将数据和滤波器结合长方体的方块来考虑,3 维数据的卷积运算会很容易理解。
7.2.7 批处理
网络间传递的是 4 维数据,对这 N 个数据进行了卷积运算。也就是说,批处理将 N 次的处理汇总成了 1 次进行。
7.3 池化层
池化是缩小高、长方向上的空间的运算。
在图像识别领域,主要使用 Max 池化。
池化层的特征
-
没有要学习的参数
- 池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
-
通道数不发生变化
- 经过池化运算,输入数据和输出数据的通道数不会发生变化。
-
对微小的位置变化具有鲁棒性(健壮)
- 输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。
7.4 卷积层和池化层的实现
7.4.1 4 维数组
所谓 4 维数据,比如数据的形状是(10, 1, 28, 28),则它对应 10 个高为 28、长为 28、通道为 1 的数据。
x = np.random.rand(10, 1, 28, 28)
x.shape(10, 1, 28, 28)
访问第 1 个数据:
x[0].shape(1, 28, 28)
访问第 1 个数据的第 1 个通道的空间数据:
x[0, 0] # 或 x[0][0]7.4.2 基于 im2col 的展开
如果老老实实地实现卷积运算,估计要重复好几层的for语句。这样的实现有点麻烦,而且,NumPy 中存在使用 for 语句后处理变慢的缺点(NumPy 中,访问元素时最好不要用for语句)。
im2col是一个函数,将输入数据展开以适合滤波器(权重)。对 3 维的输入数据应用im2col后,数据转换为 2 维矩阵(正确地讲,是把包含批数量的 4 维数据转换成了 2 维数据)。
im2col 这个名称是“image to column”的缩写,翻译过来就是“从图像到矩阵”的意思。
7.4.3 卷积层的实现
im2col(input_data, filter_h, filter_w, stride=1, pad=0)
-
input_data―由(数据量,通道,高,长)的 4 维数组构成的输入数据
-
filter_h―滤波器的高
-
filter_w―滤波器的长
-
stride―步幅
-
pad―填充
import sys, os
sys.path.append(os.pardir)
from common.util import im2col
x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75)
x2 = np.random.rand(10, 3, 7, 7) # 10 个数据
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)(9, 75)
(90, 75)
使用 im2col 来实现卷积层:
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
"""
将滤波器(权重)、偏置、步幅、填充作为参数接收。
滤波器是(FN, C, FH, FW) 的 4 维形状。
另外,FN、C、FH、FW 分别是 Filter Number(滤波器数量)、Channel、Filter Height、Filter Width 的缩写。
"""
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # 滤波器的展开
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) # 修改轴顺序
return out7.4.4 池化层的实现
池化的应用区域按通道单独展开。
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# 展开(1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
# 最大值(2)
out = np.max(col, axis=1)
# 转换(3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out池化层的实现按下面 3 个阶段:
-
展开输入数据。
-
求各行的最大值。
-
转换为合适的输出大小
7.5 CNN 的实现
class SimpleConvNet:
"""简单的 ConvNet
conv - relu - pool - affine - relu - affine - softmax
Parameters
----------
input_size : 输入大小(MNIST 的情况下为 784)
hidden_size_list : 隐藏层的神经元数量的列表(e.g. [100, 100, 100])
output_size : 输出大小(MNIST 的情况下为 10)
activation : 'relu' or 'sigmoid'
weight_init_std : 指定权重的标准差(e.g. 0.01)
指定'relu'或'he'的情况下设定“He 的初始值”
指定'sigmoid'或'xavier'的情况下设定“Xavier 的初始值”
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
# 生成层
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
conv_param['stride'], conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_layer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
"""
求损失函数
参数 x 是输入数据、t 是教师标签
"""
y = self.predict(x)
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)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def numerical_gradient(self, x, t):
"""求梯度(数值微分)
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
具有各层的梯度的字典变量
grads['W1']、grads['W2']、...是各层的权重
grads['b1']、grads['b2']、...是各层的偏置
"""
loss_w = lambda w: self.loss(x, t)
grads = {}
for idx in (1, 2, 3):
grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])
return grads
def gradient(self, x, t):
"""求梯度(误差反向传播法)
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
具有各层的梯度的字典变量
grads['W1']、grads['W2']、...是各层的权重
grads['b1']、grads['b2']、...是各层的偏置
"""
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
self.layers[key].W = self.params['W' + str(i+1)]
self.layers[key].b = self.params['b' + str(i+1)]如果使用 MNIST 数据集训练 SimpleConvNet,则训练数据的识别率为 99.82%,测试数据的识别率为 98.96%(每次学习的识别精度都会发生一些误差)。测试数据的识别率大约为 99%,就小型网络来说,这是一个非常高的识别率。
7.7 具有代表性的 CNN
7.7.1LeNet
于 1998 年首次被提出。
7.7.2 AlexNet
2012 年被提出,AlexNet 是引发深度学习热潮的导火线。
-
激活函数使用 ReLU
-
使用进行局部正规化的 LRN(Local Response Normalization)层
-
使用 Dropout
7.8 小结
-
CNN 在此前的全连接层的网络中新增了卷积层和池化层。
-
使用 im2col 函数可以简单、高效地实现卷积层和池化层。
-
通过 CNN 的可视化,可知随着层次变深,提取的信息愈加高级。
-
LeNet 和 AlexNet 是 CNN 的代表性网络。
-
在深度学习的发展中,大数据和 GPU 做出了很大的贡献。