一文教你成爲TFboys (TensorFlow入門篇)

TensorFlow越來越成爲深度學習領域最火的框架之一,本文會簡要的介紹TensorFlow的基本概念,並通過一個簡單的線性迴歸介紹這些概念的實際使用。讓我們一起學習如果修煉成爲一個TFboys吧~~~


目錄

一、Tensorflow資源

(1)Tensorflow教程資源:

(2)Tensorflow視頻資源

(3)Tensorflow項目實戰資源

二、概述

三、Tensor

四、數據流圖

五、Operation

六、Graph

(1)爲什麼要使用數據流圖

七、什麼是tf.Graph

(1)tf.Graph的創建

(2)Graph的name space

八、常量

九、placeholder和feeddict

十、變量

(1)基本概念

(2)tf.Variable()和tf.get_variable()的區別

(3)name scope和variable scope

十一、Session

十二、常見錯誤

十三、layers

十四、線性迴歸的例子

(1)定義數據

(2)定義loss

(3)定義訓練Op

(4)進行訓練


一、Tensorflow資源

(1)Tensorflow教程資源:

  1. 適合初學者的Tensorflow教程和代碼示例。該教程不光提供了一些經典的數據集,更是從實現最簡單的“Hello World”開始,到機器學習的經典算法,再到神經網絡的常用模型,一步步帶你從入門到精通,是初學者學習Tensorflow的最佳教程。
  2. 從Tensorflow基礎知識到有趣的項目應用。同樣是適合新手的教程,從安裝到項目實戰,教你搭建一個屬於自己的神經網絡。
  3. 使用Jupyter Notebook用Python語言編寫的TensorFlow教程。 本教程是基於Jupyter Notebook開發環境的Tensorflow教程,Jupyter Notebook是一款非常好用的交互式開發工具,不僅支持40多種編程語言,還可以實時運行代碼、共享文檔、數據可視化、支持markdown等,適用於機器學習、統計建模數據處理、特徵提取等多個領域。
  4. 構建您的第一款TensorFlow Android應用程序。本教程可幫助您從零開始將張量流模型引入到Android應用程序
  5. Tensorflow代碼練習。一個從易到難的Tensorflow代碼練習手冊。非常適合學習Tensorflow的小夥伴。
  6. Tensorflow中文社區 

(2)Tensorflow視頻資源

  1. TF boys 修煉指南。一個Tensorflow從零開始的公開視頻課程,課程偏基礎、入門,但知識點講的非常詳細。
  2. 煉數成金Tensorflow公開課。非常不錯的課程,推薦給大家。
  3. 當然還有臺灣國立大學李宏毅教程深度學習的課程也值得推薦給大家
  4. 英文不錯的小夥伴,也爲大家推薦一些國外大牛的英文課程
  5. 介紹了這麼多課程,怎麼能少了斯坦福大學Tensorflow系列的課程!!!話不多說,直接上鏈接。 課程主頁。課程所有的ppt和筆記notes。課程相關實戰
  6. 最後,怎麼能忘了谷歌爸爸發佈在Tensorflow官網上的視頻教程,針對Tensorflow初級學習的小夥伴還是非常不錯的一套課程,有助於大家快速入門。

(3)Tensorflow項目實戰資源

  1. 一個實現實現Alex Graves論文的隨機手寫生成的案例
  2. 基於Tensorflow的生成對抗文本到圖像合成。如下圖所示,該項目是基於Tensorflow的DCGAN模型,教大家一步步從對抗生成文本到圖像合成。
  3. 基於注意力的圖像字幕生成器:。該模型引入了基於注意力的圖像標題生成器。可以將其注意力轉移到圖像的相關部分,同時生成每個單詞。
  4. 神經網絡着色灰度圖像。一個非常有趣且應用場景非常廣的一個項目,使用神經網絡着色灰度圖像。
  5. 基於Facebook中FastText的簡單嵌入式文本分類器。該項目是源於Facebook中的FastText的想法,並在Tensorflow中實施。FastText是一款快速的文本分類器,提供而高效的文本分類和表徵學習的方法。
  6. 用Tensorflow實現“基於句子分類的卷積神經網絡”
  7. 使用OpenStreetMap功能和衛星圖像訓練TensorFlow神經網絡。該項目是通過使用OpenStreetMap(OSM)數據訓練神經網絡,進而對衛星圖像中的特徵進行分類。
  8. 用Tenflow實現YOLO:“實時對象檢測”,並支持實時在移動設備上運行的一個小項目。計算機視覺領域研究者的最佳福利。

二、概述

TensorFlow™ 是一個採用數據流圖(data flow graphs),用於數值計算的開源軟件庫。節點(Nodes)在圖中表示數學操作,圖中的線(edges)則表示在節點間相互聯繫的多維數據數組,即張量(tensor)。它靈活的架構讓你可以在多種平臺上展開計算,例如臺式計算機中的一個或多個CPU(或GPU),服務器,移動設備等等。TensorFlow 最初由Google大腦小組(隸屬於Google機器智能研究機構)的研究員和工程師們開發出來,用於機器學習和深度神經網絡方面的研究,但這個系統的通用性使其也可廣泛用於其他計算領域。

 

TensorFlow中計算的定義和計算的執行是分開的。我們編寫TensorFlow程序通常分爲兩步:定義計算圖;使用session執行計算圖。不過TensorFlow 1.5之後引入了Eager Execution,使得我們不需要定義計算圖,直接就可以執行計算,從而簡化代碼尤其是簡化調試。因爲本課程不會用到Eager Execution,所以略過,有興趣的讀者可以參考Tensorflow官方文檔。

三、Tensor

Tensor就是一個n維數組,0維數組表示一個數(scalar),1維數組表示一個向量(vector),二維數字表示一個矩陣(matrix)。

一個Tensor裏的數據都是同一種類型的,比如tf.float32或者tf.string。數組的維度個數叫作rank,比如scalar的rank是0,矩陣的rank是2。數組每一維的大小組成的list叫作shape。比如下面是一些Tensor的例子:

