前面的章節已經介紹了如何使用TensorFlow實現常用的神經網絡結構。在將這些神經網絡用于實際問題之前,需要先優化網絡中的參數。這就是訓練神經網絡的過程。訓練神經網絡十分復雜,有時需要幾天甚至幾周的時間。為了更好地管理、調試和優化神經網絡的訓練過程,TensorFlow提供了一個可視化工具TensorBoard。TensorBoard可以有效地展示TensorFlow在運行過程中的計算圖、各種指標隨著時間的變化趨勢以及訓練中使用到的圖像等信息。
本章將詳細介紹TensorBoard的使用方法。
1.將介紹TensorBoard的基礎知識,并通過TensorBoard來可視化一個簡單的TensorFlow樣例程序。在這一節中將介紹如何啟動TensorBoard,并大致講解TensorBoard提供的幾類可視化信息。
2.介紹通過TensorBoard得到的TensorFlow計算圖的可視化結果。TensorFlow計算圖保存了TensorFlow程序計算過程的所有信息。因為TensorFlow計算圖中的信息含量較多,所以TensorBoard設計了一套交互過程來更加清晰地呈現這些信息。
3.最后將詳細講解如何使用在這一節中將詳細講解如何使用TensorBoard對訓練過程進行監控,以及如何通過TensorFlow指定對訓練過程進行監控,以及如何通過TensorFlow指定需要可視化的指標。并給出完整的樣例程序介紹如何得到可視化結果。
1.TensorBoard簡介
TensorBoard是TensorFlow的可視化工具,它可以通過TensorFlow程序運行過程中輸出的日志文件可視化TensorFlow程序的運行狀態。TensorBoard和TensorFlow程序跑在不同的進程中,TensorBoard會自動讀取最新的TensorFlow日志文件,并呈現當前TensorFlow程序運行的最新狀態。以下代碼展示了一個簡單的TensorFlow程序,這個程序中完成了TensorBoard日志輸出的功能:
import tensorflow as tf
input1 = tf.constant([1.0, 2.0, 3.0], name="input1")
input2 = tf.Variable(tf.random_uniform([3]), name="input2")
output = tf.add_n([input1, input2], name="add")
writer = tf.summary.FileWriter("C:\\log", tf.get_default_graph())
writer.close()
以上程序輸出了TensorFlow計算圖的信息,所以運行Tensorboard時,可以看到這個向量相加程序計算圖可視化之后的結果。TensorBoard不需要額外的安裝過程,在TensorFlow安裝完成時,TensorBoard會被自動安裝。運行下面的命令可以啟動TensorBoard:
運行TensorBoard,并將日志的地址指向上面程序日志輸出的地址
tensorboard --logdir=C:\\log
運行上面的命令會啟動一個服務,這個服務的端口默認為6006.通過瀏覽器打開localhost:6006,可以看到下圖所示的界面:
如上圖,可以看到上面程序TensorFlow計算圖的可視化結果。
在界面右上方,點擊下拉菜單可以看到有12欄,每一欄都對應了一類信息的可視化結果,在下面的章節中將具體介紹每一欄的功能。如圖,打開tensorboard界面會默認進入SCALARS欄。因為上面的程序沒有輸出任何由EVENTS欄可視化的信息,所以這個界面顯示的是“NO scalar data was found”(沒有發現標量數據)。
2.TensorFlow計算圖可視化
在上圖中給出了一個TensorFlow計算圖的可視化效果圖。然而,從TensorBoard可視化結果中可以獲取的信息遠不止上圖所示的這些。這一節將詳細介紹如何更好地利用TensorFlow計算圖的可視化結果。
1.首先將介紹通過TensorFlow節點的命名空間整理Tensorboard可視化得到的TensorFlow計算圖。在前面介紹過,TensorFlow會將所有的計算以圖的形式組織起來。TensorBoard可視化得到的圖并不僅是將TensorFlow計算圖中的節點和邊直接可視化,它會根據每個TensorFlow計算節點的命名空間來整理可視化得到的效果,使得神經網絡的整體結構不會被過多的細節所淹沒。除了顯示TensorFlow計算圖的結構,TensorBoard還可以展示TensorFlow計算節點上的其他信息。
2.第二小節將介紹如何從可視化結果中獲取TensorFlow計算圖中的這些信息。
2.1命名空間與TensorBoard圖上節點
在上一節給出的樣例程序中只定義了一個加法操作,然而從圖中可以看到,將這個TensorFlow計算圖可視化得到的效果圖上卻有
多個節點,可以想象,一個復雜的神經網絡模型所對應的TensorFlow計算圖會比上圖中簡單的向量加法樣例程序的計算圖復雜很多,那么沒有經過整理得到的可視化效果圖并不能幫助很好地理解神經網絡模型的結構。
為了更好地組織可視化效果圖中的計算節點,TensorBoard支持通過TensorFlow命名空間來整理可視化效果圖上的節點。在TensorBoard的默認視圖中,TensorFlow計算圖中同一個命名空間下的所有節點會被縮略成一個節點,只有頂層命名空間中的節點才會被顯示在TensorBoard可視化效果圖上。在前面介紹過變量的命名空間,以及如何通過tf.variable_scope函數管理變量的命名空間。除了tf.variable_scope函數,tf.name_scope函數也提供了命名空間管理的功能。這兩個函數在大部分情況下是等價的,唯一的區別是在使用tf.get_variable函數時,以下代碼簡單地說明了這兩個函數的區別:
1.不同的命名空間
import tensorflow as tf
with tf.variable_scope("foo"):
a = tf.get_variable("bar", [1])
print a.name
with tf.variable_scope("bar"):
b = tf.get_variable("bar", [1])
print b.name
輸出:
- tf.Variable和tf.get_variable的區別。
with tf.name_scope("a"):
a = tf.Variable([1])
print a.name
a = tf.get_variable("b", [1])
print a.name
輸出:
結果分析:
使用tf.Variable函數生成變量會受到tf.name_scope影響,于是這個變量的名稱為“a/Variable”
而使用tf.get_variable函數不受tf.name_scope函數的影響,于是變量并不在b這個命名空間中。所以這個變量名稱為b
通過對命名空間管理,可以改進上一節中向量加法的樣例代碼,使得可視化得到的效果圖更加清晰。以下代碼展示了改進的方法:
with tf.name_scope("input1"):
input1 = tf.constant([1.0, 2.0, 3.0], name="input1")
with tf.name_scope("input2"):
input2 = tf.Variable(tf.random_uniform([3]), name="input2")
output = tf.add_n([input1, input2], name="add")
writer = tf.summary.FileWriter("C:\\log", tf.get_default_graph())
writer.close()
下圖顯示了改進后的可視化效果圖:
從圖中可以看到,上圖中很多的節點都被縮略到了圖中input2節點。這樣TensorFlow程序中定義的加法運算被清晰地展示了出來。需要查看input2節點中具體包含了哪些運算時,可以將鼠標移動到input2節點,并點開右上角加好“+”。下圖展示了input2節點之后的視圖:
在input2的展開圖中可以看到數據初始化相關的操作都被整理到了一起。下面將給出一個樣例程序來展示如何很好地可視化一個真實的神經網絡結構圖。本節將繼續采用之前的MNIST數字識別問題哪一章的架構,以下代碼給出了改造后的mnist_train.py程序:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import mnist_inference
- 定義神經網絡的參數。
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 3000
MOVING_AVERAGE_DECAY = 0.99
- 定義訓練的過程并保存TensorBoard的log文件。
def train(mnist):
# 輸入數據的命名空間。
with tf.name_scope('input'):
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(REGULARIZATION_RATE)
y = mnist_inference.inference(x, regularizer)
global_step = tf.Variable(0, trainable=False)
# 處理滑動平均的命名空間。
with tf.name_scope("moving_average"):
variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
variables_averages_op = variable_averages.apply(tf.trainable_variables())
# 計算損失函數的命名空間。
with tf.name_scope("loss_function"):
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'))
# 定義學習率、優化方法及每一輪執行訓練的操作的命名空間。
with tf.name_scope("train_step"):
learning_rate = tf.train.exponential_decay(
LEARNING_RATE_BASE,
global_step,
mnist.train.num_examples / BATCH_SIZE, LEARNING_RATE_DECAY,
staircase=True)
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
with tf.control_dependencies([train_step, variables_averages_op]):
train_op = tf.no_op(name='train')
writer = tf.summary.FileWriter("log", tf.get_default_graph())
# 訓練模型。
with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(TRAINING_STEPS):
xs, ys = mnist.train.next_batch(BATCH_SIZE)
if i % 1000 == 0:
# 配置運行時需要記錄的信息。
run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
# 運行時記錄運行信息的proto。
run_metadata = tf.RunMetadata()
_, loss_value, step = sess.run(
[train_op, loss, global_step], feed_dict={x: xs, y_: ys},
options=run_options, run_metadata=run_metadata)
writer.add_run_metadata(run_metadata=run_metadata, tag=("tag%d" % i), global_step=i)
print("After %d training step(s), loss on training batch is %g." % (step, loss_value))
else:
_, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
writer.close()
- 主函數。
def main(argv=None):
mnist = input_data.read_data_sets("../../datasets/MNIST_data", one_hot=True)
train(mnist)
if __name__ == '__main__':
main()
4.運行結果如下圖:
相比前面給出的mnist_train.py程序,上面程序最大的改變就是將完成類似功能的計算放到了由tf.name_scope函數生成的上下文管理器中。這樣TensorBoard可以將這些節點有效地合并,從而突出神經網絡的整體結構。因為在mnist_inference.py程序中已經使用了tf.variable_scope來管理變量的命名空間,所以這里不需要再做調整。下圖展示了新的MNIST程序的TensorFlow計算圖可視化得到的效果圖:
從圖中可以看到,TensorBoard可視化效果圖很好地展示了整個神經網絡的結構,在圖中,input節點代表了訓練神經網絡需要的輸入數據,這些輸入數據會提供給神經網絡的第一層layer1.然后神經網絡第一層layer1的結果會被傳到第二層layer2,經過layer2的計算得到前向傳播的結果。loss_function節點表示計算損失函數的過程,這個過程既依賴于前向傳播的結果來計算交叉熵(layer2到loss_function的邊),又依賴于每一層中所定義的變量來計算L2正則化損失(layer1和layer2到loss_function的邊)。loss_function的計算結果會提供給神經網絡的優化過程,也就是圖中train_step所代表的節點。綜上所述,通過TensorBoard可視化得到的效果圖可以對整個神經網絡的網絡結構有一個大致了解。
在圖中可以發現節點之間有兩種不同的邊。一種邊是通過實線表示的,這種邊刻畫了數據傳輸,邊上的箭頭方向表達了數據傳輸的方向。比如layer1和layer2之間的邊表示了layer1的輸出將會作為layer2的輸入。有些邊上的箭頭是雙向的,比如節點Variable和train_step之間的邊。這表明train_step會修改Variable的狀態,在這里也就是表明訓練過程會修改記錄訓練迭代輪數的變量。
TensorBoard可視化效果圖的邊上還標注了張量的維度信息。下圖放大了可視化得到的效果圖:
從圖中可以看出,節點input和layer1之間傳輸的張量的維度為100*784.這說明了訓練時提供的batch大小為100,輸入層節點的個數為784.當兩個節點之間傳輸的張量多于1個時,可視化效果圖上將只顯示張量的個數。效果圖上邊的粗細表示的是兩個節點之間傳輸的標量維度的總大小,而不是傳輸的標量個數。比如layer2和loss_function之間雖然傳輸了兩個張量,但其維度都比較小,所以這條邊比layer1和layer2之間的邊還要細。
TensorBoard可視化效果圖上另外一種邊是通過虛線表示的,比如圖中所示moving_average和train_step之間的邊。虛邊表達了計算之間的依賴關系,比如在程序中,通過tf.control_dependencies函數指定了更新參數滑動平均值的操作和通過反方向傳播更新變量的操作需要同時進行,于是moving_average與train_step之間存在一條虛邊。
除了手動的通過TensorFlow中的命名空間來調整TensorBoard的可視化效果圖,TensorBoard也會智能地調整可視化效果圖上的節點。TensorFlow中部分計算節點會有比較多的依賴關系,如果全部畫在一張圖上會使可視化得到的效果圖非常擁擠。于是TensorBoard將TensorFlow計算圖分成了主圖(Main Graph)和輔助圖(Auxiliary nodes)兩個部分來呈現。上面第一個圖中,左側展示的是計算圖的主要部分,也就是主圖;右側展示的是一些單獨列出來的節點,也就是輔助圖。TensorBoard會自動將連接比較多的節點放在輔助圖中,使得主圖的結構更加清晰。
除了自動的方式,TensorBoard也支持手工的方式來調整可視化效果,如下圖所示:
如圖,右鍵單擊可視化效果圖上的節點會彈出一個選項,這個選項可以將節點加入主圖或者從主圖中刪除。左鍵選擇一個節點并點擊信息框下部的選項也可以完成類似的功能。
2.2節點信息
除了展示TensorFlow計算圖的結構,TensorBoard還可以展示TensorFlow計算圖上每個節點的基本信息以及運行時消耗的時間和空間。本小節將進一步講解如何通過TensorBoard展現TensorFlow計算圖節點上的這些信息。TensorFlow計算節點的運行時間都是非常有用的信息,它可以幫助更加有針對性地優化TensorFlow程序,使得整個程序的運行速度更快。使用TensorBoard可以非常直觀地展現所有TensorFlow計算節點在某一次運行時所消耗的時間和內存。將以下代碼加入上一小節中修改后的mnist_train.py神經網絡訓練部分,就可以將不同迭代輪數時每一個TensorFlow計算節點的運行時間和消耗的內存寫入TensorBoard的日志文件中:
# 訓練模型。
with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(TRAINING_STEPS):
xs, ys = mnist.train.next_batch(BATCH_SIZE)
# 每1000輪記錄一次運行狀態
if i % 1000 == 0:
# 配置運行時需要記錄的信息。
run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
# 運行時記錄運行信息的proto。
run_metadata = tf.RunMetadata()
# 將配置信息和記錄運行信息的proto傳入運行的過程,從而記錄運行時每一個節點的時間、空間開銷信息
_, loss_value, step = sess.run(
[train_op, loss, global_step], feed_dict={x: xs, y_: ys},
options=run_options, run_metadata=run_metadata)
# 將節點在運行時的信息寫入日志文件
writer.add_run_metadata(run_metadata=run_metadata, tag=("tag%d" % i), global_step=i)
print("After %d training step(s), loss on training batch is %g." % (step, loss_value))
else:
_, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
運行以上程序,并使用這個程序輸出的日志啟動TensorBoard,就可以可視化每個TensorFlow計算節點在某一次運行時所消耗的時間和空間了。進入GRAPHS欄后,需要先選擇一次運行來查看。
如下圖所示:
如圖所示,點擊頁面左側的Session.runs選項會出現一個下拉單,在這個下拉單中會出現所有通過train_writer.add_run_metadata函數記錄的運行數據。選擇一次運行后,TensorBoard左側的Color欄中將會新出現Computetime和Memory這兩個選項。
在Color欄中選擇Compute time可以看到在這次運行中每個TensorFlow計算節點的運行時間。類似的,選擇Memory可以看到這次運行中每個TensorFlow計算節點所消耗的內存。下圖展示了在第2000輪迭代時,不同TensorFlow計算節點時間消耗的可視化效果圖:
上圖中顏色越深的節點表示時間消耗越大。從圖中可以看出,代表訓練神經網絡的train_step節點消耗的時間是最多的。通過對每一個計算節點消耗時間的可視化,可以很容易地找到TensorFlow計算圖上的性能瓶頸,這大大方便了算法優化的工作。在性能調優時,一般會選擇迭代輪數較大時的數據(比如上圖中第2000輪迭代時的數據)作為不同計算節點時間/空間消耗的標準,因為這樣可以減少TensorFlow初始化對性能的影響。
在TensorBoard界面左側的Color欄中,除了Compute time和Memory,還有Structure和Device兩個選項。除了上一圖,在上面幾個圖中,展示的可視化效果圖都是使用默認的Structure選項。在這個視圖中,灰色的節點表示沒有其他節點和它擁有相同結構。如果有兩個節點的結構相同,那么它們會被涂上相同的顏色。下圖展示了一個擁有相同結構節點的卷積神經網絡可視化得到的效果圖:
在上圖中,兩個卷積層的結構是一樣的,所以他們都被涂上了相同的顏色。最后,Color欄還可以選擇Device選項,這個選項可以根據TensorFlow計算節點運行的機器給可視化效果圖上的節點染色。在使用GPU時,可以通過這種方式直觀地看到哪些計算節點被放到了GPU上。下圖給出了一個使用了GPU的TensorFlow計算圖的可視化效果:
上圖中深灰色的節點表示對應的計算放在了GPU上,淺灰色的節點表示對應的計算放在了CPU上。具體如何使用GPU將在下一章介紹。
當點擊TensorBoard可視化效果圖中的節點時,界面的右上角會彈出一個信息卡片顯示這個節點的基本信息,如下圖:
當點擊的節點為一個命名空間時,TensorBoard展示的信息有這個命名空間內所有計算節點的輸入、輸出以及依賴關系。雖然屬性(Attribute)也會展示在卡片中,但是在代表命名空間的屬性下不會有任何內容。當Session.runs選擇了某一次運行時,節點的信息卡片上會出現這個節點運行所消耗的時間和內存等信息。
當點擊的TensorBoard可視化效果圖上的節點對應一個TensorFlow計算節點時,TensorBoard也會展示類似的信息。下圖展示了一個TensorFlow計算節點所對應的信息卡片:
3.監控指標可視化
在上一節中著重介紹了通過TensorBoard的GRAPHS欄可視化TensorFlow計算圖的結構以及在計算圖上的信息。TensorBoard除了可以可視化TensorFlow的計算圖,還可以可視化TensorFlow程序運行過程中各種有助于了解程序運行狀態的監控指標。在本節中將介紹如何利用TensorBoard中其他欄目可視化這些監控指標。除了Graphs欄,TensorBoard界面中還提供了EVENTS/IMAGES/AUDIO和HISTOGRAMS四個欄目來可視化其他的監控指標。下面的程序展示了如何將TensorFlow程序運行時的信息輸出到TensorBoard日志文件中:
1.定義變量
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
2.生成變量監控信息并定義生成監控信息日志的操作。
SUMMARY_DIR = "C:/log/supervisor.log"
BATCH_SIZE = 100
TRAIN_STEPS = 3000
# var:需要記錄的張量
# name:可視化結果中顯示的圖表名稱,這個名稱一般與變量名一致
def variable_summaries(var, name):
# 將生成監控信息的操作放到同一個命名空間下
with tf.name_scope('summaries'):
# tf.summary.histogram:記錄張量中元素的取值分布
tf.summary.histogram(name, var)
# 計算變量的平均值
mean = tf.reduce_mean(var)
tf.summary.scalar('mean/' + name, mean)
# 計算變量的標準差
stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
tf.summary.scalar('stddev/' + name, stddev)
3.生成一層全鏈接的神經網絡
def nn_layer(input_tensor, input_dim, output_dim, layer_name, act=tf.nn.relu):
# 將同一層神經網絡放在一個統一的命名空間下
with tf.name_scope(layer_name):
# 聲明神經網絡邊上的權重,并調用生成權重監控信息日志的函數
with tf.name_scope('weights'):
weights = tf.Variable(tf.truncated_normal([input_dim, output_dim], stddev=0.1))
variable_summaries(weights, layer_name + '/weights')
# 聲明神經網絡的偏置項,并調用生成偏置項監控信息日志的函數
with tf.name_scope('biases'):
biases = tf.Variable(tf.constant(0.0, shape=[output_dim]))
variable_summaries(biases, layer_name + '/biases')
with tf.name_scope('Wx_plus_b'):
preactivate = tf.matmul(input_tensor, weights) + biases
# 記錄神經網絡輸出節點在經過激活函數之前的分布
tf.summary.histogram(layer_name + '/pre_activations', preactivate)
activations = act(preactivate, name='activation')
# 記錄神經網絡節點輸出在經過激活函數之后的分布。
tf.summary.histogram(layer_name + '/activations', activations)
return activations
def main():
mnist = input_data.read_data_sets("../../datasets/MNIST_data", one_hot=True)
# 定義輸入
with tf.name_scope('input'):
x = tf.placeholder(tf.float32, [None, 784], name='x-input')
y_ = tf.placeholder(tf.float32, [None, 10], name='y-input')
# 將輸入向量還原成圖片的像素矩陣,并通過tf.summary.image將當前的圖片信息寫入日志的操作
with tf.name_scope('input_reshape'):
image_shaped_input = tf.reshape(x, [-1, 28, 28, 1])
tf.summary.image('input', image_shaped_input, 10)
hidden1 = nn_layer(x, 784, 500, 'layer1')
y = nn_layer(hidden1, 500, 10, 'layer2', act=tf.identity)
# 計算交叉熵并定義生成交叉熵監控日志的操作
with tf.name_scope('cross_entropy'):
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y, labels=y_))
tf.summary.scalar('cross_entropy', cross_entropy)
with tf.name_scope('train'):
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)
# 計算模型在當前給定數據上的正確率
with tf.name_scope('accuracy'):
with tf.name_scope('correct_prediction'):
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
with tf.name_scope('accuracy'):
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
tf.summary.scalar('accuracy', accuracy)
# tf.summary.merge_all:整理所有的日志生成操作
merged = tf.summary.merge_all()
with tf.Session() as sess:
# 初始化寫日志的writer,并將當前TensorFlow計算圖寫入日志
summary_writer = tf.summary.FileWriter(SUMMARY_DIR, sess.graph)
tf.global_variables_initializer().run()
for i in range(TRAIN_STEPS):
xs, ys = mnist.train.next_batch(BATCH_SIZE)
# 運行訓練步驟以及所有的日志生成操作,得到這次運行的日志。
summary, _ = sess.run([merged, train_step], feed_dict={x: xs, y_: ys})
# 將得到的所有日志寫入日志文件,這樣TensorBoard程序就可以拿到這次運行所對應的
# 運行信息。
summary_writer.add_summary(summary, i)
summary_writer.close()
if __name__ == '__main__':
main()
運行代碼,如下圖所示:
運行完之后,在C:/log/supervisor.log目錄,會發現有個文件events.out.tfevents.1567588311.1002330QL,在命令行執行命令:
tensorboard --logdir=C:/log/supervisor.log
再到瀏覽器訪問:http://localhost:6006
可以得到如下圖所示的結果:
從程序中可以看出,除了GRSPHS之外,TensorBoard中的每一欄對應了TensorFlow中一種日志生成函數,下表總結了這個對應關系: