引言
本文是学习 Tensorflow 官方文档的过程中的一点感悟, 本文假设你对矩阵运算有一定的了解, 具体可以看看下面资料
加载数据
首先我们得先把数据下载下来, Tensorflow 给我们提供了一个函数来进行下载, 这个函数为 read_data_sets
这个函数 read_data_sets 函数很简单, 查看在目录下面有没有文件没有就去下载, 有就解析加载, 一方面方便我们获取数据, 一方面方便我们直接开箱即食, 但是由于这个默认下载地址是需要翻墙, 所以我这里提供一个不需要翻墙的地址 http://yann.lecun.com/exdb/mnist/ , 你只需要加载下面的函数
- from tensorflow.examples.tutorials.mnist import input_data
- mnist = input_data.read_data_sets("input/", one_hot=True, source_url="http://yann.lecun.com/exdb/mnist/")
等几分钟, 数据就会下载到当前目录的 input 文件夹中, 这样你下次运行就能直接本地文件夹中加载图片数据了
观察数据
首先我们看看下载了什么数据, 打开 input 文件夹, 我们可以看到, Tensorflow 给我下载好了四个文件, 分为两组, 一组训练集一组测试集, 每组里面 2 个文件, 一个是手写图片文件, 一个标签文件(每张手写的图片代表的数字)
加载图片数据对于新手来说挺麻烦的, 为了让我们专注于模型而不是编程, Tensorflow 直接帮我们做好了加载数据的事情, 我们上面得到的 mnist 变量里面就存贮了我们这个项目所需要的数据, 我们来看看这个 mnist 有什么
我们最关心的就是 mnist 里面训练数据, 这里推荐使用 notebook 来操作这个数据集, 我们首先 mnist 的训练数据是什么
mnist 数据来源网络
mnist 数据就是上面这些图片, 我们把图片把每个像素的二值化, 然后把他们放到一个数组中, 每张图片对应一个数组
mnist 训练数据存贮在这两个变量中
- mnist.train.labels
- mnist.train.images
其中 mnist.train.images 是一个 (55000, 784) 的二维数组, 其中 mnist.train.labels.shape 是一个 (55000, 10) 的二维数组, 现在摆在我们面前的其实很简单, 通过 55000 个图片像素值来训练我们模型, 以便能让模型能给一张图片像素值来预测图片代表的数字
这些数字在人看来非常容易辨认, 但是怎么能让电脑也能辨别他呢, 这就要用到卷积神经网络的力量, 通过卷积神经网络, 电脑的准确率能到 99%, 这就非常恐怖了, 我们人有时候也会看走眼呢.
在谈卷积之前我们先谈谈我们以前的做法, 这样通过对比就能知道卷积到底做了什么优化
传统做法
其实从传统的角度来看, 其实图像识别也就是通过图片的特征值来判断图片代表的含义, 但是图片这个东西又很特殊, 相比于其他机器学习问题, 他的特征值很多, 这里我们使用 28X28 的图片就有 784 个特征, 如果我们图片尺寸再大, 这个特征值会变得非常巨大, 而且我们知道机器学习需要大量数据才能大展身手, 然而每个图片如此巨大, 训练巨大的数据集电脑也吃不消
所以我们必须要将数据进行降维, 机器学习里面有很多降维的方法, 比如 PCA,LDA 这些, 但是这些方法都有一个问题他们必须把一个图片看做一个整体输入, 也就是前面的将 28X28 转换成一个 784 的数组, 这个数组我们知道, 他丧失了一个非常重要的东西维度, 我们仔细观察上面的图片
mnist 数据来源网络
每个图片其实我们关注的都是数字的二维分布, 我们通过闭合的圆的个数来区分 8 和 0, 我们通过中间的空白部分来区分 0 和 1, 所以我们希望能使用一种新的方法来确定图片特征, 一方面能够保存图片的空间信息, 一方面能最终数据一维的结果(图片代表的数字), 这个就是卷积的引入了, 卷积从二维的角度来提取图片的特征, 相比于传统的一维提取, 它能最大程度保留图片的信息, 并且进行深度降维
从项目了解卷积
一开始学习深度学习卷积神经网络, 看了很多资料, 但是总是感觉并没有很深的理解, 至到接触这个项目, 从代码的层次上再去理解卷积才给我恍然大悟的感觉
首先先谈一谈 Tensorflow 这个库的基础知识, 由于 Python 速度有点慢, 所以 Tensorflow 的后端全部由 C++ 写的, 你可以这样理解 Tensorflow,Python 相当于一个客户端, 你可以使用一个 session(回话)与服务器 (C++) 进行交互, 这样的话, 我们在客户端可以享受 Python 的方便快捷, 也可以享受 C++ 运行的高效性, 但是这个也带来一个麻烦, 原来 Python 是一个所见即所得的, 现在运行一些东西必须使用 session 来通知服务器来运行, 我们很多中间过程就没法知道, 只能通过返回的结果来进行推断了. 在官方教程并没有讲太多中间过程, 只是一笔带过, 所以为了更好的理解卷积神经网络, 我们将会以一种很难看的方法运行 Tensorflow, 但是我们能从这个过程中对卷积的理解更加深刻
所以接下来我们基本上每个操作都会让后端运行并且分析返回结果, 为了方便叙述, 我们假设你在运行 session.run 之前都会运行这个 session.run(tf.global_variables_initializer())来初始化所以的变量
PS: 之所以要运行这个, 因为我们使用 session 与 C++ 进行交互, 如果我们 "不声明" 变量, c++ 会报错的
下面我们就从这个项目一行一行讲起
准备数据
前面我们知道, 卷积就是要从二维空间中来提取我们想要的特征, 首先我们把数据还原成二维的
x_image = tf.reshape(x, [-1,28,28,1])
x 是上面我们输入的数据, 来我们来检测一下, 首先我们声明一个 session
session = tf.Session()
再从数据集中掏出 50 张图
data = mnist.train.next_batch(50)[0]
接下来我们看看这个 x_image 变成了什么
- session.run(tf.global_variables_initializer())
- x_image_data = session.run(x_image, feed_dict={
- x: data
- })
我们输入两者的 shape
- data.shape, x_image_data.shape
- (50, 784) (50, 28, 28, 1)
我们很清楚的看到, 我们成功将一维的数组图像 (784) 变成了二维的数组图像(28X28), 其实我们生成了三维(28 X 28 X 1), 但是由于我们只有有些图片还会有多个色道(RGB), 所以我们为了兼容, 声明成 28 X 28 X 1
好的, 现在我们成功将一维图片还原成二维的, 接下来就是将他们卷起来的时候了
第一层
如果你学过一些信号处理你会发现, 深度学习使用的卷积其实并不是原始意义上的卷积, 他没有 "旋转 180" 的操作, 但是他的形式其实是类似的. 这个 "积" 的操作主要是通过矩阵运算来实现的, 为了更好的理解卷这个操作, 我从网上找了前辈们辛苦做的动图
卷积操作 - 来源网络
PS: 这个图与我们数据有点不同, 我们每张只有一个色道, 这个有三个色道, 这张图有两个卷积核, 但是我们这个第一层会使用 32 个, 但是其实原理都一样, 如果你实在理解不过来, 你可以先值看最上面那一排
我们回到这种图, 最左边就是图像输入, 中间是卷积核, 最后右边是输出, 我们可以从图中可以很清楚的看到卷积的与我们平常操作不同, 首先输入上我们是二维数据, 通过二维的卷积核进行矩阵运算, 最后我们输出二维结果, 这就是卷积的强大之处, 不但保留了原来的二维信息而且能够使用高效的矩阵运算来加速提取特征
现在我们回到代码
首先是要声明卷积核, 我们可以使用简单的方法, 将卷积核全部声明为全 0 矩阵, 但是这个有可能造成 0 梯度, 所以我们加入一点噪音, 我们看看加入噪音的卷积核是什么值
- initial = tf.truncated_normal([5, 5, 1, 32], stddev=0.1)
- W_conv1 = tf.Variable(initial)
- session.run(tf.global_variables_initializer())
- W_conv1_value = session.run(W_conv1)
- W_conv1_value.mean(), W_conv1_value.std()
- (0.001365028, 0.08744488)
我们使用 tf.truncated_normal 函数声明了 32 个 5X5X1 的随机卷积核, 看起来随机性还挺不错哦
PS: 前面 (5,5,1) 代表输入长, 宽, 色道, 后面代表输出输出数量当然我说它是 32 个它不一点为 32 个矩阵, 应该是 (色道 X 输出数量) 个卷积核, 但是我们这里只有一个色道, 所以只有 32 个, 我们可以通过 W_conv1_value.shape 查看真实的维度(当前的维度为(5, 5, 1, 32))
这个卷积核就对应上面图中间的小矩阵, 他的长宽都为 5, 图中长宽都为 3, 当然我们可以把这个长宽修改, 使用 5 是我们的经验值, 通过这个大小的卷积核能够在模型表现能力更好.
接下来我们就进行最重要的卷积操作了, 由上面图可知, 要进行卷积必须要有三维的数据与对应的卷积核进行相卷, 其实我们在图中还可以看到一个重要的东西, 卷积的步长也就是每个框移动的位置(图中的步长为 2)
还有一个较隐秘的知识, 你有没有注意到图中的数据原来是 7X7 的数据, 通过卷积核转换之后就变成了 3X3 了, 影响卷积后图像尺寸不但有步长还有框子的大小, 假如你的框是 7, 那图中只剩下一个值了, 所以我们避免尺寸减少, 我们使用周围填充 0 来使最边缘的位置卷积也成为到框子的中心, 一方面避免边缘数据流失, 一方面也能突出边缘数据(周边全为 0)
Tensorflow 为我们封装好了上面所以的方法, 我们只要通过传参过去就能改变部长, 改变填充方式, 好了现在就开始来正式 "卷" 了
- session.run(tf.global_variables_initializer())
- v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME'), feed_dict={
- x: data
- })
现在我们来看看卷完后 v 的 shape
- v.shape
- (50, 28, 28, 32)
50 代表 50 个数据,(28,28)代表图片维度, 这个 32 就是卷积核数, 50 和 32 这两个应该是固定的, 不难理解, 我们现在来看看为什么通过卷积核的 "卷", 图片还是保持 28X28 的, 这个也是在知乎上涉及到的一个问题 https://www.zhihu.com/question/46889310 , 现在我们从实验上来解决一下
首先我们看 tf.nn.conv2d 函数, 他接受四个参数, 第一个图片, 第二个卷积核, 第三个步长, 第四个卷积方式
首先问题是觉得, 卷完之后应该是变成 24 X 24, 这个理解是没错的, 我们将 pading 的值改成 VALID 再次运行
- session.run(tf.global_variables_initializer())
- v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='VALID'), feed_dict={
- x: data
- })
- v.shape
- (50, 24, 24, 32)
我们得到了 24 X 24 的图片, 这个 SAME 和 VALID 有什么区别呢, 这个区别就是填充 0 没有填充 0 的原因, SAME 在图像周边填 0 这样就能得到 28 X 28
我们也发现, 这个还有一个参数 strides, 这个就是前面填的步长, 步长的长宽就是中间两位设置的(最边上两位跟输入有关, 第一个是输入图片数量, 最后一个是图片的色道), 我们在这里使用使用 1 步长, 我们来试试 2 步长试试
- v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 2, 2, 1], padding='SAME'), feed_dict={
- x: data
- })
- v.shape
- (50, 14, 14, 32)
果然输出的图像变成 28 的 1/2 了
接下来我们就要把卷积的值丢到神经元函数里面去了, 为了符合实际, 我们加入一个偏置量 b_conv1
- def bias_variable(shape):
- initial = tf.constant(0.1, shape=shape)
- return tf.Variable(initial)
- b_conv1 = bias_variable([32])
这里我们使用 0.1 来初始化偏置量, 接下来就是丢到神经元函数, 这里我们使用 numpy 的 array 的传播性, 将 b_conv1 传递给所有的 28X28 的维度
- h_conv1 = tf.nn.relu(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1)
- v = session.run(h_conv1, feed_dict={
- x: data
- })
- v.shape
- (50, 28, 28, 32)
我们可以看到卷积完后从神经元函数生成的数据是 (50X28X28X32) 的, 最后维度由 1 变成 32, 所有我们得使用点方法来缩减数据维度, 这里我们使用卷积池的方法
卷积池
由上面可以看到, 其实很简单就是把最大的挑出来
- h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1],
- strides=[1, 2, 2, 1], padding='SAME')
这里的参数很简单我就不介绍, 这样 "瘦身" 之后, 数据的维度由 (50, 28, 28, 32) 变成(50, 14, 14, 32), 减少 4 倍
到这里我们的第一层卷积就结束了, 接下来就是第二层卷积, 为什么要多卷一次呢, 因为前一层学到的还是太少了, 要加强学习, 这层和第一层没什么差别, 所以我们就跳过这层
直接贴代码(函数就不复制了, 文档里面有)
- W_conv2 = weight_variable([5, 5, 32, 64])
- b_conv2 = bias_variable([64])
- h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
- h_pool2 = max_pool_2x2(h_conv2)
全连接层
当我们完成两层卷积之后, 我们的数据变成了 (50,7,7,64) 的四维数组了, 我们知道我们传统的机器学习其实最后都是采用二维数组来当做训练数据(X 代表特征, Y 代表样本), 所以全连接层就是把卷积给 "反卷" 过来, 这样后面你方便对接传统机器学习, 而且最后我们需要的数据也是输出的也是二维的(对一堆数据统一进行预测, 所以这里称二维), 但是这里要注意全连接层不是输出层, 所以我们可以随意设置输出的维度, 最后输出层对接再进行一次全连接层类似操作就能输出我们想输出的维度, 这里我们看看全连接层权值变量
- W_fc1 = weight_variable([7 * 7 * 64, 1024])
- b_fc1 = bias_variable([1024])
这里我们声明全连接层的权值变量 W_fc1 和偏置量 b_fc1, 我们可以看看 W_fc1 的 shape 是多少
- session.run(tf.global_variables_initializer())
- session.run(W_fc1).shape
- (3136, 1024)
我们可以看到其实就是一个二维数组维度为(3136,1024), 第一个维度跟输入有关, 第二个维度影响输出维度, 前面我们使用 tf.nn.conv2d 卷积操作来转换图片, 在全连接层我们要使用矩阵运算来转换我们的维度
矩阵运算非常有趣, 我们在前面其实也提到过一点, 就是降维的实现 PCA 就是使用矩阵运算来进行降维, 我们把数据分为 X(特征),Y(数量), 经过一次矩阵运算我们可以实现数量不变, 而特征改变, 这个就非常强大了, 我们可以随便修改矩阵参数来动态修改我们特征数量
但是矩阵运算也有一定局限性, 就是两个运算的矩阵必须是前者长与后者的宽想同, 这个跟矩阵运算特性有关, 具体可以看看矩阵运算相关资料
所以为了进行矩阵运算我们第一件事就是改变输入的 shape, 让它由四维变成二维, 以便能够与我们权值矩阵 W_fc1 进行运算
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
我们简单的使用 tf.reshape 就能把第二层卷积后的输出变量转换成 (50,7764) 的维度, 这样我们就能直接与权值矩阵 W_fc1 进行运算
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
我们这里直接将运算后的值放到激活函数里面去完成全连接层的功能
输出层
其实输出层同全连接层很类似, 我们就是把前面的变量转换成我们想输出的维度, 在进行这个输出层之前, 我们得先搞一层 Dropout 层, 这个能有效的避免神经网络的过拟合问题, 具体可以看看这篇论文
- keep_prob = tf.placeholder("float")
- h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
因为同全连接层原理类似, 输出层我就不就不详细介绍了
- W_fc2 = weight_variable([1024, 10])
- b_fc2 = bias_variable([10])
- y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
我们可以看看最后我们输出是什么
- session.run(tf.global_variables_initializer())
- session.run(y_conv, feed_dict={
- x:data, keep_prob:0.5
- }).shape
- (50, 10)
ok, 我们最后得到一个二维数组, 50 个预测结果(输出采用 OneHot 方法)
反向传播
在前面我们得到了在初始话随机权值下得到输出结果, 但是这个结果肯定是错误的, 我们必须通过修改每层的权值来修正模型, 使模型越来越聪明, 所以第一步, 我们必须 "自我反省", 了解自己与真实结果差距多少
- y_ = tf.placeholder("float", [None, 10])
- cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
我们引入 y_作为实际值(我们模型预测值为 y), 我们这里使用交叉熵来评判预测准确性, 但是单单知道 "自己错了" 没有什么卵用, 我们必须要 "改正", 这里我们使用 AdamOptimizer 优化算法来反向传播我们误差, 让模型好好 "反省改正"
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
到这里基本上差不多了, 我们已经形成了一个闭环, 预测 ->评估 ->改正 ->预测 ->......, 只有让它不断的训练下去直到我们能接受他的误差我们的模型就训练好了
- correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
- accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
- session.run(tf.initialize_all_variables())
- for i in range(18000):
- batch = mnist.train.next_batch(50)
- if i%100 == 0:
- train_accuracy = accuracy.eval(feed_dict={
- x:batch[0], y_: batch[1], keep_prob: 1.0}, session=session)
- print("step %d, training accuracy %g"%(i, train_accuracy))
- if abs(train_accuracy - 1) < 0.01:
- break
- train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5}, session=session)
由于我们使用 OneHot 方法来输出预测变量, 所以我们要使用 tf.argmax 来得到我们想要的真实数字, 经过 20000 轮训练我们正确率可以达到 99%, 至此卷积神经网络发挥他的威力.
总结
卷积神经网络是深度学习的一个很重要的组成部分, 了解卷积必须要知道为什么要用卷积, 用了有什么好处. 总而言之, 卷积并不是一个很新奇的东西, 很早在信号处理中就有应用, 但是在图像处理上由于他能保留图像维度信息从而在深度学习领域大放异彩, 这也可以看做 "是金子总会发光吧"
引用
矩阵运算
通俗理解卷积神经网络
Dropout
来源: https://juejin.im/entry/5bb09369f265da0a87266d47