3.0 # rank爲0的tensor; 一個scalar,它的shape是[](沒有shape信息),
[1., 2., 3.] # rank爲1的tensor; 一個vector,它的shape是[3]
[[1., 2., 3.], [4., 5., 6.]] # 一個rank爲2的tensor; 一個matrix,shape是[2, 3]
[[[1., 2., 3.]], [[7., 8., 9.]]] # rank爲3的tensor,shape是[2, 1, 3]

Tensorflow和numpy一樣,讀shape時應該從外向內讀。
先舉個例子:

[[1,2,3], [4,5,6]]

 [[1,2,3],
  [4,5,6]]

是一樣的,都是2行3列(shape=[2,3])。應該怎麼記呢?
這個矩陣,先拿掉最外層中括號,變成[1,2,3], [4,5,6],[1,2,3]和 [4,5,6]被逗號隔開成2塊,理解爲有2個元素,每個元素(如[1,2,3])拿掉中括號後,剩下1、2和3被逗號隔開,理解爲有3個元素,所以是shape=[2,3]。
再換個例子,如果shape=[1,1,1],那它會接收什麼樣的數據?

我們根據規則,第1個數字爲“1”表示最外層的元素個數只有1個。
[a]
第二層的數字爲“1”表示拿掉一次括號後,剩下的仍然只有1個元素.
[ [a] ]
相應的,第3個“1”表示再拿掉一次括號後還是隻剩1個元素
[ [ [a] ] ]就是結果。
shape = [1,1,2]表示數據應該是這樣的:[[[a,b]]]。
回到最開始,x應該輸入的是[[[a,b,c],[d,e,f]]]這樣格式的, 即shape=[1, 2, 3]。

 

TensorFlow使用numpy的ndarray來表示Tensor的值。有幾種重要的特殊Tensor類型,包括:

  • tf.Variable
  • tf.constant
  • tf.placeholder
  • tf.SparseTensor

除了Variable,其它類型的Tensor都是不可修改的對象,因此在一次運算的執行時它只會有一個值。但是這並不是說每次執行是值是不變的,因爲有些Tensor可能是有隨機函數生成的,每次執行都會產生不同的值(但是在一次執行過程中只有一個值)。

Tensor支持常見的slice操作,比如下面的slice得到矩陣的第4列(下標從0開始):

my_column_vector = my_matrix[:, 3]

另外Tensor經常使用的函數是reshape,用來改變它的shape,比如輸入的圖像可能是二維的矩陣比如[28,28],但是我們如果使用全連接的網絡需要把展開成一維的向量,那麼我們可以這樣:

# images是(batch, width, height, channel)
images = tf.random_uniform([32, 28, 28, 1], maxval=255, dtype=tf.int32)
print(images.shape) # (32, 28, 28, 1)
images = tf.reshape(images, [32, -1])
print(images.shape) # (32, 784)

另外一種常見的操作就是修改Tensor的數據類型,比如我們輸入的圖像是0-255的灰度值,我們需要把它變成(0,1)之間的浮點數:

images = tf.cast(images, dtype=tf.float32)
images = images/255.0
with tf.Session() as sess:
	print(sess.run(images[0]))

四、數據流圖

TensorFlow的計算圖使用數據流圖來表示。圖中有兩種類型的對象:

  • Operations(簡稱ops) 圖中的點。

    Operation表示計算,它的輸入和輸出都是Tensor。

  • Tensors 圖中的邊。

    Tensor在圖中的“流動”表示了數據的變化和處理,這也是TensorFlow名字的由來。大部分TensorFlow函數都會返回Tensor。

需要注意的是,tf.Tensor並不存儲值,它只是數據流圖中的節點,它表示一個計算,這個計算會產生一個Tensor。比如下面的例子:

a = tf.constant(3.0, dtype=tf.float32)
b = tf.constant(4.0) # 也是tf.float32,通過4.0推測出來的類型。
total = a + b
print(a)
print(b)
print(total)

它的運行結果爲:

Tensor("Const:0", shape=(), dtype=float32)
Tensor("Const_1:0", shape=(), dtype=float32)
Tensor("add:0", shape=(), dtype=float32)

print a,b和c並不會得到3,4和7。這裏的a,b和c只是Graph中的Operation,執行這些Operation纔會得到對應的Tensor值。每個Tensor都有一個數據類型dtype,tf.constant()函數會根據我們傳入的值推測其類型,對於浮點數,默認類型是tf.float32。這和傳統的編程語言有一些區別,對於c/c++/java語言來說,3.0這個字面量代表的是雙精度浮點數(double或者TensorFlow的float64),而對於Python來說只有雙精度浮點數(類型叫float)。因爲對於大部分機器學習算法來說,單精度浮點數以及夠用了,使用雙精度浮點數需要更多的內存和計算時間,而且很多GPU的雙精度浮點數計算速度要比單精度慢幾十倍(不同的架構差別很大,比如Nvidia的GTX系列雙精度慢很多,但是Nvidia的Tesla系列差別較小),因此TensorFlow默認會把3.0推測爲tf.float32。

比如我們如下最簡單的代碼:

import tensorflow as tf
a = tf.add(3, 4) 

我們使用TensorBoard(後面會介紹)可以看到實際的數據流圖如下圖所示。在數據流中每一個點表示一個Operation,比如add,每一條邊表示一個Tensor。讀者可能會奇怪,哪裏來的x和y呢?x和y是TensorFlow自動爲我們創建了兩個Tensor 3和4。因爲add函數會把兩個Tensor加起來,它需要兩個Tensor作爲參數,但是我們傳入的是兩個數字,因此TensorFlow會自動的幫我們創建兩個Constant,並且命名爲x和y。因此下面的代碼和上面的是等價的:

x=tf.constant(3, name="x")
y=tf.constant(4, name="y")
a=tf.add(x,y)

注意constant不是一個Tensor,但是它內部保存了一個Tensor,當把它作爲add的一個參數的時候,它們之間的邊就表示把x內部的Tensor傳給add。我們可以使用TensorBoard查看x的內容如下:

dtype {"type":"DT_INT32"}
value {"tensor":{"dtype":"DT_INT32","tensor_shape":{},"int_val":3}}

圖:數據流圖

如果我們執行下面的代碼:

import tensorflow as tf
a = tf.add(3, 4) 
print(a)

