TensorFlow2
這是一篇整理的文件,原教程連結簡單粗暴TensorFlow 2。0 0。4 alpha 文件
1。TensorFlow基礎
TensorFlow 使用 張量 (Tensor)作為資料的基本單位。TensorFlow 的張量在概念上等同於多維陣列,我們可以使用它來描述數學中的標量(0 維陣列)、向量(1 維陣列)、矩陣(2 維陣列)等各種量。
本教程基於 TensorFlow 的 Eager Execution 模式。在 TensorFlow 1。X 版本中, 必須 在匯入 TensorFlow 庫後呼叫 tf。enable_eager_execution() 函式以啟用 Eager Execution 模式。在 TensorFlow 2。0 版本中,Eager Execution 模式將成為預設模式,無需額外呼叫 tf。enable_eager_execution() 函式(不過若要關閉 Eager Execution,則需呼叫 tf。compat。v1。disable_eager_execution() 函式)。
import
tensorflow
as
tf
# 定義一個隨機數(標量)
random_float
=
tf
。
random
。
uniform
(
shape
=
())
# 定義一個有2個元素的零向量
zero_vector
=
tf
。
zeros
(
shape
=
(
2
))
# 定義兩個2×2的常量矩陣
A
=
tf
。
constant
([[
1。
,
2。
],
[
3。
,
4。
]])
B
=
tf
。
constant
([[
5。
,
6。
],
[
7。
,
8。
]])
張量的重要屬性是其形狀、型別和值。可以透過張量的 shape 、 dtype 屬性和 numpy() 方法獲得。
# 檢視矩陣A的形狀、型別和值
(
A
。
shape
)
# 輸出(2, 2),即矩陣的長和寬均為2
(
A
。
dtype
)
# 輸出
(
A
。
numpy
())
# 輸出[[1。 2。]
# [3。 4。]]
TensorFlow 的大多數 API 函式會根據輸入的值自動推斷張量中元素的型別(一般預設為 tf。float32 )。不過你也可以透過加入 dtype 引數來自行指定型別,例如 zero_vector = tf。zeros(shape=(2), dtype=tf。int32) 將使得張量中的元素型別均為整數。張量的 numpy() 方法是將張量的值轉換為一個 NumPy 陣列。
TensorFlow 裡有大量的 操作 (Operation),使得我們可以將已有的張量進行運算後得到新的張量。
C
=
tf
。
add
(
A
,
B
)
# 計算矩陣A和B的和
D
=
tf
。
matmul
(
A
,
B
)
# 計算矩陣A和B的乘積
1。1自動求導機制
在機器學習中,我們經常需要計算函式的導數。TensorFlow 提供了強大的自動求導機制來計算導數。以下程式碼展示瞭如何使用 tf。GradientTape() 計算函式 y(x) = x^2 在 x = 3 時的導數:
import
tensorflow
as
tf
x
=
tf
。
Variable
(
initial_value
=
3。
)
with
tf
。
GradientTape
()
as
tape
:
# 在 tf。GradientTape() 的上下文內,所有計算步驟都會被記錄以用於求導
y
=
tf
。
square
(
x
)
y_grad
=
tape
。
gradient
(
y
,
x
)
# 計算y關於x的導數
([
y
,
y_grad
])
這裡 x 是一個初始化為 3 的 變數 (Variable),使用 tf。Variable() 宣告。與普通張量一樣,變數同樣具有形狀、型別和值三種屬性。使用變數需要有一個初始化過程,可以透過在 tf。Variable() 中指定 initial_value 引數來指定初始值。這裡將變數 x 初始化為 3。 1。變數與普通張量的一個重要區別是其預設能夠被 TensorFlow 的自動求導機制所求導,因此往往被用於定義機器學習模型的引數。
tf。GradientTape() 是一個自動求導的記錄器,在其中的變數和計算步驟都會被自動記錄。在上面的示例中,變數 x 和計算步驟 y = tf。square(x) 被自動記錄,因此可以透過 y_grad = tape。gradient(y, x) 求張量 y 對變數 x 的導數。
在機器學習中,更加常見的是對多元函式求偏導數,以及對向量或矩陣的求導。這些對於 TensorFlow 也不在話下。以下程式碼展示瞭如何使用 tf。GradientTape() 計算函式 L(w, b) = \|Xw + b - y\|^2 在 w = (1, 2)^T, b = 1 時分別對 w, b 的偏導數。
X
=
tf
。
constant
([[
1。
,
2。
],
[
3。
,
4。
]])
y
=
tf
。
constant
([[
1。
],
[
2。
]])
w
=
tf
。
Variable
(
initial_value
=
[[
1。
],
[
2。
]])
b
=
tf
。
Variable
(
initial_value
=
1。
)
with
tf
。
GradientTape
()
as
tape
:
L
=
0。5
*
tf
。
reduce_sum
(
tf
。
square
(
tf
。
matmul
(
X
,
w
)
+
b
-
y
))
w_grad
,
b_grad
=
tape
。
gradient
(
L
,
[
w
,
b
])
# 計算L(w, b)關於w, b的偏導數
([
L
。
numpy
(),
w_grad
。
numpy
(),
b_grad
。
numpy
()])
這裡, tf。square() 操作代表對輸入張量的每一個元素求平方,不改變張量形狀。 tf。reduce_sum() 操作代表對輸入張量的所有元素求和,輸出一個形狀為空的純量張量(可以透過 axis 引數來指定求和的維度,不指定則預設對所有元素求和)。TensorFlow 中有大量的張量操作 API,包括數學運算、張量形狀操作(如 tf。reshape())、切片和連線(如 tf。concat())等多種型別。
1。2基礎示例:線性迴歸
import
numpy
as
np
# 定義資料,進行基本的歸一化操作。
X_raw
=
np
。
array
([
2013
,
2014
,
2015
,
2016
,
2017
],
dtype
=
np
。
float32
)
y_raw
=
np
。
array
([
12000
,
14000
,
15000
,
16500
,
17500
],
dtype
=
np
。
float32
)
X
=
(
X_raw
-
X_raw
。
min
())
/
(
X_raw
。
max
()
-
X_raw
。
min
())
y
=
(
y_raw
-
y_raw
。
min
())
/
(
y_raw
。
max
()
-
y_raw
。
min
())
X
=
tf
。
constant
(
X
)
y
=
tf
。
constant
(
y
)
a
=
tf
。
Variable
(
initial_value
=
0。
)
b
=
tf
。
Variable
(
initial_value
=
0。
)
variables
=
[
a
,
b
]
num_epoch
=
10000
optimizer
=
tf
。
keras
。
optimizers
。
SGD
(
learning_rate
=
1e-3
)
for
e
in
range
(
num_epoch
):
# 使用tf。GradientTape()記錄損失函式的梯度資訊
with
tf
。
GradientTape
()
as
tape
:
y_pred
=
a
*
X
+
b
loss
=
0。5
*
tf
。
reduce_sum
(
tf
。
square
(
y_pred
-
y
))
# TensorFlow自動計算損失函式關於自變數(模型引數)的梯度
grads
=
tape
。
gradient
(
loss
,
variables
)
# TensorFlow自動根據梯度更新引數
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
variables
))
(
a
,
b
)
在這裡,我們使用了前文的方式計算了損失函式關於引數的偏導數。同時,使用 tf。keras。optimizers。SGD(learning_rate=1e-3) 聲明瞭一個梯度下降 最佳化器 (Optimizer),其學習率為 1e-3。最佳化器可以幫助我們根據計算出的求導結果更新模型引數,從而最小化某個特定的損失函式,具體使用方式是呼叫其 apply_gradients() 方法。
注意到這裡,更新模型引數的方法 optimizer。apply_gradients() 需要提供引數 grads_and_vars,即待更新的變數(如上述程式碼中的 variables )及損失函式關於這些變數的偏導數(如上述程式碼中的 grads )。具體而言,這裡需要傳入一個 Python 列表(List),列表中的每個元素是一個 (變數的偏導數,變數) 對。比如這裡是 [(grad_a, a), (grad_b, b)] 。我們透過 grads = tape。gradient(loss, variables) 求出 tape 中記錄的 loss 關於 variables = [a, b] 中每個變數的偏導數,也就是 grads = [grad_a, grad_b],再使用 Python 的 zip() 函式將 grads = [grad_a, grad_b] 和 variables = [a, b] 拼裝在一起,就可以組合出所需的引數了。
python的zip() 函式是 Python 的內建函式。用自然語言描述這個函式的功能很繞口,但如果舉個例子就很容易理解了:如果 a = [1, 3, 5], b = [2, 4, 6],那麼 zip(a, b) = [(1, 2), (3, 4), 。。。, (5, 6)] 。即 “將可迭代的物件作為引數,將物件中對應的元素打包成一個個元組,然後返回由這些元組組成的列表”。在 Python 3 中, zip() 函式返回的是一個物件,需要呼叫 list() 來將物件轉換成列表。
在實際應用中,我們編寫的模型往往比這裡一行就能寫完的線性模型 y_pred = a * X + b (模型引數為 variables = [a, b] )要複雜得多。所以,我們往往會編寫並例項化一個模型類 model = Model() ,然後使用 y_pred = model(X) 呼叫模型,使用 model。variables 獲取模型引數。
與 Eager Execution 相對的是 Graph Execution(靜態圖)模式,即 TensorFlow 在 2018 年 3 月的 1。8 版本釋出之前所主要使用的模式。本教程以面向快速迭代開發的動態模式為主,但會在附錄中介紹靜態圖模式的基本使用,供需要的讀者查閱。
2。TensorFlow模型建立與訓練
模型的構建: tf。keras。Model 和 tf。keras。layers
模型的損失函式: tf。keras。losses
模型的最佳化器: tf。keras。optimizer
模型的評估: tf。keras。metrics
2。1模型(Model)與層(Layer)
在 TensorFlow 中,推薦使用 Keras( tf。keras )構建模型。Keras 是一個廣為流行的高階神經網路 API,簡單、快速而不失靈活性,現已得到 TensorFlow 的官方內建和全面支援。
Keras 有兩個重要的概念: 模型(Model) 和 層(Layer) 。層將各種計算流程和變數進行了封裝(例如基本的全連線層,CNN 的卷積層、池化層等),而模型則將各種層進行組織和連線,並封裝成一個整體,描述瞭如何將輸入資料透過各種層以及運算而得到輸出。在需要模型呼叫的時候,使用 y_pred = model(X) 的形式即可。Keras 在 tf。keras。layers 下內建了深度學習中大量常用的的預定義層,同時也允許我們自定義層。
Keras 模型以類的形式呈現,我們可以透過繼承 tf。keras。Model 這個 Python 類來定義自己的模型。在繼承類中,我們需要重寫
init
() (建構函式,初始化)和 call(input) (模型呼叫)兩個方法,同時也可以根據需要增加自定義的方法。
class
MyModel
(
tf
。
keras
。
Model
):
def
__init__
(
self
):
super
()
。
__init__
()
# Python 2 下使用 super(MyModel, self)。__init__()
# 此處新增初始化程式碼(包含 call 方法中會用到的層),例如
# layer1 = tf。keras。layers。BuiltInLayer(。。。)
# layer2 = MyCustomLayer(。。。)
def
call
(
self
,
input
):
# 此處新增模型呼叫的程式碼(處理輸入並返回輸出),例如
# x = layer1(input)
# output = layer2(x)
return
output
繼承 tf。keras。Model 後,我們同時可以使用父類的若干方法和屬性,例如在例項化類 model = Model() 後,可以透過 model。variables 這一屬性直接獲得模型中的所有變數,免去我們一個個顯式指定變數的麻煩。
上一章中簡單的線性模型 y_pred = a * X + b ,我們可以透過模型類的方式編寫如下:
import
tensorflow
as
tf
X
=
tf
。
constant
([[
1。0
,
2。0
,
3。0
],
[
4。0
,
5。0
,
6。0
]])
y
=
tf
。
constant
([[
10。0
],
[
20。0
]])
class
Linear
(
tf
。
keras
。
Model
):
def
__init__
(
self
):
super
()
。
__init__
()
self
。
dense
=
tf
。
keras
。
layers
。
Dense
(
units
=
1
,
activation
=
None
,
kernel_initializer
=
tf
。
zeros_initializer
(),
bias_initializer
=
tf
。
zeros_initializer
()
)
def
call
(
self
,
input
):
output
=
self
。
dense
(
input
)
return
output
# 以下程式碼結構與前節類似
model
=
Linear
()
optimizer
=
tf
。
keras
。
optimizers
。
SGD
(
learning_rate
=
0。01
)
for
i
in
range
(
100
):
with
tf
。
GradientTape
()
as
tape
:
y_pred
=
model
(
X
)
# 呼叫模型 y_pred = model(X) 而不是顯式寫出 y_pred = a * X + b
loss
=
tf
。
reduce_mean
(
tf
。
square
(
y_pred
-
y
))
grads
=
tape
。
gradient
(
loss
,
model
。
variables
)
# 使用 model。variables 這一屬性直接獲得模型中的所有變數
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
model
。
variables
))
(
model
。
variables
)
這裡,我們沒有顯式地宣告 a 和 b 兩個變數並寫出 y_pred = a * X + b 這一線性變換,而是建立了一個繼承了 tf。keras。Model 的模型類 Linear 。這個類在初始化部分例項化了一個 全連線層 ( tf。keras。layers。Dense ),並在 call 方法中對這個層進行呼叫,實現了線性變換的計算。
2。2基礎示例:多層感知機(MLP)
使用 tf。keras。datasets 獲得資料集並預處理
使用 tf。keras。Model 和 tf。keras。layers 構建模型
構建模型訓練流程,使用 tf。keras。losses 計算損失函式,並使用 tf。keras。optimizer 最佳化模型
構建模型評估流程,使用 tf。keras。metrics 計算評估指標
2。2。1資料獲取及預處理:tf。keras。datasets
class
MNISTLoader
():
def
__init__
(
self
):
mnist
=
tf
。
keras
。
datasets
。
mnist
(
self
。
train_data
,
self
。
train_label
),
(
self
。
test_data
,
self
。
test_label
)
=
mnist
。
load_data
()
# MNIST中的影象預設為uint8(0-255的數字)。以下程式碼將其歸一化到0-1之間的浮點數,並在最後增加一維作為顏色通道
self
。
train_data
=
np
。
expand_dims
(
self
。
train_data
。
astype
(
np
。
float32
)
/
255。0
,
axis
=-
1
)
# [60000, 28, 28, 1]
self
。
test_data
=
np
。
expand_dims
(
self
。
test_data
。
astype
(
np
。
float32
)
/
255。0
,
axis
=-
1
)
# [10000, 28, 28, 1]
self
。
train_label
=
self
。
train_label
。
astype
(
np
。
int32
)
# [60000]
self
。
test_label
=
self
。
test_label
。
astype
(
np
。
int32
)
# [10000]
self
。
num_train_data
,
self
。
num_test_data
=
self
。
train_data
。
shape
[
0
],
self
。
test_data
。
shape
[
0
]
def
get_batch
(
self
,
batch_size
):
# 從資料集中隨機取出batch_size個元素並返回
index
=
np
。
random
。
randint
(
0
,
np
。
shape
(
self
。
train_data
)[
0
],
batch_size
)
return
self
。
train_data
[
index
,
:],
self
。
train_label
[
index
]
在 TensorFlow 中,影象資料集的一種典型表示是 [影象數目,長,寬,色彩通道數] 的四維張量。在上面的 DataLoader 類中, self。train_data 和 self。test_data 分別載入了 60,000 和 10,000 張大小為 28*28 的手寫體數字圖片。由於這裡讀入的是灰度圖片,色彩通道數為 1(彩色 RGB 影象色彩通道數為 3),所以我們使用 np。expand_dims() 函式為影象資料手動在最後新增一維通道。
2。2。2模型的構建:tf。keras。Model和tf。keras。layers
多層感知機的模型類實現與上面的線性模型類似,使用 tf。keras。Model 和 tf。keras。layers 構建,所不同的地方在於層數增加了(顧名思義,“多層” 感知機),以及引入了非線性啟用函式(這裡使用了 ReLU 函式 , 即下方的 activation=tf。nn。relu )。該模型輸入一個向量(比如這裡是拉直的 1×784 手寫體數字圖片),輸出 10 維的向量,分別代表這張圖片屬於 0 到 9 的機率。
class
MLP
(
tf
。
keras
。
Model
):
def
__init__
(
self
):
super
()
。
__init__
()
self
。
flatten
=
tf
。
keras
。
layers
。
Flatten
()
# Flatten層將除第一維(batch_size)以外的維度展平
self
。
dense1
=
tf
。
keras
。
layers
。
Dense
(
units
=
100
,
activation
=
tf
。
nn
。
relu
)
self
。
dense2
=
tf
。
keras
。
layers
。
Dense
(
units
=
10
)
def
call
(
self
,
inputs
):
# [batch_size, 28, 28, 1]
x
=
self
。
flatten
(
inputs
)
# [batch_size, 784]
x
=
self
。
dense1
(
x
)
# [batch_size, 100]
x
=
self
。
dense2
(
x
)
# [batch_size, 10]
output
=
tf
。
nn
。
softmax
(
x
)
return
output
這裡,因為我們希望輸出 “輸入圖片分別屬於 0 到 9 的機率”,也就是一個 10 維的離散機率分佈,所以我們希望這個 10 維向量至少滿足兩個條件: * 該向量中的每個元素均在 [0, 1] 之間; * 該向量的所有元素之和為 1。 為了使得模型的輸出能始終滿足這兩個條件,我們使用 Softmax 函式 (歸一化指數函式, tf。nn。softmax )對模型的原始輸出進行歸一化。其形式為 $\sigma(\mathbf{z})_j = \frac{e^{z_j}}{\sum_{k=1}^K e^{z_k}}$。不僅如此,softmax 函式能夠凸顯原始向量中最大的值,並抑制遠低於最大值的其他分量,這也是該函式被稱作 softmax 函式的原因(即平滑化的 argmax 函式)。
2。2。3模型的訓練:tf。keras。losses和tf。keras。optimizer
定義一些模型超引數:
num_epochs
=
5
batch_size
=
50
learning_rate
=
0。001
例項化模型和資料讀取類,並例項化一個 tf。keras。optimizer 的最佳化器。
model
=
MLP
()
data_loader
=
MNISTLoader
()
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
learning_rate
)
然後迭代進行以下步驟:
從 DataLoader 中隨機取一批訓練資料;
將這批資料送入模型,計算出模型的預測值;
將模型預測值與真實值進行比較,計算損失函式(loss)。這裡使用 tf。keras。losses 中的交叉熵函式作為損失函式;
計算損失函式關於模型變數的導數; * 將求出的導數值傳入最佳化器,使用最佳化器的 apply_gradients 方法更新模型引數以最小化損失函式
num_batches
=
int
(
data_loader
。
num_train_data
//
batch_size
*
num_epochs
)
for
batch_index
in
range
(
num_batches
):
X
,
y
=
data_loader
。
get_batch
(
batch_size
)
with
tf
。
GradientTape
()
as
tape
:
y_pred
=
model
(
X
)
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
(
y_true
=
y
,
y_pred
=
y_pred
)
loss
=
tf
。
reduce_mean
(
loss
)
(
“batch
%d
: loss
%f
”
%
(
batch_index
,
loss
。
numpy
()))
grads
=
tape
。
gradient
(
loss
,
model
。
variables
)
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
model
。
variables
))
2。2。4模型的評估:tf。keras。metrics
最後,我們使用測試集評估模型的效能。這裡,我們使用 tf。keras。metrics 中的 SparseCategoricalAccuracy 評估器來評估模型在測試集上的效能,該評估器能夠對模型預測的結果與真實結果進行比較,並輸出預測正確的樣本數佔總樣本數的比例。我們迭代測試資料集,每次透過 update_state() 方法向評估器輸入兩個引數: y_pred 和 y_true ,即模型預測出的結果和真實結果。評估器具有內部變數來儲存當前評估指標相關的引數數值(例如當前已傳入的累計樣本數和當前預測正確的樣本數)。迭代結束後,我們使用 result() 方法輸出最終的評估指標值(預測正確的樣本數佔總樣本數的比例)。
在以下程式碼中,我們例項化了一個 tf。keras。metrics。SparseCategoricalAccuracy 評估器,並使用 For 迴圈迭代分批次傳入了測試集資料的預測結果與真實結果,並輸出訓練後的模型在測試資料集上的準確率。
sparse_categorical_accuracy
=
tf
。
keras
。
metrics
。
SparseCategoricalAccuracy
()
num_batches
=
int
(
data_loader
。
num_test_data
//
batch_size
)
for
batch_index
in
range
(
num_batches
):
start_index
,
end_index
=
batch_index
*
batch_size
,
(
batch_index
+
1
)
*
batch_size
y_pred
=
model
。
predict
(
data_loader
。
test_data
[
start_index
:
end_index
])
sparse_categorical_accuracy
。
update_state
(
y_true
=
data_loader
。
test_label
[
start_index
:
end_index
],
y_pred
=
y_pred
)
(
“test accuracy:
%f
”
%
sparse_categorical_accuracy
。
result
())
2。3卷積神經網路(CNN)
2。3。1使用Keras實現卷積神經網路
class
CNN
(
tf
。
keras
。
Model
):
def
__init__
(
self
):
super
()
。
__init__
()
self
。
conv1
=
tf
。
keras
。
layers
。
Conv2D
(
filters
=
32
,
# 卷積層神經元(卷積核)數目
kernel_size
=
[
5
,
5
],
# 感受野大小
padding
=
‘same’
,
# padding策略(vaild 或 same)
activation
=
tf
。
nn
。
relu
# 啟用函式
)
self
。
pool1
=
tf
。
keras
。
layers
。
MaxPool2D
(
pool_size
=
[
2
,
2
],
strides
=
2
)
self
。
conv2
=
tf
。
keras
。
layers
。
Conv2D
(
filters
=
64
,
kernel_size
=
[
5
,
5
],
padding
=
‘same’
,
activation
=
tf
。
nn
。
relu
)
self
。
pool2
=
tf
。
keras
。
layers
。
MaxPool2D
(
pool_size
=
[
2
,
2
],
strides
=
2
)
self
。
flatten
=
tf
。
keras
。
layers
。
Reshape
(
target_shape
=
(
7
*
7
*
64
,))
self
。
dense1
=
tf
。
keras
。
layers
。
Dense
(
units
=
1024
,
activation
=
tf
。
nn
。
relu
)
self
。
dense2
=
tf
。
keras
。
layers
。
Dense
(
units
=
10
)
def
call
(
self
,
inputs
):
x
=
self
。
conv1
(
inputs
)
# [batch_size, 28, 28, 32]
x
=
self
。
pool1
(
x
)
# [batch_size, 14, 14, 32]
x
=
self
。
conv2
(
x
)
# [batch_size, 14, 14, 64]
x
=
self
。
pool2
(
x
)
# [batch_size, 7, 7, 64]
x
=
self
。
flatten
(
x
)
# [batch_size, 7 * 7 * 64]
x
=
self
。
dense1
(
x
)
# [batch_size, 1024]
x
=
self
。
dense2
(
x
)
# [batch_size, 10]
output
=
tf
。
nn
。
softmax
(
x
)
return
output
2。3。2使用Keras中預定義的經典卷積神經網路結構
tf。keras。applications 中有一些預定義好的經典卷積神經網路結構,如 VGG16 、 VGG19 、 ResNet 、 MobileNet 等。我們可以直接呼叫這些經典的卷積神經網路結構(甚至載入預訓練的引數),而無需手動定義網路結構。
例如,我們可以使用以下程式碼來例項化一個 MobileNetV2 網路結構。
model
=
tf
。
keras
。
applications
。
MobileNetV2
()
當執行以上程式碼時,TensorFlow 會自動從網路上下載 MobileNetV2 網路結構,因此在第一次執行程式碼時需要具備網路連線。每個網路結構具有自己特定的詳細引數設定,一些共通的常用引數如下:
input_shape :輸入張量的形狀(不含第一維的 Batch),大多預設為 224 × 224 × 3 。一般而言,模型對輸入張量的大小有下限,長和寬至少為 32 × 32 或 75 × 75 ;
include_top :在網路的最後是否包含全連線層,預設為 True ;
weights :預訓練權值,預設為 ‘imagenet’ ,即為當前模型載入在 ImageNet 資料集上預訓練的權值。如需隨機初始化變數可設為 None ;
classes :分類數,預設為 1000。修改該引數需要 include_top 引數為 True 且 weights 引數為 None 。
以下展示一個例子,使用 MobileNetV2 網路在 tf_flowers 五分類資料集上進行訓練(為了程式碼的簡短高效,在該示例中我們使用了 TensorFlow Datasets 和 tf。data 載入和預處理資料)。透過將 weights 設定為 None ,我們隨機初始化變數而不使用預訓練權值。同時將 classes 設定為 5,對應於 5 分類的資料集。
import
tensorflow
as
tf
import
tensorflow_datasets
as
tfds
num_batches
=
1000
batch_size
=
50
learning_rate
=
0。001
dataset
=
tfds
。
load
(
“tf_flowers”
,
split
=
tfds
。
Split
。
TRAIN
,
as_supervised
=
True
)
dataset
=
dataset
。
map
(
lambda
img
,
label
:
(
tf
。
image
。
resize
(
img
,
[
224
,
224
])
/
255。0
,
label
))
。
shuffle
(
1024
)
。
batch
(
32
)
model
=
tf
。
keras
。
applications
。
MobileNetV2
(
weights
=
None
,
classes
=
5
)
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
learning_rate
)
for
images
,
labels
in
dataset
:
with
tf
。
GradientTape
()
as
tape
:
labels_pred
=
model
(
images
)
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
(
y_true
=
labels
,
y_pred
=
labels_pred
)
loss
=
tf
。
reduce_mean
(
loss
)
(
“loss
%f
”
%
loss
。
numpy
())
grads
=
tape
。
gradient
(
loss
,
model
。
trainable_variables
)
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
model
。
trainable_variables
))
RNN
DRL
Sequential/Functional建立模型
最典型和常用的神經網路結構是將一堆層按特定順序疊加起來,那麼,我們是不是隻需要提供一個層的列表,就能由 Keras 將它們自動首尾相連,形成模型呢?Keras 的 Sequential API 正是如此。透過向 tf。keras。models。Sequential() 提供一個層的列表,就能快速地建立一個 tf。keras。Model 模型並返回。
model
=
tf
。
keras
。
models
。
Sequential
([
tf
。
keras
。
layers
。
Flatten
(),
tf
。
keras
。
layers
。
Dense
(
100
,
activation
=
tf
。
nn
。
relu
),
tf
。
keras
。
layers
。
Dense
(
10
),
tf
。
keras
。
layers
。
Softmax
()
])
不過,這種層疊結構並不能表示任意的神經網路結構。為此,Keras 提供了 Functional API,幫助我們建立更為複雜的模型,例如多輸入 / 輸出或存在引數共享的模型。其使用方法是將層作為可呼叫的物件並返回張量(這點與之前章節的使用方法一致),並將輸入向量和輸出向量提供給 tf。keras。Model 的 inputs 和 outputs 引數。
inputs
=
tf
。
keras
。
Input
(
shape
=
(
28
,
28
,
1
))
x
=
tf
。
keras
。
layers
。
Flatten
()(
inputs
)
x
=
tf
。
keras
。
layers
。
Dense
(
units
=
100
,
activation
=
tf
。
nn
。
relu
)(
x
)
x
=
tf
。
keras
。
layers
。
Dense
(
units
=
10
)(
x
)
outputs
=
tf
。
keras
。
layers
。
Softmax
()(
x
)
model
=
tf
。
keras
。
Model
(
inputs
=
inputs
,
outputs
=
outputs
)
使用Model的compile、fit和evaluate方法訓練和評估模型
當模型建立完成後,透過 tf。keras。Model 的 compile 方法配置訓練過程。
model
。
compile
(
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
0。001
),
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
,
metrics
=
[
tf
。
keras
。
metrics
。
sparse_categorical_accuracy
]
)
tf。keras。Model。compile 接受 3 個重要的引數:
oplimizer :最佳化器,可從 tf。keras。optimizers 中選擇;
loss :損失函式,可從 tf。keras。losses 中選擇; * metrics :評估指標,可從 tf。keras。metrics 中選擇。
接下來,可以使用 tf。keras。Model 的 fit 方法訓練模型:
model
。
fit
(
data_loader
。
train_data
,
data_loader
。
train_label
,
epochs
=
num_epochs
,
batch_size
=
batch_size
)
tf。keras。Model。fit 接受 5 個重要的引數:
x :訓練資料;
y :目標資料(資料標籤);
epochs :將訓練資料迭代多少遍;
batch_size :批次的大小; * validation_data :驗證資料,可用於在訓練過程中監控模型的效能。
Keras 支援使用 tf。data。Dataset 進行訓練,詳見 tf。data 。
最後,使用 tf。keras。Model。evaluate 評估訓練效果,提供測試資料及標籤即可:
(
model
。
evaluate
(
data_loader
。
test_data
,
data_loader
。
test_label
))
自定義層、損失函式和評估指標
自定義層
自定義層需要繼承 tf。keras。layers。Layer 類,並重寫
init
、 build 和 call 三個方法。
class
MyLayer
(
tf
。
keras
。
layers
。
Layer
):
def
__init__
(
self
):
super
()
。
__init__
()
# 初始化程式碼
def
build
(
self
,
input_shape
):
# input_shape 是一個 TensorShape 型別物件,提供輸入的形狀
# 在第一次使用該層的時候呼叫該部分程式碼,在這裡建立變數可以使得變數的形狀自適應輸入的形狀
# 而不需要使用者額外指定變數形狀。
# 如果已經可以完全確定變數的形狀,也可以在__init__部分建立變數
self
。
variable_0
=
self
。
add_weight
(
。。。
)
self
。
variable_1
=
self
。
add_weight
(
。。。
)
def
call
(
self
,
inputs
):
# 模型呼叫的程式碼(處理輸入並返回輸出)
return
output
class
LinearLayer
(
tf
。
keras
。
layers
。
Layer
):
def
__init__
(
self
,
units
):
super
()
。
__init__
()
self
。
units
=
units
def
build
(
self
,
input_shape
):
# 這裡 input_shape 是第一次執行call()時引數inputs的形狀
self
。
w
=
self
。
add_variable
(
name
=
‘w’
,
shape
=
[
input_shape
[
-
1
],
self
。
units
],
initializer
=
tf
。
zeros_initializer
())
self
。
b
=
self
。
add_variable
(
name
=
‘b’
,
shape
=
[
self
。
units
],
initializer
=
tf
。
zeros_initializer
())
def
call
(
self
,
inputs
):
y_pred
=
tf
。
matmul
(
inputs
,
self
。
w
)
+
self
。
b
return
y_pred
在定義模型的時候,我們便可以如同 Keras 中的其他層一樣,呼叫我們自定義的層 LinearLayer。
class
LinearModel
(
tf
。
keras
。
Model
):
def
__init__
(
self
):
super
()
。
__init__
()
self
。
layer
=
LinearLayer
(
units
=
1
)
def
call
(
self
,
inputs
):
output
=
self
。
layer
(
inputs
)
return
output
自定義損失函式和評估指標
自定義損失函式需要繼承 tf。keras。losses。Loss 類,重寫 call 方法即可,輸入真實值 y_true 和模型預測值 y_pred ,輸出模型預測值和真實值之間透過自定義的損失函式計算出的損失值。下面的示例為均方差損失函式。
class
MeanSquaredError
(
tf
。
keras
。
losses
。
Loss
):
def
call
(
self
,
y_true
,
y_pred
):
return
tf
。
reduce_mean
(
tf
。
square
(
y_pred
-
y_true
))
自定義評估指標需要繼承 tf。keras。metrics。Metric 類,並重寫
init
、 update_state 和 result 三個方法。下面的示例對 SparseCategoricalAccuracy 評估指標類做了一個簡單的重實現:
class
SparseCategoricalAccuracy
(
tf
。
keras
。
metrics
。
Metric
):
def
__init__
(
self
):
super
()
。
__init__
()
self
。
total
=
self
。
add_weight
(
name
=
‘total’
,
dtype
=
tf
。
int32
,
initializer
=
tf
。
zeros_initializer
())
self
。
count
=
self
。
add_weight
(
name
=
‘count’
,
dtype
=
tf
。
int32
,
initializer
=
tf
。
zeros_initializer
())
def
update_state
(
self
,
y_true
,
y_pred
,
sample_weight
=
None
):
values
=
tf
。
cast
(
tf
。
equal
(
y_true
,
tf
。
argmax
(
y_pred
,
axis
=-
1
,
output_type
=
tf
。
int32
)),
tf
。
int32
)
self
。
total
。
assign_add
(
tf
。
shape
(
y_true
)[
0
])
self
。
count
。
assign_add
(
tf
。
reduce_sum
(
values
))
def
result
(
self
):
return
self
。
count
/
self
。
total
變數的儲存恢復tf。train。Checkpoint
Checkpoint 只儲存模型的引數,不儲存模型的計算過程,因此一般用於在具有模型原始碼的時候恢復之前訓練好的模型引數。
TensorFlow 提供了 tf。train。Checkpoint 這一強大的變數儲存與恢復類,可以使用其 save() 和 restore() 方法將 TensorFlow 中所有包含 Checkpointable State 的物件進行儲存和恢復。具體而言,tf。keras。optimizer 、 tf。Variable 、 tf。keras。Layer 或者 tf。keras。Model 例項都可以被儲存。
checkpoint
=
tf
。
train
。
Checkpoint
(
myAwesomeModel
=
model
,
myAwesomeOptimizer
=
optimizer
)
# train。py 模型訓練階段
model
=
MyModel
()
# 例項化Checkpoint,指定儲存物件為model(如果需要儲存Optimizer的引數也可加入)
checkpoint
=
tf
。
train
。
Checkpoint
(
myModel
=
model
)
# 。。。(模型訓練程式碼)
# 模型訓練完畢後將引數儲存到檔案(也可以在模型訓練過程中每隔一段時間就儲存一次)
checkpoint
。
save
(
‘。/save/model。ckpt’
)
# test。py 模型使用階段
model
=
MyModel
()
checkpoint
=
tf
。
train
。
Checkpoint
(
myModel
=
model
)
# 例項化Checkpoint,指定恢復物件為model
checkpoint
。
restore
(
tf
。
train
。
latest_checkpoint
(
‘。/save’
))
# 從檔案恢復模型引數
# 模型使用程式碼
在原始碼目錄建立一個名為 save 的資料夾並呼叫一次 checkpoint。save(‘。/save/model。ckpt’) ,我們就可以在可以在 save 目錄下發現名為 checkpoint 、 model。ckpt-1。index 、 model。ckpt-1。data-00000-of-00001 的三個檔案,這些檔案就記錄了變數資訊。呼叫 checkpoint。restore(‘。/save/model。ckpt-1’) 就可以載入字首為 model。ckpt ,序號為 1 的檔案來恢復模型。 當儲存了多個檔案時,我們往往想載入最近的一個。可以使用 tf。train。latest_checkpoint(save_path) 這個輔助函式返回目錄下最近一次 checkpoint 的檔名。例如如果 save 目錄下有 model。ckpt-1。index 到 model。ckpt-10。index 的 10 個儲存檔案, tf。train。latest_checkpoint(‘。/save’) 即返回 。/save/model。ckpt-10 。 使用 tf。train。CheckpointManager 刪除舊的 Checkpoint 以及自定義檔案編號 在模型的訓練過程中,我們往往每隔一定步數儲存一個 Checkpoint 並進行編號。不過很多時候我們會有這樣的需求: 在長時間的訓練後,程式會儲存大量的 Checkpoint,但我們只想保留最後的幾個 Checkpoint; Checkpoint 預設從 1 開始編號,每次累加 1,但我們可能希望使用別的編號方式(例如使用當前 Batch 的編號作為檔案編號)。 這時,我們可以使用 TensorFlow 的 tf。train。CheckpointManager 來實現以上需求。具體而言,在定義 Checkpoint 後接著定義一個 CheckpointManager: wzxhzdk:29 此處, directory 引數為檔案儲存的路徑, checkpoint_name 為檔名字首(不提供則預設為 ckpt ), max_to_keep 為保留的 Checkpoint 數目。 在需要儲存模型的時候,我們直接使用 manager。save() 即可。如果我們希望自行指定儲存的 Checkpoint 的編號,則可以在儲存時加入 checkpoint_number 引數。例如 manager。save(checkpoint_number=100) 。 以下提供一個例項,展示使用 CheckpointManager 限制僅保留最後三個 Checkpoint 檔案,並使用 batch 的編號作為 Checkpoint 的檔案編號。 wzxhzdk:30
TensorBoard:訓練過程視覺化
有時,你希望檢視模型訓練過程中各個引數的變化情況(例如損失函式 loss 的值)。雖然可以透過命令列輸出來檢視,但有時顯得不夠直觀。而 TensorBoard 就是一個能夠幫助我們將訓練過程視覺化的工具。
首先在程式碼目錄下建立一個資料夾(如 。/tensorboard )存放 TensorBoard 的記錄檔案,並在程式碼中例項化一個記錄器:
summary_writer
=
tf
。
summary
。
create_file_writer
(
‘。/tensorboard’
)
# 引數為記錄檔案所儲存的目錄
接下來,當需要記錄訓練過程中的引數時,透過 with 語句指定希望使用的記錄器,並對需要記錄的引數(一般是 scalar)執行 tf。summary。scalar(name, tensor, step=batch_index) ,即可將訓練過程中引數在 step 時候的值記錄下來。這裡的 step 引數可根據自己的需要自行制定,一般可設定為當前訓練過程中的 batch 序號。整體框架如下:
summary_writer
=
tf
。
summary
。
create_file_writer
(
‘。/tensorboard’
)
# 開始模型訓練
for
batch_index
in
range
(
num_batches
):
# 。。。(訓練程式碼,當前batch的損失值放入變數loss中)
with
summary_writer
。
as_default
():
# 希望使用的記錄器
tf
。
summary
。
scalar
(
“loss”
,
loss
,
step
=
batch_index
)
tf
。
summary
。
scalar
(
“MyScalar”
,
my_scalar
,
step
=
batch_index
)
# 還可以新增其他自定義的變數
每執行一次 tf。summary。scalar() ,記錄器就會向記錄檔案中寫入一條記錄。除了最簡單的標量(scalar)以外,TensorBoard 還可以對其他型別的資料(如影象,音訊等)進行視覺化。 當我們要對訓練過程視覺化時,在程式碼目錄開啟終端(如需要的話進入 TensorFlow 的 conda 環境),執行:
tensorboard
——
logdir
=。/
tensorboard
然後使用瀏覽器訪問命令列程式所輸出的網址(一般是 http:// 計算機名稱:6006),即可訪問 TensorBoard 的可視介面。 TensorBoard 的使用有以下注意事項:
如果需要重新訓練,需要刪除掉記錄資料夾內的資訊並重啟 TensorBoard(或者建立一個新的記錄資料夾並開啟 TensorBoard, ——logdir 引數設定為新建立的資料夾);
記錄資料夾目錄保持全英文。 最後提供一個例項,以前章的 多層感知機模型 為例展示 TensorBoard 的使用:
import
tensorflow
as
tf
from
zh。model。mnist。mlp
import
MLP
from
zh。model。utils
import
MNISTLoader
num_batches
=
10000
batch_size
=
50
learning_rate
=
0。001
model
=
MLP
()
data_loader
=
MNISTLoader
()
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
learning_rate
)
summary_writer
=
tf
。
summary
。
create_file_writer
(
‘。/tensorboard’
)
# 例項化記錄器
for
batch_index
in
range
(
num_batches
):
X
,
y
=
data_loader
。
get_batch
(
batch_size
)
with
tf
。
GradientTape
()
as
tape
:
y_pred
=
model
(
X
)
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
(
y_true
=
y
,
y_pred
=
y_pred
)
loss
=
tf
。
reduce_mean
(
loss
)
(
“batch
%d
: loss
%f
”
%
(
batch_index
,
loss
。
numpy
()))
with
summary_writer
。
as_default
():
# 指定記錄器
tf
。
summary
。
scalar
(
“loss”
,
loss
,
step
=
batch_index
)
# 將當前損失函式的值寫入記錄器
grads
=
tape
。
gradient
(
loss
,
model
。
variables
)
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
model
。
variables
))
資料集的構建與預處理tf。data
很多時候,我們希望使用自己的資料集來訓練模型。然而,面對一堆格式不一的原始資料檔案,將其預處理並讀入程式的過程往往十分繁瑣,甚至比模型的設計還要耗費精力。比如,為了讀入一批影象檔案,我們可能需要糾結於 python 的各種影象處理包(比如 pillow ),自己設計 Batch 的生成方式,最後還可能在執行的效率上不盡如人意。為此,TensorFlow 提供了 tf。data 這一模組,包括了一套靈活的資料集構建 API,能夠幫助我們快速、高效地構建資料輸入的流水線,尤其適用於資料量巨大的場景。
tf。data 的核心是 tf。data。Dataset 類,提供了對資料集的高層封裝。tf。data。Dataset 由一系列的可迭代訪問的元素(element)組成,每個元素包含一個或多個張量。比如說,對於一個由影象組成的資料集,每個元素可以是一個形狀為 長×寬×通道數 的圖片張量,也可以是由圖片張量和圖片標籤張量組成的元組(Tuple)。
最基礎的建立 tf。data。Dataset 的方法是使用 tf。data。Dataset。from_tensor_slices() ,適用於資料量較小(能夠整個裝進記憶體)的情況。當提供多個張量作為輸入時,張量的第 0 維大小必須相同,且必須將多個張量作為元組(Tuple,即使用 Python 中的小括號)拼接並作為輸入。
import
tensorflow
as
tf
import
numpy
as
np
X
=
tf
。
constant
([
2013
,
2014
,
2015
,
2016
,
2017
])
Y
=
tf
。
constant
([
12000
,
14000
,
15000
,
16500
,
17500
])
# 也可以使用NumPy陣列,效果相同
# X = np。array([2013, 2014, 2015, 2016, 2017])
# Y = np。array([12000, 14000, 15000, 16500, 17500])
dataset
=
tf
。
data
。
Dataset
。
from_tensor_slices
((
X
,
Y
))
for
x
,
y
in
dataset
:
(
x
。
numpy
(),
y
。
numpy
())
tf。data。Dataset 類為我們提供了多種資料集預處理方法。最常用的如:
Dataset。map(f) :對資料集中的每個元素應用函式 f ,得到一個新的資料集(這部分往往結合
http://
tf。io
進行讀寫和解碼檔案, tf。image 進行影象處理);
Dataset。shuffle(buffer_size) :將資料集打亂(設定一個固定大小的緩衝區(Buffer),取出前 buffer_size 個元素放入,並從緩衝區中隨機取樣,取樣後的資料用後續資料替換);
Dataset。batch(batch_size) :將資料集分成批次,即對每 batch_size 個元素,使用 tf。stack() 在第 0 維合併,成為一個元素。
Dataset。prefetch() :預取出資料集中的若干個元素。
使用 Dataset。batch() 將資料集劃分批次,每個批次的大小為 4。
使用 Dataset。shuffle() 將資料打散後再設定批次,快取大小設定為 10000。 tf。data。Dataset 作為一個針對大規模資料設計的迭代器,本身無法方便地獲得自身元素的數量或隨機訪問元素。因此,為了高效且較為充分地打散資料集,需要一些特定的方法。Dataset。shuffle() 採取了以下方法:
設定一個固定大小為 buffer_size 的緩衝區(Buffer); 初始化時,取出資料集中的前 buffer_size 個元素放入緩衝區; 每次需要從資料集中取元素時,即從緩衝區中隨機取樣一個元素並取出,然後從後續的元素中取出一個放回到之前被取出的位置,以維持緩衝區的大小。 因此,緩衝區的大小需要根據資料集的特性和資料排列順序特點來進行合理的設定。比如:
當 buffer_size 設定為 1 時,其實等價於沒有進行任何打散;
當資料集的標籤順序分佈極為不均勻(例如二元分類時資料集前 N 個的標籤為 0,後 N 個的標籤為 1)時,較小的緩衝區大小會使得訓練時取出的 Batch 資料很可能全為同一標籤,從而影響訓練效果。一般而言,資料集的順序分佈若較為隨機,則緩衝區的大小可較小,否則則需要設定較大的緩衝區。
以貓狗圖片二分類任務為示例,展示了使用 tf。data 結合
http://
tf。io
和 tf。image 建立 tf。data。Dataset 資料集。
import
tensorflow
as
tf
import
os
num_epochs
=
10
batch_size
=
32
learning_rate
=
0。001
data_dir
=
‘C:/datasets/cats_vs_dogs’
train_cats_dir
=
data_dir
+
‘/train/cats/’
train_dogs_dir
=
data_dir
+
‘/train/dogs/’
test_cats_dir
=
data_dir
+
‘/valid/cats/’
test_dogs_dir
=
data_dir
+
‘/valid/dogs/’
def
_decode_and_resize
(
filename
,
label
):
image_string
=
tf
。
io
。
read_file
(
filename
)
image_decoded
=
tf
。
image
。
decode_jpeg
(
image_string
)
image_resized
=
tf
。
image
。
resize
(
image_decoded
,
[
256
,
256
])
/
255。0
return
image_resized
,
label
if
__name__
==
‘__main__’
:
# 構建訓練資料集
train_cat_filenames
=
tf
。
constant
([
train_cats_dir
+
filename
for
filename
in
os
。
listdir
(
train_cats_dir
)])
train_dog_filenames
=
tf
。
constant
([
train_dogs_dir
+
filename
for
filename
in
os
。
listdir
(
train_dogs_dir
)])
train_filenames
=
tf
。
concat
([
train_cat_filenames
,
train_dog_filenames
],
axis
=-
1
)
train_labels
=
tf
。
concat
([
tf
。
zeros
(
train_cat_filenames
。
shape
,
dtype
=
tf
。
int32
),
tf
。
ones
(
train_dog_filenames
。
shape
,
dtype
=
tf
。
int32
)],
axis
=-
1
)
train_dataset
=
tf
。
data
。
Dataset
。
from_tensor_slices
((
train_filenames
,
train_labels
))
train_dataset
=
train_dataset
。
map
(
_decode_and_resize
)
# 取出前buffer_size個數據放入buffer,並從其中隨機取樣,取樣後的資料用後續資料替換
train_dataset
=
train_dataset
。
shuffle
(
buffer_size
=
23000
)
train_dataset
=
train_dataset
。
batch
(
batch_size
)
model
=
tf
。
keras
。
Sequential
([
tf
。
keras
。
layers
。
Conv2D
(
32
,
3
,
activation
=
‘relu’
,
input_shape
=
(
256
,
256
,
3
)),
tf
。
keras
。
layers
。
MaxPooling2D
(),
tf
。
keras
。
layers
。
Conv2D
(
32
,
5
,
activation
=
‘relu’
),
tf
。
keras
。
layers
。
MaxPooling2D
(),
tf
。
keras
。
layers
。
Flatten
(),
tf
。
keras
。
layers
。
Dense
(
64
,
activation
=
‘relu’
),
tf
。
keras
。
layers
。
Dense
(
2
,
activation
=
‘softmax’
)
])
model
。
compile
(
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
learning_rate
),
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
,
metrics
=
[
tf
。
keras
。
metrics
。
sparse_categorical_accuracy
]
)
model
。
fit
(
train_dataset
,
epochs
=
num_epochs
)
# 構建測試資料集
test_cat_filenames
=
tf
。
constant
([
test_cats_dir
+
filename
for
filename
in
os
。
listdir
(
test_cats_dir
)])
test_dog_filenames
=
tf
。
constant
([
test_dogs_dir
+
filename
for
filename
in
os
。
listdir
(
test_dogs_dir
)])
test_filenames
=
tf
。
concat
([
test_cat_filenames
,
test_dog_filenames
],
axis
=-
1
)
test_labels
=
tf
。
concat
([
tf
。
zeros
(
test_cat_filenames
。
shape
,
dtype
=
tf
。
int32
),
tf
。
ones
(
test_dog_filenames
。
shape
,
dtype
=
tf
。
int32
)],
axis
=-
1
)
test_dataset
=
tf
。
data
。
Dataset
。
from_tensor_slices
((
test_filenames
,
test_labels
))
test_dataset
=
test_dataset
。
map
(
_decode_and_resize
)
test_dataset
=
test_dataset
。
batch
(
batch_size
)
(
model
。
metrics_names
)
(
model
。
evaluate
(
test_dataset
))
Graph Execution 模式@tf。function
在 TensorFlow 2。0 中,推薦使用 @tf。function (而非 1。X 中的 tf。Session )實現 Graph Execution,從而將模型轉換為易於部署且高效能的 TensorFlow 圖模型。只需要將我們希望以 Graph Execution 模式執行的程式碼封裝在一個函式內,並在函式前加上 @tf。function 即可。
並不是任何函式都可以被 @tf。function 修飾!@tf。function 使用靜態編譯將函式內的程式碼轉換成計算圖,因此對函式內可使用的語句有一定限制(僅支援 Python 語言的一個子集),且需要函式內的操作本身能夠被構建為計算圖。建議在函式內只使用 TensorFlow 的原生操作,不要使用過於複雜的 Python 語句,函式引數只包括 TensorFlow 張量或 NumPy 陣列,並最好是能夠按照計算圖的思想去構建函式(換言之,@tf。function 只是給了你一種更方便的寫計算圖的方法,而不是一顆能給任何函式加速的 銀子彈 )。
import
tensorflow
as
tf
import
time
from
zh。model。mnist。cnn
import
CNN
from
zh。model。utils
import
MNISTLoader
num_batches
=
400
batch_size
=
50
learning_rate
=
0。001
data_loader
=
MNISTLoader
()
@tf。function
def
train_one_step
(
X
,
y
):
with
tf
。
GradientTape
()
as
tape
:
y_pred
=
model
(
X
)
loss
=
tf
。
keras
。
losses
。
sparse_categorical_crossentropy
(
y_true
=
y
,
y_pred
=
y_pred
)
loss
=
tf
。
reduce_mean
(
loss
)
# 注意這裡使用了TensorFlow內建的tf。print()。@tf。function不支援Python內建的print方法
tf
。
(
“loss”
,
loss
)
grads
=
tape
。
gradient
(
loss
,
model
。
variables
)
optimizer
。
apply_gradients
(
grads_and_vars
=
zip
(
grads
,
model
。
variables
))
if
__name__
==
‘__main__’
:
model
=
CNN
()
optimizer
=
tf
。
keras
。
optimizers
。
Adam
(
learning_rate
=
learning_rate
)
start_time
=
time
。
time
()
for
batch_index
in
range
(
num_batches
):
X
,
y
=
data_loader
。
get_batch
(
batch_size
)
train_one_step
(
X
,
y
)
end_time
=
time
。
time
()
(
end_time
-
start_time
)
執行 400 個 Batch 進行測試,加入 @tf。function 的程式耗時 35。5 秒,未加入 @tf。function 的純 Eager Execution 程式耗時 43。8 秒。可見 @tf。function 帶來了一定的效能提升。一般而言,當模型由較多小的操作組成的時候, @tf。function 帶來的提升效果較大。而當模型的運算元量較少,但單一操作均很耗時的時候,則 @tf。function 帶來的效能提升不會太大。
當被 @tf。function 修飾的函式第一次被呼叫的時候,進行以下操作:
在 Eager Execution 模式關閉的環境下,函式內的程式碼依次執行。也就是說,每個 tf。 方法都只是定義了計算節點,而並沒有進行任何實質的計算。這與 TensorFlow 1。X 的 Graph Execution 是一致的;
使用 AutoGraph 將函式中的 Python 控制流語句轉換成 TensorFlow 計算圖中的對應節點(比如說 while 和 for 語句轉換為 tf。while , if 語句轉換為 tf。cond 等等;
基於上面的兩步,建立函式內程式碼的計算圖表示(為了保證圖的計算順序,圖中還會自動加入一些 tf。control_dependencies 節點);
執行一次這個計算圖; * 基於函式的名字和輸入的函式引數的型別生成一個雜湊值,並將建立的計算圖快取到一個雜湊表中。
在被 @tf。function 修飾的函式之後再次被呼叫的時候,根據函式名和輸入的函式引數的型別計算雜湊值,檢查雜湊表中是否已經有了對應計算圖的快取。如果是,則直接使用已快取的計算圖,否則重新按上述步驟建立計算圖。
import
tensorflow
as
tf
import
numpy
as
np
@tf。function
def
f
(
x
):
(
“The function is running in Python”
)
tf
。
(
x
)
a
=
tf
。
constant
(
1
,
dtype
=
tf
。
int32
)
f
(
a
)
b
=
tf
。
constant
(
2
,
dtype
=
tf
。
int32
)
f
(
b
)
b_
=
np
。
array
(
2
,
dtype
=
np
。
int32
)
f
(
b_
)
c
=
tf
。
constant
(
0。1
,
dtype
=
tf
。
float32
)
f
(
c
)
d
=
tf
。
constant
(
0。2
,
dtype
=
tf
。
float32
)
f
(
d
)
f
(
1
)
f
(
2
)
f
(
1
)
f
(
0。1
)
f
(
0。2
)
f
(
0。1
)
輸出為
The
function
is
running
in
Python
1
2
2
The
function
is
running
in
Python
0。1
0。2
The
function
is
running
in
Python
1
The
function
is
running
in
Python
2
1
The
function
is
running
in
Python
0。1
The
function
is
running
in
Python
0。2
0。1
將函式內的程式碼依次運行了一遍(因此輸出了文字);
構建了計算圖,然後運行了一次該計算圖(因此輸出了 1)。這裡 tf。print(x) 可以作為計算圖的節點,但 Python 內建的 print 則不能被轉換成計算圖的節點。因此,計算圖中只包含了 tf。print(x) 這一操作;
將該計算圖快取到了一個雜湊表中(如果之後再有型別為 tf。int32 ,shape 為空的張量輸入,則重複使用已構建的計算圖)。
計算 f(b) 時,由於 b 的型別與 a 相同,所以 TensorFlow 重複使用了之前已構建的計算圖並執行(因此輸出了 2)。這裡由於並沒有真正地逐行執行函式中的程式碼,所以函式第一行的文字輸出程式碼沒有執行。計算 f(b_) 時,TensorFlow 自動將 numpy 的資料結構轉換成了 TensorFlow 中的張量,因此依然能夠複用之前已構建的計算圖。
計算 f(c) 時,雖然張量 c 的 shape 和 a 、 b 均相同,但型別為 tf。float32 ,因此 TensorFlow 重新運行了函式內程式碼(從而再次輸出了文字)並建立了一個輸入為 tf。float32 型別的計算圖。
計算 f(d) 時,由於 d 和 c 的型別相同,所以 TensorFlow 複用了計算圖,同理沒有輸出文字。
之後的計算結果則顯示出 @tf。function 對 Python 內建的整數和浮點數型別的處理方式。簡而言之,只有當值完全一致的時候, @tf。function 才會複用之前建立的計算圖,而並不會自動將 Python 內建的整數或浮點數等轉換成張量。因此,當函式引數包含 Python 內建整數或浮點數時,需要額外小心。一般而言,應當只在指定超引數等少數場合使用 Python 內建型別作為被 @tf。function 修飾的函式的引數。
import
tensorflow
as
tf
a
=
tf
。
Variable
(
0。0
)
@tf。function
def
g
():
a
。
assign
(
a
+
1。0
)
return
a
(
g
())
(
g
())
(
g
())
輸出為:
tf
。
Tensor
(
1。0
,
shape
=
(),
dtype
=
float32
)
tf
。
Tensor
(
2。0
,
shape
=
(),
dtype
=
float32
)
tf
。
Tensor
(
3。0
,
shape
=
(),
dtype
=
float32
)
可以在被 @tf。function 修飾的函數里呼叫 tf。Variable 、 tf。keras。optimizers 、 tf。keras。Model 等包含有變數的資料結構。一旦被呼叫,這些結構將作為隱含的引數提供給函式。當這些結構內的值在函式內被修改時,在函式外也同樣生效。
AutoGraph:將Python控制流轉換為TensorFlow計算圖
@tf。function 使用名為 AutoGraph 的機制將函式中的 Python 控制流語句轉換成 TensorFlow 計算圖中的對應節點。以下是一個示例,使用 tf。autograph 模組的低層 API tf。autograph。to_code 將函式 square_if_positive 轉換成 TensorFlow 計算圖:
import
tensorflow
as
tf
@tf。function
def
square_if_positive
(
x
):
if
x
>
0
:
x
=
x
*
x
else
:
x
=
0
return
x
a
=
tf
。
constant
(
1
)
b
=
tf
。
constant
(
-
1
)
(
square_if_positive
(
a
),
square_if_positive
(
b
))
(
tf
。
autograph
。
to_code
(
square_if_positive
。
python_function
))
輸出
tf
。
Tensor
(
1
,
shape
=
(),
dtype
=
int32
)
tf
。
Tensor
(
0
,
shape
=
(),
dtype
=
int32
)
def
tf__square_if_positive
(
x
):
do_return
=
False
retval_
=
ag__
。
UndefinedReturnValue
()
cond
=
x
>
0
def
get_state
():
return
()
def
set_state
(
_
):
pass
def
if_true
():
x_1
,
=
x
,
x_1
=
x_1
*
x_1
return
x_1
def
if_false
():
x
=
0
return
x
x
=
ag__
。
if_stmt
(
cond
,
if_true
,
if_false
,
get_state
,
set_state
)
do_return
=
True
retval_
=
x
cond_1
=
ag__
。
is_undefined_return
(
retval_
)
def
get_state_1
():
return
()
def
set_state_1
(
_
):
pass
def
if_true_1
():
retval_
=
None
return
retval_
def
if_false_1
():
return
retval_
retval_
=
ag__
。
if_stmt
(
cond_1
,
if_true_1
,
if_false_1
,
get_state_1
,
set_state_1
)
return
retval_
我們注意到,原函式中的 Python 控制流 if。。。else。。。 被轉換為了 x = ag__。if_stmt(cond, if_true, if_false, get_state, set_state) 這種計算圖式的寫法。AutoGraph 起到了類似編譯器的作用,能夠幫助我們透過更加自然的 Python 控制流輕鬆地構建帶有條件 / 迴圈的計算圖,而無需手動使用 TensorFlow 的 API 進行構建。
tf。TensorArray:TensorFlow動態陣列
在部分網路結構,尤其是涉及到時間序列的結構中,我們可能需要將一系列張量以陣列的方式依次存放起來,以供進一步處理。當然,在 Eager Execution 下,你可以直接使用一個 Python 列表(List)存放陣列。不過,如果你需要基於計算圖的特性(例如使用 @tf。function 加速模型執行或者使用 SavedModel 匯出模型),就無法使用這種方式了。因此,TensorFlow 提供了 tf。TensorArray ,一種支援計算圖特性的 TensorFlow 動態陣列。
import
tensorflow
as
tf
@tf。function
def
array_write_and_read
():
arr
=
tf
。
TensorArray
(
dtype
=
tf
。
float32
,
size
=
3
)
arr
=
arr
。
write
(
0
,
tf
。
constant
(
0。0
))
arr
=
arr
。
write
(
1
,
tf
。
constant
(
1。0
))
arr
=
arr
。
write
(
2
,
tf
。
constant
(
2。0
))
arr_0
=
arr
。
read
(
0
)
arr_1
=
arr
。
read
(
1
)
arr_2
=
arr
。
read
(
2
)
return
arr_0
,
arr_1
,
arr_2
a
,
b
,
c
=
array_write_and_read
()
(
a
,
b
,
c
)
輸出
tf
。
Tensor
(
0。0
,
shape
=
(),
dtype
=
float32
)
tf
。
Tensor
(
1。0
,
shape
=
(),
dtype
=
float32
)
tf
。
Tensor
(
2。0
,
shape
=
(),
dtype
=
float32
)
GPU的使用與分配tf。config
很多時候的場景是:實驗室 / 公司研究組裡有許多學生 / 研究員需要共同使用一臺多 GPU 的工作站,而預設情況下 TensorFlow 會使用其所能夠使用的所有 GPU,這時就需要合理分配顯示卡資源。 首先,透過 tf。config。experimental。list_physical_devices ,我們可以獲得當前主機上某種特定運算裝置型別(如 GPU 或 CPU )的列表,例如,在一臺具有 4 塊 GPU 和一個 CPU 的工作站上執行以下程式碼:
gpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
device_type
=
‘GPU’
)
cpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
device_type
=
‘CPU’
)
(
gpus
,
cpus
)
輸出:
[
PhysicalDevice
(
name
=
‘/physical_device:GPU:0’
,
device_type
=
‘GPU’
),
PhysicalDevice
(
name
=
‘/physical_device:GPU:1’
,
device_type
=
‘GPU’
),
PhysicalDevice
(
name
=
‘/physical_device:GPU:2’
,
device_type
=
‘GPU’
),
PhysicalDevice
(
name
=
‘/physical_device:GPU:3’
,
device_type
=
‘GPU’
)]
[
PhysicalDevice
(
name
=
‘/physical_device:CPU:0’
,
device_type
=
‘CPU’
)]
可見,該工作站具有 4 塊 GPU:GPU:0 、 GPU:1 、 GPU:2 、 GPU:3 ,以及一個 CPU CPU:0。 然後,透過 tf。config。experimental。set_visible_devices ,可以設定當前程式可見的裝置範圍(當前程式只會使用自己可見的裝置,不可見的裝置不會被當前程式使用)。例如,如果在上述 4 卡的機器中我們需要限定當前程式只使用下標為 0、1 的兩塊顯示卡(GPU:0 和 GPU:1),可以使用以下程式碼:
gpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
device_type
=
‘GPU’
)
tf
。
config
。
experimental
。
set_visible_devices
(
devices
=
gpus
[
0
:
2
],
device_type
=
‘GPU’
)
使用環境變數 CUDA_VISIBLE_DEVICES 也可以控制程式所使用的 GPU。假設發現四卡的機器上顯示卡 0,1 使用中,顯示卡 2,3 空閒,Linux 終端輸入: export CUDA_VISIBLE_DEVICES=2,3 或在程式碼中加入 import os os。environ[‘CUDA_VISIBLE_DEVICES’] = “2,3” 即可指定程式只在顯示卡 2,3 上執行。
預設情況下,TensorFlow 將使用幾乎所有可用的視訊記憶體,以避免記憶體碎片化所帶來的效能損失。不過,TensorFlow 提供兩種視訊記憶體使用策略,讓我們能夠更靈活地控制程式的視訊記憶體使用方式:
僅在需要時申請視訊記憶體空間(程式初始執行時消耗很少的視訊記憶體,隨著程式的執行而動態申請視訊記憶體);
限制消耗固定大小的視訊記憶體(程式不會超出限定的視訊記憶體大小,若超出的報錯)。 可以透過 tf。config。experimental。set_memory_growth 將 GPU 的視訊記憶體使用策略設定為 “僅在需要時申請視訊記憶體空間”。以下程式碼將所有 GPU 設定為僅在需要時申請視訊記憶體空間:
gpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
device_type
=
‘GPU’
)
for
gpu
in
gpus
:
tf
。
config
。
experimental
。
set_memory_growth
(
device
=
gpu
,
True
)
以下程式碼透過 tf。config。experimental。set_virtual_device_configuration 選項並傳入 tf。config。experimental。VirtualDeviceConfiguration 例項,設定 TensorFlow 固定消耗 GPU:0 的 1GB 視訊記憶體(其實可以理解為建立了一個視訊記憶體大小為 1GB 的 “虛擬 GPU”):
gpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
device_type
=
‘GPU’
)
tf
。
config
。
experimental
。
set_virtual_device_configuration
(
gpus
[
0
],
[
tf
。
config
。
experimental
。
VirtualDeviceConfiguration
(
memory_limit
=
1024
)])
TensorFlow 1。X 的 Graph Execution 下,可以在例項化新的 session 時傳入 tf。compat。v1。ConfigPhoto 類來設定 TensorFlow 使用視訊記憶體的策略。具體方式是例項化一個 tf。ConfigProto 類,設定引數,並在建立 tf。compat。v1。Session 時指定 Config 引數。以下程式碼透過 allow_growth 選項設定 TensorFlow 僅在需要時申請視訊記憶體空間: wzxhzdk:51 以下程式碼透過 per_process_gpu_memory_fraction 選項設定 TensorFlow 固定消耗 40% 的 GPU 視訊記憶體: wzxhzdk:52
單GPU模擬多GPU環境
當我們的本地開發環境只有一個 GPU,但卻需要編寫多 GPU 的程式在工作站上進行訓練任務時,TensorFlow 為我們提供了一個方便的功能,可以讓我們在本地開發環境中建立多個模擬 GPU,從而讓多 GPU 的程式除錯變得更加方便。以下程式碼在實體 GPU GPU:0 的基礎上建立了兩個視訊記憶體均為 2GB 的虛擬 GPU。
gpus
=
tf
。
config
。
experimental
。
list_physical_devices
(
‘GPU’
)
tf
。
config
。
experimental
。
set_virtual_device_configuration
(
gpus
[
0
],
[
tf
。
config
。
experimental
。
VirtualDeviceConfiguration
(
memory_limit
=
2048
),
tf
。
config
。
experimental
。
VirtualDeviceConfiguration
(
memory_limit
=
2048
)])
我們在 單機多卡訓練 的程式碼前加入以上程式碼,即可讓原本為多 GPU 設計的程式碼在單 GPU 環境下執行。當輸出裝置數量時,程式會輸出:
Number
of
devices
:
2