用CNN实现Digit Reconizer总结

背景简介

本文实现的是Kaggle竞赛平台上Getting Started级别的Digit Reconizer(即MNIST)。使用Tensorflow搭建了5层卷积神经网络(Convolutional Neural Network,简称CNN),最终取得测试成绩是0.98742。

正文

  • 导入相关的包
    import pandas as pd
    import tensorflow as tf
    import numpy as np
    from sklearn.preprocessing import OneHotEncoder

本文使用了numpy,pandas,scikit-learn,以及tensorflow包。其中numpy主要用于处理数据格式;pandas主要用于读写文件;使用scikit-learn包进行One-Hot格式处理;Tensorflow负责CNN的搭建及运行。

缺陷: 未进行数据可视化和数据清理工作。

  • 读取数据,并查看内容
    # 读取训练集
    train = pd.read_csv("/{你的数据文件存放位置}/digit_recognizer/train.csv")
    # 读取测试集
    test = pd.read_csv("/{你的数据文件存放位置}/digit_recognizer/test.csv")
    # shape:(42000,785)
    train_df = train.copy()
    test_df = test.copy()

首先使用pandas的read_csv()方法读取训练集和测试集,然后通过describe()和info()方法查看训练集的格式为(42000,785),其中第一列为训练集标签(即Label),剩余784列是手写图像的数据(因为图片大小是28*28)。

使用了copy()方法的初衷是防止未来操作修改了原始数据。后来经查阅相关资料发现Python中存在引用(=)、浅拷贝(copy())、深拷贝的差别(deepcopy())。正确的应该是使用deepcopy()对原始数据进行深拷贝,这样Python会重新创建一个对象,对新对象的任何修改都不会影响到原始对象。

缺陷: copy()方法使用错误,应该使用deepcopy()方法。

  • 格式化数据
    # 根据需求格式化训练集
    # x_train.shape -> (42000, 784)
    x_train = train_df.drop(['label'], axis=1).values.reshape([-1, 784]).astype(np.float32)
    # y_train.shape -> (42000, 1)
    # y_train[0] -> [1.] y_train = train_df['label'].values.reshape([-1, 1]).astype(np.float32)
    # one-hot using scikit-learn
    # onehot_encoded.shape -> (42000, 10)
    # onehot_encoded[0] -> [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
    onehot_encoded = OneHotEncoder(sparse=False).fit_transform(y_train)

格式化数据时应该注意以下几点:

  1. 格式化要和占位符的格式匹配,否则喂数据的时候会出错。
  2. 数据类型需要是float32或者float64,实验中报错提示的信息,具体为什么暂时没研究。
  3. 训练集标签(Label),即此处的y_train,需要做One-Hot处理。此处采用scikit-learn包装的方法。

缺陷:

  1. 刚入门,对数据格式没有足够的重视,实验中发现它是最重要的环节之一!
  2. 实际上代码的正确顺序应该是先写占位符,再根据占位符的shape进行训练集数据的格式化工作。
  3. One-Hot算法有多种实现方式,其一是自定义实现;其二是scikit-learn包;其三Tensorflow包也提供算法支持。未来要有能力对One-Hot算法信手拈来。
  4. 对数据格式认识不足,其实还有更简便的格式化方式,例如,x_train=train_df.iloc[:, 1:].values一步到位。
  • 定义占位符,喂数据时使用
    # 占位符
    x_ = tf.placeholder(dtype=tf.float32, shape=[None, 784])
    y_ = tf.placeholder(dtype=tf.float32, shape=[None, 10])

要注意的是,占位符的shape要和喂神经网络的数据格式匹配。

经验: (若你和Milo一样曾经研究方向是分布式)可以把神经网络当成一个分布式系统,作业被分成许多个基本任务shuffle到各个节点上。其实,基本任务就是确定(f(x) = wx + b)的参数,此处参数不是向量,而节点就是一个个神经元。输入和输出有多少个节点,就有多少个任务,也就确定了向量的维度。

  • 定义神经网络的基本单元
    """ 定义变量 """
    def weight(shape):
      return tf.Variable(tf.truncated_normal(shape, stddev=0.1))
    def bias(shape):
      return tf.Variable(tf.constant(0.1, shape=shape))
    """ 定义层 """
    def conf2d(x, w):
      return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')
    def max_pool(x):
      return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

此处Milo定义了权重、偏置、卷积层以及Max Pooling层,其中偏置项取常量0.1;卷基层的步伐是1,填充方式为SAME;Max Pooling层核大小是2*2,步伐为2,填充方式同样是SAME。

这里有必要解释一下卷积层输出大小的计算:

  1. 教科书上,以及视频教程手推卷积层大小公式为:(f(x) = frac{Input - Filter + 2 * Padding}{Stride} + 1)
  2. Tensorflow中,SAME填充方式的卷积层输出大小计算公式为:(f(x) = lceil frac{W}{Stride} rceil),跟Filter和Padding没半毛钱关系.
  3. Tensorflow中,VALID填充方式的卷积层输出大小计算公式为:(f(x) = lceil frac{W - Filter + 1}{Stride} rceil)

缺陷:

  1. 起初写代码的时候完全没考虑重构。在没定义神经网络的基本单元的时候,整段代码乱成一锅粥,十分不利于代码调试,同时也阻碍对神经网络模型的理解。所以,强烈建议进行重构工作!
  2. 计算输出输出格式不熟练。根据卷积层和Max Pooling层的参数,需要提前规划神经网络模型中每一层的大小,正确计算每一层的输出,否则将会出现意想不到的惊(yi)喜(chang)。
  • 定义神经网络模型
    # SAME:输出大小等于输入大小除以步长向上取整,s是步长大小;
    # VALID:输出大小等于输入大小减去滤波器大小加上1,最后再除以步长(f为滤波器的大小,s是步长大小)。
    # 第一层 28*28*1 -> 14*14*32
    x_images = tf.reshape(x_, [-1, 28, 28, 1])
    w_conv1 = weight([5, 5, 1, 32])
    b_conv1 = bias([32])
    h_1 = tf.nn.sigmoid(conf2d(x_images, w_conv1) + b_conv1)
    p_1 = max_pool(h_1)
    
    # 第二层 14*14*32 -> 7*7*64
    w_conv2 = weight([5, 5, 32, 64])
    b_conv2 = bias([64])
    h_2 = tf.nn.sigmoid(conf2d(p_1, w_conv2) + b_conv2)
    p_2 = max_pool(h_2)
    
    # 第三层 7*7*64 -> 4*4*128
    w_conv3 = weight([5, 5, 64, 128])
    b_conv3 = bias([128])
    h_3 = tf.nn.sigmoid(conf2d(p_2, w_conv3) + b_conv3)
    p_3 = max_pool(h_3)
    
    # 第四层 4*4*128 -> 1*1024
    w_fc1 = weight([4 * 4 * 128, 1024])
    b_fc1 = bias([1024])
    h_fc1 = tf.nn.sigmoid(tf.matmul(tf.reshape(p_3, [-1, 4 * 4 * 128]), w_fc1) + b_fc1)
    
    # 第五层 1*1024 -> 1*10
    w_fc2 = weight([1024, 10])
    b_fc2 = bias([10])
    y_conv = tf.matmul(h_fc1, w_fc2) + b_fc2
    
    # tf.nn.softmax_cross_entropy_with_logits做了softmax和交叉熵
    prediction = tf.nn.softmax(tf.matmul(h_fc1, w_fc2) + b_fc2)

经验:

  1. 卷积核的深度需要是2的N次幂。(他人经验借鉴)
  2. tf.reshape()的维度参数中,-1表示不定义张量的维度。第一层中张量的维度将被reshape为42000维。
  3. 每一层中做的操作包括:图片信息和权重w做卷积操作 —> 卷积结果加上偏置项做激活操作 —> 经过激活函数的结果进行Pooling操作。
  4. 预测时,只关心softmax结果,所以图片数据直接喂prediction即可。

缺陷:

  1. 没有重构成独立的模块。最初抽象成独立模块的时候,Milo对输入数据的格式定义不熟悉,出现了各种异常,所以决定拆开,优先确保程序成功运行。
  2. 对最后全连接的定义不清晰。究竟什么时候开始全连接?
  • 定义结果需要使用的数据
    # cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
    train_step = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(loss)
    correct_prediction = tf.equal(tf.argmax(y_conv, axis=1), tf.argmax(y_, axis=1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, dtype=tf.float32))
  1. 训练过程是要是损失函数取最小值,此处是对softmax结果做交叉熵,然后进行均值降维
  2. 精度计算是对比训练集标签实际标签
  • 题外话
    # batch
    # no batch cause: CUDNN_STATUS_INTERNAL_ERROR
    # The value of a feed cannot be a tf.Tensor object.
    # ("shuffle_batch:0", shape=(100, 42000, 784), dtype=float32)
    # ("Placeholder:0", shape=(?, 784), dtype=float32).
    # images, labels = tf.train.shuffle_batch([x_train, onehot_encoded],
    #                                           batch_size=100,
    #                                           num_threads=6,
    #                                           capacity=int(50000 * 0.4 + 3 * 100),
    #                                           min_after_dequeue=1000
    #                                         )

经验:

  1. 在训练神经网络模型的时候出现OOM的情况,想使用tf.train.shuffle_batch分批处理,结果出现异常:”The value of a feed cannot be a tf.Tensor object.”。
  2. 神经网络模型的输入和输出一定刚要和喂的数据维度相匹配!
  • 运行训练,并保存结果
    config = tf.ConfigProto()
    config.gpu_options.per_process_gpu_memory_fraction = 0.7 # maximun alloc gpu50% of MEM
    config.gpu_options.allow_growth = True # allocate dynamically
    with tf.Session(config=config) as sess:
        # for i in range(100):
        #     if i % 10 == 0:
        #     train_accuracy = accuracy.eval(session=sess, feed_dict={x_: x_train, y_: onehot_encoded})
        #     print("train_accuracy %g" % train_accuracy)
        #     train_step.run(session=sess, feed_dict={x_: x_train, y_: y_train})
        sess.run(tf.global_variables_initializer())
    
        # 迭代20次,每次都把全部数据分成42000/128个批次,分批喂入tf
        for epoch in range(20):
            print('epoch', epoch + 1)
            for batch in range(int(len(x_train) / 128)):
                batch_x = x_train[batch * 128:(batch + 1) * 128]
                batch_y = onehot_encoded[batch * 128:(batch + 1) * 128]
                sess.run(train_step, feed_dict={x_: batch_x, y_: batch_y})
    
            batch_x = x_train[int(len(x_train) / 128) * 128:]
            batch_y = onehot_encoded[int(len(x_train) / 128) * 128:]
    
            sess.run(train_step, feed_dict={x_: batch_x, y_: batch_y})
            print("accuracy: %f" % sess.run(accuracy, feed_dict={x_: batch_x, y_: batch_y}))
    
        tf.train.Saver().save(sess, "/{你的数据文件存放位置}/mnist_model.ckpt")
        tf.train.Saver().restore(sess, "//{你的数据文件存放位置}/mnist_model.ckpt")
        x_test = test_df.values.reshape([-1, 784]).astype(np.float32)
        # 直接跑图的最后一步: softmax计算能力
        result = sess.run(prediction, feed_dict={x_: x_test})
        label_test = np.argmax(result, axis=1)
        # pd.DataFrame(label_test).to_csv("//{你的数据文件存放位置}/test_result.csv")
    
        # 保存结果
        submission = pd.DataFrame({
            "ImageId": np.arange(1, 28000 + 1),
            "Label": label_test
        })
        submission.to_csv("/home/milo/Github/machinelearning/digit_recognizer/test_result.csv", index=False)

经验:

  1. 某个人没安装CUDA,还瞎吉儿Debug得很欢快,以为程序写错了!
  2. 一次性将42000张图片喂到5层神经网络结构中会出现OOM的情况,所以措施一是设置GPU内存使用阈值0.7;措施二是使用小批量处理。
  3. 小批量处理需要注意一个细节:最后一批需要在循环外单独处理。
  4. 注意输出精确度的print写法,是使用sess.run()运行accuracy方程。
  5. 使用tf.train.Saver()可以保存模型,参数是tf.session(),需要指定模型文件的存储位置。
  6. 上传Kaggle时需要注意,Kaggle要求CSV文件中要带有标签,所以需要自定义。注意细节:”ImageId”: np.arange(1, 28000 + 1),Id从1开始,直接写死就行了。

缺陷:

  1. 代码需要重构。
  2. 流程需要梳理清楚。训练跑的是优化器取损失函数最小是的方程;打印精确度跑的是对比预测值的方程;测试是直接跑神经网络模型最后的softmax方程。

实验环境

  • 硬件:
    • I5-9400F
    • 16G DDR4
    • 七彩虹RTX 2060 6G Ultra OC
  • 软件:
    • Anaconda(Python3.7.3) 1.7.2
    • Tensorflow 1.13.1
    • Pycharm 2019.1
    • cudatoolkit 10.0.130 (运用显卡的计算能力,很重要!!!)
    • cudnn 7.3.1

实验结果

待补充

总结

机器学习项目清单(摘录自《机器学习实战 基于Scikit-Learn和Tensorflow》)

  1. 架构问题,关注蓝图
  2. 获取数据
  3. 研究数据以获取灵感
  4. 准备数据以更好地将低层模型暴露给机器学习
  5. 研究各种不同的模型,并列出最好的模型
  6. 微调模型,并将其组合为最好的解决方案
  7. 提出解决方案
  8. 启动、监视、维护系统
  9. 以上清单可以随时调整
 
本文内容转自冰部落,仅供学习交流,版权归原作者所有,如涉及侵权,请联系删除。

声明:
本平台/个人所提供的关于股票的信息、分析和讨论,仅供投资者进行研究和参考之用。
我们不对任何股票进行明确的买入或卖出推荐。
投资者在做出投资决策时,应自行进行充分的研究和分析,并谨慎评估自己的风险承受能力和投资目标。
投资有风险,入市需谨慎。请投资者根据自身的判断和风险承受能力,自主决策,理性投资。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注