TensorFlow从0到1 - 12 - TensorFlow构建3层NN玩转MNIST

TensorFlow从0到1系列回顾

上一篇 11 74行Python实现手写体数字识别展示了74行Python代码完成MNIST手写体数字识别,识别率轻松达到95%。这算不上一个好成绩,不过我并不打算立即着手改善它,而是回到本系列的主线上来,用TensorFlow重新实现一遍完全相同的算法。

TF官方的Get Started中,关于MNIST准备了Beginner和Expert两个版本的实现。前者与其说是一个两层的神经网络,不如说是一种线性判别,后者则实现了CNN。两者之间差了一个经典的3层全连接NN,本篇补上。

最终基于TF的代码只有43行(忽略空行和注释)。

分析代码的方式

与逐行分析代码不同,我偏好先清理代码涉及到的语言、工具的知识点,然后再去扫描逻辑。所以“Python必知必会”、“TensorFlow必知必会”将是首先出现的章节。

当然你也可以直接跳到代码部分:

  • mnist:TF使用的MNIST数据集,注意与上一篇Python实现使用的数据集不是同一份;
  • tf_12_mnist_softmax.py:TF MNIST for ML Beginner也一并奉上,修改了原始读取MNIST数据的路径,运行时请保持与本MNIST数据集的相对位置不变;
  • tf_12_mnist_nn.py:3层全连接NN实现;

代码运行环境:

  • Python 3.5;
  • TensorFlow 1.1 CPU version。

Python必知必会

__futrue__

TF MNIST for ML Beginner代码开头部分,出现了__future__模块,导入了absolute_importdivisionprint_function。其实它们并不是导入语句,所导入的也不能直接作为对象使用:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

解释之前,先对向前兼容向后兼容做个区分。在中文语境下,它们的语义含混,极易搞反。但是看英文就会非常清楚,向前兼容Forward Compatibility,是指向未来兼容;向后兼容Backward Compatibility,是指向过去兼容。

上面看到的__future__模块,是Python提供的一种让新版本“代码”向后兼容老版本“环境”的方式。很多文章说不清楚这个概念,主要是没有区分清楚python代码的版本和python环境的版本。通常情况下,当代码是Python 3.x的时候,只要加上像上面的代码,就能在Python 2.x环境中执行(以Python 3.x的方式)。

从Python文档描述中看出,__future__模块和普通的import工作机制很不一样。它告诉解释器把其导入的未来模块替换掉现有模块,从而采用新的语义和语法的代码就可以正常执行:

A future statement is a directive to the compiler that a particular module should be compiled using syntax or semantics that will be available in a specified future release of Python. The future statement is intended to ease migration to future versions of Python that introduce incompatible changes to the language. It allows use of the new features on a per-module basis before the release in which the feature becomes standard.

此外,__future__模块引入时必须在文件的顶部,之前只允许存在注释和空行。

至于absolute_import(绝对导入方式)division(除法)print_function(打印函数)具体的兼容性(冲突)定义,可自行参考官方的PEP文档(Python Enhancement Proposals)

__name__

TF MNIST for ML Beginner代码的结尾部分:

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--data_dir', type=str, default='/MNIST/',
                        help='Directory for storing input data')
    FLAGS, unparsed = parser.parse_known_args()
    tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)

这是Python模块的一种常见构造方式:Make a script both importable and executable(既可被调用又能作为main函数独立执行)。在其作为被调用模块时,__name__为“module”,而不再是__main__,此时上面代码段不会执行。

list和numpy.array

这里区分下Python的list与NumPy的array。在做科学计算时,大多数时候我们使用后者。

Python中并没有数组,而一个看起来比较像数组的类型是list,可它的特性一定不会让你满意的:

list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list1 + list2)
print(list1 * 2)

输出:

[1, 2, 3, 4, 5, 6]
[1, 2, 3, 1, 2, 3]

NumPy中提供了array:

import numpy
array1 = numpy.array([1, 2, 3])
array2 = numpy.array([4, 5, 6])
print(array1 + array2)
print(array1 * array2)

输出:

[5 7 9]
[4 10 18]

可见numpy.array才是我们需要的,一个如此简单的*就实现了Hadamard乘积⊙。TensorFlow处理数组的方式和NumPy是一致的。

TensorFlow必知必会

输入层张量构建

在上一篇用Python实现NN的输入层时,我们构建了一个784 x 1的矩阵作为第一层神经元的输出x,网络每次只处理一幅图像。第二层神经元权重矩阵为一个30 x 784的矩阵W2,两者相乘W2·x,权重矩阵在前,前一层输出在后。

而TF的MNIST for ML Beginner代码在构建第一层神经元时,构建了一个n x 784的矩阵x,它一次可以输出n张图像(甚至全部50000张测试图像,如下图所示)。第二层神经元权重矩阵为一个784 x 30的矩阵W2,两者相乘x·W2,前一层输出在前,权重矩阵在后。

tensor

这是构建NN输入层张量时,TF与之前的Python方式上的差异。如果换个角度来理解,把TF的tensor的横坐标当作时间轴,那么n x 784就相当样本的时间序列,这样来看和Python方式在本质上几乎一样的。

InteractiveSession

在MNIST for ML Beginner代码中,使用了InteractiveSession

sess = tf.InteractiveSession()

TF文档写道:

The only difference with a regular Session is that an InteractiveSession installs itself as the default session on construction.

也就是说,调用了InteractiveSession之后,上下文就有了默认的session。

使用Session的写法:

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

使用InteractiveSession则可以简化成:

tf.InteractiveSession()
...
tf.global_variables_initializer().run()

tf.nn.softmax_cross_entropy_with_logits

在MNIST for ML Beginner代码中出现了这个API,具体的用法如下:

y = tf.matmul(x, W) + b
y_ = tf.placeholder(tf.float32, [None, 10])

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))

这个API一口气做了两件事情,将神经元的“加权和”y = tf.matmul(x, W) + b作为输入,首先计算了以柔性最大值为激活函数的神经元输出,然后又计算了交叉熵“损失”。虽然强大,但是从工程角度看它不够“简单”。在最后测试集上评估识别精度时,官方的sample code没有用真正的输出与标签进行比对:

correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))

虽然这不会对最终结果产生影响,但是更加符合理论算法的形式应该是:

correct_prediction = tf.equal(tf.argmax(tf.nn.softmax(y), 1), tf.argmax(y_, 1))

什么是logits

tf.nn.softmax_cross_entropy_with_logits名字中最后一个词“logits”,让我困惑了很久。

本质上它其实就是NN输出层神经元的加权输入zL=aL-1·WL + bL(还未叠加最后的激活函数)。可是为什么叫logits呢?

TF官方文档上对这个参数的解释是:unscaled log probabilitie,让人费解。我觉得这不是个好名字,不仅在中文机器学习术语中鲜得一见,就是老外也搞不清楚

当然,这也绝不是TF的研发人员不负责任的表现,可能是一种领域术语习惯,见维基百科对数概率词条。

tf.train.GradientDescentOptimizer

5 TensorFlow轻松搞定线性回归中,我们已经见识过了最优化计算的封装——tf.train.GradientDescentOptimizer,现在模型从简单的线性模型,变成了复杂的人工神经网络,它还是一样轻松搞定。

之所以基于TensorFlow实现相同的MNIST数字识别,代码量可以少40%,要归功于GradientDescentOptimizer。仅仅一句代码,就自动包含了前馈计算、反向传播求偏导,并自动更新所有的权重和偏置:

train_step = tf.train.GradientDescentOptimizer(3.0).minimize(loss)

reduce_sum

无论是TensorFlow还是NumPy都提供了对于张量在不同的方向上做累加计算的支持。这里给出一个玩具代码自行体会:

import tensorflow as tf
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
tf.InteractiveSession()
print(tf.reduce_sum(a).eval())
print(tf.reduce_sum(a, 0).eval())
print(tf.reduce_sum(a, 1).eval())

输出:

21
[5 7 9]
[6 15]

argmax

argmax也是基于张量的计算,求取某个方向上的最大值的下标,在做统计时十分有用。同样给出一个玩具代码自行体会:

import tensorflow as tf
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
tf.InteractiveSession()
print(tf.argmax(a).eval())
print(tf.argmax(a, 0).eval())
print(tf.argmax(a, 1).eval())
np.argmax(a)
np.argmax(a, 0)
np.argmax(a, 1)

输出:

[1 1 1]
[1 1 1]
[2 2]
5
[1 1 1]
[2 2]

输出结果返回的是最大值的下标(从0开始)。注意TensorFlow与NumPy有些许差别。

代码分析

熟悉了前面的基础知识点,再去看完整的NN实现,就会无比轻松了。整体代码分为5大块:

  • 读取数据;
  • 构建神经网络计算图;
  • 定义损失函数和优化器;
  • 执行计算图,进行NN训练;
  • 测试性能。

在TF官方MNIST for ML Beginner代码的基础上(tf_12mnist_softmax.py),只消做3处改动,即可实现与之前算法一模一样的经典3层NN。

1 追加一个有30个神经元隐藏层,使用S型函数作为激活函数:

W_2 = tf.get_variable('W_2', [784, 30], initializer=tf.random_normal_initializer())
b_2 = tf.get_variable('b_2', [30], initializer=tf.random_normal_initializer())
z_2 = tf.matmul(x, W_2) + b_2
a_2 = tf.sigmoid(z_2)

2 使用均方差(MSE)作为损失函数(为了和之前的算法保持一致):

