
圖片樣本可視化
原文第四篇中,我們介紹了官方的入門案例MNIST,功能是識別手寫的數字0-9。這是一個非常基礎的TensorFlow應用,地位相當於通常語言學習的"Hello World!"。
我們先不進入TensorFlow 2.0中的MNIST代碼講解,因為TensorFlow 2.0在Keras的幫助下抽象度比較高,代碼非常簡單。但這也使得大量的工作被隱藏掉,反而讓人難以真正理解來龍去脈。特別是其中所使用的樣本數據也已經不同,而這對於學習者,是非常重要的部分。模型可以看論文、在網上找成熟的成果,數據的收集和處理,可不會有人幫忙。
在原文中,我們首先介紹了MNIST的數據結構,並且用一個小程序,把樣本中的數組數據轉換為JPG圖片,來幫助讀者理解原始數據的組織方式。
這里我們把小程序也升級一下,直接把圖片顯示在屏幕上,不再另外保存JPG文件。這樣圖片看起來更快更直觀。
在TensorFlow 1.x中,是使用程序input_data.py來下載和管理MNIST的樣本數據集。當前官方倉庫的master分支中已經取消了這個代碼,為了不去翻倉庫,你可以在這里下載,放置到你的工作目錄。
在TensorFlow 2.0中,會有keras.datasets類來管理大部分的演示和模型中需要使用的數據集,這個我們后面再講。
MNIST的樣本數據來自Yann LeCun的項目網站。如果網速比較慢的話,可以先用下載工具下載,然后放置到自己設置的數據目錄,比如工作目錄下的data文件夾,input_data檢測到已有數據的話,不會重復下載。
下面是我們升級后顯示訓練樣本集的源碼,代碼的講解保留在注釋中。如果閱讀有疑問的,建議先去原文中看一下樣本集數據結構的圖示部分:
#!/usr/bin/env python3
# 引入mnist數據預讀准備庫
# 2.0之后建議直接使用官方的keras.datasets.mnist.load_data
# 此處為了同以前的講解對比,沿用之前的引用文件
import input_data
# tensorflow 2.0庫
import tensorflow as tf
# 引入繪圖庫
import matplotlib.pyplot as plt
# 這里使用mnist數據預讀准備庫檢查給定路徑是已經有樣本數據,
# 沒有的話去網上下載,並保存在指定目錄
# 已經下載了數據的話,將數據讀入內存,保存到mnist對象中
mnist = input_data.read_data_sets("data/", one_hot=True)
# 樣本集的結構如下:
# mnist.train 訓練數據集
# mnist.validation 驗證數據集
# mnist.test 測試數據集
# len(mnist.train.images)=55000
# len(mnist.train.images[0])=784
# len(mnist.train.labels[0])=10
def plot_image(i, imgs, labels):
# 將1維的0-1的數據轉換為標准的0-255的整數數據,2維28x28的圖片
image = tf.floor(256.0 * tf.reshape(imgs[i], [28, 28]))
# 原數據為float,轉換為uint8字節數據
image = tf.cast(image, dtype=tf.uint8)
# 標簽樣本為10個字節的數組,為1的元素下標就是樣本的標簽值
# 這里使用argmax方法直接轉換為0-9的整數
label = tf.argmax(labels[i])
plt.grid(False)
plt.xticks([])
plt.yticks([])
# 繪制樣本圖
plt.imshow(image)
# 顯示標簽值
plt.xlabel("{}".format(label))
def show_images(num_rows, num_cols, images, labels):
num_images = num_rows*num_cols
plt.figure('Train Samples', figsize=(2*num_cols, 2*num_rows))
# 循環顯示前num_rows*num_cols副樣本圖片
for i in range(num_images):
plt.subplot(num_rows, num_cols, i+1)
plot_image(i, images, labels)
plt.show()
# 顯示前4*6=24副訓練集樣本
show_images(4, 6, mnist.train.images, mnist.train.labels)
注意這個代碼只是用來把樣本集可視化。TensorFlow 2.0新特征,在這里只體現了取消Session和Session.run()。目的只是為了延續原來的講解,讓圖片直接顯示而不是保存為圖像文件,以及升級到Python3和TensorFlow 2.0的執行環境。
樣本集顯示出來效果是這樣的:

