1. 程式人生 > >tensorflow學習筆記——使用TensorFlow操作MNIST資料(2)

tensorflow學習筆記——使用TensorFlow操作MNIST資料(2)

               tensorflow學習筆記——使用TensorFlow操作MNIST資料(1)

一:神經網路知識點整理

1.1,多層:使用多層權重,例如多層全連線方式

  以下定義了三個隱藏層的全連線方式的神經網路樣例程式碼:

import tensorflow as tf

l1 = tf.matmul(x, w1)
l2 = tf.matmul(l1, w2)
y = tf.matmul(l2,w3)

  

1.2,啟用層:引入啟用函式,讓每一層去線性化

  啟用函式有多種,例如常用的 tf.nn.relu  tf.nn.tanh  tf.nn.sigoid  tf.nn.elu,下面顯示了幾種常用的非線性啟用函式的函式影象:

  樣例程式碼:

import tensorflow as tf

a = tf.nn.relu(tf.matmul(x, w1) + biase1)
y = tf.nn.relu(tf.matmul(a, w2) + biase2)

  

1.3,損失函式

  經典損失函式,交叉熵(cross entropy)用於計算預測結果矩陣 Y 和實際結果矩陣 Y_ 之間的距離樣例程式碼:

import tensorflow as tf

cross_entropy = - tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))

  

import tensorflow as tf

v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
tf.reduce_mean(tf.clip_by_value(v, 0.0, 10.0))

  對於分類問題,通常把交叉熵與softmax迴歸一起使用

import tensorflow as tf

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(y, y_)

  對於迴歸問題,通常使用mse(均方誤差函式)計算損失函式

import tensorflow as tf

mse_loss = tf.reduce_mean(tf.square(y_ - y))

# 與以下函式計算結果完全一致
dataset_size = 1000
mse_loss = tf.reduce_sum(tf.pow(y_ - y, 2)) / dataset_size

  自定義條件化的損失函式

import tensorflow as tf

loss_less = 10
loss_more = 1
loss = tf.reduce_sum(tf.where(tf.greater(y, y_), (y - y_) * loss_more, (y_ - y) * loss_less))
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

  

1.4,神經網路優化演算法,訓練優化器

  一般優化器的目標是優化權重W和偏差biases,最小化損失函式的結果,以下優化器會不斷優化W和 biases。

  Adam優化演算法是一個全域性最優點的優化演算法,引入了二次方梯度校正,相比於基礎SGD演算法,不容易陷入區域性優點,而且速度更快。

  本質上是帶有動量項的RMSprop,它利用梯度的一階矩估計和二階矩估計動態調整每個引數的學習率。Adam的優點主要在於經過偏置校正後,每一次迭代學習率都有個確定範圍,使得引數比較平穩。

 

import tensorflow as tf

LEARNING_RATE = 0.001
mse_loss = tf.reduce_mean(tf.square(y_ - y))
train_op = tf.train.AdamOptimizer(LEARNING_RATE).minimize(mse_loss)

  

1.5,優化學習率LEARNING_RATE

  在訓練神經網路時,需要設定學習率(learning rate)控制引數更新的速度,學習率決定了引數每次更新的幅度,學習率設定過大可能導致無法收斂,在極優值的兩側來回移動。學習率設定過小雖然能保證收斂性,但可能導致收斂過慢。

  為了解決學習率的問題,Tensorflow 提供了一種更加靈活的學習率設定方法——指數衰減法。tf.train.exponential_decay 函式實現了指數衰減學習率。通過這個函式,可以先使用較大的學習率來快速得到一個比較優的解,然後隨著迭代的繼續逐步減少學習率,使得模型在訓練後期更加穩定。exponential_decay 函式會指數級地減少學習率,它實現了一下程式碼的功能:

decayed_learning_rate = learning_rate * decay_rate ^(global_step / decay_steps)

  其中 decayed_learning_rate 為每一輪優化時使用的學習率,learning_rate 為事先設定的初始學習率,decay_rate 為衰減係數,decay_steps 為衰減速度。tf.train.exponential_decay 函式可以通過設定引數 staircase 選擇不同的衰減方式。

import tensorflow as tf

global_step = tf.Variable(0)
learning_rate = tf.train.exponential_decay(
    learning_rate=0.1, 
    global_step=global_step, 
    decay_steps=100, 
    decay_rate=0.96, 
    staircase=True, 
    name=None
)
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss, global_step=global_step)

  

1.6,過擬合問題(正則化)

  避免訓練出來的模型過分複雜,即模型記住了所有資料(包括噪聲引起的誤差),因此需要引入正則化函式疊加的方式,避免模型出現過擬合

import tensorflow as tf

v_lambda = 0.001
w = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w)
mse_loss = tf.reduce_mean(tf.square(y_ - y) + tf.contrib.layers.l2_regularizer(v_lambda)(w))

  

1.7,滑動平均模型

  用於控制模型的變化速度,可以控制權重W以及偏差 biases 例如 :avg_class.average(w)  avg_avergae(biases)

import tensorflow as tf

v1 = tf.Variable(0, dtype=tf.float32)
step = tf.Variable(0, trainable=False)
ema = tf.train.ExponentialMovingAverage(decay=0.99, num_updates=step)
# 每一次操作的時候,列表變數[v1]都會被更新
maintain_averages_op = ema.apply([v1]) 

with tf.Session() as sess:
    
    # 初始化
    init_op = tf.global_variables_initializer()
    sess.run(init_op)
    print(sess.run([v1, ema.average(v1)]))
    
    # 更新step和v1的取值
    sess.run(tf.assign(step, 10000))  
    sess.run(tf.assign(v1, 10))
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))

  

  

二:gfile檔案操作詳解

2.1,gfile模組是什麼?

  gfile模組定義在tensorflow/python/platform/gfile.py,但其原始碼實現主要位於tensorflow/tensorflow/python/lib/io/file_io.py,那麼gfile模組主要功能是什麼呢?

  google上的定義為:

 

翻譯過來為:

    沒有執行緒鎖的檔案I / O操作包裝器

tf.gfile模組的主要角色是:

  • 1.提供一個接近Python檔案物件的API
  • 2.提供基於TensorFlow C ++ FileSystem API的實現。 

  C ++ FileSystem API支援多種檔案系統實現,包括本地檔案,谷歌雲端儲存(以gs://開頭)和HDFS(以hdfs:/開頭)。 TensorFlow將它們匯出為tf.gfile,以便我們可以使用這些實現來儲存和載入檢查點,編寫TensorBoard log以及訪問訓練資料(以及其他用途)。但是,如果所有檔案都是本地檔案,則可以使用常規的Python檔案API而不會造成任何問題。

  以上為google對tf.gfile的說明。

2.2、gfile API介紹

  下面將分別介紹每一個gfile API!

2-1)tf.gfile.Copy(oldpath, newpath, overwrite=False)

  拷貝原始檔並建立目標檔案,無返回,其形參說明如下:

  • oldpath:帶路徑名字的拷貝原始檔;
  • newpath:帶路徑名字的拷貝目標檔案;
  • overwrite:目標檔案已經存在時是否要覆蓋,預設為false,如果目標檔案已經存在則會報錯