有點讀者可能期望得到結果7,但是實際結果卻是:

Tensor("Add:0", shape=(), dtype=int32)

原因就是add返回的就是數據流圖中的一個Operation,我們只是“定義”了一個計算圖,但是目前還沒有“執行”它。那怎麼執行它呢?我們需要創建一個Session對象,然後用這個Session對象來執行圖中的某些Operation。比如下面的代碼就會定義出計算的結果7。

import tensorflow as tf
a = tf.add(3, 4)
sess = tf.Session()
print(sess.run(a))
sess.close()

這有些麻煩,使用Eager Execution會簡單一些,但是目前它不能完全替代這種方法。上面創建Session,然後關閉Session的寫法可以使用with,這樣不會忘了關閉它。

import tensorflow as tf
a = tf.add(3, 4)
with tf.Session() as sess:
	print(sess.run(a))

五、Operation

Operation就是數據流圖中的點,TensorFlow內置了常見的Operation,包括加減乘除等算術運算,大於小於等邏輯運算,Tensor的concat、reshape、slice等操作,矩陣的乘法、求逆操作,變量的賦值、自增等操作,Softmax、relu、conv2d等神經網絡運算,用於保存模型checkpoint的save、reload等操作,隊列的enqueue、dequeue等操作。大部分的TensorFlow函數返回都是一個Operation,它並不會立刻產生效果(執行),而只是定義要做的事情。

對於常見的數學運算,比如加減乘除,爲了使用方便,TensorFlow實現了Operator的重載,因此下面的代碼最後兩行的效果是一樣的:

a=tf.constant(4)
b=tf.constant(5)
c=a+b
d=tf.add(a,b)

通常我們通過函數定義Operation之間的依賴關係,比如上面的代碼,add產生的Operation會依賴a和b。但有的時候,我們需要某個Operation在其它的一些Operation之後再執行,但是它們並沒有直接的函數關係,那麼我們可以使用tf.Graph.control_dependencies來定義這種先後順序關係。比如:

#graph g有5個ops: a, b, c, d, e
g = tf.get_default_graph()
with g.control_dependencies([a, b, c]):
	# 只有當a b c都執行和纔會執行d和e。
	d = ...
	e = ...

需要注意的是只有在control_dependencies下創建的op纔會建立這種依賴關係。比如下面的例子:

x = tf.Variable(0.0)
x_plus_1 = tf.assign_add(x, 1)

with tf.control_dependencies([x_plus_1]):
	y = x
init = tf.initialize_all_variables()

with tf.Session() as session:
	init.run()
	for i in xrange(5):
		print(y.eval())

看起來計算y的時候會依賴x_plus_1操作,從而給x加一。那麼最終似乎應該輸出[0, 1, 2, 3, 4]。但是讀者如果執行一下,會發現輸出的卻是5個0。原因在於y = x只是讓變量y指向x一樣的地址,並不會在Tensorflow的計算圖裏創建一個新的op。如果要實現上面的效果,我們可以使用tf.identity操作,這個操作實現賦值,它會在計算圖裏創建一個op用於賦值。把y = x改成就可以了。

y = tf.identity(x)

tf.identity有很多用途,其中之一是control_dependencies配合,和這個技巧在CIFAR10的多GPU例子能看到:

with tf.control_dependencies([loss_averages_op]):
	total_loss = tf.identity(total_loss)

total_loss已經定義好了,顯然不能再定義一次了。但是我們又需要讓它依賴loss_averages_op,那怎麼辦呢?tf.identity這個時候就派上用場了,它創建了一個新的op,返回的值和原來並沒有不同。

六、Graph

TensorFlow使用數據流圖來表示計算之間的依賴關係。然後通過session來執行這個圖的某個子圖(當然也可以是整個圖)來完成模型的訓練或者預測。

(1)爲什麼要使用數據流圖

數據流圖是並行編程的一種常見模型。在數據流圖裏,節點表示計算(operation),而邊表示數據(Tensor)。比如tf.matmul這個函數返回一個operation,這個operation的輸入是兩個矩陣(Tensor),輸出是這兩個矩陣的乘積。使用數據流圖有如下好處:

  • 並行計算 通過邊來顯示的定義計算的依賴關係,從而讓執行引擎更容易實現並行計算
  • 分佈式計算 同樣的道理,數據流圖使得分佈式計算變得容易
  • 編譯 TensorFlow的XLA編譯器能用數據流圖裏的信息來生成更快的代碼
  • 可移植性 通過數據流圖定義了一種語言無關的模型從而可以實現語言和平臺之間的移植。比如我們後面會介紹在生產環境常見的方式:使用Python來訓練模型,然後使用SaveModel API保存模型,然後使用C++的Tensorflow Serving來提供實時的模型預測功能。

七、什麼是tf.Graph

tf.Graph對象包括兩部分信息:

  • 圖結構

    包括點和邊,分佈代表計算和數據

  • 集合

    tf.add_to_collection可以把一個對象加到一個集合裏。比如默認構造的變量都會放到全局變量集合GraphKeys.GLOBAL_VARIABLES裏,這樣我們調用tf.global_variables_initializer()時就知道哪些變量是需要初始化的。此外Optimizer默認是通過GraphKeys.TRAINABLE_VARIABLES來找到需要學習的模型參數。所有預定義的集合都在GraphKeys定義,當然我們也可以自己定義集合,注意不要和系統定義的衝突。

(1)tf.Graph的創建

我們一般不需要自己創建tf.Graph對象,TensorFlow會自動創建一個默認的Graph對象,我們的Operation默認會加到這個Graph裏,當然我們自己創建這個對象,但一般是沒有必要的。

# 有bug的代碼!!!
import tensorflow as tf
g = tf.Graph()
with g.as_default():
	x = tf.add(3, 5)
sess = tf.Session(graph=g)
with tf.Session() as sess:
	# 會拋出異常,因爲默認的Graph裏沒有x。
	print(sess.run(x))

比如上面的代碼會有bug,我們自己創建一個Graph對象g,並且在裏面增加了Operation x,然後我們用Session()函數創建了一個Session對象sess。因爲默認的Session()函數會關聯上默認的Graph,所以sess.run(x)會找不到x這個Operation。正確(但沒必要這樣寫)的代碼是:

