第2課: TensorFlow運算
注意:運算的介紹可能比較枯燥,但是是基礎(chǔ),之后會更有趣。
1. TensorBoard
TensorFlow不僅僅是一個軟件包,它是一個包括TensorFlow,TensorBoard和Tensor Serving的套件。為了充分利用TensorFlow,我們應(yīng)該知道如何將它們結(jié)合起來使用,所以首先我們來了解TensorBoard。
Tensor是TensorFlow安裝程序自帶的圖形可視化工具,用Google自己的話說:
“你使用TensorFlow進行的例如訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)的計算可能會非常復(fù)雜和難懂。為了更簡單的理解,調(diào)試和優(yōu)化TensorFlow程序,我們提供了一個名叫TensorBoard的可視化套件?!?/p>
TensorBoard配置好后,大概是這個樣子:

當用戶在一個激活了TensorBoard的TensorFlow程序上進行運算時,這些運算都會被導(dǎo)出到事件日志(event log)文件中。TensorBoard能夠?qū)⑦@些事件日志可視化以便可以深入了解模型的計算圖和它的運行時行為。越早和越經(jīng)常的使用TensorBoard會使TensorFlow上的工作更有趣且更有成效。
接下來讓我們編寫第一個TensorFlow程序,然后使用TensorBoard可視化程序的計算圖。為了使用TensorBoard進行可視化,我們需要寫入程序的事件日志。
import tensorflow as tf
a = tf.constant(2)
b = tf.constant(3)
x = tf.add(a, b)
writer = tf.summary.FileWriter([logdir], [graph])
with tf.Session() as sess:
print(sess.run(x))
writer.close()
[graph]是程序的計算圖,你可以用tf.get_default_graph()獲得程序默認的計算圖,也可以用sess.graph獲得Session處理的計算圖。(當然你需要創(chuàng)建一個Session)不管怎樣,確定在創(chuàng)建writer之前你已經(jīng)定義了完整的計算圖,不然TensorBoard可視化的結(jié)果將會不完整。
[logdir]是你希望存儲事件日志的目錄。
接下來,打開終端,先運行剛才的Tensorflow程序,再以剛才寫入的日志目錄作為參數(shù)運行TensorBoard。
$ python3 [my_program.py]
$ tensorboard --logdir [logdir] --port [port]
最后打開瀏覽器,輸入http://localhost:[port]/(端口號自己選擇),你將會看到TensorBoard頁面。點擊Graph標簽?zāi)憧梢圆榭从嬎銏D中有3個節(jié)點:2個常量運算和一個add運算。

“Const”和“Const_1"代表a和b,節(jié)點”Add“對應(yīng)x。我們可以在代碼中給a,b和x命名讓TensorBoard了解這些運算的名字。
a = tf.constant(2, name="a")
b = tf.constant(3, name="b")
x = tf.add(a, b, name="add")
現(xiàn)在如果你再次運行程序和TensorBoard,你會看到:

計算圖自己定義了運算和運算間依賴關(guān)系,只要簡單的點擊節(jié)點就可以查看值和節(jié)點類型。