TensorFlow 2.0中的模型構建
原文第四篇中,我們使用了一個並不實用的線性回歸模型來做手寫數字識別。這樣做可以簡化中間層,從而能夠使用可視化的手段來講解機器視覺在數學上的基本原理。因為線性回歸模型我們在本系列第一篇中講過了,這里就跳過,直接說使用神經網絡來解決MNIST問題。
神經網絡模型的構建在TensorFlow 1.0中是最繁瑣的工作。我們曾經為了講解vgg-19神經網絡的使用,首先編寫了一個復雜的輔助類,用於從字符串數組的遍歷中自動構建復雜的神經網絡模型。
而在TensorFlow 2.0中,通過高度抽象的keras,可以非常容易的構建神經網絡模型。
為了幫助理解,我們先把TensorFlow 1.0中使用神經網絡解決MNIST問題的代碼原文粘貼如下:
#!/usr/bin/env python
# -*- coding=UTF-8 -*-
import input_data
mnist = input_data.read_data_sets('data/', one_hot=True)
import tensorflow as tf
sess = tf.InteractiveSession()
#對W/b做初始化有利於防止算法陷入局部最優解,
#文檔上講是為了打破對稱性和防止0梯度及神經元節點恆為0等問題,數學原理是類似問題
#這兩個初始化單獨定義成子程序是因為多層神經網絡會有多次調用
def weight_variable(shape):
#填充“權重”矩陣,其中的元素符合截斷正態分布
#可以有參數mean表示指定均值及stddev指定標准差
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
#用0.1常量填充“偏移量”矩陣
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
#定義占位符,相當於tensorFlow的運行參數,
#x是輸入的圖片矩陣,y_是給定的標注標簽,有標注一定是監督學習
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
#定義輸入層神經網絡,有784個節點,1024個輸出,
#輸出的數量是自己定義的,要跟第二層節點的數量吻合
W1 = weight_variable([784, 1024])
b1 = bias_variable([1024])
#使用relu算法的激活函數,后面的公式跟前一個例子相同
h1 = tf.nn.relu(tf.matmul(x, W1) + b1)
#定義第二層(隱藏層)網絡,1024輸入,512輸出
W2 = weight_variable([1024, 512])
b2 = bias_variable([512])
h2 = tf.nn.relu(tf.matmul(h1, W2) + b2)
#定義第三層(輸出層),512輸入,10輸出,10也是我們希望的分類數量
W3 = weight_variable([512, 10])
b3 = bias_variable([10])
#最后一層的輸出同樣用softmax分類(也算是激活函數吧)
y3=tf.nn.softmax(tf.matmul(h2, W3) + b3)
#交叉熵代價函數
cross_entropy = -tf.reduce_sum(y_*tf.log(y3))
#這里使用了更加復雜的ADAM優化器來做"梯度最速下降",
#前一個例子中我們使用的是:GradientDescentOptimizer
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
#計算正確率以評估效果
correct_prediction = tf.equal(tf.argmax(y3,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
#tf初始化及所有變量初始化
sess.run(tf.global_variables_initializer())
#進行20000步的訓練
for i in range(20000):
#每批數據50組
batch = mnist.train.next_batch(50)
#每100步進行一次正確率計算並顯示中間結果
if i%100 == 0:
train_accuracy = accuracy.eval(feed_dict={
x:batch[0], y_: batch[1]})
print "step %d, training accuracy %g"%(i, train_accuracy)
#使用數據集進行訓練
train_step.run(feed_dict={x: batch[0], y_: batch[1]})
#完成模型訓練給出最終的評估結果
print "test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels})
總結一下上面TensorFlow 1.x版本MNIST代碼中的工作:
- 使用了一個三層的神經網絡,每一層都使用重復性的代碼構建
- 每一層的代碼中,要精心計算輸入和輸出數據的格式、維度,使得每一層同上、下兩層完全吻合
- 精心設計損失函數(代價函數)和選擇回歸算法
- 復雜的訓練循環
如果你理解了我總結的這幾點,請繼續看TensorFlow 2.0的實現:
#!/usr/bin/env python3
# 引入mnist數據預讀准備庫
# 2.0之后建議直接使用官方的keras.datasets.mnist.load_data
# 此處為了同以前的講解對比,沿用之前的引用文件
import input_data
# tensorflow庫
import tensorflow as tf
# tensorflow 已經內置了keras
from tensorflow import keras
# 引入繪圖庫
import matplotlib.pyplot as plt
# 這里使用mnist數據預讀准備庫檢查給定路徑是已經有樣本數據,
# 沒有的話去網上下載,並保存在指定目錄
# 已經下載了數據的話,將數據讀入內存,保存到mnist對象中
mnist = input_data.read_data_sets("data/", one_hot=True)
# 樣本集的結構如下:
# mnist.train 訓練數據集
# mnist.validation 驗證數據集
# mnist.test 測試數據集
# len(mnist.train.images)=55000
# len(mnist.train.images[0])=784
# len(mnist.train.labels[0])=10
def plot_image(i, imgs, labels, predictions):
# 將1維的0-1的數據轉換為標准的0-255的整數數據,2維28x28的圖片
image = tf.floor(256.0 * tf.reshape(imgs[i], [28, 28]))
# 原數據為float,轉換為uint8字節數據
image = tf.cast(image, dtype=tf.uint8)
# 標簽樣本為10個字節的數組,為1的元素下標就是樣本的標簽值
# 這里使用argmax方法直接轉換為0-9的整數
label = tf.argmax(labels[i])
prediction = tf.argmax(predictions[i])
plt.grid(False)
plt.xticks([])
plt.yticks([])
# 繪制樣本圖
plt.imshow(image)
# 顯示標簽值,對比顯示預測值和實際標簽值
plt.xlabel("predict:{} label:{}".format(prediction, label))
def show_images(num_rows, num_cols, images, labels, predictions):
num_images = num_rows*num_cols
plt.figure('Predict Samples', figsize=(2*num_cols, 2*num_rows))
# 循環顯示前num_rows*num_cols副樣本圖片
for i in range(num_images):
plt.subplot(num_rows, num_cols, i+1)
plot_image(i, images, labels, predictions)
plt.show()
# 原文中已經說明了,當前是10個元素數組表示一個數字,
# 值為1的那一元素的索引就是代表的數字,這是分類算法決定的
# 下面是直接轉換為0-9的正整數,用作訓練的標簽
train_labels = tf.argmax(mnist.train.labels, 1)
# 定義神經網絡模型
model = keras.Sequential([
# 輸入層為28x28共784個元素的數組,節點1024個
keras.layers.Dense(1024, activation='relu', input_shape=(784,)),
keras.layers.Dense(512, activation='relu'),
keras.layers.Dense(10, activation='softmax')
])
# 編譯模型
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 使用訓練集數據訓練模型
model.fit(mnist.train.images, train_labels, epochs=3)
# 測試集的標簽同樣轉成0-9數字
test_labels = tf.argmax(mnist.test.labels, 1)
# 使用測試集樣本驗證識別准確率
test_loss, test_acc = model.evaluate(mnist.test.images, test_labels)
print('\nTest accuracy:', test_acc)
# 完整預測測試集樣本
predictions = model.predict(mnist.test.images)
# 圖示結果的前4*6個樣本
show_images(4, 6, mnist.test.images, mnist.test.labels, predictions)
代碼講解
通常我都是直接在注釋中對程序做仔細的講解,這次例外一下,因為我們需要從大局觀上看清楚代碼的結構。
這幾行代碼是定義神經網絡模型:
# 定義神經網絡模型
model = keras.Sequential([
# 輸入層為28x28共784個元素的數組,節點1024個
keras.layers.Dense(1024, activation='relu', input_shape=(784,)),
keras.layers.Dense(512, activation='relu'),
keras.layers.Dense(10, activation='softmax')
])
每一行實際就代表一層神經網絡的節點。在第一行中特別指明了輸入數據的形式,即可以有未知數量的樣本,每一個樣本784個字節(28x28)。實際上這個輸入樣本可以不指定形狀,在沒有指定的情況下,Keras會自動識別訓練數據集的形狀,並自動將模型輸入匹配到訓練集形狀。只是這種習慣並不一定好,除了效率問題,當樣本集出錯的時候,模型的定義也無法幫助開發者提前發現問題。所以建議產品化的模型,應當在模型中指定輸入數據類型。
除了第一層之外,之后的每一層都無需指定輸入樣本形狀。Keras會自動匹配相鄰兩個層的數據。這節省了開發人員大量的手工計算也不易出錯。
最后,激活函數的選擇成為一個參數。整體代碼看上去簡潔的令人驚訝。
接着在編譯模型的代碼中,直接指定Keras中預定義的“sparse_categorical_crossentropy”損失函數和“adam”優化算法。一個函數配合幾個參數選擇就完成了這部分工作:
# 編譯模型
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
對原本復雜的訓練循環部分,TensorFlow 2.0優化的最為徹底,只有一行代碼:
# 使用訓練集數據訓練模型
model.fit(mnist.train.images, train_labels, epochs=3)
使用測試集數據對模型進行評估同樣只需要一行代碼,這里就不摘出來了,在上面完整代碼中能看到。
可以想象,TensorFlow 2.0正式發布后,模型搭建、訓練、評估的工作量大幅減少,會催生很多由實驗性模型創新而出現的新算法。機器學習領域會再次涌現普及化浪潮。
這一版代碼中,我們還細微修改了樣本可視化部分的程序,將原來顯示訓練集樣本,改為顯示測試集樣本。主要是增加了一個圖片識別結果的參數。將圖片的識別結果同數據集的標注一同顯示在圖片的下面作為對比。
程序運行的時候,控制台輸出如下:
$ python3 mnist-show-predict-pic-v1.py
Extracting data/train-images-idx3-ubyte.gz
Extracting data/train-labels-idx1-ubyte.gz
Extracting data/t10k-images-idx3-ubyte.gz
Extracting data/t10k-labels-idx1-ubyte.gz
Epoch 1/3
55000/55000 [==============================] - 17s 307us/sample - loss: 0.1869 - accuracy: 0.9420
Epoch 2/3
55000/55000 [==============================] - 17s 304us/sample - loss: 0.0816 - accuracy: 0.9740
Epoch 3/3
55000/55000 [==============================] - 16s 298us/sample - loss: 0.0557 - accuracy: 0.9821
10000/10000 [==============================] - 1s 98us/sample - loss: 0.0890 - accuracy: 0.9743
Test accuracy: 0.9743
最終的結果表示,模型通過3次的訓練迭代之后。使用測試集數據進行驗證,手寫體數字識別正確率為97.43%。
程序最終會顯示測試集前24個圖片及預測結果和標注信息的對比:

(待續...)