import tensorflow as tf
g = tf.Graph()
with g.as_default():
	x = tf.add(3, 5)
sess = tf.Session(graph=g)
with tf.Session(graph=g) as sess:
	print(sess.run(x))

在構造Session時指定Graph爲g,這樣Session對象就會關聯上我們構造的Graph對象g,從而可以找到Operation x並且執行它。我們可以構造多個Graph,在多個Graph裏分別增加各自的Operation,但這通常是沒有意義的,因爲一個Session只能關聯一個Graph。比如下面的代碼很可能就是有問題的:

g = tf.Graph()
# a在默認的Graph裏
a = tf.constant(3)
# b在我們創建的g裏
with g.as_default():
	b = tf.constant(5)

(2)Graph的name space

後面的變量部分我們會詳細的介紹name_scope和variable_scope的區別,這裏通過例子簡單的介紹name_scope。

c_0 = tf.constant(0, name="c")  # => 名字爲"c"

# 已經有重名的對象了,Tensorflow會自動加上後綴
c_1 = tf.constant(2, name="c")  # => 名字爲"c_1"

# 使用name scope,所有下面的變量會加上name scope爲前綴
with tf.name_scope("outer"):
	c_2 = tf.constant(2, name="c")  # => 名字爲"outer/c"
	
	# name scope的嵌套
	with tf.name_scope("inner"):
		c_3 = tf.constant(3, name="c")  # => 名字爲"outer/inner/c"
	
	# outer/c已經有了,因此在變量名後面加後綴
	c_4 = tf.constant(4, name="c")  # => 名字爲"outer/c_1"
	
	# name scope已經存在,會自動在name scope後面加後綴
	with tf.name_scope("inner"):
		c_5 = tf.constant(5, name="c")  # => 名字爲"outer/inner_1/c"

八、常量

我們可以使用tf.constant()構造一個常量Operation,注意它返回的是一個Operation而不是Tensor。這個函數的原型是:

constant(value, dtype=None, shape=None, name="Const", verify_shape=False)

value是一個Tensor,表示常量的值。dtype是它的數據類型,我們可以傳入shape。如果verify_shape是True,那麼它會檢查傳入的value.shape是否shape一樣,如果不一樣就會拋出異常。value參數可以是Python數組或者numpy數組來,比如:

tf.constant([[0, 1], [2, 3]])

我們也可以用特定的值來構造一個常量:

tf.zeros([2, 3], tf.int32) ==> [[0, 0, 0], [0, 0, 0]]
tf.ones([2, 2], tf.float32) ==> [[1. 1.], [1. 1.]]
tf.fill([2, 3], 8) ==> [[8, 8, 8], [8, 8, 8]]

另外我們也可以用lin_space和range來構造序列:

tf.lin_space(10.0, 13.0, 4) ==> [10. 11. 12. 13.]
tf.range(3, 18, 3) ==> [3 6 9 12 15]

我們還可以用函數來生成隨機的常量Operation。常見的函數包括:

  • tf.random_normal 生成正態分佈的隨機常量
  • tf.truncated_normal 生成正態分佈的常量,超出均值兩倍標準差的會去掉並重新採樣
  • tf.random_uniform 生成某個區間均勻分佈的常量
  • tf.random_shuffle 隨機打散一個Tensor
  • tf.random_crop 隨機crop一個Tensor的一部分
  • tf.multinomial 多項分佈的隨機常量
  • tf.random_gamma gamma分佈的隨機常量

常量的值(Tensor)是保存在圖的定義之中的,我們前面也用TensorBoard看到過。這會帶來一個問題——當常量很大的時候會使得圖的加載變得很慢。

九、placeholder和feeddict

在定義TensorFlow的計算圖時,模型的參數通常都是定義爲變量,此外還有一些不變的值定義爲常量。但是還有一類特殊的值,那就是訓練數據。它不是變量,因爲它的生命週期就是一個batch,而不需要對它進行更新。同時它也不是常量,因爲每個batch的值都是不同的。對於這類特殊的Tensor,我們通常用PlaceHolder來表示它。PlaceHolder顧名思義就是一個“佔位符”,在定義圖的時候不需要提供值(也不需要像變量那樣提供初始值),只是定義它的類型和shape。但是在Session.run()的時候我們需要把值“feed”進去,從而表示這一個batch的訓練數據。如果忘記feed值,TensorFlow會拋出運行時的異常。比如下面是常見的錯誤:

a = tf.placeholder(tf.float32, shape=[], name="a")
b = tf.constant(1, tf.float32, name="b")
c = a + b # short for tf.add(a, b)
with tf.Session() as sess: 
	print(sess.run(c))
	
# 會拋出異常: InvalidArgumentError (see above for traceback): 
# You must feed a value for placeholder tensor 'a' with dtype float and shape []

正確的代碼是:

a = tf.placeholder(tf.float32, shape=[], name="a")
b = tf.constant(1, tf.float32, name="b")
c = a + b # short for tf.add(a, b)
with tf.Session() as sess:
	print(sess.run(c, feed_dict={a:2.0}))

我們定義了a是一個標量(scalar, shape是[]),因此我們需要在run的時候通過參數feed_dict傳入a的實際值。上面的代碼我們明確的指定了PlaceHolder的shape是一個標量,我們也可以不指定或者部分指定,比如:

a=tf.placeholder(tf.float32, shape=None, name="a") # 不是建議的用法
b=tf.placeholder(tf.float32, shape=[None, 3], name="b") # batch可變,這是常見用法
with tf.Session() as sess:
	sess.run(a, feed_dict={a:[1,2]})
	sess.run(a, feed_dict={a:[[1, 2],[3,4]]})
	sess.run(b, feed_dict={b:[[1,2,3]]})
	#下面的代碼會拋出異常,因爲b的shape是[None,3],
	# 所以它一定是二維Tensor,但是第一維可以任意
	# sess.run(b, feed_dict={b:[1, 2, 3]})

