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)
下面展示一下,relu處理的結果:
import tensorflow as tf y = tf.nn.relu([1, 0, -0.2, 2, 3, -5]) print(y) # Tensor("Relu:0", shape=(6,), dtype=float32) with tf.Session() as sess: print(y) print(y.eval()) # Tensor("Relu:0", shape=(6,), dtype=float32) # [1. 0. 0. 2. 3. 0.] # 使用 tf.convert_to_tensor函數把numpy轉為tensor類數據 res = tf.convert_to_tensor(y.eval()) print(res) # Tensor("Const:0", shape=(6,), dtype=float32)
這里注意:想學習的就是如何查看 tensor類型的數據的值。因為我們在建立圖(Graph)的時候,只定義tensor的結構形狀信息,並沒有執行數據的操作,而print函數只能打印輸出 tensor的 shape信息,而不能直接顯示tensor的值。所以我們需要執行會話session,在會話中打印值,而是由.eval() 相當於把tensor類型數據轉為numpy,再輸出。
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)]))
二,MNIST的無監督學習
下面將基於無監督學習的中的一個簡單應用——自編碼器(autoencoder),TensorFlow搭建一個自編碼網絡,並用它在MNIST數據集上訓練。
其實已經學習過了 tensorflow學習筆記——自編碼器及多層感知器
所以這里簡單將代碼走個流程。
2.1 自編碼器網絡
其實前面學習的訓練MNIST數據的網絡都是有監督學習,它的重要特征是數據是有標記的,無標記的數據應該用什么樣的網絡模型來學習呢?下面我們就來介紹一個網絡模型——自編碼網絡。
自編碼網絡如下:
這里再啰嗦一遍,自編碼網絡的作用是將輸入樣本壓縮到隱藏層,然后解壓,在輸出端重建樣本。最終輸出層神經元數量等於輸入層神經元的數量。
這里主要有兩個過程:壓縮和解壓。壓縮依靠的時輸入數據(圖像,文字,聲音)本身存在不同程度的冗余信息,自動編碼網絡通過學習去掉這些冗余信息,把有用的特征輸入到隱藏層中。這個和主成分分析(PCA)有些類似,要找到可以代表源數據的主要成分。其實,如果激活函數不使用Sigmoid等非線性函數,而是由線性函數,就是PCA模型。可以想象,如果數據都是完全隨機,相互獨立,同分布的,自編碼網絡就很難學習到一個有效的壓縮模型。
壓縮過程一方面要限制隱藏神經元的數量,來學習一些有意義的特征,另一方面還希望神經元大部分時間是被抑制的,當神經元的輸出接近1時,認為是被激活的,接近0時認為是被抑制的。希望部分神經元處於被抑制狀態,這種規則稱為稀疏性限制。
多個隱藏層的主要作用是,如果輸入的數據是圖像,第一層會學習如何識別邊,第二層會學習如何去組合邊,從而構成輪廓,角等,更高層會學習如何去組合更有意義的特征。例如,如果輸入數據是人臉圖像的話,更高層會學習如何識別和組合眼睛,鼻子,嘴等人臉器官。
2,TensorFlow的自編碼網絡實現
下面我們還以MNIST數據集為例,學習一下自編碼器的運用。
1,加載數據,首先設置訓練的超參數,包括學習率,訓練的輪(epoch)數(全部數據訓完一遍稱為一輪),每輪訓練的數據多少,每隔多少輪顯示一次訓練結果:
# _*_coding:utf-8_*_ import tensorflow as tf import numpy as np from tensorflow.examples.tutorials.mnist import input_data # 設置訓練超參數 learning_rate = 0.01 # 學習率 training_epochs = 20 # 訓練的輪數 batch_size = 256 # 每次訓練的數據多少 display_step = 1 # 每隔多少輪顯示一次訓練結果 # 下面參數表示從測試集中選擇10張圖片去驗證自編碼器的結果 examples_to_show = 10 # 定義輸入數據,這里是無監督學習,所以只需要輸入圖片數據,不需要標記數據 X = tf.placeholder('float', [None, n_input]) # 下面初始化權重和定義網絡結構,我們設計這個自動編碼網絡含有兩個隱含層 # 第一個隱含層神經元為256個,第二個隱藏層神經元為128個 # 定義網絡參數如下: n_hidden_1 = 256 # 第一個隱藏層神經元個數,也是特征值個數 n_hidden_2 = 128 # 第二個隱藏層神經元個數,也是特征值個數 n_input = 784 # 輸入數據的特征值個數,28*28=784 # 初始化每一層的權重和偏置,如下: weights = { 'encoder_h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])), 'encoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])), 'decoder_h1': tf.Variable(tf.random_normal([n_hidden_2, n_hidden_1])), 'decoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_input])), } biases= { 'encoder_b1': tf.Variable(tf.random_normal([n_hidden_1])), 'encoder_b2': tf.Variable(tf.random_normal([n_hidden_2])), 'decoder_b1': tf.Variable(tf.random_normal([n_hidden_1])), 'decoder_b2': tf.Variable(tf.random_normal([n_input])), } # 定義自動編碼模型的網絡結構,包括壓縮和解壓兩個過程 # 定義壓縮函數 def encoder(x): # encoder hidden layer with sigmoid activation #1 layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['encoder_h1']), biases['encoder_b1'])) # decoder hidden layer with sigmoid activation #2 layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['encoder_h2']), biases['encoder_b2'])) return layer_2 # 定義解壓函數 def decoder(x): # encoder hidden layer with sigmoid activation #1 layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['decoder_h1']), biases['decoder_b1'])) # decoder hidden layer with sigmoid activation #2 layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['decoder_h2']), biases['decoder_b2'])) return layer_2 # 構建模型 encoder_op = encoder(X) decoder_op = decoder(encoder_op) # 下面構建損失函數和優化器 # 這里損失函數采用“最小二乘法” 對原始數據集合輸出的數據集進行平方差並取均值運算 # 優化器采用RMSPropOptimizer y_pred = decoder_op # 得出預測值 y_true = X # 得出真實值,即輸入值 # 定義損失函數和優化器 cost = tf.reduce_mean(tf.pow(y_true-y_pred, 2)) optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(cost)
(2),訓練數據及其評估模型
# 在一個會話中啟動圖,開始訓練和評估 with tf.Session() as sess: sess.run(init) total_batch = int(mnist.train.num_examples / batch_size) # 開始訓練 for epoch in range(training_epochs): for i in range(total_batch): batch_xs, batch_ys = mnist.train.next_batch(batch_size) # run optimization op(backprop) and cost op(to get loss value) _, c = sess.run([optimizer, cost], feed_dict={X: batch_xs}) # 每一輪,打印出一次損失值 if epoch % display_step == 0: print("Epoch: ", "%04d" % (epoch + 1), "cost=", "{:.9f}".format(c)) print("Optimization Finished!") # 對測試集應用訓練好的自動編碼網絡 encode_decode = sess.run(y_pred, feed_dict={X: mnist.test.images[:examples_to_show]}) # 比較測試集原始圖片和自動編碼網絡的重建結果 f, a = plt.subplots(2, 10, figsize=(10, 2)) for i in range(examples_to_show): a[0][i].imshow(np.reshape(mnist.test.images[i], (28, 28))) # 測試集 a[1][i].imshow(np.reshape(encode_decode[i], (28, 28))) # 重建結果 f.show() plt.draw() plt.waitforbuttonpress()
訓練輸出每一輪的損失值,結果如下:
Epoch: 0001 cost= 0.224611133 Epoch: 0002 cost= 0.185801953 Epoch: 0003 cost= 0.167944789 Epoch: 0004 cost= 0.155772462 Epoch: 0005 cost= 0.148120180 Epoch: 0006 cost= 0.141713008 Epoch: 0007 cost= 0.140479833 Epoch: 0008 cost= 0.138582915 Epoch: 0009 cost= 0.129859537 Epoch: 0010 cost= 0.127602220 Epoch: 0011 cost= 0.124348059 Epoch: 0012 cost= 0.120832287 Epoch: 0013 cost= 0.118068665 Epoch: 0014 cost= 0.119204752 Epoch: 0015 cost= 0.117326580 Epoch: 0016 cost= 0.116811723 Epoch: 0017 cost= 0.116068400 Epoch: 0018 cost= 0.110866800 Epoch: 0019 cost= 0.112090074 Epoch: 0020 cost= 0.111832522 Optimization Finished!
可以看出隨着訓練次數的增多,損失值趨於減少。
測試集的圖片和經過自動編碼器重建特征后的圖片對比如下:上面一行是測試集的圖片,下面一行對應的是經過自動編碼器重建后的結果。
總的代碼如下:
# _*_coding:utf-8_*_ import tensorflow as tf import numpy as np from tensorflow.examples.tutorials.mnist import input_data import matplotlib.pyplot as plt mnist = input_data.read_data_sets('MNIST_data/', one_hot=True) # 設置訓練超參數 learning_rate = 0.01 # 學習率 training_epochs = 20 # 訓練的輪數 batch_size = 256 # 每次訓練的數據多少 display_step = 1 # 每隔多少輪顯示一次訓練結果 # 下面參數表示從測試集中選擇10張圖片去驗證自編碼器的結果 examples_to_show = 10 # 下面初始化權重和定義網絡結構,我們設計這個自動編碼網絡含有兩個隱含層 # 第一個隱含層神經元為256個,第二個隱藏層神經元為128個 # 定義網絡參數如下: n_hidden_1 = 256 # 第一個隱藏層神經元個數,也是特征值個數 n_hidden_2 = 128 # 第二個隱藏層神經元個數,也是特征值個數 n_input = 784 # 輸入數據的特征值個數,28*28=784 # 定義輸入數據,這里是無監督學習,所以只需要輸入圖片數據,不需要標記數據 X = tf.placeholder('float', [None, n_input]) # 初始化每一層的權重和偏置,如下: weights = { 'encoder_h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])), 'encoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])), 'decoder_h1': tf.Variable(tf.random_normal([n_hidden_2, n_hidden_1])), 'decoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_input])), } biases = { 'encoder_b1': tf.Variable(tf.random_normal([n_hidden_1])), 'encoder_b2': tf.Variable(tf.random_normal([n_hidden_2])), 'decoder_b1': tf.Variable(tf.random_normal([n_hidden_1])), 'decoder_b2': tf.Variable(tf.random_normal([n_input])), } # 定義自動編碼模型的網絡結構,包括壓縮和解壓兩個過程 # 定義壓縮函數 def encoder(x): # encoder hidden layer with sigmoid activation #1 layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['encoder_h1']), biases['encoder_b1'])) # decoder hidden layer with sigmoid activation #2 layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['encoder_h2']), biases['encoder_b2'])) return layer_2 # 定義解壓函數 def decoder(x): # encoder hidden layer with sigmoid activation #1 layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['decoder_h1']), biases['decoder_b1'])) # decoder hidden layer with sigmoid activation #2 layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['decoder_h2']), biases['decoder_b2'])) return layer_2 # 構建模型 encoder_op = encoder(X) decoder_op = decoder(encoder_op) # 下面構建損失函數和優化器 # 這里損失函數采用“最小二乘法” 對原始數據集合輸出的數據集進行平方差並取均值運算 # 優化器采用RMSPropOptimizer y_pred = decoder_op # 得出預測值 y_true = X # 得出真實值,即輸入值 # 定義損失函數和優化器 cost = tf.reduce_mean(tf.pow(y_true - y_pred, 2)) optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(cost) # 在一個會話中啟動圖,開始訓練和評估 with tf.Session() as sess: tf.global_variables_initializer().run() total_batch = int(mnist.train.num_examples / batch_size) # 開始訓練 for epoch in range(training_epochs): for i in range(total_batch): batch_xs, batch_ys = mnist.train.next_batch(batch_size) # run optimization op(backprop) and cost op(to get loss value) _, c = sess.run([optimizer, cost], feed_dict={X: batch_xs}) # 每一輪,打印出一次損失值 if epoch % display_step == 0: print("Epoch: ", "%04d" % (epoch + 1), "cost=", "{:.9f}".format(c)) print("Optimization Finished!") # 對測試集應用訓練好的自動編碼網絡 encode_decode = sess.run(y_pred, feed_dict={X: mnist.test.images[:examples_to_show]}) # 比較測試集原始圖片和自動編碼網絡的重建結果 f, a = plt.subplots(2, 10, figsize=(10, 2)) for i in range(examples_to_show): a[0][i].imshow(np.reshape(mnist.test.images[i], (28, 28))) # 測試集 a[1][i].imshow(np.reshape(encode_decode[i], (28, 28))) # 重建結果 f.show() plt.draw() plt.waitforbuttonpress()
三,將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()
上面程式可以將MNIST數據集中所有的訓練數據存儲到一個TFRecord 文件中。當數據量較大時,也可以將數據寫入多個 TFRecord 文件。TensorFlow對從文件列表中讀取數據提供了很好的支持。
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) # 使用前向傳播的結果計算正確率,如果需要對未知的樣本進行分類, # 那么使用 tf.argmax(y, 1)就可以得到輸入樣例的預測類別了 correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) # 通過變量重命名的方式來加載模型,這樣在前向傳播的過程中就不需要調用求滑動平均的函數獲取平均值 # 這樣就可以完全共享之前mnist_inference.py中定義的前向傳播過程 variable_averages = tf.train.ExponentialMovingAverage( mnist_train.MOVING_AVERAGE_DECAY ) variables_to_restore = variable_averages.variables_to_restore() saver = tf.train.Saver(variables_to_restore) # 每隔EVAL_INTERVAL_SECS 秒調用一次計算正確率的過程以檢測訓練過程中正確率的變化 while True: with tf.Session() as sess: # tf.train.get_checkpoint_state函數會通過checkpoint文件 # 自動找到目錄中最新模型的文件名 ckpt = tf.train.get_checkpoint_state( mnist_train.MODEL_SAVE_PATH ) if ckpt and ckpt.model_checkpoint_path: # 加載模型 saver.restore(sess, ckpt.model_checkpoint_path) # 通過文件名得到模型保存時迭代的輪數 global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1] accuracy_score = sess.run(accuracy, feed_dict=validate_feed) print("After %s training step(s) , validation accuracy ='%g"%(global_step, accuracy_score)) else: print("No checkpoint file found") return time.sleep(EVAL_INTERVAL_SECS) def main(argv=None): mnist = input_data.read_data_sets('data', one_hot=True) evalute(mnist) if __name__ == '__main__': main()
上面的代碼會每隔10秒運行一次,每次運行都是讀取最新保存的模型,並在MNIST驗證數據上計算模型的正確率。如果需要離線預測未知數據的類別(比如這個樣例程序可以判斷手寫數字圖片中所包含的數字),只需要將計算正確率的部分改為答案輸出即可。運行上面代碼可以得到類似下面的結果,注意因為這個程序每10秒會自動運行一次,而訓練程序不一定每10秒輸出一個新模型,所以下面的結果中會發現有些模型被測試了多次,一般在解決真實問題時,不會這么頻繁的運行評測程序。
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 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 After 29001 training step(s) , validation accuracy ='0.9856 ... ...
知識儲備:gfile文件操作詳解
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、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的差別僅僅在於“無阻塞”,即該函數會無阻賽以較快的方式獲取文本操作句柄