上一节介绍了ANN,ANN可以说是最简单的神经网络了,但是ANN却包含了神经网络需要的基础框架,之后的网络都是在此基础上删删补补。本节介绍CNN(Convolutional Neural Network,卷积神经网络),CNN常被用于图像识别、语音识别等各种场合。
CNN的整体结构
CNN和ANN一样,可以像堆乐高积木一样一层一层的来组装,在CNN中,新出现了卷积层(Convolution)和池化层(Pooling)。
卷积层
在ANN中出现的Affine层也被称为全连接层,即相邻层的所有神经元之间都有连接,但是全连接层有个问题,就是不能表征数据的形状,举个例子,在图像识别中,图像通常是由长、高和通道来表示的3维数据,在全连接层中,需要将这3维数据拉平成1维数据,这样就会丢失了图像关于形状的相关信息。
而卷积层则保持形状不变,当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层,因此,在 CNN 中,可能会正确理解到图像具有的形状信息。
卷积运算
卷积层进行的处理就是卷积运算,卷积运算相当于图像处理中的滤波器运算 或核运算 ,以下面的一个例子来介绍一下卷积运算
在这个例子中,输入数据是有高长方向的形状的数据,滤波器也一样,有高长方向上的维度。假设用(height, width)表示数据和滤波器的形状,则在本例中,输入大小是 (4, 4),滤波器大小是 (3, 3),输出大小是 (2, 2),下图具体介绍了卷积运算的计算顺序。
对于输入数据,卷积运算以一定间隔滑动滤波器的窗口,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和,如果存在偏置的话,则偏置会被加到滤波器的所有元素上。
填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如 0 等),这称为填充 (padding),是卷积运算中经常会用到的处理。在下图中,对大小为 (4, 4) 的输入数据应用了幅度为 1 的填充。“幅度为 1 的填充”是指用幅度为 1 像素的 0 填充周围。
通过填充,大小为 (4, 4) 的输入数据变成了 (6, 6) 的形状。然后,应用大小为 (3, 3) 的滤波器,生成了大小为 (4, 4) 的输出数据。
步幅
应用滤波器的位置间隔称为步幅 (stride),之前的例子中步幅都是 1,如果将步幅设为 2,,应用滤波器的窗口的间隔变为 2 个元素,就像下图所示
综合填充和步幅可以发现,增大步幅后,输出大小会变小。而增大填充后,输出大小会变大,假设输入大小为 (H , W ),滤波器大小为 (FH , FW ),输出大小为 (OH , OW ),填充为 P ,步幅为 S,则输出大小的计算公式如下所示
O H = H + 2 P − F H S + 1 O W = W + 2 P − F W S + 1 OH=\frac{H+2P-FH}{S}+1 \\
OW=\frac{W+2P-FW}{S}+1
O H = S H + 2 P − F H + 1 O W = S W + 2 P − F W + 1
多维数据的卷积运算
多维数据的卷积运算与2维数据的卷积运算同理,只是滤波器的维度和输入数据的维度相同,如下图所示,通道数为 C 、高度为 H 、长度为 W 的数据的形状可以写成(C , H , W ),滤波器的维度相同,为(C , FH , FW ),但是,为了要在通道方向上也拥有多个卷积运算的输出,就需要用到多个滤波器(权重),假设滤波器的个数为FN,则输出的特征图也生成了 FN 个,果将这 FN 个特征图汇集在一起,就得到了形状为 (FN , OH , OW ) 的方块,并且每个通道只有一个偏置,偏置的形状是 (FN , 1, 1),滤波器的输出结果的形状是 (FN , OH , OW ),这两个方块相加时,要对滤波器的输出结果 (FN , OH , OW ) 按通道加上相同的偏置值,将这个方块传给下一层,就是 CNN 的处理流。
池化层
池化层的作用是缩小高、长方向上的空间运算,如下图所示进行将 2 × 2 的区域集约成 1 个元素的处理,缩小空间大小。
池化层可以分为最大池化和平均池化等,上图中是最大池化,即选取目标区域中最大的元素作为输出,池化层具有以下特征:
没有要学习的参数 :池化层和卷积层不同,没有要学习的参数,池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数
通道数不发生变化 :经过池化运算,输入数据和输出数据的通道数不会发生变化
对微小的位置变化具有鲁棒性(健壮) :输入数据发生微小偏差时,池化仍会返回相同的结
卷积层和池化层的实现
前面简单的介绍了卷积层和池化层的原理,现在来用Python来实现这两个层,同ANN,这两个层也需要forward和
backward方法。
卷积层的实现
在利用Python实现卷积层时,用到了im2col函数,im2col函数将输入数据展开以适合滤波器(权重),不需要我们用重复好几层for循环语句来实现,im2col函数的定义为
im2col (input_data, filter_h, filter_w, stride=1 , pad=0 )
使用im2col来实现卷积层的代码如下
class Convolution :def __init__ (self, W, b, stride=1 , pad=0 ) : 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 out def backward (self, dout) : FN, C, FH, FW = self.W.shape dout = dout.transpose(0 ,2 ,3 ,1 ).reshape(-1 , FN) self.db = np.sum(dout, axis=0 ) self.dW = np.dot(self.col.T, dout) self.dW = self.dW.transpose(1 , 0 ).reshape(FN, C, FH, FW) dcol = np.dot(dout, self.col_W.T) dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) return dx
注意,在forward的实现中,最后会将输出大小转换为合适的形状。转换时使用了 NumPy 的 transpose函数,transpose会更改多维数组的轴的顺序,在进行卷积层的反向传播时,必须进行 im2col的逆处理,卷积层的反向传播和上一节ANN的Affine 层的实现方式一样。
池化层的实现
池化层和卷积层有点不同,就是在池化的情况下,在通道方向上是独立的,池化需要按通道单独展开,就如下图所示
池化层的代码实现也是大同小异
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 self.x = None self.arg_max = None 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) col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1 , self.pool_h*self.pool_w) arg_max = np.argmax(col, axis=1 ) out = np.max(col, axis=1 ) out = out.reshape(N, out_h, out_w, C).transpose(0 , 3 , 1 , 2 ) self.x = x self.arg_max = arg_max return out def backward (self, dout) : dout = dout.transpose(0 , 2 , 3 , 1 ) pool_size = self.pool_h * self.pool_w dmax = np.zeros((dout.size, pool_size)) dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten() dmax = dmax.reshape(dout.shape + (pool_size,)) dcol = dmax.reshape(dmax.shape[0 ] * dmax.shape[1 ] * dmax.shape[2 ], -1 ) dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad) return dx
CNN的实现
已经实现了卷积层和池化层,现在来组合这些层,搭建进行手写数字识别的 CNN。
简单CNN的实现
首先构建一个3层的CNN网络结果,网络结果如下所示
可以看出网络的构成是Convolution - ReLU - Pooling -Affine - ReLU - Affine - Softmax ,整个步骤也和之前介绍的ANN一样,这里就直接贴上代码了
import pickleimport numpy as npfrom collections import OrderedDictclass 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']、...是各层的偏置 """ self.loss(x, t) 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
细心的小伙伴应该已经注意到了在上面代码的注释中提到了He初始值 和xavier初始值 ,设置神经网络的初始值也是一门学问,设置一组优秀的初始值可以避免梯度消失 的问题,目前比较常见的就是He初始值和xavier初始值,具体该什么网络选取什么初始值还需要多调研调研 。
复杂CNN的实现
上一节中介绍的简单的CNN网络结构在MNIST任务中已经可以取得98%的准确率,可以说十分优秀了,但是加深网络可以获得99%的准确率,当然了并不代表着网络越深,学习的效果越好,接下来实现一个负责的网络,这个网络参考了VGG网络,网络连接比较复杂,这里使用的卷积层全都是 3 × 3 的小型滤波器,特点是随着层的加深,通道数变大(卷积层的通道数从前面的层开始按顺序以 16、16、32、32、64、64 的方式增加),插入池化层,以逐渐减小中间数据的空间大小,并且,后面的全连接层中使用了 Dropout 层,并基于Adam进行最优化。
Dropout 是一种抑制过拟合的方法,在学习的过程中随机删除神经,训练时,随机选出隐藏层的神经元,然后将其删除,被删除的神经元不再进行信号的传递,每传递一次数据,就会随机选择要删除的神经元,然后在测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出,Dropout 的图示如下
实现也分为forward和backward,实现代码如下
class Dropout : def __init__ (self, dropout_ratio=0.5 ) : self.dropout_ratio = dropout_ratio self.mask = None def forward (self, x, train_flg=True) : if train_flg: self.mask = np.random.rand(*x.shape) > self.dropout_ratio return x * self.mask else : return x * (1.0 - self.dropout_ratio) def backward (self, dout) : return dout * self.mask
现在来说下Adam,Adam是一种参数更新的最优化方法,结合了Momentum和AdaGrad ,简单来说,Momentum以物理规律来表示梯度的移动,AdaGrad 为参数的每个元素适当地调整更新步伐,这里就直接把代码贴上去,有兴趣的小伙伴可以自行了解
class Adam : """Adam (http://arxiv.org/abs/1412.6980v8)""" def __init__ (self, lr=0.001 , beta1=0.9 , beta2=0.999 ) : self.lr = lr self.beta1 = beta1 self.beta2 = beta2 self.iter = 0 self.m = None self.v = None def update (self, params, grads) : if self.m is None : self.m, self.v = {}, {} for key, val in params.items(): self.m[key] = np.zeros_like(val) self.v[key] = np.zeros_like(val) self.iter += 1 lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter) for key in params.keys(): self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key]) self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key]) params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7 )
好了好了,返回之前说到的复杂网络,复杂网络可以用下图来表示
这个网络使用 He 初始值作为权重的初始值,使用 Adam 更新权重参数,把上述内容总结起来,这个网络有如下特点
基于 3×3 的小型滤波器的卷积层
激活函数是 ReLU
全连接层的后面使用 Dropout 层
基于 Adam 的最优化
使用 He 初始值作为权重初始值
这个复杂网络的代码如下
import pickleimport numpy as npfrom collections import OrderedDictclass DeepConvNet : """识别率为99%以上的高精度的ConvNet 网络结构如下所示 conv - relu - conv- relu - pool - conv - relu - conv- relu - pool - conv - relu - conv- relu - pool - affine - relu - dropout - affine - dropout - softmax """ 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 ) : 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) 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(0.5 )) self.layers.append(Affine(self.params['W8' ], self.params['b8' ])) self.layers.append(Dropout(0.5 )) self.last_layer = SoftmaxWithLoss() 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) : self.loss(x, t) dout = 1 dout = self.last_layer.backward(dout) tmp_layers = self.layers.copy() tmp_layers.reverse() for layer in tmp_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