【Get】用深度學習識別手寫數字

CoorChice發表於2018-10-19

前置參考讀物:

《機器學習,看完就明白了》傳送門

獲取資料來源

訓練資料直接使用開源的手寫資料集MNIST。

MNIST資料集是一個開源的手寫資料庫。它提供了大量的資料樣本作為訓練集和驗證集。這個資料集擁有 60000 個訓練樣本,和 10000 個測試樣本。

MNIST 官網(一個很 low 的網站)傳送門:http://yann.lecun.com/exdb/mnist/

image_mnist_web

就是上面那幾個紅色的、帶下劃線的!!!

如果你下載的 tensorflow 包括了 mnist 的例子的話,很幸運,你可以直接引用到資料載入的工具:

from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets
複製程式碼

否則的話,你需要自己寫一個工具,用於訓練資料的下載和讀取。但你可以在這個地址中找到這段程式碼。

【mnist.py 傳送門】

我們實際在外面,需要呼叫的就是 read_data_sets 這個函式。

關於資料下載,你可以直接到 CoorChice 給出的 MNIST 官網上直接下好資料,然後使用 read_data_sets函式從儲存路徑讀取就行。

開始構建網路

定義幾個輔助函式

首先,先抽象出幾個函式,用來建立 權重、偏置量、卷積核和池化層。它們是這樣的。

# 定義一個用於建立 權重 變數的函式
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    var = tf.Variable(initial)
    # 記錄每一個權重,因為後面要使用正則化
    # 至於原因,後面具體再說
    tf.add_to_collection(tf.GraphKeys.WEIGHTS, var)
    return var


# 定義一個用於建立 偏置量 變數的函式
def bias_variable(shape):
    # 初始化值為0.1
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)


# 構建卷積函式
# 該卷積核每次在長、寬上移動一個步長,padding採用"SAME"策略
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')


# 該池化核大小為2x2,長、寬上步長為2,padding採用"SAME"策略
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
複製程式碼

在上面這段程式碼中,CoorChice 有必要解釋解釋權重 weight 的生成。

tf.truncated_normal(shape, mean, stddev)
複製程式碼

這個函式會從一個正太分佈(該正太分佈的均值為 mean,預設為0)中產生隨機數,什麼意思呢?

image_truncated

看上圖,stddev 表示標準差,就是圖中橫座標上的取值,也就是說,這個函式會從 [mean ± 2stddev] 的範圍內產生隨機數,如果產生的隨機數不在這個範圍內,就會再次隨機產生,直到隨機數落在這個範圍內。

至於為什麼要用這種方法來初始化 weight 呢?這是根據各位大牛們的經驗所得,使用這種方式產生的 weight 不容易出現梯度消失或者爆炸的問題。

反正這也是一門玄學。

構建網路結構

先看一下完整程式碼,我們在逐一解釋。

import tensorflow as tf

class CnnModel_MNIST:
    def __init__(self):
        # 建立佔位tensor,用於裝載資料
        self.x_data = tf.placeholder(tf.float32, [None, 784])
        self.y_data = tf.placeholder(tf.float32, [None, 10])

        # -----------------------構建第一層卷積-----------------------
        with tf.name_scope('hidden1'):
            W_conv1 = weight_variable([5, 5, 1, 32])
            b_conv1 = bias_variable([32])
            x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
            h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
            h_pool1 = max_pool_2x2(h_conv1)

        # -----------------------構建第二層卷積------------------------
        with tf.name_scope('hidden2'):
            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)

        # -----------------------構建密集(全)連結層------------------------
        with tf.name_scope('FC1'):
            W_fc1 = weight_variable([7 * 7 * 64, 1024])
            b_fc1 = bias_variable([1024])
            h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
            h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

        # -----------------------加入Dropout------------------------
        with tf.name_scope('dropout'):
            self.keep_prob = tf.placeholder(tf.float32)
            h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)

        # -----------------------構建輸出層------------------------
        with tf.name_scope('output'):
            W_fc2 = weight_variable([1024, 10])
            b_fc2 = bias_variable([10])

            self.y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

複製程式碼

建立資料輸入的佔位符

x_data = tf.placeholder(tf.float32, [None, 784])
y_data = tf.placeholder(tf.float32, [None, 10])
複製程式碼

