之前的章节已经实现了卷积层和池化层,现在来组合这些层,搭建进行手写数字识别的 CNN 。这里要实现如图7-23 所示的 CNN 。
class SimpleConvNet: """简单的ConvNet conv - relu - pool - affine - relu - affine - softmax Parameters ---------- • input_dim―输入数据的维度:(通道,高,长) • conv_param―卷积层的超参数(字典)。字典的关键字如下: filter_num―滤波器的数量 filter_size―滤波器的大小 stride―步幅 pad―填充 • hidden_size―隐藏层(全连接)的神经元数量 • output_size―输出层(全连接)的神经元数量 • weitght_int_std―初始化时权重的标准差 """ 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))卷积层的超参数通过名为 conv_param 的字典传入。我们设想它会像 {'filter_num':30,'filter_size':5, 'pad':0, 'stride':1} 这样,保存必要的超参数值。
# 初始化权重 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)学习所需的参数是第1 层的卷积层和剩余两个全连接层的权重和偏置。将这些参数保存在实例变量的 params 字典中。
将第 1 层的卷积层的权重设为关键字 W1 ,偏置设为关键字 b1 。同样,分别用关键字 W2 、b2 和关键字W3 、b3 来保存第 2 个和第 3 个全连接层的权重和偏置。
# 生成层 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()从最前面开始按顺序向有序字典(OrderedDict)的 layers 中添加层。只有最后的 SoftmaxWithLoss 层被添加到别的变量 lastLayer 中。
像这样初始化后,进行推理的 predict 方法和求损失函数值的 loss 方法就可以像下面这样实现。
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)参数 x 是输入数据,t是教师标签。用于推理的 predict 方法从头开始依次调用已添加的层,并将结果传递给下一层。
在求损失函数的 loss 方法中,除了使用 predict 方法进行的 forward 处理之外,还会继续进行 forward 处理,直到到达最后的 SoftmaxWithLoss 层。
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参数的梯度通过误差反向传播法(反向传播)求出,通过把正向传播和反向传播组装在一起来完成。因为已经在各层正确实现了正向传播和反向传播的功能,所以这里只需要以合适的顺序调用即可。最后,把各个权重参数的梯度保存到 grads 字典中。这就是 SimpleConvNet 的实现。
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)]