卷积神经网络(Convolutional Neural Network)简称为CNN。几乎所有的深度学习比赛中都在广泛使用卷积神经网络。

7.1 CNN的结构

先回顾一下之前学习的神经网络。
相邻层的所有神经元之间都有连接,这称为全连接

前面我们用Affine层实现了全连接
全连接

这是利用ReLU层和Affine层构建的一个四层神经网络

而CNN的模式大概是这样的:
全连接
也就是说,层与层之间有些用卷积层(Convolution)或池化层(Pooling)。

7.2 卷积层

7.2.1 全连接的问题

全连接层的问题是:忽视了输入数据的形状

比如处理一个10×10的输入图像,全连接层会把他处理成一个一维的100×1的数据。这个处理过程丢失了数据的形状。

在图像处理过程中,这个形状所代表的信息有可能非常重要

而在卷积神经网络中,可以将三维输入的数据按照三维来处理。

在卷积神经网络中,会把输入输出数据称作特征图
其中输入数据被称为输入特征图
输出数据被称为输出特征图

7.2.2 卷积运算

如图:
卷积运算的图示
上图展示的是如下运算:
卷积运算

相当于一个窗口不停滑动的过程
其中滑动时进行的运算是点乘求和运算

上面的大矩阵是输入数据
小矩阵是权重值(滤波器)

一般在卷积运算之后还要加上偏置

下面介绍填充幅度和步幅
上面的例子中,填充幅度为0,步幅为1;
步幅很容易理解:就是每次平移的格数。
所谓填充幅度,是指在原图周围填充上特定数值(比如0)
填充幅度

思考:当输入数据为H×W,步幅为S,填充幅度为M,滤波器大小为(FH,FW)输出数据应该是?

高为,宽为

7.2.5 三维卷积运算

假如输入的是三维数据呢?
我们可以使用一个三维的滤波器。(之前的滤波器是二维的)

填充幅度

但这样运算会丢失维度(输入是三维,输出是二维)
所以采用下面的方法:
填充幅度
这样得到的是三维数据。
可以看作:使用的是一个四维的滤波器

7.3 池化层

池化层的作用是缩小数据量。

如图是一个执行max操作的池化层:
填充幅度

一般来说,池化层的窗口大小会设定成和步幅相同的值。

池化层的特征

  • 没有需要学习的参数
  • 通道数不发生变化
  • 对数据的微小变化不敏感

7.4 池化层和卷积层的实现

CNN中传递的是四维数据
(batch_num, channel,width,height)

7.4.1 生成四维数组

import numpy as np 
x = np.random.rand(10,1,28,28)

生成的是一个10×1×28×28的四维数据

基于im2col的展开

卷积运算由于牵扯到较高维度,需要嵌套for循环。这在运算起来比较麻烦。
im2col可以将高维数据转换成二维矩阵,也能再将运算过的矩阵恢复为高维数据。

具体实现不表。

当转换成矩阵后,就可以高效的进行卷积运算。

卷积层具体实现


class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W #滤波器(权重)
        self.b = b
        self.stride = stride #步幅
        self.pad = pad#填充

        # 保存的中间变量(反向传播时用到)
        self.x = None   
        self.col = None
        self.col_W = None

        # 重み・バイアスパラメータの勾配
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape #滤波器的形状
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((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)#恢复数据的形状

        self.x = x
        self.col = col
        self.col_W = col_W

        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

池化层的实现

与卷积层不同,池化层对每个通道会单独处理。

class Pooling:
    def __init__(self, pool_h, pool_w, stride=2, 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:
conv - relu - pool - affine - relu - affine - softmax

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient


class SimpleConvNet:
    """単純なConvNet

    conv - relu - pool - affine - relu - affine - softmax

    Parameters
    ----------
    input_size : 输入大小(对于MINIST来说为784)
    hidden_size_list : 隐藏层的层数
    output_size : 输出(使用MNIST时为10)
    activation : 'relu' or 'sigmoid'
    weight_init_std : 权重的标准差设定(比如0.01)
        'relu'下使用He值
        'sigmoid'下使用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]#输入大小,input_dim保存着输入数据的形状
        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)]

用于测试的源码如下:

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from simple_convnet import SimpleConvNet
from common.trainer import Trainer

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)

# 処理に時間のかかる場合はデータを削減 
#x_train, t_train = x_train[:5000], t_train[:5000]
#x_test, t_test = x_test[:1000], t_test[:1000]

max_epochs = 20

network = SimpleConvNet(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)

trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=max_epochs, mini_batch_size=100,
                  optimizer='Adam', optimizer_param={'lr': 0.001},
                  evaluate_sample_num_per_epoch=1000)
trainer.train()

# パラメータの保存
network.save_params("params.pkl")
print("Saved Network Parameters!")

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, trainer.train_acc_list, marker='o', label='train', markevery=2)
plt.plot(x, trainer.test_acc_list, marker='s', label='test', markevery=2)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

7.6 CNN可视化

7.6.1 CNN的卷积层究竟在“观察”什么呢?

我们将CNN的滤波器可视化,将权重显示为颜色,可以发现在学习前后,滤波器有了明显变化:
CNN可视化

滤波器在“观察”什么,答案就是它在观察边缘(颜色变化的分界线)和斑块(局部的块状区域)等。

CNN可视化
由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现的CNN会将这些原始信息传递给后面的层。

7.6.2 基于分层结构的信息提取

上面观察的仅仅是CNN中的一层,下面以AlexNet为例,介绍多层CNN的工作过程。
CNN可视化

上面的图像可以概括为:

  • CNN的卷积层中提取的信息。
  • 第1层的神经元对边缘或斑块有响应
  • 第3层对纹理有响应
  • 第5层对物体部件有响应
  • 最后的全连接层对物体的类别(狗或车)有响应

这是一个由简单到复杂、逐步加深的过程

7.7 具有代表性的CNN

7.7.1 LeNet

1998年被提出的CNN网络,可以说是CNN的鼻祖
LeNet
与当前CNN的区别主要是:
1、池化层采用随机采样(现在更多采用Max操作)
2、激活函数为sigmoid(现在更多采用ReLU)

7.7.2 AlexNet

LeNet

AlexNet的特点是:
• 激活函数使用ReLU。
• 使用进行局部正规化的LRN(Local Response Normalization)层。
• 使用Dropout。