我們需要先建立兩個佔位變數來容納待會兒輸入的資料,然後把它們帶著到網路中進行運算。

  • x_data 是用來容納訓練資料的,它的 shape 形狀在這裡被莫名其妙的定義為 [None, 784]。其實這裡是有學問的,且聽 CoorChice 慢慢道來。

    第一維定義為 None 表示不確定,後面會被實際的數值替代。這樣做是因為我們一開始並不知道會有多少張圖片資料會被輸入。或者當我們採取 mini-batch 的梯度下降策略時,可以自由的設定 batch 的大小。

    第二個維度定義為 784,這完全是因為我們資料集中的圖片大小被統一為了 28*28 。

  • y_data 是用來容納訓練資料的標籤的,它的 shape 之所以被定義為 [None, 10] ,是因為它的第一維為 None 與 x_data 具有相同理由,而第二維為 10 是因為我們總共有 0~9 共 10 種類別的數字。

構建第一層網路

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
複製程式碼

權值 w 的構建,

W_conv1 = weight_variable([5, 5, 1, 32])
複製程式碼

傳入的陣列表示 w 的形狀,其實就定義了該層卷積核的大小和數量:

  • 每個卷積核的大小為 5x5

  • 輸入通道數為 1,因為我們用的圖片是灰度圖。如果是用不帶透明通道的 rgb 彩色圖該值就設為 3,如果在帶了透明通道的 rgba 彩色圖該值就設為 4

  • 該層輸出通道數為 32,即該層有 32 個卷積核。對於隱藏層中每個卷積層中的卷積核大小如何的確定,再次強調,這是一個玄學,憑感覺設定。最靠譜的方法是用一些公開的網路模型,照著巨人們的設定,畢竟是經過反覆嘗試論證出來的。

偏置量 b 的構建

b_conv1 = bias_variable([32])
複製程式碼

b 的大小和卷積核個數相對應就行了。什麼意思呢?就是每個卷積核和輸入卷積後,再加上一個偏置量就好。回顧一下卷積核的結構。

wx + b
複製程式碼

改變輸入資料的形狀

x_image = tf.reshape(self.x_data, [-1, 28, 28, 1])
複製程式碼

這行程式碼作用是改變我們輸入資料的形狀,讓它可以和我們的卷積核進行卷積。因為輸入的資料 x_data 是一個 Nx784 的張量,所以需要先變為 M x 28 x 28 x 1 的形狀才能進行運算。

第一個維度的 -1 表示大小待定,優先滿足後面 3 個維度,最後再計算第一個維度的大小。也就是這樣的:N x 784 / (28 x 28 x 1)

第二、第三維度實際上表示每張輸入圖片的大小需改為 28 x 28。

第四維度表示通道數,灰度為 1,rgb 為 3,rgba 為 4 。

構建卷積並加上啟用函式

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
複製程式碼

這行程式碼包含了 3 個操作:

  • conv2d(x_image, W_conv1),將輸入與該層的所有卷積核進行卷積運算

這是一個卷積運算的動態示意圖。

每次從輸入中取出一個和卷積核大小相同的張量進行卷積運算。

image_conv_compute

其意義就是,將卷積核旋轉 180 度,然後再輸入進行計算。

image_conv_compute2

由於涉及到旋轉操作,所以卷積核的大小通常會取奇數,這樣能讓卷積核有一個明顯的旋轉中心。

  • conv2d(x_image, W_conv1) + b_conv1,每次卷積後加上一個偏置量

  • tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1),最後加上一個啟用函式,增加非線性的變換

此處使用的啟用函式是比較流行的 ReLu 函式,它的的函式式很簡單:

y = max(x, 0)
複製程式碼

影像也比較直觀:

image

ReLu啟用函式的好處在於,由於它在第一象限就是 x,所以能夠大量的減少計算,從而加速收斂。同時它天生就能減小梯度消失發生的可能性,不過梯度爆炸還是可能會發生。

池化

h_pool1 = max_pool_2x2(h_conv1)
複製程式碼

最後,再加上一個池化層,能夠壓縮權值數量,減小最後模型的大小,提高模型的泛化性。