Note:如果你運行了程序很多次,在日志目錄會有多個事件日志文件。TensorBoard只會顯示最后一個計算圖并且警告有多個日志文件,如果想避免警告就刪除不需要的日志文件。
當然,TensorBoard能做的遠遠不止于可視化計算圖,這里我們將介紹它最重要的一些功能。
2. 常量運算(Constant op)
創(chuàng)建常量運算很直接。
tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False)
# constant of 1d tensor (vector)
a = tf.constant([2, 2], name="vector")
# constant of 2x2 tensor (matrix)
b = tf.constant([[0, 1], [2, 3]], name="matrix")
你可以用指定值初始化一個特定維度的tensor,就像Numpy一樣。
tf.zeros(shape, dtype=tf.float32, name=None)
# create a tensor of shape and all elements are zeros
tf.zeros([2, 3], tf.int32) ==> [[0, 0, 0], [0, 0, 0]]
tf.zeros_like(input_tensor, dtype=None, name=None, optimize=True)
# create a tensor of shape and type (unless type is specified) as the input_tensor but all elements are zeros.
# input_tensor [[0, 1], [2, 3], [4, 5]]
tf.zeros_like(input_tensor) ==> [[0, 0], [0, 0], [0, 0]]
tf.ones(shape, dtype=tf.float32, name=None)
# create a tensor of shape and all elements are ones
tf.ones([2, 3], tf.int32) ==> [[1, 1, 1], [1, 1, 1]]
tf.ones_like(input_tensor, dtype=None, name=None, optimize=True)
# create a tensor of shape and type (unless type is specified) as the input_tensor but all elements are ones.
# input_tensor is [[0, 1], [2, 3], [4, 5]]
tf.ones_like(input_tensor) ==> [[1, 1], [1, 1], [1, 1]]
tf.fill(dims, value, name=None)
# create a tensor filled with a scalar value.
tf.fill([2, 3], 8) ==> [[8, 8, 8], [8, 8, 8]]
你可以創(chuàng)建一個常量序列
tf.lin_space(start, stop, num, name=None)
# create a sequence of num evenly-spaced values are generated beginning at start. If num > 1, the values in the sequence increase by (stop - start) / (num - 1), so that the last one is exactly stop.
# comparable to but slightly different from numpy.linspace
tf.lin_space(10.0, 13.0, 4, name="linspace") ==> [10.0 11.0 12.0 13.0]
tf.range([start], limit=None, delta=1, dtype=None, name='range')
# create a sequence of numbers that begins at start and extends by increments of delta up to but not including limit
# slight different from range in Python
tf.range(3, 18, 3) ==> [3, 6, 9, 12, 15]
tf.range(3, 1, -0.5) ==> [3, 2.5, 2, 1.5]
tf.range(5) ==> [0, 1, 2, 3, 4]
需要注意的是和Numpy的序列不同,TensorFlow的序列是不能迭代的。
for _ in np.linspace(0, 10, 4): # OK
for _ in tf.linspace(0.0, 10.0, 4): # TypeError: 'Tensor' object is not iterable.
for _ in range(4): # OK
for _ in tf.range(4): # TypeError: 'Tensor' object is not iterable.
你也可以創(chuàng)建服從指定分布的隨機常量。
tf.random_normal
tf.truncated_normal
tf.random_uniform
tf.random_shuffle
tf.random_crop
tf.multinomial
tf.random_gamma
tf.set_random_seed
3. 數(shù)學(xué)運算
TensorFlow的數(shù)學(xué)運算符很標準,你可以在這里找到完整的列表。
- TensorFlow大量的除法運算
TensorFlow至少支持7種除法運算,做著或多或少一樣的事情:tf.div, tf.divide, tf.truediv, tf.floordiv, tf.realdiv, tf.truncateddiv, tf.floor_div。創(chuàng)建這些運算的人一定非常喜歡除法。。。
a = tf.constant([2, 2], name='a')
b = tf.constant([[0, 1], [2, 3]], name='b')
with tf.Session() as sess:
print(sess.run(tf.div(b, a))) ? [[0 0] [1 1]]
print(sess.run(tf.divide(b, a))) ? [[0. 0.5] [1. 1.5]]
print(sess.run(tf.truediv(b, a))) ? [[0. 0.5] [1. 1.5]]
print(sess.run(tf.floordiv(b, a))) ? [[0 0] [1 1]]
print(sess.run(tf.realdiv(b, a))) ? # Error: only works for real values
print(sess.run(tf.truncatediv(b, a))) ? [[0 0] [1 1]]
print(sess.run(tf.floor_div(b, a))) ? [[0 0] [1 1]]
- tf.add_n
將多個tensor相加。
tf.add_n([a, b, b]) => equivalent to a + b + b
- 點積
注意tf.matmul不是點積,而是使用tf.tensordot。
a = tf.constant([10, 20], name='a')
b = tf.constant([2, 3], name='b')
with tf.Session() as sess:
print(sess.run(tf.multiply(a, b))) ? [20 60] # element-wise multiplication
print(sess.run(tf.tensordot(a, b, 1))) ? 80
下面是Python中的運算,摘自Fundamentals of DeepLearning。