2-2)tf.gfile.MkDir(dirname)

  建立一個目錄,dirname為目錄名字,無返回。

2-3)tf.gfile.Remove(filename)

  刪除檔案,filename即檔名,無返回。

2-4)tf.gfile.DeleteRecursively(dirname)

  遞迴刪除所有目錄及其檔案,dirname即目錄名,無返回。

2-5)tf.gfile.Exists(filename)

  判斷目錄或檔案是否存在,filename可為目錄路徑或帶檔名的路徑,有該目錄則返回True,否則False。

2-6)tf.gfile.Glob(filename)

  查詢匹配pattern的檔案並以列表的形式返回,filename可以是一個具體的檔名,也可以是包含萬用字元的正則表示式。

2-7)tf.gfile.IsDirectory(dirname)

  判斷所給目錄是否存在,如果存在則返回True,否則返回False,dirname是目錄名。

2-8)tf.gfile.ListDirectory(dirname)

  羅列dirname目錄下的所有檔案並以列表形式返回,dirname必須是目錄名。

2-9)tf.gfile.MakeDirs(dirname)

  以遞迴方式建立父目錄及其子目錄,如果目錄已存在且是可覆蓋則會建立成功,否則報錯,無返回。

2-10)tf.gfile.Rename(oldname, newname, overwrite=False)

  重新命名或移動一個檔案或目錄,無返回,其形參說明如下:

  • oldname:舊目錄或舊檔案;
  • newname:新目錄或新檔案;
  • overwrite:預設為false,如果新目錄或新檔案已經存在則會報錯,否則重新命名或移動成功。

2-11)tf.gfile.Stat(filename)

  返回目錄的統計資料,該函式會返回FileStatistics資料結構,以dir(tf.gfile.Stat(filename))獲取返回資料的屬性如下:


2-12)tf.gfile.Walk(top, in_order=True)

  遞迴獲取目錄資訊生成器,top是目錄名,in_order預設為True指示順序遍歷目錄,否則將無序遍歷,每次生成返回如下格式資訊(dirname, [subdirname, subdirname, ...], [filename, filename, ...])。

2-13)tf.gfile.GFile(filename, mode)

  獲取文字操作控制代碼,類似於python提供的文字操作open()函式,filename是要開啟的檔名,mode是以何種方式去讀寫,將會返回一個文字操作控制代碼。

tf.gfile.Open()是該介面的同名,可任意使用其中一個!

2-14)tf.gfile.FastGFile(filename, mode)

  該函式與tf.gfile.GFile的差別僅僅在於“無阻塞”,即該函式會無阻賽以較快的方式獲取文字操作控制代碼

 

三,將MNIST資料集轉換為TFRecord檔案

  TFRecord 檔案中的資料都是通過 tf.train.Example Protocol Buffer 的格式儲存的。以下為 tf.train.Example 的資料結構:

message Example {
	Features features = 1;
};
message Features{
	map<string, Feature> feature = 1;
};
message Feature {
	oneof kind {
		BytesList bytes_list = 1;
		FloatList float_list = 1;
		Int64List int64_list = 3;
	}
};

  tf.train.Example 中包含了一個從屬性名稱到取值的字典。其中屬性名稱為一個字串,屬性的取值可以為字串(BytesList),實數列表(FloatList)或者整數列表(Int64List)。

3.1,將MNIST資料集中的所有檔案儲存到一個TFRecord檔案

# _*_coding:utf-8_*_
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import os
import numpy as np

# 生成整數的屬性
def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))


# 生成字串型的屬性
def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))


def create_records():
    '''
    實現將 MNIST 資料集 轉化為records
    注意:讀取的影象資料預設是uint8,然後轉化為tf 的字串型BytesList 儲存,
    :return:
    '''
    # 匯入MNIST資料集
    mnist = input_data.read_data_sets('data/', dtype=tf.uint8, one_hot=True)
    images = mnist.train.images
    labels = mnist.train.labels
    # 訓練影象的解析度,作為example的屬性
    pixels = images.shape[1]
    num_examples = mnist.train.num_examples


    # 儲存TFRecord檔案的地址
    filename = 'record/output_mnist.tfrecords'

    # 建立一個writer來寫TFRecord 檔案
    writer = tf.python_io.TFRecordWriter(filename)

    # 將每張圖片都轉化為一個Example
    for i in range(num_examples):
        # 將影象轉為字串
        image_raw = images[i].tostring()

        # 將一個樣例轉化為Example  Protocol Buffer 並將所有資訊寫入這個資料結構
        example = tf.train.Example(features=tf.train.Features(
            feature={
                'pixels': _int64_feature(pixels),
                'label': _int64_feature(np.argmax(labels[i])),
                'image_raw': _bytes_feature(image_raw)
            }
        ))

        # 將Example寫入TFRecord 檔案
        writer.write(example.SerializeToString())

    print("data processing success")
    writer.close()

if __name__ == '__main__':
    dir_name = 'record'
    if not os.path.exists(dir_name):
        os.mkdir(dir_name)
    create_records()

  

3.2,讀取封裝好的MNIST資料的TFRecord檔案

#_*_coding:utf-8_*_
import tensorflow as tf

def read_tfrecord():
    """
    讀取tfrecord檔案
    :return:
    """
    filename = 'record/output.tfrecords'
    # 建立一個佇列來維護輸入檔案列表
    filename_queue = tf.train.string_input_producer([filename])

    # 建立一個reader來讀取TFRecord檔案中Example
    reader = tf.TFRecordReader()

    # 從檔案中讀取一個Example
    _, serialized_example = reader.read(filename_queue)

    # 用FixedLenFeature 將讀入的Example解析成 tensor
    features = tf.parse_single_example(
        serialized_example,
        features={
            'image_raw': tf.FixedLenFeature([], tf.string),
            'pixels': tf.FixedLenFeature([], tf.int64),
            'label': tf.FixedLenFeature([], tf.int64)
        }
    )

    # tf.decode_raw 將字串解析成影象對應的畫素陣列
    images = tf.decode_raw(features['image_raw'], tf.uint8)
    labels = tf.cast(features['label'], tf.int32)
    pixels = tf.cast(features['pixels'], tf.int32)

    init_op = tf.global_variables_initializer()

    with tf.Session() as sess :
        sess.run(init_op)
        # 啟動多執行緒處理輸入資料
        coord = tf.train.Coordinator()
        threads = tf.train.start_queue_runners(sess=sess, coord=coord)

        # 每次執行讀出一個Example,當所有樣例讀取完之後,在此樣例中程式會重頭讀取
        for i in range(10):
            # 在會話中取出image 和 label
            image, label = sess.run([images, labels])
            print(label)
        coord.request_stop()
        coord.join(threads)

