• CNN的学习记录


    0. 一重山

        上次讲完了CNN的“启蒙导师”LeNet-5,不知道小猫咪会不会对本猫咪的笨笨教程有一点点的满意呢。我也想和猫咪好好的讨论呢。学完LeNet肯定是要进入CNN的。因为个人觉得CNN就是,延续了“卷积采样-降维”这个特点的同时,加入了一些新操作(当然也不复杂),这些新操作意外的达到了很好的效果。因为对图片的卷积实质上提取特征的同时降维,所以CNN在图像上的变种千千万万。

        本文依然从网络结构入手,聊一聊CNN这些新操作都是些什么,大致的网络布局是如何(因为CNN的变种实在太多,本文就按照我的代码的例子进行入手)。然后从代码入手实战跑一跑看看结果如何加深对CNN的理解。(不要,不,没有,不存在,别想了,guna~)猫咪超可爱的,那么就一起开始学习吧。

    1. 两重山

      1.1 与LeNet-5的对比

        一般的CNN都是按照图1的重复过程不断的“卷积-非线性激活函数-池化”(conv-relu-pool),最后再加上全连接层的信息汇总。可以发现,CNN在LeNet-5的基础上主要添加了非线性激活函数(relu)这个过程,或者一些相同步骤中的细节操作发生了一些变化(例如pool层。对比LeNet的池化层C2来说,输入到该层数据为28*28*6的矩阵,该层用步长为2的2*2窗口进行“降维采样”,得到14*14*6的矩阵。而CNN中的池化层采用的Maxpool的方法,采样方式发生了变化,在后续会更详细的进行说明)。在卷积采样上两者几乎没有差别。第2节的示例代码中,采用了两个“conv-relu-pool”层,然后将矩阵同样“铺平”输入两个全连接层中进行“总结性”运算,也就是总共4层的一个CNN网络。接下来,我们对conv-relu-pool层的操作进行解释。

    图1 CNN的一般流程

       1.2 CNN中的conv-relu-pool

        首先是conv。也就是卷积,卷积过程与LeNet-5中没有什么差别,具体的细节可能根据实验效果而定。比如对于同样32*32的初始输入,用不同大小窗口采样会得到不同的采样结果(5*5的窗口得到28*28,3*3的窗口得到30*30),窗口大小根据最后准确率来定,所以不太用纠结到底是多大最好。当然可以想见,窗口过大会导致提取的信息过少,窗口过小导致运算量和层与层直接的参数剧增(层与层之间参数个数计算见LeNet-5中的解释)。知道原理即可。

        然后是relu激活函数。猫咪可能听说过神经网络中著名的sigmod函数和tanh函数,relu函数也是激活函数的一种。在这一节就顺便把常用的激活函数函数也给猫咪说啦。本质上,这些激活函数都是做一个映射工作,以sigmod函数为例,它的表达式为,函数图像如图2所示:

    图3 sigmod函数图像

    参数z可以是一个线性组合(这句话很重要,如图4)。例如conv卷积,使用5*5的窗口后将32*32的初始图片变为28*28后。那么我使用一个步长为4的4*4的窗口(无窗口任何重叠)作为sigmod激活函数的采样窗口,可以得到一个7*7的结果。具体结果的由来如图4所示。也就是说,经过类似sigmod激活函数后,我们进一步的“降采样”并且将矩阵每个元素的值域进行了新的映射(sigmod映射为(0,1))。

     

    图4 sigmod激活函数工作示例

    同样的,relu函数就不再赘述啦(它的表达式比较复杂),只要知道它是通过一个表达式进行映射就可以了。relu的函数图像如下图。可以看到,线性性质会让它更容易收敛,在训练时求导也会更容易(我们不用太关心这个问题,因为都是机器在做嘻嘻,了解就好)。

    图5 relu函数

         接着是pool。在CNN的pool层中使用了maxpool方法。通过LeNet-5的pool层我们知道,这一层主要是为了降采样维度的。maxpool也不例外,它同样也是使用一个采样窗口进行采样,但不再是“加权求和”,而是选用窗口内最大的值最为该区域的代表。例如步长为2,窗口大小2*2的maxpool(无窗口重叠),如图6所示。图中的值只是为了举个例子,如果激活函数使用的sigmod,那么值都应该在(0,1)之间。

    图6 maxpool

       1.3 CNN中的全连接层

        和LeNet-5的全连接层没有什么区别。从conv-relu-maxpool层后得到的依然是矩阵。它可能用了类似LeNet-5那样很多的窗口,采样了很多的大小相同的矩阵。到了全连接层就是要综合这些信息。方法和LeNet-5类似。比如从conv-relu-maxpool后得到一个20*20的矩阵,我先设计第一个全连接层输出为20,那么这两层之间的参数个数就为20*20*20+20(如果是y=wx+b形式)。最后再设计一个全连接层,将上一层输出的20映射到第二个全连接层的10个输出(对应数字0-9)即可。

    2. 山高天远烟水寒

      2.1 代码

        接下来是代码部分啦。这是一个4层的CNN网络来实现MNIST手写数字识别。这次不需要单独下载数据集(数据集不是很大可以放心食用),数据集写在了import里面更方便猫咪一点。我使用的python3.5和tensorflow1.6.0版本,不用修改可以直接运行。

    #---------------------------------------
    #
    # 简单的4层CNN模型
    # 应用于MNIST手写库
    #---------------------------------------
    #
    # 数据集无需单独下载,代码中会下载
    # 数据集下载至代码根目录下temp文件中
    # temp包含4个.gz文件:测试集的图片和label,训练集的图片和label
    #---------------------------------------
    #
    # Author:ZQH
    # Data:2018-04-11
    # For cat Dan
    #---------------------------------------

    import matplotlib.pyplot as plt
    import numpy as np
    import tensorflow as tf
    from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets
    from tensorflow.python.framework import ops
    ops.reset_default_graph()

    # 开始计算图会话
    sess = tf.Session()

    # 创建temp文件夹,导入数据。数据源自上面import内容
    data_dir = 'temp'
    mnist = read_data_sets(data_dir)

    # 加载数据集,将其reshape为28*28的矩阵
    train_xdata = np.array([np.reshape(x, (28,28)) for x in mnist.train.images])
    test_xdata = np.array([np.reshape(x, (28,28)) for x in mnist.test.images])

    # 转换label为one-hot编码向量(例如1为[0000000001],8为[010000000])
    train_labels = mnist.train.labels
    test_labels = mnist.test.labels

    # 设置模型参数。由于图像是灰度图像,图像深度为1,则颜色通道为1
    batch_size = 100
    learning_rate = 0.005
    evaluation_size = 500
    image_width = train_xdata[0].shape[0]
    image_height = train_xdata[0].shape[1]
    target_size = max(train_labels) + 1
    num_channels = 1 # 颜色通道=1
    generations = 500
    eval_every = 5
    conv1_features = 25
    conv2_features = 50
    max_pool_size1 = 2 # N*N窗口大小->第一个max-pool层
    max_pool_size2 = 2 # N*N窗口大小->第二个max-pool层
    fully_connected_size1 = 100

    # 为数据集声明占位符(tensorflow通过占位符获取数据)
    # 同时声明训练数据集变量和测试数据集变量
    x_input_shape = (batch_size, image_width, image_height, num_channels)
    x_input = tf.placeholder(tf.float32, shape=x_input_shape)
    y_target = tf.placeholder(tf.int32, shape=(batch_size))
    eval_input_shape = (evaluation_size, image_width, image_height, num_channels)
    eval_input = tf.placeholder(tf.float32, shape=eval_input_shape)
    eval_target = tf.placeholder(tf.int32, shape=(evaluation_size))

    # 声明卷积层的权重和偏置
    conv1_weight = tf.Variable(tf.truncated_normal([4, 4, num_channels, conv1_features],
                                                   stddev=0.1, dtype=tf.float32))
    conv1_bias = tf.Variable(tf.zeros([conv1_features], dtype=tf.float32))

    conv2_weight = tf.Variable(tf.truncated_normal([4, 4, conv1_features, conv2_features],
                                                   stddev=0.1, dtype=tf.float32))
    conv2_bias = tf.Variable(tf.zeros([conv2_features], dtype=tf.float32))

    # 声明全连接层的权重和偏置
    resulting_width = image_width // (max_pool_size1 * max_pool_size2)
    resulting_height = image_height // (max_pool_size1 * max_pool_size2)
    full1_input_size = resulting_width * resulting_height * conv2_features
    full1_weight = tf.Variable(tf.truncated_normal([full1_input_size, fully_connected_size1],
                              stddev=0.1, dtype=tf.float32))
    full1_bias = tf.Variable(tf.truncated_normal([fully_connected_size1], stddev=0.1, dtype=tf.float32))
    full2_weight = tf.Variable(tf.truncated_normal([fully_connected_size1, target_size],
                                                   stddev=0.1, dtype=tf.float32))
    full2_bias = tf.Variable(tf.truncated_normal([target_size], stddev=0.1, dtype=tf.float32))


    # 声明算法模型。首先创建一个模型函数my_conv_net()
    def my_conv_net(input_data):
        # 第一个Conv-ReLU-MaxPool层
        conv1 = tf.nn.conv2d(input_data, conv1_weight, strides=[1, 1, 1, 1], padding='SAME')
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_bias))
        max_pool1 = tf.nn.max_pool(relu1, ksize=[1, max_pool_size1, max_pool_size1, 1],
                                   strides=[1, max_pool_size1, max_pool_size1, 1], padding='SAME')

        # 第二个Conv-ReLU-MaxPool层
        conv2 = tf.nn.conv2d(max_pool1, conv2_weight, strides=[1, 1, 1, 1], padding='SAME')
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_bias))
        max_pool2 = tf.nn.max_pool(relu2, ksize=[1, max_pool_size2, max_pool_size2, 1],
                                   strides=[1, max_pool_size2, max_pool_size2, 1], padding='SAME')

        # 把输出“铺平”为1*N的向量,为全连接层的计算做准备
        final_conv_shape = max_pool2.get_shape().as_list()
        final_shape = final_conv_shape[1] * final_conv_shape[2] * final_conv_shape[3]
        flat_output = tf.reshape(max_pool2, [final_conv_shape[0], final_shape])

        # 第一个全连接层
        fully_connected1 = tf.nn.relu(tf.add(tf.matmul(flat_output, full1_weight), full1_bias))

        # 第二个全连接层
        final_model_output = tf.add(tf.matmul(fully_connected1, full2_weight), full2_bias)
       
        return(final_model_output)

    # 声明训练模型
    model_output = my_conv_net(x_input)
    test_model_output = my_conv_net(eval_input)

    # 因为预测结果是单分类(0-9哪个数字),所以使用softmax函数为损失函数
    loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=model_output, labels=y_target))

    # 创建训练集、测试集和预测函数
    prediction = tf.nn.softmax(model_output)
    test_prediction = tf.nn.softmax(test_model_output)

    # 创建精度函数,评估模型准确度
    def get_accuracy(logits, targets):
        batch_predictions = np.argmax(logits, axis=1)
        num_correct = np.sum(np.equal(batch_predictions, targets))
        return(100. * num_correct/batch_predictions.shape[0])

    # 创建优化器函数,声明训练步长
    my_optimizer = tf.train.MomentumOptimizer(learning_rate, 0.9)
    train_step = my_optimizer.minimize(loss)

    # 初始化模型变量
    init = tf.global_variables_initializer()
    sess.run(init)

    # 开始训练。迭代500次后可以达到96%~97%准确度
    # 没有LeNet高是因为网络层数不够深等原因
    train_loss = []
    train_acc = []
    test_acc = []
    for i in range(generations):
        rand_index = np.random.choice(len(train_xdata), size=batch_size)
        rand_x = train_xdata[rand_index]
        rand_x = np.expand_dims(rand_x, 3)
        rand_y = train_labels[rand_index]
        train_dict = {x_input: rand_x, y_target: rand_y}
       
        sess.run(train_step, feed_dict=train_dict)
        temp_train_loss, temp_train_preds = sess.run([loss, prediction], feed_dict=train_dict)
        temp_train_acc = get_accuracy(temp_train_preds, rand_y)
       
        if (i+1) % eval_every == 0:
            eval_index = np.random.choice(len(test_xdata), size=evaluation_size)
            eval_x = test_xdata[eval_index]
            eval_x = np.expand_dims(eval_x, 3)
            eval_y = test_labels[eval_index]
            test_dict = {eval_input: eval_x, eval_target: eval_y}
            test_preds = sess.run(test_prediction, feed_dict=test_dict)
            temp_test_acc = get_accuracy(test_preds, eval_y)
           
            # 记录并输出结果
            train_loss.append(temp_train_loss)
            train_acc.append(temp_train_acc)
            test_acc.append(temp_test_acc)
            acc_and_loss = [(i+1), temp_train_loss, temp_train_acc, temp_test_acc]
            acc_and_loss = [np.round(x,2) for x in acc_and_loss]
            print('Generation # {}. Train Loss: {:.2f}. Train Acc (Test Acc): {:.2f} ({:.2f})'.format(*acc_and_loss))
       
       
    # 使用Matplotlib模块绘制损失函数和准确度函数
    eval_indices = range(0, generations, eval_every)
    # 损失函数
    plt.plot(eval_indices, train_loss, 'k-')
    plt.title('Softmax Loss per Generation')
    plt.xlabel('Generation')
    plt.ylabel('Softmax Loss')
    plt.show()

    # 训练和测试时的准确度函数
    plt.plot(eval_indices, train_acc, 'k-', label='Train Set Accuracy')
    plt.plot(eval_indices, test_acc, 'r--', label='Test Set Accuracy')
    plt.title('Train and Test Accuracy')
    plt.xlabel('Generation')
    plt.ylabel('Accuracy')
    plt.legend(loc='lower right')
    plt.show()

    2.2 运行结果

    训练共计500轮。输出除了训练的准确度外,还将loss和准确度信息记录下来绘制图形更直观一点。如下图所示。

    图X 500轮后训练结果

    图X 训练和测试时的准确率

    图X softmax作为损失函数的变化情况

    3. 相思枫叶丹

      本文就先到这里啦。其实回头来看的话,CNN在LeNet-5的基础上最大的进步就是在conv-relu-pool这里,它采用了更多的非线性因素,而不像LeNet-5重复进行采样和降维。可能更多的细节和参数

    需要猫咪去代码里面体会,但大概的过程确实如此。我们在学习神经网络和机器学习过程中,要学会应用(不求甚解),但熟练了就会发现好像也就那么回事。因为最难的计算部分是计算机做的,最有

    创造性的算法是别人写的嘻嘻~

    代码是一定调通了哒,注释也检查了很多遍了。希望能对猫咪有帮助。有疑问可以和秋涵君一起讨论~

    一重山,两重山,山高天远烟水寒,相思枫叶丹。今天,也是爱喵的一天。

     
     
     
     
     
     
     

     

     
  • 相关阅读:
    人事不干人事,这算什么事
    java 单例模式
    Java 中类的加载顺序
    新的挑战
    读《月亮和六便士》所思
    读《奇特的一生》后的一些感悟
    BTrace
    Java技能树
    Android Intent
    一个ListView优化的例子
  • 原文地址:https://www.cnblogs.com/catallen/p/8794735.html
Copyright © 2020-2023  润新知