這裡使用了一個 2x2 的 max_pooling,且步長取 1。可以回到上面定義的 max_pool_2x2(x) 函式回顧一下。

max_pooling 實際就是取一個 2x2 張量中的最大值,這樣能夠過濾掉一些並不是很重要的噪聲。

【Get】用深度學習識別手寫數字

構建第二層網路

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)
複製程式碼

第二層和第一層構建的套路基本是一樣的,其實我們再多加幾層也都是按照這個模式走的。

需要注意的就是第二層中,w 的輸入通道數為上一層最後的輸出通道數,也就是 hpool1 的輸出,這裡直接算出了是 32,因為上面已經定義好了啊。

如果不確定的時候,可以通過這種方式來確定輸入通道數:

in = h_pool1.get_shape()[-1].value
複製程式碼

[-1] 表示不管 h_pool1 的形狀如何,都取它最後一維的大小。

構建全連結層

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
複製程式碼

全連線層則起到將學到的“分散式特徵表示”對映到樣本標記空間的作用

這句話看起來很具體,然而並不知道它在說些什麼。

那麼,我們試著理解一下,先看看全連結層的結構。

定義 w 和 b

W_fc1 = weight_variable([7 * 7 * 64, 1024])
複製程式碼

上面這行程式碼,將fc1 的 w 形狀設定為 [7 * 7 * 64, 1024]。第一個維度大小為 7 * 7 * 64,因為經過前面一層的卷積層的池化層後,輸出的就是一個 7 * 7 * 64 的張量,所以這裡的輸入就是上一層的輸出。嗯,這一點在整個神經網路裡都是這樣。

第二個維度大小為 1024。這個就有點詭異了!為什麼是這個數值?

實際上,這是我們可以自己隨便設定的,它表示該全連結層的神經元個數,數量越多計算耗時越長,但是數量過少,對前面提取出來的特徵的分類效果又不夠好。

因此,我們的全連結層擁有的特徵數量就是 7 x 7 x 64 x 1024。數量還是比較驚人的。

b_fc1 = bias_variable([1024])
複製程式碼

同樣,b 的數量需要對應於 w 的最後一個維度,也就是一個神經元對應一個偏置量b。

變形輸入

h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
複製程式碼

為了能和上面定義的全連結層特徵張量相乘,需要把輸入的張量變形。其實對於每一個 7 x 7 x64 的輸入張量而言,就是將它們展平成一個一維的向量。

第一個維度取 -1 同上面提到過的意思一樣,最後確定這個維度。實際上就是最後一個池化層輸出的數量。

構建線性函式,加上 ReLu 函式

h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
複製程式碼

上面的程式碼還是按照 wx + b 的線性公式構建函式,然後加上 ReLu 函式增加非線性變化。

這一波計算,清晰的表達出了,當一個來自最後一層池化層的 [7 x 7 x 64] 的輸出經過全連結層後,就被平鋪成了一個 [1 x 1024] 的向量。相當於把前面分散的特徵全部連結在了一起,這也就是為什麼說前面的卷積核是從區域性觀察,而全連結層是從全域性的視野去觀察的,因為它這一層整合所有前面的特徵。整合了所有的特徵,我們就可以進行後續的分類操作了。

至此,相信你對全連結層有了一個大概的瞭解。從中可以看出一些貓膩來。

  • 全連結層增加模型的複雜度,因為增加了很多神經元來擴充特徵集。也因此,它有助於提升模型的準確率。

  • 但隨著特徵數量的爆炸式增加,訓練速度必然會變慢。而且如果全連結層設定的神經元數量過多,會出現過擬合的現象。所以,需要適當的設定,不能一味的貪多。

加入 Dropout

self.keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)
複製程式碼

在全連結層之後,往往會跟著 Dropout 操作。由於在神經網路中,神經元的個數非常爆炸,往往會產生過擬合的問題,特別是引入了全連結層這種操作之後。所以我們需要做些什麼來讓過擬合發生的概率減小一些。

Dropout 就是一種很流行的方案。

h_fc1_drop = tf.nn.dropout(h_fc1, self.keep_prob)
複製程式碼

這行程式碼的第二個引數,我們可以動態的傳入一個數值,表示每個神經元有多大的概率失效,其實就是不參與計算。