if __name__ == '__main__':
    read_tfrecord()

  

四,TensorFlow訓練神經網路

  在神經網路的結構上,深度學習一方面需要使用啟用函式實現神經網路模型的去線性化,另一方面需要一個或多個隱藏層使得神經網路的結構更深,以解決複雜問題,在訓練神經網路時,我們學習了使用帶指數衰減的學習率設定,使用正則化來避免過度擬合,以及使用滑動平均模型來使得最終模型更加健壯。以下程式碼給出了一個在MNIST資料集上實現這些功能的完整的TensorFlow程式。

#_*_coding:utf-8_*_
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# MNIST 資料集相關的常數
# 輸入層的節點數,對於MNIST資料集,這個就等於圖片的畫素
INPUT_NODE = 784
# 輸出層的節點數,這個等於類別的數目,因為在MNIST資料集中需要區分的
# 是0-9這個10個數字,所以這裡輸出層的節點數為10
OUTPUT_NODE = 10

# 配置神經網路的引數
# 隱藏層節點數,這裡使用只有一個隱藏層的網路結構作為樣例
# 這個隱藏層有500個節點
LAYER1_NODE = 500
# 一個訓練batch中的訓練資料個數,數字越小時,訓練過程越接近隨機梯度下降
# 數字越大時,訓練越接近梯度下降
BATCH_SIZE = 100

# 基礎的學習率
LEARNING_RATE_BASE = 0.8
# 學習率的衰減率
LEARNING_RATE_DECAT = 0.99
# 描述模型複雜度的正則化項在損失函式中的係數
REGULARIZATION_RATE = 0.0001
# 訓練輪數
TRAINING_STEPS = 30000
# 滑動平均衰減率
MOVING_AVERAGE_DECAY = 0.99

# 一個輔助函式,給定神經網路的輸入和所有引數,計算神經網路的前向傳播結果
# 在這裡定義了一個使用ReLU啟用函式的三層全連線神經網路
# 通過加入隱藏層實現了多層網路結構,通過ReLU啟用函式實現了去線性化
# 在這個函式中也支援傳入用於計算引數平均值的類,這樣方便在測試時使用滑動平均模型
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    # 當沒有提供滑動平均類時,直接使用引數當前的取值
    if avg_class == None:
        # 計算隱藏層的前向傳播結果,這裡使用了ReLU啟用函式
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)

        # 計算輸出層的前向傳播結果,因為在計算損失函式時會一併計算softmax函式
        # 所以這裡不需要加入啟用函式,而且不加入softmax不會影響預測結果
        # 因為預測時使用的是不同型別對應節點輸出值的相對大小,
        # 有沒有softmax層對最後分類的結果沒有影響
        # 於是在計算整個神經網路的前向傳播時可以不加入最後的softmax層
        return tf.matmul(layer1, weights2) + biases2
    else:
        # 首先使用avg_class.average 函式來計算得出變數的滑動平均值
        # 然後再計算相應的神經網路前向傳播結果
        layer1 = tf.nn.relu(
            tf.matmul(input_tensor, avg_class.average(weights1)) +
            avg_class.average(biases1)
        )
        return tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)

# 訓練模型的過程
def train(mnist):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')

    # 生成隱藏層的引數
    weights1 = tf.Variable(
        tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
    biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))

    # 生成輸出層的引數
    weights2 = tf.Variable(
        tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
    biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))

    # 計算在當前引數下神經網路前向傳播的結果。這裡給出的用於計算滑動平均的類為None
    # 所以函式不會使用引數的滑動平均值
    y = inference(x, None, weights1, biases1, weights2, biases2)

    # 定義儲存訓練輪數的變數,這個變數不需要計算滑動平均值
    # 這裡指定這個變數為不可訓練的變數(trainable=False)
    # 在使用TensorFlow訓練神經網路時,一般會將代表訓練輪數的變數指定為不可訓練的引數
    global_step = tf.Variable(0, trainable=False)

    # 給定滑動平均衰減率和訓練輪數的變數,初始化滑動平均類。
    # 當給定訓練輪數的變數可以加快訓練早期變數的更新速度
    varibale_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )

    # 在所有代表神經網路引數的變數上使用滑動平均。
    # 其他輔助變數(比如global_step)就不需要了
    # tf.trainable_variables 返回的就是圖上集合GraphKeys.TRAINABLE_VARIABLES中的元素
    # 這個集合的元素就是所有沒有指定trainable=False 的引數
    varibale_averages_op = varibale_averages.apply(tf.trainable_variables())

    # 計算使用了滑動平均之後的前向傳播結果。因為滑動平均不會改變變數本身的額取值
    # 而是會委會一個影子變數來記錄其滑動平均值,所以當需要使用這個滑動平均值時,需要明確呼叫average函式
    average_y = inference(x, varibale_averages, weights1, biases1, weights2, biases2)

    # 計算交叉熵作為刻畫預測值和真實值之間差距的損失函式,這裡使用了TensorFlow中提供的
    # sparse_softmax_cross_entropy_with_logits函式來計算交叉熵
    # 當分類問題中只有一個正確答案時,可以使用這個函式來加速交叉熵的計算
    # MNIST問題的圖片中只包含了一個0-9中的一個數字,所以可以使用這個函式來計算交叉熵損失
    # 這個函式的第一個引數是神經網路不包含softmax層的前向傳播結果,第二個是訓練資料的正確答案
    #因為標準答案是一個長度為10的一位陣列,而該函式需要提供了一個正確的答案數字
    #所以需要使用 tf.argmax函式來得到正確答案對應的類別編號
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    # 計算在當前batch中所有樣例的交叉熵平均值
    cross_entropy_mean = tf.reduce_mean(cross_entropy)

    # 計算L2正則化損失函式
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    # 計算模型的正則化損失,一般只計算神經網路邊上權重的正則化損失,而不使用偏置項
    regularization = regularizer(weights1) + regularizer(weights2)
    # 總損失等於交叉熵損失和正則化損失的和
    loss = cross_entropy_mean + regularization
    # 設定指數衰減的學習率
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,  #基礎的學習率,隨著迭代的進行,更新變數時使用的
        # 學習率在這個基礎上遞減
        global_step,         # 學習率在這個基礎上遞減
        mnist.train.num_examples / BATCH_SIZE,  # 過完所有的訓練資料需要的迭代次數
        LEARNING_RATE_DECAT,   # 學習率衰減速度
        staircase=True
    )
    # 使用tf.train.GradientDescentOptimizer優化演算法來優化損失函式
    # 注意這裡損失函式包含了交叉熵損失和L2正則化損失
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

    # 在訓練神經網路模型時,沒過一遍資料即需要通過反向傳播來更新神經網路中的引數
    # 又要更新每一個引數的滑動平均值,為了一次完成多個操作,西面兩行程式和下面程式碼是等價的
    # train_op = tf.group(train_step, varibale_averages_op)
    with tf.control_dependencies([train_step, varibale_averages_op]):
        train_op = tf.no_op(name='train')

    # 檢驗使用了滑動平均模型的神經網路前向傳播結果是否正確。tf.argmax(average_y, 1)
    # 計算每一個樣例的預測答案,其中average_y 是一個 batch_size*10的二維陣列,
    # 每一行表示一個樣例的前向傳播結果。tf.argmax的第二個引數“1”表示選取最大值的的操作
    # 僅在第一個維度中進行,也即是說,只有每一行選取最大值對應的下標
    # 於是得到的結果是一個長度為batch的一維陣列,這個一維陣列中的值就表示了
    # 每一個樣例對應的數字識別結果
    # tf.rqual 判斷兩個張量的每一維是否相等,如果相等返回True,否則返回False。
    correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1))
    # 這個運算首選將一個布林型的數值轉換為實數型,然後計算平均值,這個平均值
    # 就是模型在這一組資料上的正確率
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    # 初始化會話並開始訓練過程
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        # 準備驗證資料,一般在神經網路的訓練過程中會通過驗證資料來判斷
        # 大致判斷停止的條件和評判訓練的效果
        validate_feed = {x: mnist.validation.images,
                         y_: mnist.validation.labels}
        #準備測試資料,在真實的應用中,這部分資料在訓練時是不可見的
        # 下面資料只是作為模型優劣的最後評價標準
        test_feed = {x: mnist.test.images, y_: mnist.test.labels}

        # 迭代地訓練神經網路
        for i in range(TRAINING_STEPS):
            # 每1000輪輸出一次在驗證資料集上的測試結果
            if i % 1000 == 0:
                # 計算滑動平均模型在驗證資料上的結果,因為MNIST資料集比較小,
                # 所以一次可以處理所有的驗證資料
                # 當神經網路模型比較複雜或者驗證資料比較大,太大的batch會導致計算時間過長
                # 甚至發生記憶體溢位的錯誤
                validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                print('After %d training step(s), validation accuracy using average model is %g'%(i, validate_acc))
            # 產生這一輪使用的一個batch的訓練資料,並執行訓練過程
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            sess.run(train_op, feed_dict={x:xs, y_:ys})
        # 在訓練結束後,在測試資料集上檢測神經網路模型的最終正確率
        test_acc = sess.run(accuracy, feed_dict=test_feed)
        print('After %d training step(s), test accuracy using average model is %g'%(TRAINING_STEPS, test_acc))