使用None的好處是我們在run的時候可以隨意feed任何shape的Tensor,理論上我們可以編寫更加靈活的代碼。但缺點是這會導致代碼容易出錯,並且調試變得困難。一般我們都建議指定PlaceHolder的大小,或者只讓它的batch那個維度是None,從而可以傳入不同大小的batch。除了PlaceHolder,我們也可以feed一個變量或者常量的值,這會使得本次run的時候這些常量或者變量的值使用我們的值,但是變量本身不會被修改。比如:

a=tf.placeholder(tf.float32, shape=[], name="a")
b=tf.constant(2.0, name="b")
c=tf.Variable(3.0)
d=a+b+c
with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	print(sess.run(d, feed_dict={a:1.0})) # 6.0
	print(sess.run(d, feed_dict={a:1.0, b:4.0, c:6.0})) # 11.0
	print(sess.run(c)) # 3.0

上面的代碼,我們定義了PlaceHolder a,常量b和變量c(初始值3),d=a+b+c。初始化所有的變量後,我們可以執行d,傳入a=1.0,這時可以計算出d是6.0。但是我們也可以feed進去常量b和變量c的值爲4.0和6.0,這個時候計算出d是11.0。上面的run並不會改變變量c(當然更不會改變常量d)的值。

十、變量

(1)基本概念

變量適合用來表示共享的持久化的Tensor,其中最常用的就是用來表示模型的參數。下面的代碼可以創建變量:

m = tf.Variable([[0, 1], [2, 3]], name="matrix")
W = tf.Variable(tf.zeros([784,10]))

細心的讀者可能會發現,tf.constant()的第一個字母是小寫的,而tf.Variable()是大寫。這是TensorFlow的開發者隨意命名的結果嗎?答案是否定的。tf.constant()返回的是一個Operation,而tf.Variable()返回的是一個對象Variable。Variable封裝了很多Operation,比如initializer是這個變量的初始化Operation、value用於獲得變量內部的Tensor、assign用於給變量賦值。除了tf.Variable()之外,我們也可以是函數tf.get_variable()來創建或者重用變量。

變量和常量不同,變量的生命週期是從創建開始一直到Session結束才結束。而且變量在使用前一定要初始化,因此下面的代碼是不對的:

W = tf.get_variable("W", shape=(784, 10), initializer=tf.zeros_initializer())
with tf.Session() as sess:
	print(sess.run(W))

正確的代碼爲:

W = tf.get_variable("W", shape=(784, 10), initializer=tf.zeros_initializer())
with tf.Session() as sess:
	sess.run(W.initializer)
	print(sess.run(W))

通常一個圖中會定義很多變量,一個個的調用很麻煩。我們可以使用tf.global_variables_initializer(),這個函數會返回圖中所有的全局變量的初始化Operation,我們運行它就可以初始化所有的全局變量:

W = tf.get_variable("W", shape=(784, 10), initializer=tf.zeros_initializer())
b = tf.get_variable("b), shape=(10), initializer=tf.zeros_initializer())
c = tf.get_variable("c", shape=(), initializer=tf.zeros_initializer())
with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	print(sess.run(W))

那什麼是“全局變量”呢?TensorFlow的變量可以屬於一個或者多個(甚至0個)集合,所謂的集合只是爲了把變量分組,使用起來方便。一個變量可以不屬於任何集合,也可以同時屬於多個集合。默認情況下,我們創建的變量都會加到一個叫作GLOBAL_VARIABLES的集合裏,也就是“全局變量”。而tf.global_variables_initializer()返回的就是這個集合裏的所有變量,我們可以查看這個函數的源代碼來證實這一點:

@tf_export("initializers.global_variables", "global_variables_initializer")
def global_variables_initializer():
	return variables_initializer(global_variables())
	
@tf_export("global_variables")
def global_variables(scope=None):
	return ops.get_collection(ops.GraphKeys.GLOBAL_VARIABLES, scope)

我們也可以tf.variables_initializer初始化部分變量:

W = tf.get_variable("W", shape=(784, 10), initializer=tf.zeros_initializer())
b = tf.get_variable("b), shape=(10), initializer=tf.zeros_initializer())
c = tf.get_variable("c", shape=(), initializer=tf.zeros_initializer())
with tf.Session() as sess:
	sess.run(tf.variables_initializer([W, b]))
	print(sess.run(W))

上面的代碼值初始化了變量W和b,c是沒有初始化的。Variable.assign()函數可以給變量賦值(它返回一個賦值的Operation),讀者可以分析一下下面這段代碼會輸出什麼?

W = tf.Variable(10)
W.assign(100)
with tf.Session() as sess:
	sess.run(W.initializer)
	print(W.eval())

答案是10,您答對了嗎?如果您的答案是100,那麼請注意:W.assgin(100)只是返回一個賦值100的Operation,但是我們並沒有在Session裏執行它(甚至沒有把這個Operation保存下來。不過它仍然在計算圖裏,即使沒有被執行)。如果需要賦值,正確的代碼應該是:

W = tf.Variable(10)
assign_op = W.assign(100)
with tf.Session() as sess:
	sess.run(W.initializer)
	sess.run(assign_op)
	print(W.eval())

變量的值是保存在Session中的,它的生命週期是超過一次Session的執行的,比如下面的代碼:

my_var = tf.Variable(2, name="my_var") 
my_var_times_two = my_var.assign(2 * my_var)
with tf.Session() as sess:
	sess.run(my_var.initializer)
	sess.run(my_var_times_two) # my_var現在是4
	sess.run(my_var_times_two) # my_var現在是8
	sess.run(my_var_times_two) # my_var現在是16

另外變量是保存在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

(2)tf.Variable()和tf.get_variable()的區別

TensorFlow的文檔裏推薦儘量使用tf.get_variable()。tf.Variable()必須提供一個初始值,這一般通過numpy(或者Python)的隨機函數來生成,初始值在傳入的時候已經確定(但還沒有真正初始化變量,還需要用session.run來初始化變量)。而tf.get_variable()一般通過initializer來進行初始化,這是一個函數,它可以更加靈活的根據網絡的結構來初始化。比如我們調用tf.get_variable()時不提供initializer,那麼默認會使用tf.glorot_uniform_initializer,它會根據輸入神經元的個數和輸出神經元的個數來進行合適的初始化,從而使得模型更加容易收斂。

另外一個重要的區別就是tf.Variable()總是會(成功的)創建一個變量,如果我們提供的名字重複,那麼它會自動的在後面加上下劃線和一個數字,比如:

from __future__ import print_function
from __future__ import division

import tensorflow as tf
x1 = tf.Variable(1, name="x")
x2 = tf.Variable(2, name="x")
print(x1)
print(x2)

# tf.Variable會自動生成名字
x3 = tf.Variable(3)
print(x3)

輸出的結果爲:

<tf.Variable 'x_2:0' shape=() dtype=int32_ref>
<tf.Variable 'x_3:0' shape=() dtype=int32_ref>
<tf.Variable 'Variable:0' shape=() dtype=int32_ref>

而tf.get_variable()一定需要提供變量名(tf.Variable()可以不提供name,系統會自動生成),而且它檢測變量是否存在,默認情況下已經存在會拋出異常。因此tf.get_variable()強制我們爲不同的變量提供不同的名字,從而讓Graph更加清晰。除此之外,tf.get_variable()可以讓我們更加容易的複用變量。

如果一個變量由tf.Variable()得到,那麼共享(複用)它的唯一辦法就是把這個變量傳給需要用到的地方。這種方法看起來很簡單自然,但是在實際的應用中卻很麻煩。原因之一就是tf.layers裏的layer(包括我們自己封裝的類似的類)隱藏了很多細節,它創建的變量都是對象的成員變量,使用它的人甚至不知道它創建了哪些變量。比如tf.layers.Dense創建了一個矩陣kernle和一個bias(可選),假設我們想讓兩個tf.layers.Dense的參數實現共享,那麼tf.layers.Dense必須把所有的參數都對外暴露,而且我們在構造第二個tf.layers.Dense時需要傳入。這就要求使用tf.layers.Dense的人瞭解代碼的細節,這就破壞了封裝的要求。比如哪天tf.layers.Dense的實現發生了改變,我們不用兩個參數kernel和bias,而是把這兩個參數合併爲一個大的矩陣,那麼所有用到它的地方都需要修改,這就非常麻煩。

因此TensorFlow通過get_variable()提供了另外一種通過名字來共享變量的方式,它需要和variable_scope配合。我們可以通過tf.variable_scope構造一個variable scope,然後通過variable scope來共享變量:

with tf.variable_scope("myscope") as scope:
	x = tf.get_variable(name="x", initializer=tf.constant([1, 2, 3]))
	print(x)
# 不加reuse=True會拋出異常,不允許創建同名的變量。
# with tf.variable_scope("myscope") as scope:
#    x = tf.get_variable(name="x", dtype=tf.int32, initializer=tf.constant([1, 2, 3]))

with tf.variable_scope("myscope", reuse=True) as scope:
	# 注意要加上dtype=tf.int32
	x = tf.get_variable(name="x", dtype=tf.int32, initializer=tf.constant([1, 2, 3]))
	# 我們可以不提供initializer
	# x = tf.get_variable(name="x", dtype=tf.int32)
	print(x)

代碼的輸出是:

<tf.Variable 'myscope/x:0' shape=(3,) dtype=int32_ref>
<tf.Variable 'myscope/x:0' shape=(3,) dtype=int32_ref>

我們首先在名字爲myscope的variable scope裏創建變量x,打印它的名字發現Tensorflow自動在名字x前加上了scope的名字和/作爲前綴。接着我們又在一個新的名字叫myscope的scope裏創建變量x,需要注意的是這次調用tf.variable_scope是我們傳入了參數reuse=True,如果不傳這個參數,那麼執行第二次get_variable會拋出異常,因爲默認情況下變量是不共享的。另外一個需要注意的地方是我們在第二次調用tf.get_variable時傳入了參數dtype=tf.int32,如果不傳會怎麼樣呢?我們修改後運行一下會得到如下異常:

ValueError: Trying to share variable myscope/x, but specified dtype float32 and found dtype int32_ref.

爲什麼這樣呢?因爲我們第一次創建變量時傳入了初始值[1,2,3],TensorFlow會推測我們的變量的dtype是tf.int32,第二次是重用變量,它會忽略我們傳入的initializer,這個時候它會任務dtype是默認的tf.float32,而之前我們創建的變量是tf.int32,這就發生運行時異常了。

除了在tf.variable_scope傳入reuse=True,我們還可以用下面的方式來更加細粒度的控制變量共享:

with tf.variable_scope("myscope") as scope:
	x = tf.get_variable(name="x", initializer=tf.constant([1, 2, 3]))
	y = tf.get_variable(name="y1", initializer=tf.constant(1))
	print(x)
	print(y)

with tf.variable_scope("myscope") as scope:
	# y不共享
	y = tf.get_variable(name="y2", initializer=tf.constant(2))
	scope.reuse_variables()
	x = tf.get_variable(name="x", dtype=tf.int32)
	print(x)
	print(y)

它的輸出是:

<tf.Variable 'myscope/x:0' shape=(3,) dtype=int32_ref>
<tf.Variable 'myscope/y1:0' shape=() dtype=int32_ref>
<tf.Variable 'myscope/x:0' shape=(3,) dtype=int32_ref>
<tf.Variable 'myscope/y2:0' shape=() dtype=int32_ref>

注意scope.reuse_variables()一旦調用後,這這個scope裏的變量都是共享的,TensorFlow沒有一個函數能夠把它改成不共享的。因此我們首先需要把不共享的變量創建好,然後調用scope.reuse_variables(),接着再使用共享的變量。

另外需要提示讀者的是:雖然我們示例代碼的兩個with tf.variable_scope(“myscope”) as scope隔得很近,但是在實際代碼中這兩行代碼可以隔得很遠,甚至不在一個文件裏,因此這種方式的共享變量會很方便。

tf.layers裏的layer都是通過這種方式來共享變量的,比如我們想共享兩個Dense:

x1 = tf.placeholder(dtype=tf.float32, shape=[None, 3], name="x1")
x2 = tf.placeholder(dtype=tf.float32, shape=[None, 3], name="x2")
with tf.variable_scope("myscope") as scope:
	l1 = tf.layers.Dense(units=2)
	h11 = l1(x1)
with tf.variable_scope("myscope", reuse=True) as scope:
	l2 = tf.layers.Dense(units=2)
	h12 = l2(x2)
with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	print(sess.run([h11, h12], feed_dict={x1: [[1, 2, 3]], x2: [[2, 4, 6]]}))

我們可以發現,雖然每次結果不同,但h12總是h11的兩倍,這說明它們的參數確實是共享的。

(3)name scope和variable scope

除了variable scope,TensorFlow還有一個name scope。variable scope的目的是爲了共享變量,而name scope的目的是爲了便於組織變量的namespace。下面我們通過一個例子來比較這兩個scope。

with tf.name_scope("my_scope"):
	v1 = tf.get_variable("var1", [1], dtype=tf.float32)
	v2 = tf.Variable(1, name="var2", dtype=tf.float32)
	a = tf.add(v1, v2)
	
	print(v1.name)  # var1:0
	print(v2.name)  # my_scope/var2:0
	print(a.name)   # my_scope/Add:0

with tf.variable_scope("my_scope"):
	v1 = tf.get_variable("var1", [1], dtype=tf.float32)
	v2 = tf.Variable(1, name="var2", dtype=tf.float32)
	a = tf.add(v1, v2)
	
	print(v1.name)  # my_scope/var1:0
	print(v2.name)  # my_scope_1/var2:0
	print(a.name)   # my_scope_1/Add:0

第一個scope是name_scope,在這裏面通過tf.Variable()或者其它函數產生的變量的名字都會加上scope的名字爲前綴,但是tf.get_variable()創建的變量會忽略name scope。因此v1的名字沒有my_scope作爲前綴。

第二個scope是variable_scope,所有的變量都會加上scope的名字。值得注意的是:name scope和variable scope可以同名。對於同名的scope,tf.Variable()或者其它函數產生的變量會自動給第二個scope加上數字的後綴以避免重名。但是對於tf.get_variable(),加的一定是scope的名字(而不會加後綴),如果有重名的而又不是reuse=True,那麼就會拋異常。上面的例子中兩個get_variable都是使用名字”var1”,但是第一個在name scope裏,所以它的名字叫”var1:0”,而第二個在variable scope裏,所以名字叫”my_scope/var1:0”。它們的名字其實是不同的。

總結一下:tf.get_varialbe()只會使用variable_scope而且變量名一定是scope名/變量名。而tf.Variable()會同時使用name_scope和variable_scope,並且在重名的時候通過後綴避免重名。而如果兩個scope重名的時候(不管是name scope和variable scope重名還是兩個name scope重名),tf.Variable()發現重名變量時會給scope name加後綴(而不是給變量名加後綴)。這一點也可以從下面這個例子驗證:


with tf.name_scope("my_scope"):
	v2 = tf.Variable(1, name="var2", dtype=tf.float32)
	print(v2.name)

with tf.name_scope("my_scope"):
	v2 = tf.Variable(1, name="var2", dtype=tf.float32)
	print(v2.name)

它的運行結果是:

my_scope/var2:0
my_scope_1/var2:0

十一、Session

TensorFlow使用tf.Session()對象表示客戶端程序(通常是Python代碼)和Tensorflow執行引擎(C++代碼)之間的聯繫。實際的代碼執行可能在本地的多個設備上執行(比如多個CPU和GPU),也可能在遠程的設備上原型。

我們定義好Graph之後TensorFlow並不會執行任何計算,爲了進行計算,我們需要初始化一個tf.Session對象,通常我們把它叫作session。session封裝了TensorFlow運行時的狀態並且可以運行TensorFlow的Operation。我們可以把Graph看成Python源代碼py文件,而tf.Session看成Python的可執行文件。比如下面的例子執行兩個常量的相加:

import tensorflow as tf
a = tf.constant(3.0, name="a")
b = tf.constant(4.0, name="b")
c = tf.add(a, b)
with tf.Session() as sess:
	print(sess.run(c))

tf.Session()會返回一個Session對象。它封裝了一個環境,在這個環境裏Operation可以得到執行,Tensor可以求值。此外Session也會給變量分配空間。我們可以使用Session對象的run函數來執行想要的Operation,TensorFlow會自動執行它依賴的其它的Operation,這些要執行的Operation和它的依賴就構成了整個圖的一個子圖,不需要的Operation也不會被執行。比如:

x = 2
y = 3
add_op = tf.add(x, y)
mul_op = tf.multiply(x, y)
useless = tf.multiply(x, add_op)
pow_op = tf.pow(add_op, mul_op)
with tf.Session() as sess:
z = sess.run(pow_op)

我們計算pow_op並不需要useless,因此TensorFlow也不會計算它的值。Session對象的run函數如下:

def run(fetches, feed_dict=None, options=None, run_metadata=None)

fetches參數表示要執行的一個或者多個Operation,feed_dict表示要feed的PlaceHolder,後面兩個參數暫不介紹。在定義圖的時候我們可以指定Operation放置的設備,這樣可以實現分佈式的計算。比如下面的代碼把Operation指定到第一個GPU上:

with tf.device('/gpu:0'):
	a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], name='a')
	b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], name='b')
	c = tf.multiply(a, b) 
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
print(sess.run(c))