loss = tf.reduce_mean(tf.norm(y_ - a_3, axis=1)**2) / 2

3 设置超参数保持和之前算法一致:30次迭代,10个样本为一个mini batch,测试样本数取50000,学习率3.0:

train_step = tf.train.GradientDescentOptimizer(3.0).minimize(loss)
...
for epoch in range(30):
    for _ in range(5000):
        batch_xs, batch_ys = mnist.train.next_batch(10)
        ...

基于上述修改,结果出现了一个小状况:无论如何调整学习率,增加迭代次数,训练后模型的准确率最多只能达到60%。

参数初始化的坑

仔细比对之前算法的Python实现,终于发现了一处不起眼的差异:对参数W和b的初始化方式不同。立即由全零初始化改为随机初始化,再对模型进行训练,正确率终于达到了预期的95%。

W_2 = tf.get_variable('W_2', [784, 30], initializer=tf.random_normal_initializer())
b_2 = tf.get_variable('b_2', [30], initializer=tf.random_normal_initializer())
...
W_3 = tf.get_variable('W_3', [30, 10], initializer=tf.random_normal_initializer())
b_3 = tf.get_variable('b_3', [10], initializer=tf.random_normal_initializer())

对比

对上一篇中的Python实现和本篇的TF实现做了一个简单的对比。

从代码量来看,TF实现只需要43行,完胜Python实现的74行。这是因为几乎所有的算法TF都进行了封装:

  • 梯度下降算法;
  • 前馈;
  • 反向传播算法;
  • 激活函数;

再看识别率。由于算法完全相同,所以识别率基本一致,都在95%上下浮动。

最后看执行效率。在相同运算量下,两者的运行时间相差悬殊,TF的计算图模式体现出了巨大的性能优势(对计算图的介绍见2 TensorFlow内核基础),对50000张训练数据,进行30次迭代训练:

  • Python实现:4 min 17 sec
  • TF实现:1 min 48 sec

注:数据在笔记本上运行得到,i5-4300U CPU,12GB RAM。

总体来说,TF完胜:一半的代码,两倍的速度。

附完整代码

tf_12_mnist_nn.py:

import argparse
import sys
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf

FLAGS = None


def main(_):
    # Import data
    mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)

    # Create the model
    x = tf.placeholder(tf.float32, [None, 784])
    # W_2 = tf.Variable(tf.zeros([784, 30]))
    W_2 = tf.get_variable(
        'W_2', [784, 30], initializer=tf.random_normal_initializer())
    # b_2 = tf.Variable(tf.zeros([30]))
    b_2 = tf.get_variable(
        'b_2', [30], initializer=tf.random_normal_initializer())
    z_2 = tf.matmul(x, W_2) + b_2
    a_2 = tf.sigmoid(z_2)

    # W_3 = tf.Variable(tf.zeros([30, 10]))
    W_3 = tf.get_variable(
        'W_3', [30, 10], initializer=tf.random_normal_initializer())
    # b_3 = tf.Variable(tf.zeros([10]))
    b_3 = tf.get_variable(
        'b_3', [10], initializer=tf.random_normal_initializer())
    z_3 = tf.matmul(a_2, W_3) + b_3
    a_3 = tf.sigmoid(z_3)

    # Define loss and optimizer
    y_ = tf.placeholder(tf.float32, [None, 10])
    # loss = tf.losses.tf.losses.mean_squared_error(y_, a_3)
    loss = tf.reduce_mean(tf.norm(y_ - a_3, axis=1)**2) / 2
    train_step = tf.train.GradientDescentOptimizer(3.0).minimize(loss)

    sess = tf.InteractiveSession()
    tf.global_variables_initializer().run()

    # Train
    best = 0
    for epoch in range(30):
        for _ in range(5000):
            batch_xs, batch_ys = mnist.train.next_batch(10)
            sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
        # Test trained model
        correct_prediction = tf.equal(tf.argmax(a_3, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_sum(tf.cast(correct_prediction, tf.int32))
        accuracy_currut = sess.run(accuracy, feed_dict={x: mnist.test.images,
                                                        y_: mnist.test.labels})
        print("Epoch %s: %s / 10000" % (epoch, accuracy_currut))
        best = (best, accuracy_currut)[best <= accuracy_currut]

    # Test trained model
    print("best: %s / 10000" % best)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--data_dir', type=str, default='/MNIST/',
                        help='Directory for storing input data')
    FLAGS, unparsed = parser.parse_known_args()
    tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)

下载tf_12_mnist_nn.py

上一篇 11 NN基本功:74行Python实现手写体数字识别
下一篇 13 AI驯兽师:神经网络调教综述


共享协议:署名-非商业性使用-禁止演绎(CC BY-NC-ND 3.0 CN)
转载请注明:作者黑猿大叔(简书)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容