形象點描述就是這樣一個過程。每個神經元進行運算前,都按照設定的 keep_prob 概率決定它要不要參與計算。比如 keep_prob=0.5 的話,表示每個神經元有 50% 的概率失效。

不難看出,Dropout 操作能夠一定程度上加快訓練速度,同時降低過擬合的可能性。

構建輸出層

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

self.y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
複製程式碼

最後一層輸出層,我們就可以得到一個結果了。

輸出層的函式結構仍然是 wx+b 的線性結構。

這裡有必要解釋一下輸出的 w 的形狀。第一個維度不用說,就是上一層輸出的結果,這裡上一層是從全連結層輸出出來 1024 個神經元。第二個維度是我們分類的總類目數,由於識別的是 0-9 的手寫數字,所以總共有 10 個類別。

構建好線性函式後,在加入一個非線性的啟用函式。在分類場景中, Softmax 是一個在輸出層被廣泛使用的啟用函式。

這是 Softmax 的公式,很容易看出,它的值域為 [0, 1]。這就比較厲害,直接就轉成一個個的概率了。就是說,每個輸出對應類別的概率是多少。

再這個看看形象的示意圖理解一下。

這,就是網路!

image_graph

最後,這個網路就成型了。這其實是一個很簡單的網路,總共 4 層,包含兩個卷積層,一個全連結層和一個輸出層。

從圖中可以清晰的看到資料的流向。

就要開始訓練了

# coding=utf-8

import time
from input_data import *
from cnn_utils import *
from cnn_model import CnnMnistNetwork

train_times = 35000
base_path = "../mnist/"
save_path = base_path + str(train_times) + "/"

# 讀取資料
mnist = read_data_sets("MNIST_data/", one_hot=True)

# 建立網路
network = CnnMnistNetwork()
x_data = network.x_data
y_data = network.y_data
y_conv = network.y_conv
keep_prob = network.keep_prob
# ------------------------構建損失函式---------------------
with tf.name_scope("cross_entropy"):
    # 建立正則化物件,此處使用的是 L2 範數
    regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 50000))
    # 應用正則化到引數集上
    reg_term = tf.contrib.layers.apply_regularization(regularization)
    # 在損失函式中加入正則化項
    cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
tf.scalar_summary('loss', cross_entropy)
with tf.name_scope("train_step"):
    # 使用 Adam 進行損失函式的梯度下降求解
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# ------------------------構建模型評估函式---------------------
with tf.name_scope("accuracy"):
    with tf.name_scope("correct_prediction"):
        # 對比預測結果和標籤
        correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_data, 1))
    with tf.name_scope("accuracy"):
        # 計算準確率
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
tf.scalar_summary('accuracy', accuracy)

# 建立會話
sess = tf.InteractiveSession()

# 合併 summary
summary_merged = tf.merge_all_summaries()
train_writer = tf.train.SummaryWriter(save_path + "graph/train", sess.graph)
test_writer = tf.train.SummaryWriter(save_path + "graph/test")

start_time = int(round(time.time() * 1000))

# 初始化引數
sess.run(tf.initialize_all_variables())

for i in range(train_times):
    # 從訓練集中取出 50 個樣本進行一波訓練
    batch = mnist.train.next_batch(50)
    if i % 100 == 0:
        summary, train_accuracy = sess.run([summary_merged, accuracy],
                                           feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 1.0})
        test_writer.add_summary(summary, i)
        consume_time = int(round(time.time() * 1000)) - start_time
        print("當前共訓練 " + str(i) + "次, 累計耗時:" + str(consume_time) + "ms,實時準確率為:%g" % (train_accuracy))
    # 記錄訓練時資料,每訓練1000次儲存一次訓練資訊
    if i % 1000 == 0:
        run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
        run_metadata = tf.RunMetadata()
        # 訓練一次,dropout 的引數設定為 0.5
        summary, _ = sess.run([summary_merged, train_step],
                              feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5}, options=run_options,
                              run_metadata=run_metadata)
        train_writer.add_run_metadata(run_metadata, str(i))
        train_writer.add_summary(summary, i)
    else:
        summary, _ = sess.run([summary_merged, train_step],
                              feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5})
        train_writer.add_summary(summary, i)
    # 每訓練 2000 次儲存一次模型
    if i != 0 and i % 2000 == 0:
        test_accuracy = int(
            accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
        save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)