# 主程式入口
def main(argv=None):
    # 宣告處理MNIST資料集的類,這個類在初始化的時候會自動下載資料
    mnist = input_data.read_data_sets('data', one_hot=True)
    train(mnist)

# Tensorflow 提供的一個主程式入口,tf.app.run會呼叫上面定義的main函式
if __name__ == '__main__':
    # tf.app.run(main=None)
    main()

  執行上面的程式,得到的結果如下:

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
After 0 training step(s), validation accuracy using average model is 0.143
After 1000 training step(s), validation accuracy using average model is 0.9776
After 2000 training step(s), validation accuracy using average model is 0.9816
After 3000 training step(s), validation accuracy using average model is 0.9832
After 4000 training step(s), validation accuracy using average model is 0.9838
After 5000 training step(s), validation accuracy using average model is 0.9836
... ...
After 27000 training step(s), validation accuracy using average model is 0.9846
After 28000 training step(s), validation accuracy using average model is 0.9844
After 29000 training step(s), validation accuracy using average model is 0.9844
After 30000 training step(s), test accuracy using average model is 0.9842

  從上面的結果可以看出,在訓練初期,隨著訓練的進行,模型在驗證資料集上的表現越來越好。從第4000輪開始,模型在驗證資料集上的表現就開始波動,這說明模型已經接近極小值了。所以迭代也就結束了。

程式碼中一些錯誤的改正

  這些程式碼均來自與《TensorFlow實戰:Google深度學習框架》這本書,原因是TensorFlow比較高,相對書上的版本,一些API都變了。所以有些函式在書中的程式是錯誤的,所以程式在執行的時候就會報錯。

錯誤1如下:

ValueError: Only call `sparse_softmax_cross_entropy_with_logits` with named 
arguments (labels=..., logits=..., ...)

  解決:這個原因是函式的API發生了變化,我們需要新增labels 和 logits。

  修改程式碼如下:

原始碼:
# 計算交叉熵及其平均值
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(y,tf.argmax(y_, 1))

修改後的程式碼:
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y,logits=tf.argmax(y_, 1))


***********出現新的錯誤*************
ValueError: Rank mismatch: Rank of labels (received 2) should equal rank of
 logits minus 1 (received 1).

我們將程式碼修改如下:
labels=tf.argmax(y_, 1),logits=y

  後面的原因是因為計算交叉熵的時候,比較的兩個概率分佈放反了。因為交叉熵是衡量一個概率分佈區表達另外一個概率分佈的難度,值越低越好,所以是用預測的結果來表達正確的標籤。

   當執行完例子,會報如下錯誤:

Traceback (most recent call last):
  File "E:\PyCharm 2019.1.3\helpers\pydev\pydevconsole.py", line 221, in do_exit
    import java.lang.System
  File "E:\PyCharm 2019.1.3\helpers\pydev\_pydev_bundle\pydev_import_hook.py", line 21, in do_import
    module = self._system_import(name, *args, **kwargs)
ModuleNotFoundError: No module named 'java'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "E:\PyCharm 2019.1.3\helpers\pydev\_pydev_bundle\pydev_umd.py", line 197, in runfile
    pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
  File "E:\PyCharm 2019.1.3\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "E:/backup/pycode.py", line 180, in <module>
    tf.app.run(main=None)
  File "E:\Anaconda3\envs\python36\lib\site-packages\tensorflow\python\platform\app.py", line 48, in run
    _sys.exit(main(_sys.argv[:1] + flags_passthrough))
  File "E:\PyCharm 2019.1.3\helpers\pydev\pydevconsole.py", line 226, in do_exit
    os._exit(args[0])
TypeError: an integer is required (got type NoneType)

  小小分析一波,我們發現已經執行完程式碼了。執行完後會報錯誤提示:

TypeError: an integer is required (got type NoneType)

  經檢視,是TensorFlow版本問題,在r0.11之前的版本里,tf.app.run的程式碼如下:

def run(main=None):
   f = flags.FLAGS
   flags_passthrough = f._parse_flags()
   main = main or sys.modules['__main__'].main
   sys.exit(main(sys.argv[:1] + flags_passthrough))

  沒有argv 引數,argv引數是在 r0.12後加入的。所以需要升級版本。其實不升級版本也可以,我們這樣執行即可。

if __name__ == '__main__':
    # 我們這裡不使用TensorFlow提供的主程式入口 tf.app.run函式
    # tf.app.run(main=None)
    main()

  