4. 數(shù)據(jù)類型
- Python原生類型
TensorFlow兼容Python的原生數(shù)據(jù)類型,例如:boolean,integer,float和string等。單獨的值轉(zhuǎn)換為0維tensor(標量,scalar),列表轉(zhuǎn)換為1維tensor(向量,vector),列表的列表轉(zhuǎn)換為2維tensor(矩陣,matrix),以此類推。
t_0 = 19 # Treated as a 0-d tensor, or "scalar"
tf.zeros_like(t_0) # ==> 0
tf.ones_like(t_0) # ==> 1
t_1 = [b"apple", b"peach", b"grape"] # treated as a 1-d tensor, or "vector"
tf.zeros_like(t_1) # ==> [b'' b'' b'']
tf.ones_like(t_1) # ==> TypeError
t_2 = [[True, False, False],
[False, False, True],
[False, True, False]] # treated as a 2-d tensor, or "matrix"
tf.zeros_like(t_2) # ==> 3x3 tensor, all elements are False
tf.ones_like(t_2) # ==> 3x3 tensor, all elements are True
- TensorFlow原生類型
TensorFlow也有自己的原生類型:tf.int32, tf.float32,還有更令人興奮的類型:tf.bfloat, tf.complex, tf.quint,完整的類型列表在這里。
- Numpy數(shù)據(jù)類型
到此為止,你可能已經(jīng)注意到Numpy和TensorFlow數(shù)據(jù)類型的相似性。TensorFlow被設(shè)計為和Numpy無縫集成,這個軟件包已經(jīng)成為數(shù)據(jù)科學(xué)的通用語。
TensorFlow的數(shù)據(jù)類型是基于Numpy的,實際上np.int32 == tf.int32返回True。你可以將Numpy類型傳給TensorFlow函數(shù)。
tf.ones([2, 2], np.float32) ==> [[1.0 1.0], [1.0 1.0]]
5. 變量
- 創(chuàng)建變量
使用tf.Variable創(chuàng)建變量,應(yīng)該注意的是tf.constant是小寫的而tf.Variable是大寫的,這是因為tf.constant是一個運算而tf.Variable是一個含有多個運算的類。
x = tf.Variable(...)
x.initializer # init
x.value() # read op
x.assign(...) # write op
x.assign_add(...)
# and more
傳統(tǒng)的創(chuàng)建變量方式為
tf.Variable(<initial-value>, name=<optional-name>)
s = tf.Variable(2, name="scalar")
m = tf.Variable([[0, 1], [2, 3]], name="matrix")
W = tf.Variable(tf.zeros([784,10]))
而TensorFlow推薦使用tf.get_variable來創(chuàng)建變量,這樣有利于變量的共享。
tf.get_variable(
name,
shape=None,
dtype=None,
initializer=None,
regularizer=None,
trainable=True,
collections=None,
caching_device=None,
partitioner=None,
validate_shape=True,
use_resource=None,
custom_getter=None,
constraint=None
)
s = tf.get_variable("scalar", initializer=tf.constant(2))
m = tf.get_variable("matrix", initializer=tf.constant([[0, 1], [2, 3]]))
W = tf.get_variable("big_matrix", shape=(784, 10), initializer=tf.zeros_initializer())
- 初始化變量
你必須在使用變量之前初始化它們,否則將會報FailedPreconditionError: Attempting to use uninitialized value的錯誤。想查看所有沒初始化的變量,你可以將它們打印出來:
print(session.run(tf.report_uninitialized_variables()))
簡單的將所有變量一次性初始化的方法為:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
在這種情況下你用tf.Session.run()獲得的是一個initializer而不是之前的tensor運算。
如果只初始化一部分變量,你可以使用tf.variables_initializer():
with tf.Session() as sess:
sess.run(tf.variables_initializer([a, b]))
你也可以用tf.Variable.initializer()一個個的初始化變量:
with tf.Session() as sess:
sess.run(W.initializer)
還有一種初始化變量方式是從一個文件讀取,我們會在后面的課程提到。
- 計算(Evaluate)變量的值
和tensor類似,可以用session獲取變量的值。
# V is a 784 x 10 variable of random values
V = tf.get_variable("normal_matrix", shape=(784, 10),
initializer=tf.truncated_normal_initializer())
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
print(sess.run(V))
也可以通過tf.Variable.eval()來獲取變量的值。
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
print(V.eval())
- 給變量賦值
我們可以通過tf.Variable.assign()來給變量賦值。
W = tf.Variable(10)
W.assign(100)
with tf.Session() as sess:
sess.run(W.initializer)
print(W.eval()) # >> 10
為什么輸出的10而不是100?W.assign(100)并沒有給W賦值而是創(chuàng)建了一個assign運算,要想使這個運算生效我們可以在session中運行這個運算。
W = tf.Variable(10)
assign_op = W.assign(100)
with tf.Session() as sess:
sess.run(assign_op)
print(W.eval()) # >> 100
注意這次我們沒有初始化W,因為assign為我們做了。實際上initializer就是一個assign運算,它用初始值來初始化變量。
# in the source code
self._initializer_op = state_ops.assign(self._variable, self._initial_value, validate_shape=validate_shape).op
為了簡化變量的加減運算,TensorFlow提供了tf.Variable.assign_add()和tf.Variable.assign_sub()方法。不同于tf.Variable.assign(),這兩個方法不會初始化變量,因為它們依賴變量的初始值。
W = tf.Variable(10)
with tf.Session() as sess:
sess.run(W.initializer)
print(sess.run(W.assign_add(10))) # >> 20
print(sess.run(W.assign_sub(2))) # >> 18
因為TensorFlow的Session們維護著各自的值,每個Session擁有變量屬于Session自己的當前值。
W = tf.Variable(10)
sess1 = tf.Session()
sess2 = tf.Session()
sess1.run(W.initializer)
sess2.run(W.initializer)
print(sess1.run(W.assign_add(10))) # >> 20
print(sess2.run(W.assign_sub(2))) # >> 8
print(sess1.run(W.assign_add(100))) # >> 120
print(sess2.run(W.assign_sub(50))) # >> -42
sess1.close()
sess2.close()
當你有一個依賴其它變量的變量時,假設(shè)你聲明了U = W * 2
# W is a random 700 x 10 tensor
W = tf.Variable(tf.truncated_normal([700, 10]))
U = tf.Variable(W * 2)
在這種情況下,你應(yīng)該使用initialized_value()方法去保證在使用W的值去初始化U之前W已經(jīng)被初始化。
U = tf.Variable(W.initialized_value() * 2)
6. 交互Session(Interactive Session)
你有時候會看到InteractiveSession代替Session,它們之間唯一的不同是InteractiveSession會把自己設(shè)置為默認的Session,這樣你就可以直接調(diào)用run()和eval()方法。這樣方便了在Shell和IPython Notebook中使用,但是當有多個Session時使問題變得復(fù)雜。
sess = tf.InteractiveSession()
a = tf.constant(5.0)
b = tf.constant(6.0)
c = a * b
print(c.eval()) # we can use 'c.eval()' without explicitly stating a session
sess.close()
tf.get_default_session()方法返回當前線程的默認Session。
7. 控制依賴關(guān)系(Control Dependencies)
有時候,我們擁有兩個或者更多獨立的運算,而我們希望指定哪些運算應(yīng)該先運行。這個情況下,我們可以使用tf.Graph.control_dependencies([control_inputs])。
# your graph g have 5 ops: a, b, c, d, e
with g.control_dependencies([a, b, c]):
# `d` and `e` will only run after `a`, `b`, and `c` have executed.
d = ...
e = …
8. 導(dǎo)入數(shù)據(jù)
8.1 傳統(tǒng)的方法: placehoder和feed_dict
TensorFlow 程序一般由兩個階段:
- 組裝一個計算圖
- 用Session在計算圖中執(zhí)行運算和評估變量
我們可以在不管計算所需的數(shù)值的情況下組裝計算圖,這個在不知道輸入數(shù)據(jù)的情況下定義函數(shù)是一樣的。
在計算圖組裝完成后,我們可以用placeholder將自己的數(shù)據(jù)灌入計算圖中:
tf.placeholder(dtype, shape=None, name=None)
a = tf.placeholder(tf.float32, shape=[3]) # a is placeholder for a vector of 3 elements
b = tf.constant([5, 5, 5], tf.float32)
c = a + b # use the placeholder as you would any tensor
with tf.Session() as sess:
print(sess.run(c))
當我們嘗試通過Session計算c的值時,我們將會獲得一個錯誤,因為我們需要獲得a的值。我們可以通過feed_dict來向placeholder灌數(shù)據(jù),它是一個字典。
with tf.Session() as sess:
# compute the value of c given the value of a is [1, 2, 3]
print(sess.run(c, {a: [1, 2, 3]})) # [6. 7. 8.]
這時我們再查看TensorBoard,計算圖如下:

我們可以向placeholder中多次灌入不同的值。
with tf.Session() as sess:
for a_value in list_of_a_values:
print(sess.run(c, {a: a_value}))
你也可以向不是placeholder的tensor灌入數(shù)值,所有的tensor都是可以灌值的,可以用tf.Graph.is_feedable(tensor)方法查看一個tensor是否是可以灌值的。
a = tf.add(2, 5)
b = tf.multiply(a, 3)
with tf.Session() as sess:
print(sess.run(b)) # >> 21
# compute the value of b given the value of a is 15
print(sess.run(b, feed_dict={a: 15})) # >> 45
feed_dict對測試你的模型非常有用,當你有一個很大的計算圖但只想測試其中某一塊時,你可以灌入假值來避免在不必要的運算上浪費時間。
8.2 新的方法:tf.data
這個方法需要配合例子來講,所以我們會在下一課線性和邏輯回歸中涉及。
9.lazy loading的陷阱
現(xiàn)在TensorFlow中最常見的不是bug的bug叫做“l(fā)azy loading”。Lazy loading是一種設(shè)計模式,即在你要使用一個對象時才初始化對象。在TensorFlow的場景中,它的含義是在要執(zhí)行一個運算時才創(chuàng)建這個運算。例如:
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
writer = tf.summary.FileWriter('graphs/lazy_loading', sess.graph)
for _ in range(10):
sess.run(tf.add(x, y))
print(tf.get_default_graph().as_graph_def())
writer.close()
現(xiàn)在我們看看TensorBoard:

打印計算圖的定義:
print(tf.get_default_graph().as_graph_def())
得到如下結(jié)果:
node {
name: "Add_1"
op: "Add"
input: "x_1/read"
input: "y_1/read"
attr {
key: "T"
value {
type: DT_INT32
}
}
}
…
…
…
node {
name: "Add_10"
op: "Add"
...
}
你可能回想:“這很愚蠢,為什么我要在一個相同的值上計算兩次?”,然后認為這是一個bug。這種情況會經(jīng)常發(fā)生,比如在你想在訓(xùn)練集的每個batch上計算損失函數(shù)或做預(yù)測時,如果你不注意,可能會添加巨量的無用運算。
Note:在翻譯這篇文章時,譯者用TensorFlow 1.8版本做了實驗,這個bug應(yīng)該沒有了。