# 在測試集計算準確率
summary, test_accuracy = sess.run([summary_merged, accuracy],
                                  feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0})
train_writer.add_summary(summary)
print("測試集準確率:%g" % (test_accuracy))

print("訓練完成!")
train_writer.close()
test_writer.close()
# 儲存模型
save_model(save_path, sess, train_times)

複製程式碼

先放一波完整程式碼,後面再挑著重點說一說。

其中涉及到一些深度學中的基本概念,本篇篇幅已經夠長了,CoorChice 就不在這裡過多解釋了。如果還不清楚,可以先跳到以下這篇文章,花個幾分鐘瞭解了基本概念後再繼續往下。

《機器學習,看完就明白了》傳送門

構建損失函式

# 建立正則化物件,此處使用的是 L2 範數
regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 50000))
# 應用正則化到引數集上
reg_term = tf.contrib.layers.apply_regularization(regularization)
# 在損失函式中加入正則化項
cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
tf.scalar_summary('loss', cross_entropy)
with tf.name_scope("train_step"):
# 使用 Adam 進行損失函式的梯度下降求解
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
複製程式碼

實際上,構建損失函式的關鍵程式碼就兩行:

cross_entropy = (-tf.reduce_sum(y_data * tf.log(y_conv)) + reg_term)
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
複製程式碼

第一行構建出一個 交叉熵 損失函式,第二行對損失函式做梯度下降獲得 tensor。

這裡使用了 Adam優化演算法 能夠為不同的引數動態的計算不同的自適應學習率,這與 SGD 恆定不變的學習率有區別。這種優化演算法使得各個引數的變化比較平穩,計算消耗的記憶體更小,收斂也會更快一點。

但,網上看到有說它的訓練效果不如 SGD 的 ???

本來,損失函式的構建到此就結束了,反手就可以開始訓練了。但是 CoorChice 在訓練過程中發現,每次當訓練進行到 2w 多次的時候就會出現梯度爆炸的現象。突然的 loss 就都變為 NaN,accuracy 本來好好的 0.99 呢,驟降的趨近於 0 !

於是 CoorChice 二話不說,開啟 Google 就是一通搜尋,網上說的各種各樣的原因都有。

這種問題也不太好確定具體是因為哪一個原因導致的,於是就加個正則化試試。結果就好了!

來看看正則化是怎麼加。

前面在構建 w 函式里加了一行程式碼:

tf.add_to_collection(tf.GraphKeys.WEIGHTS, var)
複製程式碼

目的就是為了把每個 w 放到集合中,以便此時進行正則化使用。

此處,CoorChice 選擇使用高階一點的 L2正規化,加入正則化後的 Loss 公式如下:

c0就是原本的損失函式部分,這裡就是 交叉熵,這部分又被稱作是 經驗風險。後面的一部分就是我們的 L2正則化式了,它實際就是把每個權重平方後求和,然後除以 w 的數量,在乘以個重要度係數。正則化的部分又叫作 結構風險,因為它是基於 w 計算出的一個數值,加在 交叉熵 上,從而每次增大交叉熵的值,也就是增大梯度,達到懲罰loss的效果。它一定程度上削弱了網路中特徵值的作用,從而使模型的泛化性提高,也就能進一步的避免過擬合發生的可能。

再回過頭看看上面的程式碼,就理解正則化是如何加入到網路中的。

構建評估模型

# 對比預測結果和標籤
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_data, 1))
# 計算準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
複製程式碼

這兩行程式碼,構建了一個用於評估模型準確率的 accuracy tensor。第一行實際就是比較了一下預測值和真實值,結果是一個 bool 向量,在第二行中轉化為浮點數,求個平均就是準確率了。

開始訓練啦!

for i in range(train_times):
    # 從訓練集中取出 50 個樣本進行一波訓練
    batch = mnist.train.next_batch(50)
    summary, _ = sess.run([summary_merged, train_step],
                              feed_dict={x_data: batch[0], y_data: batch[1], keep_prob: 0.5})
    # 每訓練 2000 次儲存一次模型
    if i != 0 and i % 2000 == 0:
        test_accuracy = int(
            accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
        save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)