使用tf.device可以手動指定Operation的設備,”/gpu:0”表示第一個GPU,”/gpu:1”表示第二個GPU,”/cpu:0”表示放置在CPU上。注意沒有”cpu:1”的寫法,在TensorFlow裏,CPU被看成“一個”設備,即使你有多個CPU,你也沒有辦法指定某個計算在哪個CPU上執行,因爲目前的CPU架構一般是對應用層透明的,某個線程調度到哪個CPU上是由操作系統來決定的。

十二、常見錯誤

一個常見的錯誤就是在run的時候“延遲”創建Operation,比如下面的代碼:

x = tf.Variable(1, name='x')
y = tf.Variable(2, name='y')

with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	for _ in range(10000):
		sess.run(tf.add(x, y))

上面的代碼會在Graph裏創建10000個add節點,這會造成極大的資源浪費。正確的代碼是把圖的定義和圖的執行分開:

x = tf.Variable(1, name='x')
y = tf.Variable(2, name='y')
z = x + y
with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	for _ in range(10000):
		sess.run(z)

十三、layers

前面章節我們使用了最基本的Operation來定義全連接層,卷積層等,爲了複用,我們對它做了封裝。這些基本的網絡結構在複雜的網絡中被經常使用,如果大家都使用自己的封裝,這會重複製造輪子。因此TensorFlow提供了tf.layers模塊,它實現了大部分常見的網絡結構,避免重複勞動。 下面我們簡單的介紹tf.layers的使用,通過一個全連接的layer Dense來作爲示例。後面我們會詳細介紹更多layers的用法。