五,使用驗證資料集判斷模型效果

  在上面程式中我們使用了神經網路解決MNIST問題,在程式的開始設定了初始學習率,學習率衰減率,隱藏層節點數量,迭代輪數等七種不同的引數。那麼如何設定這些引數的取值呢?在大部分情況下,配置神經網路的這些引數都是需要通過實驗來調整的。雖然一個神經網路模型的效果最終是通過測試資料來判斷的,但是我們不能直接通過模型在測試資料上的效果來選擇引數,使用測試資料來選取引數可能會導致神經網路模型過度擬合測試資料,從而失去對未知資料的預判能力。因為一個神經網路模型的最終目標是對未知資料提供判斷,所以為了評估模型在未知資料上的效果,需要保證測試資料在訓練過程中是不可見的。只有這樣才能保證通過測試資料評估出來的效果和真實應用場景下模型對未知資料預判的效果是接近的。於是,為了評測神經網路模型在不同引數取值下的效果,一般會從訓練資料中抽取一部分作為驗證資料。使用驗證資料就可以評判不同引數取值下模型的表現。除了使用驗證資料,還可以採用交叉驗證(cross validation)的方式來驗證模型效果。但因為神經網路訓練時間本身就比較長,採用 cross validation 會花費大量的時間,所以在海量資料的情況下,一般會更多的採用驗證資料集的形式來評測模型的效果。

  為了說明驗證資料在一定程度上可以作為模型效果的評判標準,我們將對比在不同迭代輪數的情況下,模型在驗證資料和測試資料上的正確率。為了同時得到同一個模型在驗證資料和測試資料上的正確率,可以在每1000輪的輸出中加入在測試資料集上的正確率。當我們再上門程式碼中加入下面程式碼,就可以得到每1000輪迭代後,使用了滑動平均的模型在驗證資料和測試資料上的正確率。

if i % 1000 == 0:
    validate_acc = sess.run(accuracy, feed_dict=validate_feed)
    # 計算滑動平均模型在測試資料和驗證資料上的正確率
    test_acc = sess.run(accuracy, feed_dict=test_feed)
    print('After %d training step(s), validation accuracy using average model is %g,'
          'test accuracy using average model is %g'%(i, validate_acc, test_acc))

  下圖給出了上面程式碼得到的每1000輪滑動平均模型在不同資料集上的正確率曲線,其中灰色曲線表示隨著迭代輪數的增加,模型在驗證資料上的正確率,黑色曲線表示在測試資料上的正確率。從圖中可以看出雖然兩條曲線不會完全重合,但是這兩條曲線的趨勢基本一樣,而且他們的相關關係(correlation coeddicient)大於0.9999。這意味著在MNIST問題上,完全可以通過模型在驗證資料上的表現來判斷一個模型的優劣。

  當然,以上結論是針對MNIST這個資料集的額,對其它問題,還需要具體問題具體分析。不同問題的資料分散式不一樣的。如果驗證資料分佈不能很好地代表測試資料分佈。那麼模型在這兩個資料集上的表現就有可能會不一樣。所以,驗證資料的選取方法是非常重要的,一般來說選取的驗證資料分佈越接近測試資料分佈,模型在驗證資料上的表現越可以體現模型在測試資料上的表現。通過上面的實驗,至少可以說明通過神經網路在驗證資料上的效果來選取模型的引數是一個可行的方案。

六,不同模型效果比較

  下面通過MNIST資料集來比較值錢提到的不同優化方法對神經網路模型正確率的影響。下面將使用神經網路模型在MNIST測試資料集上的正確率作為評價不同優化方法的標準。並且一個模型在MNIST測試資料集上行的正確率簡稱為“正確率”。之前提到了設計神經網路時的五種優化方法。在神經網路結構的設計上,需要使用啟用函式和多層隱藏層。在神經網路優化時,可以使用指數衰減的學習率,加入正則化的損失函式以及滑動平均模型,在下圖中給出了在相同神經網路引數下,使用不同優化方法啊,經過3000輪訓練迭代後,得到的最終模型的正確率。下圖的結果包含了使用所有優化方法訓練得到的模型和不用其中某一項優化方法訓練得到的模型。通過這種方式,可以有效驗證每一項優化方法的效果。

  從圖中可以很明顯的看出,調整神經網路的結構對最終的正確率有非常大的影響。沒有隱藏層或者啟用函式時,模型的正確率只有大約92.6%,這個數字要遠遠小於使用了隱藏層和啟用函式時可以達到的大約98.4%的正確率。這說明神經網路的結構對最終模型的效果有本質性的影響。

   從上圖的結果可以發現使用滑動平均模型,指數衰減的學習率和使用正則化帶來的正確率的提升並不是特別明顯。其中使用了所有優化演算法的模型和不使用滑動平均的模型以及不適用指數衰減的學習率的模型都可以達到大約98.4%的正確率。這是因為滑動平均模型和指數衰減的學習率在一定程度上都是限制神經網路中引數更新的速度,然而在MNIST資料上,因為模型收斂的速度很快,所以這兩種優化對最終模型的影響不大。從之前的結果可以看出,當模型迭代達到4000輪的時候正確率就已經接近最終的正確率了。而在迭代的早期,是否使用滑動平均模型或者指數衰減的學習率對訓練的結果影響相對較小,下圖顯示了不同迭代輪數時,使用了所有優化方法的模型的正確率與平均絕對梯度的變化趨勢。

  下圖顯示了不同迭代輪數時,正確率與衰減之後的學習率的變化趨勢:

  從圖中可以看出前4000輪迭代對模型的改變是最大的。在4000輪之後,因為梯度本身比較小,所以引數的改變也就是比較緩慢了。於是滑動平均模型或者指數衰減的學習率的作用也就沒有那麼突出了。從上圖可以看到,學習率曲線呈現出階梯狀衰減,在前4000輪時,衰減之後的學習率和最初的學習率差距並不大。那麼,這是否能說明這些優化方法作用不大呢?答案是否定的,當問題更加複雜時,迭代不會這麼快接近收斂,這時滑動平均模型和指數衰減的學習率可以發揮更大的作用。比如在Cifar-10 影象分類資料集上,使用滑動平均模型可以將錯誤率降低11%,而使用指數衰減的學習率可以將錯誤率降低7%。

  相對滑動平均模型和直屬衰減學習率,使用加入正則化的損失函式給模型效果帶來的提升要相對顯著。使用了正則化損失函式的神經網路模型可以降低大約6%的錯誤率(從1.69%降低到1.59%)。下圖給出了正則化給模型優化過程帶來的影響。並且下面兩個圖給出了不同損失函式的神經網路模型。一個模型只最小化交叉熵損失,另一個模型優化的是交叉熵和L2正則化的損失的和。下面先給出這個模型優化函式的宣告語句:

cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, 
                                                                               labels=tf.argmax(y_, 1))
