【DL学习笔记06】深度学习入门——基于Python的理论与实现(ch07: 卷积神经网络 CNN)

 

目录

1. 整体结构

2. 卷积层

全连接层存在的问题

卷积运算

3维数据的卷积运算

批处理

3. 池化层

4. 卷积层和池化层的实现

卷积层

池化层的实现

5. CNN的实现

6. CNN的可视化

第1层权重的可视化

基于分层结构的信息提取

7. 具有代表性的CNN

LeNet

AlexNet


CNN被用于图像识别、语音识别等各种场景

1. 整体结构

  • CNN通过组装层来构建,新出现了卷积层(Convolution层)和池化层(Pooling层)
  • 全连接(fully-connected):相邻层的所有神经元之间都有连接,用Affine层实现
  • 基于全连接层(Affine层):Affine - ReLU(Sigmoid)
  • 基于CNN网络:Convolution - ReLU - Pooling

2. 卷积层

全连接层存在的问题

  • 在全连接层中,相邻的神经元全部连接在一起,输出的数量可以任意决定
  • 向全连接层输入时,会将3维数据拉平为1维数据,因此数据的形状会被“忽视”
  • 卷积层可以保持形状不变,会以3维数据的形式接收输入数据,并同样传输至下一层
  • CNN中,卷积层的输入数据称为输入特征图,输出为输出特征图,统称为特征图

卷积运算

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

 

 

  • 对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用

  • 偏置:向应用了滤波器的元素加上某个固定值

  • 填充:在进行卷积层的处理之前,向输入数据的周围填入固定的数据(比如0),为了调整输出的大小,避免反复进行卷积运算时输出不断缩小到1,导致无法进行卷积运算

  • 步幅:应用滤波器的位置间隔

3维数据的卷积运算

  • 和2维数据相比,纵深方向(通道方向)上增加了特征图
  • 通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出
  • 滤波器的通道数只能设定为和输入数据的通道数相同的值
  • 结合方块思考
    • 3维数据:(channel,height,width)(通道数,高度,长度)
    • 滤波器
      • 1个滤波器:(channel,height,width),数据输出是1张特征图
      • n个滤波器:(output_channel,input_channel,height,width)(滤波器个数,通道数,高度,长度),数据输出是n张特征图

批处理

  • 需要将在各层间传递的数据保存为4维数据,(batch_num,channel,height,width)

3. 池化层

池化层是缩小高、长方向上的空间的运算,在图像识别领域,主要使用Max池化,取出目标区域的最大值,此外还有Average池化等。一般来说,池化的窗口大小会和步幅设定成相同的值

  • 特征:
    • 没有要学习的参数
    • 通道数不发生变化
    • 对微小的位置变化具有健壮性

4. 卷积层和池化层的实现

  • NumPy中存在使用for语句后处理变慢的缺点。因此卷积运算我们不适用for语句,而是使用im2col这个函数实现

  • im2col函数(image to column)可以将输入数据展开以适合滤波器(权重)。会在所有应用滤波器的地方进行这个展开处理

  • 函数的原理和实现可以参考im2col的原理和实现_dwyane12138的博客-CSDN博客_im2col

    def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
        """
    
        Parameters
        ----------
        input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
        filter_h : 滤波器的高
        filter_w : 滤波器的长
        stride : 步幅
        pad : 填充
    
        Returns
        -------
        col : 2维数组
        """
        N, C, H, W = input_data.shape
        out_h = (H + 2*pad - filter_h)//stride + 1
        out_w = (W + 2*pad - filter_w)//stride + 1
    
        img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
        col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
    
        for y in range(filter_h):
            y_max = y + stride*out_h
            for x in range(filter_w):
                x_max = x + stride*out_w
                col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
    
        col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
        return col
    
    # 用于反向传播的逆处理
    def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
        """
    
        Parameters
        ----------
        col :
        input_shape : 输入数据的形状(例:(10, 1, 28, 28))
        filter_h :
        filter_w
        stride
        pad
    
        Returns
        -------
    
        """
        N, C, H, W = input_shape
        out_h = (H + 2*pad - filter_h)//stride + 1
        out_w = (W + 2*pad - filter_w)//stride + 1
        col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
    
        img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
        for y in range(filter_h):
            y_max = y + stride*out_h
            for x in range(filter_w):
                x_max = x + stride*out_w
                img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
    
        return img[:, :, pad:H + pad, pad:W + pad]
    
  • 使用im2col展开输入数据后,再将卷积层的滤波器纵向展开为1列,并计算2个矩阵的乘积即可

卷积层

  • 先举个例子!

    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)
    col2 = im2col(x2, 5, 5, stride=1, pad=0)
    print(col2.shape)  # (90,75)
    
    • 第一个是批大小为1,通道为3的7 x 7的数据,第二个是批大小改为10
    • 第2维的元素个数均为75,是滤波器的元素个数的总和
  • 实现

    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):
            # Filter Number(滤波器数量),Channel,Filter Height,Filter Width
            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)
            # 用im2col函数展开输入数据,用reshape将滤波器展开为2维数组
            # 这里用了reshape(FN, -1)表示有FN行,然后-1会自动计算有几列
            col = im2col(x, FH, FW, self.strde, self.pad)
            col_W = self.W.reshape(FN, -1).T
            out = np.dot(col, col_W) + self.b
            # transpose会更改多维数组的轴的顺序
            # 将原来的形状(N, H, W, C) 还原为 (N, C, H, W)
            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)
            # im2col的逆处理->col2im
            dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
    
            return dx
    

