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)
轉載請注明:作者黑猿大叔(簡書)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容