# 計算在當前batch中所有樣例的交叉熵平均值
cross_entropy_mean = tf.reduce_mean(cross_entropy)

#*********************************************************
# 只最小化交叉熵損失
train_step = tf.train.GradientDescentOptimizer(learning_rate
                                               ).minimize(cross_entropy_mean,global_step=global_step)

#*********************************************************
# 優化交叉熵和L2正則化損失的和
loss = cross_entropy_mean + regluaraztion
train_step = tf.train.GradientDescentOptimizer(learning_rate
                                               ).minimize(loss, global_step=global_step)

  下圖灰色和黑色的實線給出了兩個模型正確率的變化趨勢,虛線給出了在當前訓練batch上的交叉熵損失:

  從圖中可以看出,只優化交叉熵的模型在訓練資料上的交叉熵損失(灰色虛線)要比優化總損失的模型更小(黑色虛線)。然而在測試資料上,優化總損失的模型(褐色實線)卻要比只優化交叉熵的模型(灰色實線)。這其中緣故,就是過擬合問題,只優化交叉熵的模型可以更好地擬合訓練資料(交叉熵損失更小),但是卻不能很好的挖掘資料中潛在的規律來判斷未知的測試資料,所以在測試資料上的正確率低。

   下圖顯示了不同模型的損失函式的變化趨勢,左側顯示了只優化交叉熵的模型損失函式的變化規律,可以看到隨著迭代的進行,正則化損失是在不斷加大的。因為MNIST問題相對比較簡單,迭代後期的梯度很小,所以正則化損失的增長也不快。如果問題更加複雜,迭代後期的梯度更大,就會發現總損失(交叉熵損失加上正則化損失)會呈現出一個U字形,在圖的右側,顯示了優化總損失的模型損失函式的變化規律,從圖中可以看出,這個模型的正則化損失部分也可以隨著迭代的進行越來越小,從而使得整體的損失呈現一個逐步遞減的趨勢。

 

  總的來說,通過MNIST資料集有效地驗證了啟用函式,隱藏層可以給模型的效果帶來質的飛躍。由於MNIST問題本身相對簡單,滑動平均模型,指數衰減的學習率和正則化損失對最終正確率的提升效果不明顯。但是通過進一步分析實驗的結果,可以得出這些優化方法確實可以解決神經網路優化過程中的問題。當需要解決的問題和使用到的神經網路模型更加複雜時,這些優化方法將更有可能對訓練效果產生更大的影響。

七,變數管理

   上面我們將計算神經網路前向傳播結果的過程抽象成了一個函式。通過這種方式在訓練和測試的過程中可以統一呼叫同一個函式來得模型的前向傳播結果。

  在上面程式碼中,這個函式定義如下:

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):

  在定義中可以看到,這個函式的引數中包含了神經網路中的所有引數。然而,當神經網路的結構更加複雜,引數更多時,就需要一個更好的方式來傳遞和管理神經網路中的引數了。TensorFlow提供了通過變數名稱來建立或者獲取一個變數的機制。通過這個機制,在不同的函式中可以直接通過變數的名字來使用變數,而不需要將變數通過 tf.get_variable 和 tf.get.variable_scope 函式實現的。下面將分別介紹如何使用這兩個函式。

  前面學習了通過tf.Variable 函式來建立一個變數。除了tf.Variable函式,TensorFlow還提供了tf.get_variable函式來建立或者獲取變數。當 tf.get_variable用於建立變數時,它和tf.Variable的功能是基本等價的。以下程式碼給出了通過兩個函式建立同一個變數的樣例:

# 下面兩個定義是等價的
v = tf.get_variable('v', shape=[1],
                    initializer=tf.constant_initializer(1.0))

v = tf.Variable(tf.constant(1.0, shape=[1]), name='v')

  從上面的程式碼可以看出,通過tf.get_variable 和 tf.get.variable_scope 函式建立變數的過程基本是一致的。tf.get_variable 函式呼叫時提供的維度(shape)資訊以及初始化方法(initializer)的引數和tf.Variable 函式呼叫時提供的初始化過程中的引數也類似。TensorFlow中提供的initializer 函式和之前提到的隨機數以及常量生成函式大部分是一一對應的。比如,在上面的樣例程式中使用到的常數初始化函式 tf.constant_initializer 和常數生成函式 tf.constant 功能上就是一直的。TensorFlow 提供了七種不同的初始化函式,下表總結了他們的功能和主要引數。

  tf.get_varibale 函式與 tf.Variable 函式最大的區別在於指定變數名稱的引數。對於 tf.Variable 函式,變數名稱是一個可選的引數,通過 name='v' 的形式給出。但是對於 tf.get_variable 函式,變數名稱是一個必填的引數。tf.get_variable會根據這個名字去建立或者獲取變數。在上面的樣例程式中,tf.get_variable首先會檢視去建立一個名字為 v 的引數,如果建立失敗(比如已經有同名的引數),那麼這個程式就會報錯。這是為了避免無意識的變數複用造成的錯誤。比如在定義神經網路引數時,第一層網路的權重已經叫 weights 了,那麼在建立第二層神經網路時,如果引數名仍然叫 weights,就會觸發變數重用的錯誤。否則兩層神經網路共用一個權重會出現一些比較難以發現的錯誤。如果需要通過 tf.get_variable 獲取一個已經建立的變數,需要通過 tf.variable_scope 函式來生成一個上下文管理器,並明確指定在這個上下文管理器中, tf.get_variable將直接獲取已經生成的變數。下面給出一段程式碼來說明如何通過tf.variable_scope 函式來控制 tf.get_variable 函式獲取已經建立過的變數。

# 在名字為foo的名稱空間內建立名字為 v 的變數
with tf.variable_scope('foo'):
    v = tf.get_variable(
        'v', [1], initializer=tf.constant_initializer(1.0)
    )

# 因為在名稱空間foo中已經存在名字為 v 的變數,所以下面的程式碼將會報錯
with tf.variable_scope('foo'):
    v = tf.get_variable('v', [1])

'''
報錯如下:
ValueError: Variable foo/v already exists, disallowed. Did you mean 
to set reuse=True in VarScope?
'''

# 在生成上下文管理器時,將引數reuse設定為True,這樣tf.get_variable函式將直接獲取已經宣告的函式
with tf.variable_scope('foo', reuse=True):
    v1 = tf.get_variable('v', [1])
    print(v == v1)
    # 輸出為True,代表v,v1代表的是相同的TensorFlow中變數

# 將引數reuse設定為True時,tf.variable_scope將只能獲取已經建立過的變數
# 因為在名稱空間bar中還沒有建立變數 v 所以下面的程式碼將會報錯
with tf.variable_scope('bar', reuse=True):
    v = tf.get_variable('v', [1])