池化层

池化层和卷积层相同,都使用im2col展开输入数据,不过池化层在通道方向上是独立的

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)

        # 展开,第二步是为了实现池化的应用区域按通道单独展开
        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)
        # 最大值
        out = np.max(col, axis=1)
        # 转换
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        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

5. CNN的实现

这里我们搭建进行手写数字识别的CNN

  • 网络构成:Convolution - ReLU - Pooling - Affine - ReLU - Affine - Softmax

  • 初始化部分:

    class SimpleConvNet:
        def __init__(self, input_dim=(1, 28, 28), conv_param=None,
                     hidden_size=100, output_size=10, weight_init_std=0.01):
            """
            :param input_dim:输入数据的维度:(通道,高,长)
            :param conv_param:卷积层的超参数(字典)。字典的关键字如下:
                    filter_num - 滤波器数量;filter_size - 滤波器大小
                    stride - 步幅;pad - 填充
            :param hidden_size:隐藏层(全连接)的神经元数量
            :param output_size:输出层(全连接)的神经元数量
            :param weight_init_std:初始化时权重的标准差
            """
    				# 将超参数从字典中取出来,计算卷积层的输出大小
            if conv_param is None:
                conv_param = {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1}
            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))
    				# 权重参数的初始化
    				# 包括第1层的卷积层和剩余两个全连接层的权重和偏置
    				self.params = {'W1': weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size),
    				               'b1': np.zeros(filter_num),
    				               'W2': weight_init_std * np.random.randn(pool_output_size, hidden_size),
    				               'b2': np.zeros(hidden_size),
    				               'W3': weight_init_std * np.random.randn(hidden_size, output_size),
    				               'b3': np.zeros(output_size)}
    
  • 推理和求损失函数值:

    # 推理
        def predict(self, x):
            for layer in self.layers.values():
                x = layer.forward()
          return x
    
    # 求损失函数值
    def loss(self, x, t):
        y = self.predict(x)
        return self.last_layer.forward(y, t)
    
  • 误差反向传播法求梯度

    def gradient(self, x, t):
        # forward
        self.loss(x, t)
    
        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
    
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
    
        # 设定
        grads = {'W1': self.layers['Conv1'].dW, 'b1': self.layers['Conv1'].db,
                 'W2': self.layers['Affine1'].dW, 'b2': self.layers['Affine1'].db,
                 'W3': self.layers['Affine2'].dW, 'b3': self.layers['Affine2'].db}
    
        return grads
    

6. CNN的可视化

第1层权重的可视化

  • 上述对MNIST数据集的学习中,第1层的卷积层的权重形状是(30,1,5,5),意味着滤波器可以可视化为1通道的灰度图像

        

  • 可以发现,学习前的滤波器是随即进行初始化的,学习后的滤波器变成了有规律的图像,比如从白变到黑的滤波器、含有块状区域(blob)的滤波器等
  • 有规律的滤波器在“观察”边缘(颜色变化的分界线)和斑块(局部的块状区域)等
  • 卷积层的滤波器会提取边缘或斑块等原始信息,而实现的CNN会将这些原始信息传递给后面的层

基于分层结构的信息提取

  • 第1层的卷积层中提取了边缘或斑块等“低级”信息,但如果堆叠了多层卷积层,随着层次加深,提取的信息也愈加复杂、抽象。最开始的层对简单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体部件有响应
  • 下一节会介绍堆叠了多层卷积层和池化层最后经过全连接层输出结果的AlexNet

7. 具有代表性的CNN

LeNet

是进行手写数字识别的网络

 

 

AlexNet

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我来改进一下ch07_09.cpp中的冒泡排序算法。冒泡排序算法的主要思想是比较相邻两个元素的大小,如果前一个元素比后一个元素大,就将它们交换位置,这样一轮比较下来,最大的元素就会被移动到数组的末尾。我们可以通过增加一些优化来提高冒泡排序的效率,比如记录上一次交换的位置,下一轮比较时只需要比较到上一次交换的位置,因为该位置之后的元素已经是有序的。 下面是改进后的代码: ```c++ #include <iostream> using namespace std; void bubbleSort(int arr[], int n) { int i, j, lastSwap; for (i = 0; i < n - 1; i++) { lastSwap = n - 1; // 初始化为数组末尾 for (j = 0; j < lastSwap; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); lastSwap = j; // 更新上一次交换的位置 } } if (lastSwap == n - 1) break; // 如果这一轮没有发生交换,说明数组已经有序,结束循环 } } int main() { int arr[] = {5, 2, 8, 4, 7, 1, 3, 9, 6}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); cout << "排序后的数组:"; for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; return 0; } ``` 这里我们增加了一个变量`lastSwap`来记录上一次交换的位置,每次内层循环时都将`lastSwap`初始化为数组的末尾。当发生交换时,将`lastSwap`更新为当前交换的位置。下一轮比较时,只需要比较到`lastSwap`即可。如果这一轮没有发生交换,说明数组已经有序,结束循环。这样可以减少比较的次数,提高冒泡排序的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值