為什么選擇TensorFlow?
自從12年AlexNet獲得ImageNet大賽的冠軍后,深度學習開始流行起來,也因為硬件的快速發展GPU並行計算配合易用的API,讓深度學習以及神經網絡大方光彩。
深度學習的框架其實有很多,目前來說最火的還要數PyTorch,TensorFlow以及Keras。其中Pytorch比較適合學術研究,自己搞着玩,如果工業實踐就不太適合了。TensorFlow由於時間比較久,學起來比較困難,不過有完整的開發,部署方案,還有大量的GitHub項目可供參考。Keras則是TensorFlow的一個高級API,同類的還有TensorFlow的TFLearn等等。
總結來說,如果是學生的話,只是為了論文或者學習,那么推薦Pytorch;如果是公司的開發者,想要在業務員中使用深度學習,推薦直接使用TensorFlow,如果使用最新的1.14,那么官網的示例里面就已經是Keras了;如果是從GitHub上面下載了源碼想要學習,那就得去學習對應版本的TensorFlow API了。
在總結一下TensorFlow的優點:
- 易用性:有對應Python的API
- 可移植性:一套代碼就可以適應單個或者多個CPU、GPU、移動設備等
- 靈活性:可以部署在樹莓派、安卓、windows、ios、linux等上
- 可視化:有tensorboard提供開發的可視化界面,方便跟蹤調參
- 檢查點:可以通過檢查點記錄保存實驗數據
- 自動微積分:自動求解梯度
- 龐大的社區:一年內擁有10000+的開發者,3000+的項目
- 大量基於TensorFlow的項目代碼
在使用TensorFlow的公司包括:Google ,OpenAI,DeepMind,SnapChat,Airbus,eBay等。
下面就來學習下TensorFlow的基礎知識,TensorFlow不僅提供了基礎的語法,還提供了一些簡化的API:
- TF Learn,tf.contrib.learn,基於scikit-learn風格的API
- TF Slim,tf.contrib.slim,輕量級的tf構建API,可以自動配置默認值,簡化使用
- Keras,更高級更抽象的API,使用Keras之后,就像疊積木一樣創建模型,不過對於背后的原理隱藏的太深太深
TensorFlow的名字中已經說明了它最重要的兩個概念——Tensor和Flow。Tensor就是張量,張量這個概念在數學或者物理學中可以有不同的解釋,但是這里我們不強調它本身的含義。在TensorFlow中,張量可以被簡單地理解為多維數組,Flow翻譯成中文就是“流”,它直觀的表述計算的編程系統。TensorFlow中的每一個計算都是計算圖上的一個節點,而節點之間的邊描述了計算之間的依賴關系。
TensorFlow計算模型——計算圖的概念
計算圖是TensorFlow中最基本的一個概念,TensorFlow中所有計算都會被轉化為計算圖上的一個節點。
在TensorFlow程序中,所有的數據都通過張量的形式來表示。從功能的角度上看,張量可以被簡單理解為多為數組。其中零階張量表示標量(scalar),也即是一個數(張量的類型也可以是字符串)。第一階張量為向量(vector),也就是一個一維數組;第 n 階張量可以理解為一個 n 維數組。但是張量在TensorFlow中的實現並不是直接采用數組的形式,它只是對TensorFlow中運算結果的引用。在張量中並沒有真正保存數字,它保存的是如何得到這些數字的計算過程。以向量加法為例,當運行如下代碼的時候,得到的不是加法的結果,而是對結果的一個引用。
#_*_coding:utf-8_*_ import tensorflow as tf # tf.constant 是一個計算,這個計算的結果為一個張量,保存在變量a中 a = tf.constant([1.0, 2.0], name='a') b = tf.constant([2.0, 3.0], name='b') result = a + b # print(result) # Tensor("add:0", shape=(2,), dtype=float32)
從上面的結果來看,TensorFlow的張量和Numpy的數組不同,他計算的結果不是一個具體的數字,而是一個張量的結構。從上面結果來看,一個張量主要保存了三個屬性,名字(name),維度(shape)和類型(type)。
張量的第一個屬性名字不僅是一個張量的唯一標識符,它同樣也給出了這個張量是如何計算的,TensorFlow的計算都可以通過計算圖的模型來建立,而計算圖上的每一個節點代表一個計算,計算的結果就保存在張量之中。所以張量和計算圖上節點所代表的計算結果是對應的。所以張量的命名就可以通過“node : src_output”的形式來給出。其中node為節點的名稱,src_output 表示當前張量來自節點的第幾個輸出。比如上面的“add:0” 就說明了result這個張量是計算節點“add” 輸出的第一個結果(編號從0 開始)。
張量的第二個屬性是張量的維度。這個屬性描述了一個張量的維度信息,比如上面樣例中 shape = (2, ) 說明了張量 result 是一個一維數組,這個數組的長度為2。維度是張量一個很重要的屬性,圍繞張量的維度TensorFlow也給出了很多有用的運算。
張量的第三個屬性就是類型(type),每一個張量會有一個唯一的類型。TensorFlow 會對參與運算的所有張量進行類型的檢查,當發現類型不匹配的時候會報錯,比如下面的代碼就會得到類型不匹配的錯誤:
#_*_coding:utf-8_*_ import tensorflow as tf # tf.constant 是一個計算,這個計算的結果為一個張量,保存在變量a中 a = tf.constant([1, 2], name='a') b = tf.constant([2.0, 3.0], name='b') result = a + b
這段代碼和上面例子基本一模一樣,唯一不同就是把其中一個加數的小數點去掉了。這會使得加數 a 的類型為整數而加數 b 的類型為實數,這樣程序就會報類型不匹配的錯誤:
ValueError: Tensor conversion requested dtype int32 for Tensor with dtype float32: 'Tensor("b:0", shape=(2,), dtype=float32)'
如果將第一個加數指定成實數類型 ,如下:
a = tf.constant([1, 2], name='a', dtype=tf.float32)
那么兩個加數的類型相同,就不會報錯了。如果不指定類型,則會默認為 int32,而帶小數的則會默認為float32.所以一般建議通過指定dtype來明確指出變量或者常量的類型。
tensorflow中的數據類型列表
張量的使用
和TensorFlow的計算模型相比,TensorFlow的數據模型相比較簡單。張量使用主要可以總結為兩大類。
第一類用途是對中間計算結果的引用。當一個計算包含很多中間結果時,使用張量可以大大提高代碼的可讀性。比如上面的例子。
第二類是當計算圖構造完成之后,張量可以用來獲得計算結果,也就是得到真實的數字,雖然張量本身沒有存儲具體的數字,但是通過下面的Session就可以得到具體的數字。
會話(TensorFlow運行模型——Session)
下面學習如何使用會話(session)來執行定義好的運算,會話擁有並管理TensorFlow程序運行時的所有資源。當所有計算完成之后需要關閉會話來幫助系統回收資源,否則就可能出現資源泄露的問題。TensorFlow中使用會話的模式一般有兩種,第一種模式需要明確調用會話生成函數和關閉會話函數,這么模式如下:
# 創建一個會話 sess = tf.Session() # 使用這個創建好的會話來得到關心的運算結果 # 比如可以調用 sess.run(result) 來得到張量計算的結果 sess.run(...) # 關閉會話使得本次運算中使用到的資源可以被釋放 sess.close()
使用這種模式的時候,在所有計算完成之后,需要明確調用Session.close 函數來關閉會話並釋放資源。然而,當程序因為異常而退出時,關閉會話的函數可能就不會被執行而導致資源泄露。為了解決異常退出時資源釋放的問題,TensorFlow可以通過Python的上下文管理器來使用會話,也就是可以利用 with 代碼塊生成Session,限制作用域,代碼如下:
# 創建一個會話,並通過python中的上下文管理器來管理這個會話 with tf.Session() as sess: # 使用這創建好的會話來計算關心的結果 sess.run(...) # 不需要再調用“Session.close()” 函數來關閉會話 # 當上下文退出時會話關閉和資源釋放也自動完成了。
通過Python上下文管理器的機制,只要將所有的計算放在'with' 的內部就可以。當上下文管理器退出時候會自動釋放所有資源。這樣即解決了因為異常退出時資源釋放的問題,同時也解決了忘記調用Session.close 函數而產生的資源泄露問題。
Session 函數中沒有傳入參數,表明該代碼將會依附於(如果還沒有創建會話,則會創建新的會話)默認的本地會話。生成會話之后,所有的 tf.Variable 實例都會通過調用各自初始化操作的 sess.run() 函數進行初始化。
init = tf.initialize_all_variables() sess.run(init)
在通過initializer給變量賦值固然可行,但是當變量的數據增多后,或者變量之間存在依賴關系時,單個調用的方案就比較麻煩了。所以使用上述代碼更加便捷。
sess.run() 方法將會運行圖表中與作為參數傳入的操作相對應的完整子集。在初始調用時, init操作只包含了變量初始化程序 tf.group。圖標的其他部分不會再這里,而是在下面的訓練訓練運行。
在交互式環境中(比如Python腳本或者Jupyter的編譯器下),通過設置默認會話的方式來獲得張量的取值更加方便。所有TensorFlow提供了一種在交互式環境下直接構建默認會話的函數,這和函數就是tf.InteractiveSession.使用這個函數會自動將生成的會話注冊為默認會話。下面代碼展示了tf.InteractiveSession 函數的用法:
sess = tf.InteractiveSession() print(result.eval()) # 其實 sess.run(result) 和 result.eval(session=sess)) 功能相同 sess.close()
通過tf.InteractiveSession 函數可以省去將產生的會話注冊為默認會話的過程。
變量在被使用前,需要通過會話(session)運行其初始化方法完成初始化賦值。
sess.run(tf.global_variables_initializer)
注意:在新版本的tensorflow中,使用下面代碼替換上面代碼,不然會報 Warning。
sess.run(tf.global_variables_initializer)
神經網絡參數與TensorFlow變量tf.Variable()
神經網絡中的參數是神經網絡實現分類或者回歸問題中重要的部分。在TensorFlow中變量(tf.Variable()的作用就是保存和更新神經網絡中的參數),下面學習一下變量的定義:
def __init__(self, initial_value=None, trainable=True, collections=None, validate_shape=True, caching_device=None, name=None, variable_def=None, dtype=None, expected_shape=None, import_scope=None):
- initial_value:初始化的值,可以是隨機數,常數或者通過其他變量的初始值得到的。
- trainable:標記是否加入GraphKeys.TRAINABLE_VARIABLES集合
- validate_shape:如果為False則可以更改shape
- dtype:變量的類型,不可改變
下面代碼給出了一種在TensorFlow中聲明一個2*3的矩陣變量的方法:
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
首先它調用了TensorFlow變量的聲明函數tf.Variable。在變量聲明函數中給出了初始化這個變量的方法。TensorFlow中變量的初始值可以設置成隨機數,常數或者是同其他變量的初始值計算得到。在上面的樣例中,tf.reandom_normal([2, 3], stddev=2)會產生一個2*3的矩陣,矩陣的元素均值為0,標准差為2 的隨機數。tf.random_normal函數可以通過參數mean來指定平均值,在沒有指定時默認為0,通過滿足正態分布的隨機數來初始化神經網絡中的參數是一個非常有用的方法,除了正態分布的隨機數,TensorFlow還提供了一些其他的隨機數生成器,下圖列出了TensorFlow目前支持的所有隨機數生成器
TensorFlow也支持通過常數來初始化一個變量,下圖給出了TensorFlow中常用的常量聲明方法:
在神經網絡中,偏置項(bias)通常會使用常數來設置初始值,下面代碼給出了一個例子:
# 下面會產生一個初始值為0且長度為3 的變量 biases = tf.Variable(tf.zeros([3]))
當然,TensorFlow也支持通過其他變量的初始值來初始化新的變量,下面給出了具體的方法:
# 聲明w1 w2兩個變量 w1 = tf.Variable(weights.initialized_value()) w2 = tf.Variable(weights.initialized_value() * 2.0)
以上代碼中,w1的初始值被設置成了與weights變量相同,w2的初始值則是weights初始值的兩倍。在TensorFlow中,一個變量的初始化過程需要被明確的調用。
類似於張量,維度(shape)和類型(type)也是變量最重用的兩個屬性,和大部分程序語言類似,變量的類型是不可改變的。一個變量在被構建之后,它的類型就不能再改變量。
如下代碼會報出類型不匹配的錯誤:
# 聲明w1 w2兩個變量 # 定義神經網絡的參數 w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w1') w2 = tf.Variable(tf.random_normal([2, 3], dtype=tf.float64, stddev=1), name='w2') w1.assign(w2) ''' 程序會報錯: TypeError: Input 'value' of 'Assign' Op has type float64 that does not match type float32 of argument 'ref'. '''
維度是另外一個重要的屬性,和類型不大一樣的是,維度在程序運行中是有可能改變的,但是需要設置參數,如下:
# 聲明w1 w2兩個變量 # 定義神經網絡的參數 w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w1') w2 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w2') w1.assign(w1, w2) ''' 程序會報錯(維度不匹配的錯誤): TypeError: Expected bool for argument 'use_locking' not <tf.Variable 'w2:0' shape=(2, 3) dtype=float32_ref>. ''' # 下面代碼可以被成功執行 tf.assign(w1, w2, validate_shape=False)
雖然TensorFlow支持更改變量的維度,但是這種做法比較罕見。
常用Tensorflow函數筆記
下面是幾個在Tensorflow中常用的函數
(1)tf.argmax(input, axis=None, name=None, dimension=None) 此函數是對矩陣按行或列計算最大值 參數 input:輸入Tensor axis:0表示按列,1表示按行 name:名稱 dimension:和axis功能一樣,默認axis取值優先。新加的字段 返回:Tensor 行或列的最大值下標向量 (2)tf.equal(a, b) 此函數比較等維度的a, b矩陣相應位置的元素是否相等,相等返回True,否則為False 返回:同維度的矩陣,元素值為True或False (3)tf.cast(x, dtype, name=None) 將x的數據格式轉化成dtype.例如,原來x的數據格式是bool, 那么將其轉化成float以后,就能夠將其轉化成0和1的序列。反之也可以 (4)tf.reduce_max(input_tensor, reduction_indices=None, keep_dims=False, name=None) 功能:求某維度的最大值 (5)tf.reduce_mean(input_tensor, reduction_indices=None, keep_dims=False, name=None) 功能:求某維度的均值 參數1--input_tensor:待求值的tensor。 參數2--reduction_indices:在哪一維上求解。0表示按列,1表示按行 參數(3)(4)可忽略 例:x = [ 1, 2 3, 4] x = tf.constant([[1,2],[3,4]], "float") tf.reduce_mean(x) = 2.5 tf.reduce_mean(x, 0) = [2, 3] tf.reduce_mean(x, 1) = [1.5, 3.5] (6)tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None) 從截斷的正態分布中輸出隨機值 shape: 輸出的張量的維度尺寸。 mean: 正態分布的均值。 stddev: 正態分布的標准差。 dtype: 輸出的類型。 seed: 一個整數,當設置之后,每次生成的隨機數都一樣。 name: 操作的名字。 (7)tf.random_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None) 從標准正態分布中輸出隨機值 (8) tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None) 在給定的4D input與 filter下計算2D卷積 1,輸入shape為 [batch, height, width, in_channels]: batch為圖片數量,in_channels為圖片通道數 2,第二個參數filter:相當於CNN中的卷積核,它要求是一個Tensor, 具有[filter_height, filter_width, in_channels, out_channels]這樣的shape, 具體含義是[卷積核的高度,卷積核的寬度,圖像通道數,卷積核個數],要求類型與 參數input相同,有一個地方需要注意,第三維in_channels,就是參數input的第四維 3,第三個參數strides:卷積時在圖像每一維的步長,這是一個一維的向量,長度4 4,第四個參數padding:string類型的量,只能是"SAME","VALID"其中之一, 這個值決定了不同的卷積方式(后面會介紹) 5,第五個參數:use_cudnn_on_gpu:bool類型,是否使用cudnn加速,默認為true 結果返回一個Tensor,這個輸出,就是我們常說的feature map,shape仍然是 [batch, height, width, channels]這種形式。 (9)tf.nn.max_pool(value, ksize, strides, padding, name=None) 參數是四個,和卷積很類似: 第一個參數value:需要池化的輸入,一般池化層接在卷積層后面,所以輸入 通常是feature map,依然是[batch, height, width, channels]這樣的shape 第二個參數ksize:池化窗口的大小,取一個四維向量,一般是[1, height, width, 1], 因為我們不想在batch和channels上做池化,所以這兩個維度設為了1 第三個參數strides:和卷積類似,窗口在每一個維度上滑動的步長,一般也是[1, stride,stride, 1] 第四個參數padding:和卷積類似,可以取'VALID' 或者'SAME' 返回一個Tensor,類型不變, shape仍然是[batch, height, width, channels]這種形式 (10) tf.reshape(tensor, shape, name=None) 函數的作用是將tensor變換為參數shape的形式。 其中shape為一個列表形式,特殊的一點是列表中可以存在-1。-1代表的含義是不用我 們自己指定這一維的大小,函數會自動計算,但列表中只能存在一個-1。(當然如果存 在多個-1,就是一個存在多解的方程了) (11)tf.nn.dropout(x, keep_prob, noise_shape=None, seed=None,name=None) 為了減少過擬合,隨機扔掉一些神經元,這些神經元不參與權重的更新和運算 參數: x : 輸入tensor keep_prob : float類型,每個元素被保留下來的概率 noise_shape : 一個1維的int32張量,代表了隨機產生“保留/丟棄”標志的shape。 seed : 整形變量,隨機數種子。 name : 名字,沒啥用。
通過TensorFlow游樂場了解神經網絡
首先我們通過TensorFlow游樂場來快速了解神經網絡的主要功能。TensorFlow游樂場是一個通過網頁瀏覽器就可以訓練的簡單神經網絡並實現了可視化訓練過程的工具。
TensorFlow游樂場的地址:http://playground.tensorflow.org/
從上圖中可以看出,TensorFlow的左側提供四個不同的數據集來測試神經網絡。默認的數據為左上角被框出來的那個。被選中的數據也會顯示在上面最右邊的“OUTPUT”欄目下。在這個數據中,可以看到一個二維平面上有藍色或者橙色的點,每一個小點都代表了一個樣例,而點的顏色代表了樣例的標簽。因為點的顏色只有兩種,所有這是一個二分類問題。在這里舉這么一個例子來說明這個數據可以代表的實際問題。假設需要判斷某工廠生產的零件是否合格,那么藍色的點可以表示所有合格的零件,而橙色代表不合格的零件。這樣判斷一個零件是否合格就變成了區分點的顏色。
為了將一個實際問題對應到屏幕上不同顏色點的划分,還需要將實際問題中的實體,比如上述例子中的零件,變成屏幕上的一個點。這就是特征提取解決的問題。還是以零件為例,可以用零件的長度和質量來大致描述一個零件。這樣一個物理意義上的零件就可以被轉化成長度和質量這兩個數字。在機器學習中,所有用於描述實體的數字的組合就是一個實體的特征向量(feature vector)。而特征向量的提取對機器學習的效果至關重要,通過特征提取就可以將實際問題中的實體轉化為空間中的點。假設使用長度和質量作為一個零件的特征向量,那么每個零件就是二維平面上的一個點。TensorFlow游樂園中Features一欄對應了特征向量。
特征向量是神經網絡的輸入,神經網絡的主體結構顯示了在上圖的中間位置。目前主流的神經網絡都是分層的結構,第一層是輸入層,代表特征向量中每一個特征的取值。比如如果一個零件的長度是0.5,那么x1的值就是0.5。同一層的節點不會相互連接,而且每一層只和下一層連接,直到最后一層作為輸出層得到計算的結果。在二分類問題中,比如判斷零件是否合格,神經網絡的輸出層往往只包含一個節點。在二分類問題中,比如判斷零件是否合格,神經網絡的輸出層往往只包含一個節點,而這個節點會輸出一個實數值。通過這個輸出值和一個事先設定的閾值,就可以判斷結果是零件合格,反之則零件不合格,一般可以認為當輸出值離閾值越遠得到的答案越可靠。
在輸入和輸出層之間的神經網絡叫做隱藏層,一般一個神經網絡的隱藏層越多,這個神經網絡就越“深”。而所謂深度學習中的這個“深度”和神經網絡的層數也是密切相關的。在TensorFlow游樂場中可以通過點擊加或者減來增加或者減少神經網絡隱藏層的數量。處理可以選擇深刻網絡的深度,TensorFlow游樂場也支持選擇神經網絡每一層的節點數以及學習率(learning rate),激活函數(activation),正則化(regularization)。
所以通過神經網絡解決分類問題主要可以分為以下四個步驟:
1,提取問題中實體的特征向量作為神經網絡的輸入。不同的實體可以提取不同的特征向量。
2,定義神經網絡的結構,並定義如何從神經網絡的輸入得到輸出。這個過程可以是神經網絡的前向傳播算法
3,通過訓練數據來調整神經網絡中參數的取值,這就是訓練神經網絡的過程。
4,使用訓練好的神經網絡來預測未知的數據。
前向傳播算法
下面學習一下最簡單的全連接網絡結構的前向傳播算法,並且將展示如何通過TensorFlow來實現這個算法。
下面首先了解神經元的結構,神經元是一個神經網絡的最小單位,下面顯示一個最簡單的神經元結構:
從上圖可以看出,一個神經元有多個輸入和一個輸出。每個神經元的輸入既可以是其他神經元的輸出,也可以是整個神經網絡的輸入。所謂神經網絡的結構就是指的不同神經元之間的連接結構。一個最簡單的神經元結構的輸出就是所有輸入的加權和,而不同輸入的權重就是神經元的參數。神經網絡的優化過程就是優化神經元中的參數取值的過程。
下圖給出了一個簡單的判斷零件是否合格的三層全連接神經網絡,之所以稱為全連接神經網絡是因為相鄰兩層之間任意兩個節點之間都有連接。
下圖展示一個判斷零件是否合格的三層神經網絡結構圖:
計算神經網絡的前向傳播結構需要三部分信息。第一個部分是神經網絡的輸入,這個輸入就是從實體中提取的特征向量。比如上面有兩個輸入,一個是零件的長度x1,一個是零件的質量 x2,第二個部分為神經網絡的連接結構。神經網絡是由神經元構成的,神經網絡的結構給出不同神經元之間輸入輸出的連接關系。神經網絡中的神經元也可以稱為節點。在上圖中 a11節點有兩個輸入 ,分別是x1 和 x2的輸出。而 a11 的輸出則是節點 y 的輸入。最后一個部分是每個神經元中的采納數。我們用W來表示神經元中的參數。W的上標表名了神經網絡的層數,比如W(1) 表示第一層節點的參數,而W(2) 表示第二層節點的參數。W的下標表明了連接節點編號,比如W(1) 1,2 表示連接 x1 和 a12節點的邊上的權重。這里我們假設權重是已知的。
當我們給定神經網絡的輸入,神經網絡的結構以及邊上權重,就可以通過前向傳播算法來計算出神經網絡的輸出,下圖展示了這個神經網絡前向傳播的過程:
上圖給出來輸入層的取值,從輸入層開始一層一層地使用前向傳播算法,首先隱藏層中有三個節點,每一個節點的取值都是輸入層取值的加權和。當求出輸出值的閾值,判斷是否大於0,這樣就可以判斷是否合格。上面整個過程就是前向傳播的算法。
當然前向傳播的算法可以表示為矩陣乘法,將輸入 x1 x2 組織成一個1*2 的矩陣x = [x1, x2],而W(1) 組織成一個2*3 的矩陣:
這樣通過矩陣乘法可以得到隱藏層三個節點所組成的向量取值:
類似的輸出層可以表示為:
這樣就可以將前向傳播算法通過矩陣乘法的方式表達出來了。在TensorFlow中矩陣政法是非常容易實現的。以下代碼實現了神經網絡的前向傳播過程:
# 定義神經網絡前向傳播的過程 a = tf.matmul(x, w1) y = tf.matmul(a, w2)
其中 tf.matmul 實現了矩陣乘法的功能。
以下樣例介紹了如何通過遍歷實現神經網絡的參數並實現前向傳播的過程:
#_*_coding:utf-8_*_ import tensorflow as tf # 定義神經網絡的參數 # 聲明w1 w2兩個變量,這里還通過seed設定了隨機種子,這樣可以保證運行結果一樣 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是一個1*2的矩陣 x = tf.constant([[0.7, 0.9]]) # 定義神經網絡前向傳播的過程 a = tf.matmul(x, w1) y = tf.matmul(a, w2) sess = tf.Session() # 這里不能直接通過sess.run(y)來獲取y的取值 # 因為w1和w2 都還沒有運行初始化過程,下面分別初始化兩個變量 sess.run(w1.initializer) sess.run(w2.initializer) print(sess.run(y)) # 輸出[[3.957578]] sess.close()
從代碼中可以看出,當聲明了變量w1 w2之后,可以通過w1 w2來定義神經網絡的前向傳播過程並得到中間結果 a 和最后答案 y 。但是這些被定義的計算在這一步中並不是真正的運算,當需要運行這些計算並得到具體的數字的時候,需要進入TensorFlow程序第二步。
在第二步,我們會聲明一個會話(session),然后通過會話計算結果。
通過TensorFlow訓練神經網絡模型
使用監督學習的方式設置神經網絡參數需要有一個標注好的訓練數據集。以判斷零件是否合格為例,這個標注好的訓練數據集就是手機的一批合格零件和一批不合格零件。監督學習最重要的思想就是在已知答案的標注數據集上,模型給出的預測結果要盡量接近真實的答案。通過調整神經網絡中的參數對訓練數據進行擬合,可以使得模型對未知的樣本提供預測的能力。
在神經網絡優化算法中,最常用的方法是反向傳播算法(backpropagation),下圖展示了使用反向傳播算法訓練神經網絡的流程圖:
從上圖可以看出,反向傳播算法實現了一個迭代的過程。在每次迭代的開始,首先需要選取一小部分訓練數據,這一小部分數據叫做一個batch。然后這個batch的樣例會通過前向傳播算法得到神經網絡模型的額預測結果。因為訓練數據都是由正確答案標注的,所以可以計算出當前神經網絡模型的預測答案與正確答案之間的差距。最后,基於這預測值和真實值之間的差距,反向傳播算法會相應的更新神經參數的取值,使得在這個batch上神經網絡模型的預測結果和真實答案更加接近。
通過TensorFlow實現反向傳播算法的第一步是使用TensorFlow表達一個batch的數據,在之前我們使用常量來表達,但是如果每輪迭代中選取的數據都要通過常量來表示,那么TensorFlow都會在計算圖中增加一個節點。一般來說,一個神經網絡的訓練過程會需要經過幾百萬輪甚至幾億輪的迭代,這樣計算圖就會非常大,而且利用率很低。為了避免這個問題,TensorFlow提供了placeholder機制用於提供輸入數據。placeholder相當於定義了一個位置,這個位置中的數據在程序運行時再指定。這樣在程序中就不需要生成大量常量來提供輸入數據,而只需要將數據通過placeholder傳入TensorFlow計算圖。在placeholder定義時,這個位置上的數據類型是需要指定的。和其他張量一樣,placeholder的類型也是不可以改變的。placeholder中數據的維度信息是可以根據提供的數據推導出來,所以不一定給出。
下面給出了通過placeholder實現前向傳播算法:
import tensorflow as tf # 定義神經網絡的參數 # 聲明w1 w2兩個變量,這里還通過seed設定了隨機種子,這樣可以保證運行結果一樣 w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1)) w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 定義placeholder作為存放輸入數據的地方,這里維度也不一定要定義 # 但是如果維度是確定的,那么給出維度可以降低出錯的概率 x = tf.placeholder(tf.float32, shape=(1, 2), name='input') # 定義神經網絡前向傳播的過程 a = tf.matmul(x, w1) y = tf.matmul(a, w2) sess = tf.Session() # 這里不能直接通過sess.run(y)來獲取y的取值 # 因為w1和w2 都還沒有運行初始化過程,下面分別初始化兩個變量 init_op = tf.global_variables_initializer() sess.run(init_op) ''' 下面一行將報錯: InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'input' with dtype float and shape [1,2] ''' # print(sess.run(y)) # 下面一行將會得到之前一樣的輸出結果 print(sess.run(y, feed_dict={x: [[0.7, 0.9]]})) # [[3.957578]] sess.close()
在這段程序中替換了原來通過常量定義的輸入 x ,在新的程序中計算前向傳播結果時,需要提供一個feed_dict 來指定 x 的取值。 feed_dict 是一個字典(map),在字典中需要給出每個用到的placeholder的取值,如果某個需要的placeholder沒有被指定取值,那么在程序運行時候會報錯。
在上面的樣例程序中,如果將輸入的1*2 矩陣改為 n*2 的矩陣,那么就可以得到 n 個樣例的前向傳播結果了。其中 n*2 的矩陣的每一行為一個樣例數據。這樣前向傳播的結果為 n*1 的矩陣,這個矩陣的每一行就代表了一個樣例的前向傳播結果,下面程序給出一個實例:
import tensorflow as tf # 定義神經網絡的參數 # 聲明w1 w2兩個變量,這里還通過seed設定了隨機種子,這樣可以保證運行結果一樣 w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1)) w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 定義placeholder作為存放輸入數據的地方,這里維度也不一定要定義 # 但是如果維度是確定的,那么給出維度可以降低出錯的概率 # x = tf.placeholder(tf.float32, shape=(1, 2), name='input') x =tf.placeholder(tf.float32, shape=(3, 2), name='input') # 定義神經網絡前向傳播的過程 a = tf.matmul(x, w1) y = tf.matmul(a, w2) sess = tf.Session() # 這里不能直接通過sess.run(y)來獲取y的取值 # 因為w1和w2 都還沒有運行初始化過程,下面分別初始化兩個變量 init_op = tf.global_variables_initializer() sess.run(init_op) # 因為x 在定義時指定了 n 為3,所以在運行前向傳播過程時需要提供三個樣例數據 print(sess.run(y, feed_dict={x: [[0.7, 0.9], [0.1, 0.4], [0.5, 0.8]]})) ''' 輸出結果為: [[3.957578 ] [1.1537654] [3.1674924]] ''' sess.close()
上面的樣例中展示了一次性計算多個樣例的前向傳播結果。在運行時,需要將3個樣例組成一個3*2的矩陣傳入placeholder。計算得到的結果為3*1 的矩陣。
在得到一個batch的前向傳播結果之后,需要定義一個損失函數來刻畫當前的預測值和真實答案之間的差距。然后通過反向傳播算法來調整神經網絡參數的取值使得差距可以被縮小。下面定義一個簡單的額損失函數,並通過TensorFlow定義反向傳播的算法。
# 定義損失函數來刻畫預測值與真實值的差距 cross_entropy = -tf.reduce_mean( y_ * tf.log(tf.clip_by_value(y, le-10, 1.0)) ) # 定義學習率 learning_rate = 0.001 # 定義反向傳播算法來優化神經網絡中的采納數 train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)
在上面代碼中,cross_entropy 定義了真實值和預測值之間的交叉熵(cross entropy),這是分類問題中一個常用的損失函數,第二行 train_step 定義了反向傳播的優化方法。目前TensorFlow支持7種不同的優化器,比較常用的優化方法有三種:
tf.train.GradientDescentOptimizer tf.train.AdamOptimizer tf.train.MomentumOptimizer
完整神經網絡樣例程序
代碼如下:
# _*_coding:utf-8_*_ import tensorflow as tf # Numpy 是一個科學計算的工具包,這里通過Numpy工具包生成模擬數據集 from numpy.random import RandomState # 定義訓練數據batch的大小 batch_size = 8 # 定義神經網絡的參數 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的一個維度上使用None可以方便的表示使用不大的batch大小, # 在訓練時需要把數據分成比較小的batch,在測試的時候,可以一次性的使用全部的數據 # 但是數據集比較大的是,將大量數據放入一個batch可能會導致內存溢出。 x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input') y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input') # 定義神經網絡前向傳播的過程 a = tf.matmul(x, w1) y = tf.matmul(a, w2) # 定義損失函數來刻畫預測值與真實值的差距 cross_entropy = -tf.reduce_mean( y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)) ) # 定義學習率 learning_rate = 0.001 # 定義反向傳播算法來優化神經網絡中的采納數 train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy) # 通過隨機數生成一個模擬數據集 rdm = RandomState(1) dataset_size = 128 X = rdm.rand(dataset_size, 2) # 定義規則來給出樣本的標簽,在這里所有x1+x2<1 的樣例都被認為是正樣本(比如零件合格) # 而其他為負樣本(比如零件不合格)和TensorFlow游樂場中的表示法不大一樣的地方式 # 這里使用0表示負樣本,1來表示正樣本,大部分解決分類問題的神經網絡都會采用0和1的表示方法 Y = [[int(x1 + x2 < 1)] for (x1, x2) in X] # 創建一個會話來運行TensorFlow with tf.Session() as sess: init_op = tf.global_variables_initializer() # 初始化變量 sess.run(init_op) print(sess.run(w1)) print(sess.run(w2)) ''' 在訓練之前神經網絡參數的值 w1 = [[-0.8113182 1.4845988 0.06532937] [-2.4427042 0.0992484 0.5912243 ]] w2 = [[-0.8113182 ] [ 1.4845988 ] [ 0.06532937]] ''' # 設定訓練的輪數 STEPS = 5000 for i in range(STEPS): # 每次選取batch_size 個樣本進行訓練 start = (i * batch_size) % dataset_size end = min(start + batch_size, dataset_size) # 通過選取的樣本訓練神經網絡並更新參數 sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start: end]}) if i % 1000 == 0: # 每隔一段時間計算在所有數據上的交叉熵並輸出 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)) ''' 輸出結果: After 0 training step(s), cross_entropy on all data is 0.0674925 After 1000 training step(s), cross_entropy on all data is 0.0163385 After 2000 training step(s), cross_entropy on all data is 0.00907547 After 3000 training step(s), cross_entropy on all data is 0.00714436 After 4000 training step(s), cross_entropy on all data is 0.00578471 通過這個結果可以發現隨着訓練的進行,交叉熵是逐漸變小的 交叉熵越小說明預測的結果和真實的結果差距越小 ''' print(sess.run(w1)) print(sess.run(w2)) ''' 在訓練之后神經網絡參數的值 w1 = [[-1.9618275 2.582354 1.6820377] [-3.4681718 1.0698231 2.11789 ]] w2 = [[-1.824715 ] [ 2.6854665] [ 1.418195 ]] 從和開始的神經網絡參數值對比,我們發現這兩個參數的取值是已經發生變化 這個變化就是訓練的結果,它使得這個神經網絡能更好的擬合提供的訓練數據集 '''
上面的程序實現了訓練神經網絡的全部過從,從這段程序中可以總結出訓練神經網絡的過程分為以下三個步驟:
- 1,定義神經網絡的結構和前向傳播的輸出結果
- 2,定義損失函數以及選擇反向傳播優化的算法
- 3,生成會話(tf.Session)並且在訓練數據上反復進行反向傳播優化算法
無論神經網絡的結構如何變化,這三個步驟是不變的。
tf.Variable() & tf.get_variable()
tf.Variable() 和 tf.get_variable() 都可以用來創建變量,但是前者會自動保證唯一性,而后者不能保證唯一性。
我們可以對比兩個函數:
# 新建一個變量,變量值是 initial_value Variable(initial_value=None, trainable=True, collections=None, validate_shape=True, caching_device=None,name=None, expected_shape=None, import_scope=None, constraint=None) # 獲取具有這些參數的現有變量或者創建一個新變量。(可以創建共享變量) # 如果該name的變量還未定義,則新創建一個,如果依據定義了,則直接獲取該變量 get_variable(name, shape=None, dtype=None, initializer=None, regularizer=None, trainable=True, collections=None, caching_device=None, partitioner=None, validate_shape=True, use_resouce=None, constraint=None)
下面舉個例子來說明二者的不同之處:
#_*_coding:utf-8_*_ ''' 下面例子來說明 tf.Variable() 和 tf.get_variable() 的不同之處 ''' import tensorflow as tf with tf.variable_scope('scope1'): w1 = tf.Variable(1, name='w1') w2 = tf.get_variable(name='w2', initializer=2.) with tf.variable_scope('scope1', reuse=True): w1_p = tf.Variable(1, name='w1') w2_p = tf.get_variable(name='w2', initializer=3.) print('w1', w1) print('w1_p', w1_p) # w1 <tf.Variable 'scope1/w1:0' shape=() dtype=int32_ref> # w1_p <tf.Variable 'scope1_1/w1:0' shape=() dtype=int32_ref> print('w2', w2) print('w2_p', w2_p) # w2 <tf.Variable 'scope1/w2:0' shape=() dtype=float32_ref> # w2_p <tf.Variable 'scope1/w2:0' shape=() dtype=float32_ref> print(w1 is w1_p, w2 is w2_p) # False True
我們可以看出, tf.Variable()會自動處理沖突問題,如上面代碼所示。而tf.get_variable()會判斷是否已經存在該name的變量,如果有,且該變量空間的reuse=True,那么就可以直接共享之前的值,如果沒有,則重新創建。(注意:如果沒有將reuse設置為True,則會提示沖突發生)。錯誤如下:
ValueError: Variable scope1/w2 already exists, disallowed. Did you mean to set reuse=True in VarScope? Originally defined at:
因為代碼的最后一句語句是是判斷上述變量是否相等,可以看出,通過get_variable()定義的變量是完全等價的,即使后一句 get_variable 是將 initializer 設為3,但是由於 name='w2' 的變量已經存在,並且 reuse=True,則直接引用之前定義的,這樣就可以用 get_variable() 來定義共享變量。
在生成上下文管理器時,若設置reuse=True,tf.variable_scope將只能獲取已經創建過的變量,如果空間中沒有變量則會報錯。如果reuse=False 或者 reuse=None,tf.get_variable將創建新的變量。而且同名變量已經存在,會報錯。
tf.get_variable & tf.variable_scope
tf.get_variable 函數可以用來創建或者獲取變量,當創建變量時,與 tf.Variable是一樣的。
tf.variable_scope 函數生成一個上下文管理器,用於控制 tf.get_variable。
這里,我們會發現, tf.get_variable() 在使用時,一般會和 tf.varibale_scope() 配套使用,需要指定它的作用域空間,這樣在引用的使用的使用就可以通過設置指定的scope的 reuse=True進行引用。
#_*_coding:utf-8_*_ ''' 變量生成之 tf.get_variable 與 tf.variable_scope reuse參數 ''' import tensorflow as tf with tf.variable_scope('a'): v1 = tf.get_variable("v", [1], initializer=tf.constant_initializer(1.0)) # with tf.variable_scope("a"): # 報錯 ValueError: Variable a/v already exists, # v2 = tf.get_variable("v", [1]) with tf.variable_scope("a", reuse=True): v3 = tf.get_variable("v", [1]) print(v3 == v1) # True with tf.variable_scope("b", reuse=True): # 報錯 ValueError: Variable b/v does not exist, or was not created with tf.get_variable(). v4 = tf.get_variable("v",[1])
變量作用域:tf.variable_scope & tf.name_scope
在TensorFlow中有兩個作用域(scope),分別是 tf.variable_scope() 和 tf.name_scope()。 其中name_scope() 是給 op_name加前綴,指定op的作用域空間 ,op是指操作。而variable_scope() 是給get_variable() 創建的變量的名字加前綴,表明作用域空間,也可以用於處理命名沖突。
variable_scope 示例
variable_scope 變量作用域機制在TensorFlow中主要由兩部分組成:
當 tf.get_variable_scope().reuse == False時,varibale_scope 作用域只能用來創建新變量:
上面程序會拋出 ValueError 錯誤,因為 v 這個變量已經被定義過了,但 tf.get_variable_scope().reuse默認為FALSE,所以不能重復使用。
當 tf.get_variable_scope().reuse ==True 時,作用域可以共享變量:
1,獲取變量作用域
可以直接通過 tf.varibale_scope() 來獲取變量作用域:
如果在開啟的一個變量作用域里使用預先定義的一個作用域,則會跳過當前變量的作用域,保持預先存在的作用域不變。
2,變量作用域的初始化
變量作用域可以默認攜帶一個初始化器,在這個作用域中的子作用域或變量都可以繼承或者重寫父作用域初始化器中的值,方法如下:
上面講的是 variable_name,那么對於 op_name呢?在varibale_scope 作用域下的操作,也會被加上前綴:
varibale_scope 主要用在循環神經網絡(RNN)的操作中,其中需要大量的共享變量。
name_scope 示例
TensorFlow中常常有數以千計的節點,在可視化的過程中很難一下展示出來,因此用 name_scope為變量划分范圍,在可視化中,這表示在計算圖中的一個層級。name_scope會影響 op_name,不會影響用 get_varibale()創建的變量,而會影響通過Varibale() 創建的變量,因此:
可以看出,tf.name_scope() 返回的是一個字符串,如上述的“bar”。name_scope對用 get_varibale()創建的變量的名字不會有任何影響,而Varibale() 創建的操作會被加上前綴,並且會給操作機上名字前綴。
tf.cast() 數據類型轉換
tf.cast() 函數的作用是執行 tensorflow中張量數據類型轉換,比如讀入的圖片如果是 int8 類型的,一般在訓練前把圖像的數據格式轉換為float32。
cast()定義:
cast(x, dtype, name=None)
第一個參數 x:待轉換的數據(張量)
第二個參數dtype:目標數據類型
第三個參數name:可選參數,定義操作的名稱
int32轉換為float32 代碼:
#_*_coding:utf-8_*_ import tensorflow as tf t1 = tf.Variable([1, 2, 3, 4, 5]) t2 = tf.cast(t1, dtype=tf.float32) print('t1: {}'.format(t1)) print('t2:{}'.format(t2)) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) sess.run(t2) print(t2.eval()) print(sess.run(t2))
輸出如下:
t1: <tf.Variable 'Variable:0' shape=(5,) dtype=int32_ref> t2:Tensor("Cast:0", shape=(5,), dtype=float32) [1. 2. 3. 4. 5.] [1. 2. 3. 4. 5.]
tf.argmax的使用
tf.argmax(vector, 1):返回的是vector中的最大值的索引號,如果vector是一個向量,那就返回一個值,如果是一個矩陣,那就返回一個向量,這個向量的每一個維度都是相對應矩陣行的最大值元素的索引號。
import tensorflow as tf import numpy as np A = [[1,3,4,5,6]] B = [[1,3,4], [2,4,1]] with tf.Session() as sess: print(sess.run(tf.argmax(A, 1))) print(sess.run(tf.argmax(B, 1))) --------------------- 輸出: [4] [2 1]
參考文獻:
https://blog.csdn.net/uestc_c2_403/article/details/72232807
此文是自己的學習筆記總結,學習於《TensorFlow深度學習框架》,俗話說,好記性不如爛筆頭,寫寫總是好的,所以若侵權,請聯系我,謝謝。