3.1 TensorFlow 計(jì)算模型——計(jì)算圖
TensorFlow 是一個(gè)通過計(jì)算圖的形式來表述計(jì)算的編程系統(tǒng)绞绒。TensorFlow 中的每一個(gè)計(jì)算都是計(jì)算圖上的一個(gè)節(jié)點(diǎn)喳篇,而節(jié)點(diǎn)之間的邊描述了計(jì)算之間的依賴關(guān)系阅畴。
TensorFlow 的程序一般可以分為兩個(gè)階段。在第一個(gè)階段需要定義計(jì)算圖中所有的計(jì)算唇辨。第二個(gè)階段為執(zhí)行計(jì)算秧均。以下代碼給出了計(jì)算定義階段的樣例:
import tensorflow as tf
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
在 TensorFlow 程序中,系統(tǒng)會(huì)自動(dòng)維護(hù)一個(gè)默認(rèn)的計(jì)算圖荷并,通過tf.get_default_graph
函數(shù)可以獲取當(dāng)前默認(rèn)的計(jì)算圖合砂。以下代碼示意了如何獲取默認(rèn)計(jì)算圖以及如何查看一個(gè)運(yùn)算所屬的計(jì)算圖:
print(a.graph is tf.get_default_graph())
除了使用默認(rèn)的計(jì)算圖,TensorFlow 支持通過
tf.Graph
函數(shù)來生成新的計(jì)算圖
import tensorflow as tf
g1 = tf.Graph()
with g1.as_default():
v = tf.get_variable("v", initializer = tf.zeros_initializer()(shape = [1]))
g2 = tf.Graph()
with g2.as_default():
v = tf.get_variable("v", initializer = tf.ones_initializer()(shape = [1]))
with tf.Session(graph = g1) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("", reuse = True):
print(sess.run(tf.get_variable("v")))
with tf.Session(graph = g2) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("", reuse = True):
print(sess.run(tf.get_variable("v")))
以上代碼產(chǎn)生了兩個(gè)計(jì)算圖源织,每個(gè)計(jì)算圖中定義了一個(gè)名字為“v”的變量翩伪。在計(jì)算圖 g1 和 g2 中,分別將 v 初始化為 0 和 1谈息≡狄伲可以看到在運(yùn)行不同的計(jì)算圖時(shí),變量 v 的值也不一樣侠仇。
TensorFlow 中的計(jì)算圖不僅可以用來隔離張量和計(jì)算轻姿,還提供了管理張量和計(jì)算的機(jī)制。計(jì)算圖可以通過
tf.Graph.device
函數(shù)來指定運(yùn)行計(jì)算的設(shè)備逻炊。這為 TensorFlow 使用 GPU 提供了機(jī)制互亮。以下程序可以將加法計(jì)算跑在 GPU 上:
import tensorflow as tf
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
g = tf.Graph()
with g.device('/gpu:0'):
result = a + b
sess = tf.Session()
print(sess.run(result))
注意到警告信息
Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
,加入以下代碼忽略此警告:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
3.2 TensorFlow 數(shù)據(jù)模型——張量
在 TensorFlow 中余素,所有的數(shù)據(jù)都通過張量的形式來表示豹休。從功能的角度上看,張量可以簡單理解為多維數(shù)組桨吊。其中零階張量表示標(biāo)量(scalar)威根,也就是一個(gè)數(shù)窑眯;第一階張量為向量(vector),也就是一個(gè)一維數(shù)組医窿;第 n 階張量可以理解為一個(gè) n 維數(shù)組磅甩。但張量在 TensorFlow 中的實(shí)現(xiàn)并不是直接采用數(shù)組的形式,它只是對 TensorFlow 中運(yùn)算結(jié)果的引用姥卢。在張量中并沒有真正保存數(shù)字卷要。它保存的是如何得到這些數(shù)字的計(jì)算過程。
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = tf.add(a, b, name = "add")
print(result)
TensorFlow 計(jì)算的結(jié)果不是一個(gè)具體的數(shù)字独榴,而是一個(gè)張量的結(jié)構(gòu)僧叉。一個(gè)張量中主要保存了三個(gè)屬性:名字(name)、維度(shape)和類型(type)棺榔。
張量的命名通過 node:src_output 的形式來給出瓶堕。其中 node 為節(jié)點(diǎn)的名稱,src_output 表示當(dāng)前張量來自節(jié)點(diǎn)的第幾個(gè)輸出症歇。比如上圖中的
add:0
就說明 result 這個(gè)張量是計(jì)算節(jié)點(diǎn) add 輸出的第一個(gè)結(jié)果郎笆。張量的維度描述了一個(gè)張量的維度信息,比如上圖中
shape=(2,)
說明了張量 result 是一個(gè)長度為2的一維數(shù)組忘晤。張量的第三個(gè)屬性是類型宛蚓,每一個(gè)張量有唯一的類型。TensorFlow 會(huì)對參與運(yùn)算的所有張量進(jìn)行類型的檢查设塔,當(dāng)發(fā)現(xiàn)類型不匹配時(shí)會(huì)報(bào)錯(cuò)凄吏。
通過指定類型避免出錯(cuò):
a = tf.constant([1, 2], name="a", dtype=tf.float32)
TensorFlow 支持 14 種不同的類型,主要包括了實(shí)數(shù)(tf.float32闰蛔、tf.float64)痕钢、整數(shù)(tf.int8、tf.int16序六、tf.int32任连、tf.int64、tf.uint8)难咕、布爾型(tf.bool)和復(fù)數(shù)(tf.complex128)课梳。
張量的第一類用途是對中間計(jì)算結(jié)果的引用。當(dāng)一個(gè)計(jì)算包含很多中間結(jié)果時(shí)余佃,使用張量可以大大提高代碼的可讀性暮刃。
張量的第二類用途是當(dāng)計(jì)算圖構(gòu)造完成之后,張量可以通過會(huì)話來獲得計(jì)算結(jié)果爆土,也就是得到真實(shí)的數(shù)字椭懊。
3.3 TensorFlow 運(yùn)行模型——會(huì)話
會(huì)話(session)擁有并管理 TensorFlow 程序運(yùn)行時(shí)的所有資源。所有計(jì)算完成后需要關(guān)閉會(huì)話來幫助系統(tǒng)回收資源,否則可能出現(xiàn)資源泄漏氧猬。TensorFlow 中使用會(huì)話的模式一般有兩種背犯,第一種模式需要明確調(diào)用會(huì)話生成函數(shù)和關(guān)閉會(huì)話函數(shù),這種模式的代碼流程如下:
# 創(chuàng)建一個(gè)會(huì)話
sess = tf.Session()
# 使用該會(huì)話來得到運(yùn)算結(jié)果
sess.run(...)
# 關(guān)閉會(huì)話盅抚,釋放資源
sess.close()
當(dāng)程序因?yàn)楫惓6顺鰰r(shí)漠魏,關(guān)閉會(huì)話的函數(shù)可能不會(huì)被執(zhí)行從而導(dǎo)致資源泄漏。為了解決異常退出時(shí)資源釋放的問題妄均,TensorFlow 可以通過 Python 的上下文管理器來使用會(huì)話:
with tf.Session() as sess:
# 使用該會(huì)話來得到運(yùn)算結(jié)果
sess.run(...)
# 當(dāng)上下文退出時(shí)會(huì)話關(guān)閉和資源釋放自動(dòng)完成
通過 Python 上下文管理機(jī)制柱锹,只要將所有的計(jì)算放在with
的內(nèi)部就可以解決因?yàn)楫惓M顺鰰r(shí)資源釋放的問題和忘記調(diào)用 Session.close()
函數(shù)而產(chǎn)生的資源泄漏。
TensorFlow 不會(huì)自動(dòng)生成默認(rèn)的會(huì)話丰包,而是需要手動(dòng)指定禁熏。當(dāng)默認(rèn)的會(huì)話被指定之后可用通過tf.Tensor.eval
函數(shù)來計(jì)算一個(gè)張量的取值办铡,以下代碼展示了通過設(shè)定默認(rèn)會(huì)話計(jì)算張量的取值:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
sess = tf.Session()
with sess.as_default():
print(result.eval())
以下代碼也可以完成相同的功能
print(sess.run(result))
print(result.eval(session=sess))
在交互式環(huán)境下得湘,通過設(shè)置默認(rèn)會(huì)話的方式來獲取張量的取值更加方便。所以 TensorFlow 提供了在交互式環(huán)境下直接構(gòu)建默認(rèn)會(huì)話的函數(shù)
tf.InteractiveSession()
秦爆,該函數(shù)會(huì)自動(dòng)將生產(chǎn)的會(huì)話注冊為默認(rèn)會(huì)話寄症。
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
sess = tf.InteractiveSession()
print(result.eval())
sess.close()
3.4 TensorFlow 實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)
在 TensorFlow 中宙彪,變量(tf.Variable)的作用就是保存和更新神經(jīng)網(wǎng)絡(luò)中的參數(shù)。一般使用隨機(jī)數(shù)給 TensorFlow 中的變量初始化瘸爽,以下示例代碼給出了聲明一個(gè) 2×3 的矩陣變量的方法:
weights = tf.Variable(tf.random_normal([2, 3], stddev=2))
該矩陣中的元素是均值為 0您访,標(biāo)準(zhǔn)差為 2 的隨機(jī)數(shù)铅忿。tf.random_normal
函數(shù)通過參數(shù) mean
來指定平均值剪决,默認(rèn)為 0。通過滿足正態(tài)分布的隨機(jī)數(shù)來初始化神經(jīng)網(wǎng)絡(luò)中的參數(shù)是非常常用的方法檀训。
TensorFlow 隨機(jī)數(shù)生成函數(shù)
函數(shù)名稱 | 隨機(jī)數(shù)分布 | 主要參數(shù) |
---|---|---|
tf.random_normal | 正態(tài)分布 | 平均值柑潦、標(biāo)準(zhǔn)差、取值類型 |
tf.truncated_normal | 正態(tài)分布峻凫,如果隨機(jī)值偏離平均值超過2個(gè)標(biāo)準(zhǔn)差渗鬼,將重新隨機(jī) | 平均值、標(biāo)準(zhǔn)差荧琼、取值類型 |
tf.random_uniform | 均勻分布 | 最小譬胎、最大取值,取值類型 |
tf.random_gamma | Gamma分布 | 形狀參數(shù)alpha命锄、尺度參數(shù)beta堰乔、取值類型 |
TensorFlow 常數(shù)生成函數(shù)
函數(shù)名稱 | 功能 | 樣例 |
---|---|---|
tf.zeros | 產(chǎn)生全0的數(shù)組 | tf.zeros([2, 3], tf.int32) |
tf.ones | 產(chǎn)生全1的數(shù)組 | tf.ones([2, 3], tf.int32) |
tf.fill | 產(chǎn)生一個(gè)全部為給定數(shù)字的數(shù)組 | tf.fill([2, 3], 9) |
tf.constant | 產(chǎn)生一個(gè)給定值的常量 | tf.constant([1, 2, 3]) |
在神經(jīng)網(wǎng)絡(luò)中,偏置項(xiàng)(bias)通常會(huì)使用常數(shù)來設(shè)置初始值脐恩。以下代碼生成了一個(gè)初始值全部為 0 且長度為 3 的變量:
biases = tf.Variable(tf.zeros([3]))
除了使用隨機(jī)數(shù)或者常數(shù)镐侯, TensorFlow 也支持通過其他變量的初始值來初始化新的變量:
w2 = tf.Variable(weights.initialized_value())
w3 = tf.Variable(weights.initialized_value() * 2.0)
以下樣例演示了如何通過變量實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的參數(shù)并實(shí)現(xiàn)前向傳播的過程:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 聲明w1、w2兩個(gè)變量驶冒,并通過seed參數(shù)設(shè)定隨機(jī)種子
w1 = tf.Variable(tf.random_normal((2, 3), stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal((3, 1), stddev=1, seed=1))
# 將輸入的特征向量定義為常量
x = tf.constant([[0.7, 0.9]])
# 前向傳播算法
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
# 初始化w1苟翻、w2
sess.run(w1.initializer)
sess.run(w2.initializer)
print(sess.run(y))
sess.close()
雖然直接調(diào)用每個(gè)變量的初始化過程是可行的韵卤,但當(dāng)變量數(shù)目增多或者變量之間存在依賴關(guān)系時(shí),單個(gè)調(diào)用方案比較麻煩崇猫。TensorFlow 提供了
tf.global_variables_initializer()
函數(shù)實(shí)現(xiàn)初始化所有變量的過程沈条。
init_op = tf.global_variables_initializer()
sess.run(init_op)
所有的變量都會(huì)被自動(dòng)加入到 GraphKeys.VARIABLES
集合中。通過 tf.global_variables
函數(shù)可以拿到當(dāng)前計(jì)算圖上的所有變量诅炉,有助于持久化整個(gè)計(jì)算圖的運(yùn)行狀態(tài)拍鲤。當(dāng)構(gòu)建機(jī)器學(xué)習(xí)模型時(shí),可以通過變量聲明函數(shù)中的 trainable
參數(shù)來區(qū)分需要優(yōu)化的參數(shù)(比如神經(jīng)網(wǎng)絡(luò)中的參數(shù))和其他參數(shù)(比如迭代的輪數(shù))汞扎。如果聲明變量時(shí)參數(shù) trainable
為 True季稳,那么這個(gè)變量將會(huì)被加入到 GraphKeys.TRAINABLE_VARIABLES
集合〕浩牵可以通過 tf.trainable_variables()
函數(shù)得到所有需要優(yōu)化的參數(shù)景鼠。TensorFlow 中提供的神經(jīng)網(wǎng)絡(luò)優(yōu)化算法會(huì)將GraphKeys.TRAINABLE_VARIABLES
集合中的變量作為默認(rèn)的優(yōu)化對象。
在神經(jīng)網(wǎng)絡(luò)優(yōu)化算法中痹扇,最常用的方法是反向傳播算法(backpropagation)铛漓。反向傳播算法實(shí)現(xiàn)了一個(gè)迭代的過程。在每次迭代的開始鲫构,首先需要選取一小部分訓(xùn)練數(shù)據(jù)浓恶,這一小部分?jǐn)?shù)據(jù)叫作一個(gè) batch。該 batch 的樣例通過前向傳播算法得到神經(jīng)網(wǎng)絡(luò)模型的預(yù)測結(jié)果结笨,計(jì)算預(yù)測結(jié)果與正確答案的差距包晰。基于預(yù)測值與真實(shí)值之間的差距炕吸,反向傳播算法會(huì)相應(yīng)更新神經(jīng)網(wǎng)絡(luò)參數(shù)的取值伐憾,使得在這個(gè) batch 上神經(jīng)網(wǎng)絡(luò)預(yù)測的結(jié)果與真實(shí)答案更接近。
通過 TensorFlow 實(shí)現(xiàn)反向傳播算法的第一步是使用 TensorFlow 表達(dá)一個(gè) batch 的數(shù)據(jù):
x = tf.constant([[0.7, 0.9]])
如果每輪迭代選取的數(shù)據(jù)都通過常量來表示赫模,那么計(jì)算圖將會(huì)非常大树肃,導(dǎo)致利用率很低。TensorFlow 提供了 placeholder
機(jī)制用于提供輸入數(shù)據(jù)瀑罗,以下代碼給出了通過 placeholder
實(shí)現(xiàn)前向傳播算法:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
# 將輸入的特征向量定義為常量
x = tf.placeholder(tf.float32, shape=(1, 2), name="input")
# 前向傳播算法
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
# 初始化
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(y, feed_dict={x: [[0.7, 0.9]]}))
sess.close()
計(jì)算前向傳播結(jié)果時(shí)胸嘴,需要提供一個(gè)
feed_dict
來指定 x 的取值。feed_dict
是一個(gè)字典(map)斩祭,在字典中需要給出每個(gè)用到的 placeholder
的取值劣像。如果將輸入的 1×2 矩陣改為 n×2 的矩陣,就可以得到 n 個(gè)樣例的前向傳播結(jié)果停忿,以下代碼示例:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
# 將輸入的特征向量定義為常量
x = tf.placeholder(tf.float32, shape=(3, 2), name="input")
# 前向傳播算法
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
# 初始化
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(y, feed_dict={x: [[0.7, 0.9], [0.1, 0.4], [0.5, 0.8]]}))
sess.close()
在得到一個(gè) batch 的前向傳播結(jié)果后驾讲,需要定義一個(gè)損失函數(shù)來刻畫當(dāng)前預(yù)測值與真實(shí)值之間的差距。然后通過反向傳播算法來調(diào)整神經(jīng)網(wǎng)絡(luò)參數(shù)的取值使得差距可以被縮小。
以下是一個(gè)完整的在模擬數(shù)據(jù)集上訓(xùn)練神經(jīng)網(wǎng)絡(luò)解決二分類問題的樣例程序:
import tensorflow as tf
from numpy.random import RandomState
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 定義訓(xùn)練數(shù)據(jù)batch的大小
batch_size = 8
# 定義神經(jīng)網(wǎng)絡(luò)的參數(shù)
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
# 在shape的一個(gè)維度上使用None可以方便使用不同的batch大小
x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input')
y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input')
# 定義神經(jīng)網(wǎng)絡(luò)前向傳播的過程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
# 定義損失函數(shù)和反向傳播算法
y = tf.sigmoid(y)
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)) + (1-y_) * tf.log(tf.clip_by_value(1-y, 1e-10, 1.0)))
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)
# 通過隨機(jī)數(shù)生成一個(gè)模擬數(shù)據(jù)集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 定義規(guī)則來給出樣本的標(biāo)簽
Y = [[int(x1+x2 < 1)] for (x1, x2) in X]
# 創(chuàng)建會(huì)話
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(w1))
print(sess.run(w2))
# 設(shè)定訓(xùn)練輪數(shù)
STEPS = 5000
for i in range(STEPS):
# 每次選取batch_size個(gè)樣本進(jìn)行訓(xùn)練
start = (i * batch_size) % dataset_size
end = min(start+batch_size, dataset_size)
# 通過選取的樣本訓(xùn)練神經(jīng)網(wǎng)絡(luò)并更新參數(shù)
sess.run(train_step, feed_dict={x: X[start: end], y_: Y[start: end]})
if (i % 1000 == 0):
# 每隔一段時(shí)間計(jì)算在所有數(shù)據(jù)上的交叉熵并輸出
total_cross_entropy = sess.run(cross_entropy, feed_dict={x: X, y_: Y})
print("After %d training step(s), cross entropy on all data is %g" % (i, total_cross_entropy))
print(sess.run(w1))
print(sess.run(w2))