'''
報錯如下:
ValueError: Variable bar/v does not exist, or was not created with tf.get_variable().
 Did you mean to set reuse=None in VarScope?'''

  上面的樣例簡單地說明了通過 tf.variable_scope函式可以控制 tf.get_variable函式的語義。當 tf.variable_scope函式使用引數 reuse=True生成上下文管理器時,這個上下文管理器內所有的tf.get_variable函式會直接獲取已經建立的變數。如果變數不存在,則 tf.get_variable函式將會報錯;相反,如果 tf.variable_scope函式使用引數 reuse=None或者 reuse=False建立上下文管理器,tf.get_variable操作將建立新的變數。如果同名的變數已經存在,則 tf.get_variable函式將報錯,TensorFlow中 tf.variable_scope 函式是可以巢狀的,下面程式說明了當 tf.varibale_scope 函式巢狀時,reuse引數的取值是如何確定的:

with tf.variable_scope('root'):
    # 可以通過td.get_varable_scope().reuse函式來獲取當前上下文管理器中reuse引數的取值
    print(tf.get_variable_scope().reuse)
    # 輸出為False 即最外層reuse是False

    # 新建一個巢狀的上下文管理器,並制定reuse為True
    with tf.variable_scope('foo', reuse=True):
        print(tf.get_variable_scope().reuse)
        # 輸出為True,因為我們設定了其reuse為True

        # 新建一個巢狀的上下文管理器但不指定reuse,這時候reuse的取值會和上一層層保持一致
        with tf.variable_scope('bar'):
            print(tf.get_variable_scope().reuse)
            # 輸出為True,沒有設定的話,將和上一層保持一致
    print(tf.get_variable_scope().reuse)
    # 這裡輸出為False。退出reuse設定為True的上下文之後,reuse的值又回到了False

  tf.variable_scope函式生成的上下文管理器也會建立一個TensorFlow中的名稱空間,在名稱空間內建立的變數名稱都會帶上這個名稱空間名作為字首。所以 tf.variable_scope函式除了可以控制tf.get_variable 執行的功能之外,這個函式也提供了一個管理變數名稱空間的方式,以下程式碼顯示瞭如何通過tf.variable_scope來管理變數的名稱:

v1 = tf.get_variable('v', [1])
print(v1.name)
# 輸出 v:0
# 其中 v 為變數的名稱  0表示這個變數是生成變數這個運算的第一個結果

with tf.variable_scope('foo'):
    v2 = tf.get_variable('v', [1])
    print(v2.name)
    # 輸出 foo/v:0
    # 在tf.variable_scope中建立的變數,名稱前面會加入名稱空間的名稱
    # 並通過/來分隔名稱空間的名稱和變數的名稱

with tf.variable_scope('foo'):
    with tf.variable_scope("bar"):
        v3 = tf.get_variable('v', [1])
        print(v3.name)
        # 輸出 foo/bar/v:0
        # 名稱空間可以巢狀,同時遍歷的名稱也會加入所有名稱空間的名稱作為字首
    v4 = tf.get_variable('v1', [1])
    print(v4.name)
    # 輸出 foo/v1:0
    # 當名稱空間退出之後,變數名稱也就不會再被加入其字首了

# 建立一個名稱為空的名稱空間,並設定reuse=True
with tf.variable_scope("", reuse=True):
    v5 = tf.get_variable('foo/bar/v', [1])
    # 可以直接通過帶名稱空間名稱的變數名來獲取其他名稱空間下的變數
    # 比如這裡指定名稱 foo/bar/v 來獲取在名稱空間 foo/bar/中建立的變數
    print(v5==v3)
    # 輸出結果為 : True

    v6 = tf.get_variable('foo/v1', [1])
    print(v6==v4)
    # 輸出結果為 : True

  通過tf.variable_scope 和 tf.get_variable 函式,下面程式碼對計算前向傳播結果的函式 做了一些改進:

def inference(input_tensor, reuse=False):
    # 定義第一層神經網路的變數和前向傳播過程
    with tf.variable_scope('layer1', reuse=reuse):
        # 根據傳進來的reuse來判斷是建立新變數還是使用已經建立好的
        # 在第一次構造網路時需要建立新的變數,以後每次呼叫這個函式直接使用reuse=True
        # 在之後,就不需要每次將變數傳進來了。
        weights = tf.get_variable('weights', [INPUT_NODE, LAYER1_NODE],
                                  initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable('biases', [OUTPUT_NODE],
                                 initializer=tf.constant_initializer(0.0))
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)

    # 類似的定義第二層神經網路的變數和前向傳播過程
    with tf.variable_scope('layer2', reuse=reuse):
        weights = tf.get_variable('weights', [LAYER1_NODE, OUTPUT_NODE],
                                  initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable("biases", [OUTPUT_NODE],
                                 initializer=tf.constant_initializer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
    # 返回最後的前向傳播結果
    return layer2

x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
y = inference(x)

# 在程式中需要使用訓練好的神經網路進行推導時,可以直接呼叫 inference(new_x, True)
# 如果需要使用滑動平均模型可以參考之前的程式碼,把計算滑動平均的類傳到inference函式中
# 獲取或者建立變數的部分不需要改變
new_x = ...
new_y = inference(new_x, True)

  使用上面的程式碼,就不需要再將所有的變數都作為引數傳遞到不同的函式中了。當神經網路結構更加複雜,引數更多時,使用這種變數管理的方式將大大提高程式的可讀性。

八,TensorFlow最佳實踐樣例程式

  在上面已經給出了一個完整的TensorFlow程式來解決MNIST問題,然而這個程式的可擴充套件性並不好,因為計算前向傳播的函式需要將所有變數都傳入,當神經網路結構變得更加複雜,引數更多時,程式可讀性會變得非常差。而且這種方式會導致程式中有大量的冗餘程式碼,降低程式設計的效率。並且上面程式並沒有持久化訓練好的模型。當程式退出時,訓練好的模型也就無法被使用了,這導致得到的模型無法被重用。更嚴重的是,一般神經網路模型訓練的時候都比較長,少則幾個小時,多則幾天甚至幾周。如果在訓練過程中程式宕機了,那麼沒有儲存訓練的中間結果會浪費大量的時間和資源。所以,在訓練的過程中需要每隔一段時間儲存一次模型訓練的中間結果。

  結合變數管理機制和TensorFlow模型持久化機制,我們將學習一個TensorFlow訓練設計網路的最佳實踐。將訓練和測試分成兩個獨立的程式,這可以使得每一個元件更加靈活。比如訓練神經網路的程式可以持續輸出訓練好的模型,而測試程式可以每隔一段時間檢測最新模型的正確率,如果模型效果更好,則將這個模型提供給產品使用。除了將不同功能模型分開,我們還將前向傳播的過程抽象成一個單獨的庫函式,因為神經網路的前向傳播過程在訓練和測試的過程中都會用到,所以通過庫函式的方式使用起來即可更加方便,又可以保證訓練和測試過程中使用的前向傳播方法一定是一致的。

  下面將重構之前的程式來解決MNIST問題,重構之後的程式碼將會被拆成3個程式,第一個是 mnist_inference.py,它定義了前向傳播的過程以及神經網路中的引數,第二個是 mnist_train.py 它定義了神經網路的訓練過程,第三個是 mnist_eval.py ,它定義了測試過程。

  下面給出 mnist_inference.py中的的內容:

#_*_coding:utf-8_*_
import tensorflow as tf

# 定義神經網路結構相關引數
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500

# 通過get_variable函式來獲取變數。在訓練神經網路時會建立這些變數
# 在測試時會通過儲存的模型載入這些變數的取值
# 因為可以在變數載入時將滑動平均變數重新命名,所以可以通過同樣的名字在訓練時使用變數本身
# 而且在測試時使用變數的滑動平均值,在這個函式中也會將變數的正則化損失加入損失集合
def get_weight_variable(shape, regularizer):
    weights = tf.get_variable(
        'weights', shape,
        initializer=tf.truncated_normal_initializer(stddev=0.1)
    )
    # 當給出正則化生成函式時,將當前變數的正則化損失計入名字為losses的集合
    #這裡使用add_to_collection函式將一個張量加入一個集合,而這個集合的的名稱為losses
    # 這是自定義的集合,不再TensorFlow自主管理的集合列表中
    if regularizer != None:
        tf.add_to_collection('losses', regularizer(weights))
    return weights

# 定義神經網路的前向傳播過程
def inference(input_tensor, regularizer):
    # 宣告第一層神經網路的變數並完成前向傳播過程
    with tf.variable_scope('layer1'):
        # 這裡通過 tf.get_variable或tf.Variable沒有本質區別
        # 因為在訓練或者是測試中沒有在同一個程式中多次呼叫這個函式
        # 如果在同一個程式中多次呼叫,在第一次呼叫之後就需要將reuse引數設定為True
        weights = get_weight_variable(
            [INPUT_NODE, LAYER1_NODE], regularizer
        )
        biases = tf.get_variable(
            'biases', [LAYER1_NODE],
            initializer=tf.constant_initializer(0.0)
        )
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)

    # 類似的宣告第二層神經網路的變數並完成前向傳播過程
    with tf.variable_scope('layer2'):
        weights = get_weight_variable(
            [LAYER1_NODE, OUTPUT_NODE], regularizer
        )
        biases = tf.get_variable(
            'biases', [OUTPUT_NODE],
            initializer=tf.constant_initializer(0.0)
        )
        layer2 = tf.matmul(layer1, weights) + biases

    # 返回最後前向傳播的結果
    return layer2

  在這段程式碼中定義了神經網路的前向傳播演算法,無論是訓練時還是測試時,都可以直接呼叫 Inference 這個函式,而不用關心具體的神經網路結構,使用定義好的前向傳播過程,下面程式碼給出了神經網路的訓練程式 mnist_train.py的程式碼:

#_*_coding:utf-8_*_
import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 載入 mnist_inference.py 中定義的常量和前向傳播的函式
import mnist_inference

# 配置神經網路的引數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
# 模型儲存的路徑和檔名
MODEL_SAVE_PATH = 'model1/'
MODEL_NAME = 'model.ckpt'

def train(mnist):
    # 定義輸入輸出 placeholder
    x = tf.placeholder(
        tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input'
    )
    y_ = tf.placeholder(
        tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input'
    )

    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    # 直接使用bookmnost_inference.py中定義的前向傳播過程
    y = bookmnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定義損失函式,學習率,滑動平均操作以及訓練過程
    variable_averages = tf.train.ExponentialMovingAverage(
        MOVING_AVERAGE_DECAY, global_step
    )
    variable_averages_op = variable_averages.apply(
        tf.trainable_variables()
    )
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=y, labels=tf.argmax(y_, 1)
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE,
        LEARNING_RATE_DECAY
    )
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(
        loss, global_step=global_step
    )
    with tf.control_dependencies([train_step, variable_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()

        # 在訓練過程中不再測試模型在驗證資料上的表現
        # 驗證和測試的過程都將會有一個獨立的程式完成
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            _, loss_value, step = sess.run([train_op, loss, global_step],
                                           feed_dict={x: xs, y_: ys})
            # 每1000輪儲存一次模型
            if i % 1000 == 0:
                # 輸出當前的訓練情況,這裡只輸出了模型在當前訓練batch上的損失函式大小
                # 通過損失函式的大小可以大概瞭解訓練的情況,在驗證集上正確率資訊會有一個單獨的程式來生成
                print("Afer %d training step(s), loss on training batch is %g"%(step, loss_value))

                # 儲存當前模型,注意這裡給出的global_step引數,這樣可以讓每個被儲存模型的檔名末尾加上訓練點額輪數
                saver.save(
                    sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),
                    global_step=global_step
                )

def main(argv=None):
    mnist = input_data.read_data_sets('data', one_hot=True)
    train(mnist)

if __name__ == '__main__':
    main()

  執行上面的程式,可以得到類似下面的程式碼:

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
Afer 1 training step(s), loss on training batch is 2.97567
Afer 1001 training step(s), loss on training batch is 0.228694
Afer 2001 training step(s), loss on training batch is 0.161067
Afer 3001 training step(s), loss on training batch is 0.148354
Afer 4001 training step(s), loss on training batch is 0.121623
Afer 5001 training step(s), loss on training batch is 0.106996
Afer 6001 training step(s), loss on training batch is 0.104219
Afer 7001 training step(s), loss on training batch is 0.112429
Afer 8001 training step(s), loss on training batch is 0.077253
... ...
Afer 29001 training step(s), loss on training batch is 0.03582

  在新的訓練程式碼中,不再將訓練和測試跑一起,訓練過程中,每1000輪輸出一次在當前訓練batch上損失函式的大小來大致估計訓練的效果。在上面的程式中,每1000輪儲存一次訓練好的模型,這樣可以通過一個單獨的測試程式,更加方便的在滑動平均模型上做測試,下面給出測試程式 mnist_eval.py

 

#_*_coding:utf-8_*_
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 載入 mnist_inference.py 和 mnist_train.py中定義的常量和函式
import mnist_inference
import mnist_train

# 每10秒載入一次最新的模型,並在測試資料上測試最新模型的正確率
EVAL_INTERVAL_SECS = 10

def evalute(mnist):
    with tf.Graph().as_default() as g:
        # 定義輸入輸出格式
        x = tf.placeholder(
            tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input'
        )
        y_ = tf.placeholder(
            tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input'
        )
        validate_feed = {x: mnist.validation.images,
                         y_: mnist.validation.labels}

        # 直接通過呼叫封裝好的函式來計算前向傳播的額結果
        #因為測試時不關注正則化損失的值,所以這裡用於計算正則化損失函式被設定為None
        y = mnist_inference.inference(x, None)

        # 使用前向傳播的結果計算正確率,如果需要對未知