DL-深度学习入门-基于Python的理论与实现-7-卷积神经网络

《深度学习入门》第 7 章学习笔记。

正文

png

7.2 卷积层

7.2.1 全连接层存在的问题

全连接层存在的问题:数据的形状被“忽视”。

卷积神经网络可以保持形状不变。

卷积层的输入数据称为输入特征图(input feature map)

输出数据称为输出特征图(output feature map)

7.2.2 卷积运算

png

卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的“滤波器运算”。

将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。

7.2.3 填充

png

在卷积层的处理之前,向输入数据的周围填入固定的数据,称为填充(padding)

7.2.4 步幅

png

应用滤波器的位置间隔称为步幅(stride)

假设输入大小为 (H,W)(H,W),滤波器大小为 (FH,FW)(FH, FW),输出大小为 (OH,OW)(OH,OW),填充为 PP,步幅为 SS

OH=H+2PFHS+1OW=W+2PFWS+1OH=\frac{H+2P-FH}{S}+1\\OW=\frac{W+2P-FW}{S}+1

7.2.5 3 维数据的卷积运算

png

7.2.6 结合方块思考

将数据和滤波器结合长方体的方块来考虑,3 维数据的卷积运算会很容易理解。

png

7.2.7 批处理

网络间传递的是 4 维数据,对这 N 个数据进行了卷积运算。也就是说,批处理将 N 次的处理汇总成了 1 次进行。

7.3 池化层

png

池化是缩小高、长方向上的空间的运算。

在图像识别领域,主要使用 Max 池化。

池化层的特征

  • 没有要学习的参数

    • 池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。
  • 通道数不发生变化

    • 经过池化运算,输入数据和输出数据的通道数不会发生变化。
  • 对微小的位置变化具有鲁棒性(健壮)

    • 输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。

7.4 卷积层和池化层的实现

7.4.1 4 维数组

所谓 4 维数据,比如数据的形状是(10, 1, 28, 28),则它对应 10 个高为 28、长为 28、通道为 1 的数据。

python
x = np.random.rand(10, 1, 28, 28)
x.shape
(10, 1, 28, 28)

访问第 1 个数据:

python
x[0].shape
(1, 28, 28)

访问第 1 个数据的第 1 个通道的空间数据:

python
x[0, 0]  # 或 x[0][0]

7.4.2 基于 im2col 的展开

如果老老实实地实现卷积运算,估计要重复好几层的for语句。这样的实现有点麻烦,而且,NumPy 中存在使用 for 语句后处理变慢的缺点(NumPy 中,访问元素时最好不要用for语句)。

im2col是一个函数,将输入数据展开以适合滤波器(权重)。对 3 维的输入数据应用im2col后,数据转换为 2 维矩阵(正确地讲,是把包含批数量的 4 维数据转换成了 2 维数据)。

png

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―填充

python
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 来实现卷积层:

python
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 out

7.4.4 池化层的实现

png

池化的应用区域按通道单独展开。

python
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 的实现

python
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

png

7.7.1LeNet

png

于 1998 年首次被提出。

7.7.2 AlexNet

png

2012 年被提出,AlexNet 是引发深度学习热潮的导火线。

  • 激活函数使用 ReLU

  • 使用进行局部正规化的 LRN(Local Response Normalization)层

  • 使用 Dropout

7.8 小结

  • CNN 在此前的全连接层的网络中新增了卷积层和池化层。

  • 使用 im2col 函数可以简单、高效地实现卷积层和池化层。

  • 通过 CNN 的可视化,可知随着层次变深,提取的信息愈加高级。

  • LeNet 和 AlexNet 是 CNN 的代表性网络。

  • 在深度学习的发展中,大数据和 GPU 做出了很大的贡献。