使用layers非常簡單,和我們之前自己封裝的類似,比如我們可以用tf.layers.Dense來實現一個線性模型,這個layer進行計算outputs = activation(inputs * kernel + bias)。下面是代碼示例:

x = tf.placeholder(tf.float32, shape=[None, 3])
linear_model = tf.layers.Dense(units=1)
y = linear_model(x)

layers需要輸入和輸出的大小來構造合適的參數。輸出參數需要顯示的指定,比如上面的代碼輸出的大小是1。而輸入的大小它會從輸入“推測”出來,因此如果輸入是PlaceHolder,我們需要指定其大小(batch大小可能不指定),比如上面我們指定了輸入是[None, 3],因此當執行y = linear_model(x)的時候,TensorFlow就知道需要構造全連接層的參數矩陣應該是[3, 1]的。

通過layers定義的Graph我們也需要初始化變量,layers封裝的變量會放到全局變量裏,因此我們可以通過global_variables_initializer來初始化變量:

init = tf.global_variables_initializer()
sess.run(init)

對於上面使用layers定義的Graph,我們可以用session來執行它:

print(sess.run(y, {x: [[1, 2, 3],[4, 5, 6]]}))

如果我們多次運行上面的代碼,其結果是不一樣的,因爲參數的初始化是隨機的。我們可以在Dense的構造函數傳入初始化函數,比如glorot_uniform_initializer。那麼讀者可能會問,上面的代碼我們沒有指定,那麼tf.layers.Dense會使用哪個初始化函數呢?根據文檔(https://www.tensorflow.org/api_docs/python/tf/layers/Dense),bias使用的是零來初始化,而kernel使用了tf.get_variable()函數的默認初始化函數,而tf.get_variable()默認會使用 glorot_uniform_initializer。 glorot_uniform_initializer會用一個均勻分佈來初始化變量,它的範圍是(-limit, limit),其中limit=sqrt(6 / (fan_in + fan_out))。

上面我們首先定義linear_model=tf.layers.Dense(),然後y=linear_model(x),通常我們不需要用到linear_model這個Dense對象,我們也可以使用更加簡化的寫法:

x = tf.placeholder(tf.float32, shape=[None, 3])
y = tf.layers.dense(x, units=1)

init = tf.global_variables_initializer()
sess.run(init)

print(sess.run(y, {x: [[1, 2, 3], [4, 5, 6]]}))

十四、線性迴歸的例子

下面我們通過一個簡單的線性迴歸例子來學習上面的這些基本概念是怎麼應用的。我們的線性迴歸非常簡單,輸入是一個浮點數x,輸出是y,我們假設它們是一種簡單的線性關係:y=wx+b。參數w和b是我們需要預測的值。

(1)定義數據

我們首先定義輸入x和真實的y:

x = tf.constant([[1], [2], [3], [4]], dtype=tf.float32)
y_true = tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32)

\subsubsection{定義模型}

linear_model = tf.layers.Dense(units=1)
y_pred = linear_model(x)

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(y_pred))

我們用前面介紹的tf.layers.Dense來定義一個全連接層(沒有激活函數,因此就是線性函數)。剛開始參數w和b是隨機初始化的,因此預測出來的值很真實的值差別很大。

(2)定義loss

爲了優化模型,我們首先需要定義損失函數。我們可以使用基本的數學函數來定義損失,但是tf.losses封裝了很多常見的損失函數,我們可以直接避免自己的實現錯誤,而且通常tf.losses提供的實現要比我們自己手寫的高效和stable。這裏我們使用最小均分誤差損失函數:

loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)
print(sess.run(loss))

(3)定義訓練Op

定義了損失函數之後,我們就需要使用梯度下降算法來不斷調整參數使得損失不斷減少,我們當然可以自己求損失對參數的梯度。但這通常很繁瑣而且容易出錯,我們使用各種深度學習框架最主要目的(之一)就是使用自動梯度。而TensorFlow就是基於自動差分(auto diff)的框架,我們通過使用tf.optimizer.Optimizer對象(的子類),然後使用這個對象的minimize()方法就可以產生一個Operation,通過session.run()就可以不斷的執行梯度下降。我們只是簡單的定義了Optimizer以及調用它的minimize()方法,TensorFlow在背後幫我們做了很多事情,包括在Graph中創建用於反向計算梯度的Operation。當然Optimizer也提供了一些底層的方法讓我們可以計算梯度,然後自己來用梯度來修改變量。後面的一些複雜場景,我們會介紹這些底層的用法。

optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

上面的代碼我們首先創建一個GradientDescentOptimizer對象,這是最簡單的隨機梯度下降方法的封裝,我們需要傳入learning rate,這裏我們傳入0.01。然後調用它的minimize(loss)方法得到一個train的Operation。如果我們用session.run來運行這個Operation,TensorFlow就會首先進行前向的計算,通過輸入計算loss,然後計算loss對參數的梯度,然後用梯度和learning rate更新參數。所有這些過程都被封裝好了!

(4)進行訓練

定義好了訓練的Operation之後,接下來就要進行訓練了,我們通過session.run(train)來進行訓練。實際的訓練我們通常是使用隨機梯度下降(這裏是使用了梯度下降,一次計算所有訓練數據的梯度),因此我們需要每次使用不同的batch大小的數據來訓練,那就不能用tf.constant來定義訓練數據了,而是要使用placeholder配合feeddict或者使用更加高效的tf.dataset API。

for i in range(100):
	_, loss_value = sess.run((train, loss))
	print(loss_value)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章