複製程式碼

首先從訓練集中隨機的取出 50 個樣本作為一次訓練的輸入。為什麼要這麼幹呢?因為訓練集有赤裸裸的 60000 個樣本啊!訓練一次太耗時了,特別是在平時開發用的筆記本上。

這麼做理論上準確率沒有全集訓練高,但是也能達到 99% 的準確率,卻能節省巨量的時間,這點理論上的準確率還是可以適當的捨棄的。

接著就呼叫 sess.run(train_step) 開始一次訓練了,注意此處由於加了 dropout,所以每次 feed_dict 中需要設定它的值。

if i != 0 and i % 2000 == 0:
        test_accuracy = int(
            accuracy.eval(feed_dict={x_data: mnist.test.images, y_data: mnist.test.labels, keep_prob: 1.0}) * 100)
        save_model(base_path + str(i) + "_" + str(test_accuracy) + "%/", sess, i)
複製程式碼

CoorChice 每訓練兩千次,在測試集上測試一下,然後儲存一下模型。這是良好的習慣。因為一旦訓練起來,很多不可控的因素,多儲存些模型,後面還可以挑最合的。

上面這個過程 CoorChice 迴圈了整整 35000 次!電腦全速運轉了一晚上才訓練完成。

上圖就是一堆存檔的模型資料夾,可以看到,一開始其實準確率其實也不低,97%。隨著訓練次數的增加,準確率就穩定在了 99% 了。

使用模型進行識別

現在,模型已經訓練好了,接下來就可以使用這個模型識別我們自己手寫的數字了。

# coding=utf-8

import numpy as np
from PIL import Image
import os
from cnn_model import CnnMnistNetwork
import tensorflow.python as tf

train_times = 20000
num = 5
image_path = "num_images_test/num"
CKPT_DIR = "../mnist/" + str(train_times) + "_99%"
# 將數字圖片縮放為標準的 28*28,接著進行灰度處理
img = Image.open(image_path + str(num) +".png").resize((28, 28), Image.ANTIALIAS).convert("L")
# os.system("open " + image_path + str(num) + ".png")
flatten_img = np.reshape(img, 784)
arr = np.array([1 - flatten_img])
print(arr)

# 建立模型對應的網路
network = CnnMnistNetwork()
x_data = network.x_data
y = network.y_conv
keep_prob = network.keep_prob
# 建立會話
sess = tf.InteractiveSession()
# 初始化引數
sess.run(tf.initialize_all_variables())
saver = tf.train.Saver()
ckpt = tf.train.get_checkpoint_state(CKPT_DIR)
if ckpt and ckpt.model_checkpoint_path:
    # 讀取恢復模型
    saver.restore(sess, ckpt.model_checkpoint_path)
    # 載入資料,進行識別
    y = sess.run(y, feed_dict={x_data: arr, keep_prob:1.0})
    # 取最大可能
    result = str(np.argmax(y, 1))
    print("\n期望結果" + str(num) + ", 預測結果:" + result)
    os.system("open num_images_test/num" + result[1] + ".png")
else:
    print("沒有模型")
複製程式碼

使用模型比較簡單,就是讀取一張圖片,然後建立出模型所對應的網路結構來,接著讀取模型,輸入圖片,就能得到識別結果了。

閒扯兩句

MNIST 資料的訓練相當於是機器學習的 HelloWorld 程式,我們構建了一個 4 層的簡單的網路進行訓練識別,最後得到的模型準確率也是不錯的。

完整的體驗瞭如何從 0 開始構建一個神經網路,然後儲存模型,再讀取模型進行識別。總的來說,這個過程思路還是比較簡單的,關鍵就在於一些引數設定,還有出現問題如何去解決。比如,CoorChice 在訓練過程中就碰到了 NaN 的問題。機器學習還是比較依靠經驗的一門技術,需要在不斷的實戰中總結出一套自己的分析、解決問題的套路來。

  • 抽出空餘時間寫文章分享需要信仰,還請各位看官動動小手點個贊,快給 CoorChice 充值信仰吧 ?
  • CoorChice 會不定期的分享乾貨,想要上車只需進到 CoorChice的【個人主頁】 點個關注就好了哦。





相關文章