人工智能難題不僅是計算機科學問題,更是數學、認知 科學和哲學問題。− François Chollet
Keras
是一個主要由 Python 語言開發的開源神經網絡計算庫,最初由 François Chollet編寫,它被設計爲高度模塊化和易擴展的高層神經網絡接口,使得用戶可以不需要過多的專業知識就可以簡潔、快速地完成模型的搭建與訓練。Keras 庫分爲前端和後端,其中後端一般是調用現有的深度學習框架實現底層運算,如 Theano、CNTK、TensorFlow 等,前端接口是 Keras 抽象過的一組統一接口函數。用戶通過 Keras 編寫的代碼可以輕鬆的切換不同的後端運行,靈活性較大。正是由於 Keras 的高度抽象和易用特性,截止到 2019 年,Keras 市場份額達到了 26.6%,增長 19.7%,在同類深度學習框架中僅次於 TensorFlow(數據來自 KDnuggets)。
TensorFlow 與 Keras 之間存在既競爭,又合作的交錯關係,甚至連 Keras 創始人都在Google 工作。早在 2015 年 11 月,TensorFlow 就被加入 Keras 後端支持。從 2017 年開始,Keras 的大部分組件被整合到 TensorFlow 框架中。2019 年,在 TensorFlow 2 版本中,Keras被正式確定爲 TensorFlow 的高層唯一接口 API,取代了 TensorFlow 1 版本中自帶的tf.layers
等高層接口。也就是說,現在只能使用 Keras 的接口來完成 TensorFlow 層方式的模型搭建與訓練。在 TensorFlow 中,Keras 被實現在 tf.keras
子模塊中。
Keras
與 tf.keras
有什麼區別與聯繫呢?其實 Keras 可以理解爲一套搭建與訓練神經網絡的高層 API 協議,Keras 本身已經實現了此協議,安裝標準的 Keras 庫就可以方便地調用TensorFlow、CNTK 等後端完成加速計算;在 TensorFlow 中,也實現了一套 Keras 協議,即 tf.keras
,它與 TensorFlow 深度融合,且只能基於 TensorFlow 後端運算,並對TensorFlow 的支持更完美。對於使用 TensorFlow 的開發者來說,tf.keras
可以理解爲一個普通的子模塊,與其他子模塊,如 tf.math
,tf.data
等並沒有什麼差別。下文如無特別說明,Keras 均指代 tf.keras,而不是標準的 Keras 庫。
8.1 常見功能模塊
Keras 提供了一系列高層的神經網絡相關類和函數,如經典數據集加載函數、網絡層類、模型容器、損失函數類、優化器類、經典模型類等。
對於經典數據集,通過一行代碼即可下載、管理、加載數據集,這些數據集包括Boston 房價預測數據集、CIFAR 圖片數據集、MNIST/FashionMNIST 手寫數字圖片數據集、IMDB 文本數據集等。我們已經介紹過,不再敖述.
8.1.1 常見網絡層類
對於常見的神經網絡層,可以使用張量方式的底層接口函數來實現,這些接口函數一般在 tf.nn
模塊中。更常用地,對於常見的網絡層,我們一般直接使用層方式來完成模型的搭建,在 tf.keras.layers
命名空間(下文使用 layers
指代 tf.keras.layers
)中提供了大量常見網絡層的類,如全連接層、激活函數層、池化層、卷積層、循環神經網絡層等。對於這些網絡層類,只需要在創建時指定網絡層的相關參數,並調用__call__
方法即可完成前向計算。在調用__call__
方法時,Keras 會自動調用每個層的前向傳播邏輯,這些邏輯一般實現在類的call 函數中。
以 Softmax 層爲例,它既可以使用 tf.nn.softmax
函數在前向傳播邏輯中完成 Softmax運算,也可以通過 layers.Softmax(axis)
類搭建 Softmax 網絡層,其中 axis 參數指定進行softmax 運算的維度。首先導入相關的子模塊,實現如下:
import tensorflow as tf
# 導入 keras 模型,不能使用 import keras,它導入的是標準的 Keras 庫
from tensorflow import keras
from tensorflow.keras import layers # 導入常見網絡層類
然後創建 Softmax 層,並調用__call__
方法完成前向計算:
x = tf.constant([2.,1.,0.1]) # 創建輸入張量
layer = layers.Softmax(axis=-1) # 創建 Softmax 層
out = layer(x) # 調用 softmax 前向計算,輸出爲 out
經過 Softmax 網絡層後,得到概率分佈 out 爲:
<tf.Tensor: id=2, shape=(3,), dtype=float32, numpy=array([0.6590012,
0.242433 , 0.0985659], dtype=float32)>
當然,也可以直接通過 tf.nn.softmax()
函數完成計算,代碼如下:
out = tf.nn.softmax(x) # 調用 softmax 函數完成前向計算
8.1.2 網絡容器
對於常見的網絡,需要手動調用每一層的類實例完成前向傳播運算,當網絡層數變得較深時,這一部分代碼顯得非常臃腫。可以通過 Keras
提供的網絡容器 Sequential
將多個網絡層封裝成一個大網絡模型,只需要調用網絡模型的實例一次即可完成數據從第一層到最末層的順序傳播運算。
例如,2 層的全連接層加上單獨的激活函數層,可以通過 Sequential 容器封裝爲一個網絡。
# 導入 Sequential 容器
# 導入Sequential容器
from tensorflow.keras import layers,Sequential
network=Sequential([ #封裝成一個網絡
layers.Dense(3,activation=None),#全連接層,此處不使用激活函數
layers.ReLU(),#激活函數層
layers.Dense(2,activation=None),#全連接層,此處不使用激活函數
layers.ReLU() #激活函數層
])
x=tf.random.normal([4,3])
out=network(x)# 輸入從第一層開始,逐層傳播至輸出層,並返回輸出層的輸出
print(out)
Sequential 容器也可以通過 add()
方法繼續追加新的網絡層,實現動態創建網絡的功能:
layers_num = 2 # 堆疊 2 次
network = Sequential([]) # 先創建空的網絡容器
for _ in range(layers_num):
network.add(layers.Dense(3)) # 添加全連接層
network.add(layers.ReLU())# 添加激活函數層
network.build(input_shape=(4, 4)) # 創建網絡參數
network.summary()
上述代碼通過指定任意的 layers_num 參數即可創建對應層數的網絡結構,在完成網絡創建時,網絡層類並沒有創建內部權值張量等成員變量,此時通過調用類的 build 方法並指定輸入大小,即可自動創建所有層的內部張量。通過 summary()函數可以方便打印出網絡結構和參數量,打印結果如下:
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_2 (Dense) multiple 15
_________________________________________________________________
re_lu_2 (ReLU) multiple 0
_________________________________________________________________
dense_3 (Dense) multiple 12
_________________________________________________________________
re_lu_3 (ReLU) multiple 0
=================================================================
Total params: 27
Trainable params: 27
Non-trainable params: 0
_________________________________________________________________
可以看到 Layer 列爲每層的名字,這個名字由 TensorFlow 內部維護,與 Python 的對象名並不一樣。Param#列爲層的參數個數,Total params
項統計出了總的參數量,Trainable params
爲總的待優化參數量,Non-trainable params
爲總的不需要優化的參數量。讀者可以簡單驗證一下參數量的計算結果。
當我們通過 Sequential
容量封裝多個網絡層時,每層的參數列表將會自動併入Sequential 容器的參數列表中,不需要人爲合併網絡參數列表,這也是 Sequential 容器的便捷之處。Sequential 對象的 trainable_variables
和 variables
包含了所有層的待優化張量列表和全部張量列表,例如:
# 打印網絡的待優化參數名與 shape
for p in network.trainable_variables:
print(p.name, p.shape) # 參數名和形狀
dense_2/kernel:0 (4, 3)
dense_2/bias:0 (3,)
dense_3/kernel:0 (3, 3)
dense_3/bias:0 (3,)
Sequential
容器是最常用的類之一,對於快速搭建多層神經網絡非常有用,應儘量多使用來簡化網絡模型的實現。
8.2 模型裝配、訓練與測試
在訓練網絡時,一般的流程是通過前向計算獲得網絡的輸出值,再通過損失函數計算網絡誤差,然後通過自動求導工具計算梯度並更新,同時間隔性地測試網絡的性能。對於這種常用的訓練邏輯,可以直接通過 Keras
提供的模型裝配與訓練等高層接口實現,簡潔清晰。
8.2.1 模型裝配
在 Keras
中,有 2 個比較特殊的類:keras.Model
和 keras.layers.Layer
類。其中 Layer類是網絡層的母類,定義了網絡層的一些常見功能,如添加權值、管理權值列表等。
Model 類是網絡的母類,除了具有 Layer 類的功能,還添加了保存模型、加載模型、訓練與測試模型等便捷功能。Sequential 也是 Model 的子類,因此具有 Model 類的所有功能。
接下來介紹 Model 及其子類的模型裝配與訓練功能。我們以 Sequential 容器封裝的網絡爲例,首先創建 5 層的全連接網絡,用於 MNIST 手寫數字圖片識別,代碼如下:
# 創建 5 層的全連接網絡
network = Sequential([layers.Flatten(input_shape=(28,28)),
layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
#network.build(input_shape=(4,28,28))
print(network.summary())
創建網絡後,正常的流程是循環迭代數據集多個 Epoch,每次按批產生訓練數據、前向計算,然後通過損失函數計算誤差值,並反向傳播自動計算梯度、更新網絡參數。這一部分邏輯由於非常通用,在 Keras 中提供了 compile()
和 fit()
函數方便實現上述邏輯。首先通過compile
函數指定網絡使用的優化器對象、損失函數類型,評價指標等設定,這一步稱爲裝配。例如:
#導入優化器,損失函數模塊
from tensorflow.keras import optimizers,losses
#模型裝配
# 採用Adam優化器,學習率爲0.01;採用交叉熵損失函數,包含softmax
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'] #設置測量指標爲準確率
)
在 compile()
函數中指定的優化器、損失函數等參數也是我們自行訓練時需要設置的參數,並沒有什麼特別之處,只不過 Keras 將這部分常用邏輯內部實現了,提高開發效率。
8.2.2 模型訓練
模型裝配完成後,即可通過 fit()
函數送入待訓練的數據集和驗證用的數據集,這一步稱爲模型訓練。例如:
def preprocess(x, y):
# [b, 28, 28], [b]
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)
y = tf.one_hot(y, depth=10)
return x, y
from tensorflow.keras import datasets
(x, y), (x_test, y_test) = datasets.mnist.load_data() #(6000,28,28) (1000,28,28)
#batchsz = 512
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(512)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(1000).map(preprocess).batch(512)
# 指定訓練集爲 train_db,驗證集爲 val_db,訓練 5 個 epochs,每 2 個 epoch 驗證一次
# 返回訓練軌跡信息保存在 history 對象中
history = network.fit(train_db, epochs=5, validation_data=val_db,
validation_freq=2)
其中 train_db 爲 tf.data.Dataset
對象,也可以傳入 Numpy Array 類型的數據;epochs
參數指定訓練迭代的 Epoch 數量;validation_data
參數指定用於驗證(測試)的數據集和驗證的頻率validation_freq
。
運行上述代碼即可實現網絡的訓練與驗證的功能,fit
函數會返回訓練過程的數據記錄history
,其中 history.history
爲字典對象,包含了訓練過程中的 loss、測量指標等記錄項,我們可以直接查看這些訓練數據,例如:
history.history # 打印訓練記錄
# 歷史訓練準確率
{'accuracy': [0.00011666667, 0.0, 0.0, 0.010666667, 0.02495],
'loss': [2465719710540.5845, # 歷史訓練誤差
78167808898516.03,
404488834518159.6,
1049151145155144.4,
1969370184858451.0],
'val_accuracy': [0.0, 0.0], # 歷史驗證準確率
# 歷史驗證誤差
'val_loss': [197178788071657.3, 1506234836955706.2]}
fit()
函數的運行代表了網絡的訓練過程,因此會消耗相當的訓練時間,並在訓練結束後才返回,訓練中產生的歷史數據可以通過返回值對象取得。可以看到通過 compile&fit 方式實現的代碼非常簡潔和高效,大大縮減了開發時間。但是因爲接口非常高層,靈活性也降低了,是否使用需要用戶自行判斷。
8.2.3 模型測試
Model 基類除了可以便捷地完成網絡的裝配與訓練、驗證,還可以非常方便的預測和測試。關於驗證和測試的區別,我們會在過擬合一章詳細闡述,此處可以將驗證和測試理解爲模型評估的一種方式。
通過 Model.predict(x)
方法即可完成模型的預測,例如:
# 加載一個 batch 的測試數據
x,y = next(iter(test_db))
print('predict x:', x.shape) # 打印當前 batch 的形狀
out = network.predict(x) # 模型預測,預測結果保存在 out 中
print(out)
其中 out 即爲網絡的輸出。通過上述代碼即可使用訓練好的模型去預測新樣本的標籤信息。
如果只是簡單的測試模型的性能,可以通過 Model.evaluate(db)
循環測試完 db 數據集上所有樣本,並打印出性能指標,第一個指標代表loss,第二個指標代表accuracy。例如:
network.evaluate(test_db) # 模型測試,測試在 db_test 上的性能表現
[0.12344565894454718, 0.9658]
8.3 模型保存與加載
模型訓練完成後,需要將模型保存到文件系統上,從而方便後續的模型測試與部署工作。實際上,在訓練時間隔性地保存模型狀態也是非常好的習慣,這一點對於訓練大規模的網絡尤其重要。一般大規模的網絡需要訓練數天乃至數週的時長,一旦訓練過程被中斷或者發生宕機等意外,之前訓練的進度將全部丟失。如果能夠間斷地保存模型狀態到文件系統,即使發生宕機等意外,也可以從最近一次的網絡狀態文件中恢復,從而避免浪費大量的訓練時間和計算資源。因此模型的保存與加載非常重要。
在 Keras 中,有三種常用的模型保存與加載方法。
8.3.1 張量方式
網絡的狀態主要體現在網絡的結構以及網絡層內部張量數據上,因此在擁有網絡結構源文件的條件下,直接保存網絡張量參數到文件系統上是最輕量級的一種方式。我們以MNIST 手寫數字圖片識別模型爲例,通過調用 Model.save_weights(path)
方法即可將當前的網絡參數保存到 path 文件上,代碼如下:
bakup_network = network
# 保存模型參數到文件上
network.save_weights('weights.ckpt')
上述代碼將 network 模型保存到 weights.ckpt
文件上。在需要的時候,先創建好網絡對象,然後調用網絡對象的 load_weights(path)
方法即可將指定的模型文件中保存的張量數值寫入到當前網絡參數中去,例如:
# 保存模型參數到文件上
network.save_weights('weights.ckpt')
print('saved weights.')
del network # 刪除網絡對象
# 重新創建相同的網絡結構
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
# 從參數文件中讀取數據並寫入當前網絡
network.load_weights('weights.ckpt')
print('loaded weights!')
這種保存與加載網絡的方式最爲輕量級,文件中保存的僅僅是張量參數的數值,並沒有其它額外的結構參數。但是它需要使用相同的網絡結構才能夠正確恢復網絡狀態,因此一般在擁有網絡源文件的情況下使用。
8.3.2 網絡方式
我們來介紹一種不需要網絡源文件,僅僅需要模型參數文件即可恢復出網絡模型的方法。通過 Model.save(path)
函數可以將模型的結構以及模型的參數保存到 path 文件上,在不需要網絡源文件的條件下,通過 keras.models.load_model(path)
即可恢復網絡結構和網絡參數。
首先將 MNIST 手寫數字圖片識別模型保存到文件上,並且刪除網絡對象:
# 保存模型結構與模型參數到文件
network.save('model.h5')
print('saved total model.')
del network # 刪除網絡對象
此時通過 model.h5 文件即可恢復出網絡的結構和狀態,不需要提前創建網絡對象,代碼如下:
# 從文件恢復網絡結構與網絡參數
network = keras.models.load_model('model.h5')
可以看到,model.h5 文件除了保存了模型參數外,還應保存了網絡結構信息,不需要提前創建模型即可直接從文件中恢復出網絡 network 對象。
8.3.3 SavedModel 方式
TensorFlow 之所以能夠被業界青睞,除了優秀的神經網絡層 API 支持之外,還得益於它強大的生態系統,包括移動端和網頁端等的支持。當需要將模型部署到其他平臺時,採用 TensorFlow 提出的 SavedModel
方式更具有平臺無關性。
通過 tf.saved_model.save (network, path)
即可將模型以 SavedModel 方式保存到 path 目錄中,代碼如下:
# 保存模型結構與模型參數到文件
tf.saved_model.save(network, 'model-savedmodel')
print('saving savedmodel.')
del network # 刪除網絡對象
此時在文件系統 model-savedmodel 目錄上出現瞭如下網絡文件,如圖 8.1 所示:
用戶無需關心文件的保存格式,只需要通過 tf.saved_model.load
函數即可恢復出模型對象,我們在恢復出模型實例後,完成測試準確率的計算,實現如下:
print('load savedmodel from file.')
# 從文件恢復網絡結構與網絡參數
network = tf.saved_model.load('model-savedmodel')
# 準確率計量器
acc_meter = tf.metrics.CategoricalAccuracy()
for x,y in test_db: # 遍歷測試集
pred = network(x) # 前向計算
acc_meter.update_state(y_true=y, y_pred=pred) # 更新準確率統計
# 打印準確率
print("Test Accuracy:%f" % acc_meter.result())
8.4.1 自定義網絡層
對於自定義的網絡層,至少需要實現初始化__init__
方法和前向傳播邏輯 call 方法。我們以某個具體的自定義網絡層爲例,假設需要一個沒有偏置向量的全連接層,即 bias 爲0,同時固定激活函數爲 ReLU 函數。儘管這可以通過標準的 Dense 層創建,但我們還是通過實現這個“特別的”網絡層類來闡述如何實現自定義網絡層。
首先創建類,並繼承自 Layer 基類。創建初始化方法,並調用母類的初始化函數,由於是全連接層,因此需要設置兩個參數:輸入特徵的長度 inp_dim
和輸出特徵的長度outp_dim
,並通過 self.add_variable(name, shape)
創建 shape 大小,名字爲 name 的張量𝑾,並設置爲需要優化。代碼如下:
class MyDense(layers.Layer):
# 自定義網絡層
def __init__(self,inp_dim,outp_dim):
super(MyDense,self).__init__()
#創建權值張量並添加到類管理列表中,設置爲需要優化
self.add_variable('w',[inp_dim,outp_dim],trainable=True)
需要注意的是,self.add_variable
會返回張量𝑾的 Python 引用,而變量名 name 由TensorFlow 內部維護,使用的比較少。我們實例化 MyDense 類,並查看其參數列表,例如:
net = MyDense(4,3) # 創建輸入爲 4,輸出爲 3 節點的自定義層
net.variables,net.trainable_variables # 查看自定義層的參數列表
# 類的全部參數列表
[<tf.Variable 'w:0' shape=(4, 3) dtype=float32, numpy=
array([[ 0.6118245 , -0.61598533, -0.35566133],
[ 0.2789786 , -0.03844213, -0.11899394],
[-0.01769835, 0.28213632, -0.922646 ],
[ 0.18603265, 0.65625775, 0.2696042 ]], dtype=float32)>]
# 類的待優化參數列表
[<tf.Variable 'w:0' shape=(4, 3) dtype=float32, numpy=
array([[ 0.6118245 , -0.61598533, -0.35566133],
[ 0.2789786 , -0.03844213, -0.11899394],
[-0.01769835, 0.28213632, -0.922646 ],
[ 0.18603265, 0.65625775, 0.2696042 ]], dtype=float32)>]…
可以看到𝑾張量被自動納入類的參數列表。
通過修改爲self.kernel = self.add_variable('w', [inp_dim, outp_dim], trainable=False)
,我們可以設置𝑾張量不需要被優化,此時再來觀測張量的管理狀態:
class MyDense(layers.Layer):
# 自定義網絡層
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 創建權值張量並添加到類管理列表中,設置爲需要優化
self.kernel = self.add_variable('w', [inp_dim, outp_dim],trainable=False)
# 創建輸入爲 4,輸出爲 3 節點的自定義層
net = MyDense(4,3)
# 查看自定義層的參數列表
net.variables,net.trainable_variables
# 類的全部參數列表
[<tf.Variable 'Variable:0' shape=(4, 3) dtype=float32, numpy=
array([[ 0.02653543, -0.91281503, 1.0557984 ],
[-0.6090471 , -0.09816596, 2.015274 ],
[ 0.06185807, 0.3806991 , 1.3561277 ],
[-0.04293933, -0.27208307, -1.1275163 ]], dtype=float32)>]
# 類的待優化參數列表
[]
可以看到,此時張量並不會被 trainable_variables
管理。此外,類初始化中創建爲 tf.Variable
類型的類成員變量也會自動納入張量管理中,例如:
# 通過 tf.Variable 創建的類成員也會自動加入類參數列表
self.kernel = tf.Variable(tf.random.normal([inp_dim, outp_dim]),trainable=False)
打印出管理的張量列表如下:
class MyDense(layers.Layer):
# 自定義網絡層
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 創建權值張量並添加到類管理列表中,設置爲需要優化
self.kernel = tf.Variable('w', [inp_dim, outp_dim],trainable=False)
# 創建輸入爲 4,輸出爲 3 節點的自定義層
net = MyDense(4,3)
# 查看自定義層的參數列表
net.variables,net.trainable_variables
# 類的全部參數列表
[<tf.Variable 'Variable:0' shape=(4, 3) dtype=float32, numpy=
array([[ 0.02653543, -0.91281503, 1.0557984 ],
[-0.6090471 , -0.09816596, 2.015274 ],
[ 0.06185807, 0.3806991 , 1.3561277 ],
[-0.04293933, -0.27208307, -1.1275163 ]], dtype=float32)>]
# 類的待優化參數列表
[]
完成自定義類的初始化工作後,我們來設計自定義類的前向運算邏輯,對於這個例子,只需要完成𝑶 = 𝑿@𝑾矩陣運算,並通過固定的 ReLU 激活函數即可,代碼如下:
class MyDense(layers.Layer):
# 自定義網絡層
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 創建權值張量並添加到類管理列表中,設置爲需要優化
self.kernel = self.add_weight('w', [inp_dim, outp_dim], trainable=True)
def call(self, inputs, training=None):
# 實現自定義類的前向計算邏輯
# X@W
out = inputs @ self.kernel
# 執行激活函數運算
out = tf.nn.relu(out)
return out
如上所示,自定義類的前向運算邏輯實現在 call(inputs, training=None)
函數中,其中 inputs代表輸入,由用戶在調用時傳入;training 參數用於指定模型的狀態:training 爲 True 時執行訓練模式,training 爲 False 時執行測試模式,默認參數爲 None,即測試模式。由於全連接層的訓練模式和測試模式邏輯一致,此處不需要額外處理。對於部分測試模式和訓練模式不一致的網絡層,需要根據 training 參數來設計需要執行的邏輯。
8.4.2 自定義網絡
在完成了自定義的全連接層類實現之後,我們基於上述的“無偏置的全連接層”來實現 MNIST 手寫數字圖片模型的創建。
自定義網絡類可以和其他標準類一樣,通過 Sequential
容器方便地封裝成一個網絡模型:
from tensorflow.keras import Sequential
network=Sequential([
MyDense(784,256),#使用自定義的層
MyDense(256,128),
MyDense(128,64),
MyDense(64,32),
MyDense(32,10)
])
network.build(input_shape=(None,28*28))
print(network.summary())
可以看到,通過堆疊我們的自定義網絡層類,一樣可以實現 5 層的全連接層網絡,每層全連接層無偏置張量,同時激活函數固定地使用 ReLU 函數。
Sequential 容器適合於數據按序從第一層傳播到第二層,再從第二層傳播到第三層,以此規律傳播的網絡模型。對於複雜的網絡結構,例如第三層的輸入不僅是第二層的輸出,還有第一層的輸出,此時使用自定義網絡更加靈活。下面我們來創建自定義網絡類,首先創建類,並繼承自 Model 基類,分別創建對應的網絡層對象,代碼如下:
class MyModel(keras.Model):
# 自定義網絡類,繼承自Model基類
def __init__(self):
super(MyModel,self).__init__()
#完成網絡內需要的網絡層的創建工作
self.fc1=MyDense(28*28,256)
self.fc2=MyDense(256,128)
self.fc3=MyDense(128,64)
self.fc4=MyDense(64,32)
self.fc5=MyDense(32,10)
然後實現自定義網絡的前向運算邏輯,代碼如下:
def call(self,inputs,training=None):
x=self.fc1(inputs)
x=self.fc2(x)
x=self.fc3(x)
x=self.fc4(x)
x=self.fc5(x)
return x
這個例子可以直接使用第一種方式,即 Sequential 容器包裹實現。但自定義網絡的前向計算邏輯可以自由定義,更爲通用,我們會在卷積神經網絡一章看到自定義網絡的優越性。
基於以上內容,我們來總結下代碼:
from tensorflow.keras import datasets,Sequential,optimizers,losses
import matplotlib.pyplot as plt
plt.rcParams['font.size'] = 16
plt.rcParams['font.family'] = ['STKaiti']
plt.rcParams['axes.unicode_minus'] = False
def preprocess(x,y):
x=tf.cast(x,dtype=tf.float32)/255.
y=tf.cast(y,tf.int32)
y=tf.one_hot(y,depth=10)
return x,y
def load_dataset():
(x,y),(x_test,y_test)=datasets.mnist.load_data()
batchsz=512
train_db=tf.data.Dataset.from_tensor_slices((x,y))
train_db=train_db.shuffle(1000).map(preprocess).batch(batchsz)
test_db=tf.data.Dataset.from_tensor_slices((x_test,y_test))
test_db=test_db.shuffle(1000).map(preprocess).batch(batchsz)
return train_db,test_db
def build_network():
# 創建5層的全連接網絡
network=Sequential([
layers.Flatten(input_shape=(28,28)),
layers.Dense(256,activation='relu'),
layers.Dense(128,activation='relu'),
layers.Dense(64,activation='relu'),
layers.Dense(32,activation='relu'),
layers.Dense(10)
])
network.summary()
"""
模型裝配:
採用Adam優化器,學習率爲0.01;
採用交叉熵損失函數,包含softmax
from_logits=False:output經過softmax輸出的概率值
from_logits=True:output經過網絡直接輸出的logits張量
"""
network.compile(optimizer=optimizers.Adam(learning_rate=0.01),
loss=losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'] #設置測量指標爲準確率
)
return network
def train(network,train_db,test_db,epochs=5):
history=network.fit(train_db,epochs=epochs,validation_data=test_db,
validation_freq=2)
#print(history.history)
return network,history
def test_one_data(network,test_db):
x,y=next(iter(test_db))
print('predict x:',x.shape)
out=network.predict(x)
print(out)
def test_model(network,test_db):
network.evaluate(test_db)
def picture(epochs,history):
x = range(epochs)
plt.figure(figsize=(10, 6))
plt.subplots_adjust(wspace=0.5)
plt.subplot(1, 2, 1)
# 繪製MES曲線
plt.title('訓練誤差曲線')
plt.plot(x, history.history['loss'], color='blue')
plt.xlabel('Epoch')
plt.ylabel('MSE')
plt.show()
# 繪製Accuracy曲線
plt.subplot(1, 2, 2)
plt.title('準確率')
plt.plot(x, history.history['accuracy'], color='blue')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.show()
plt.savefig('model_energy.svg')
plt.close()
def main():
epochs=30
train_db,test_db=load_dataset()
network=build_network()
network,history=train(network,train_db,test_db,epochs)
test_one_data(network,test_db)
test_model(network,test_db)
picture(epochs,history)
if __name__=="__main__":
main()
看一下loss和Accuracy.
8.5 模型樂園
對於常用的網絡模型,如 ResNet、VGG 等,不需要手動創建網絡,可以直接從keras.applications
子模塊中通過一行代碼即可創建並使用這些經典模型,同時還可以通過設置 weights
參數加載預訓練的網絡參數,非常方便。
8.5.1 加載模型
以 ResNet50 網絡模型爲例,一般將 ResNet50 去除最後一層後的網絡作爲新任務的特徵提取子網絡,即利用在 ImageNet 數據集上預訓練好的網絡參數初始化,並根據自定義任務的類別追加一個對應數據類別數的全連接分類層或子網絡,從而可以在預訓練網絡的基礎上快速、高效地學習新任務。
首先利用 Keras
模型樂園加載 ImageNet 預訓練好的 ResNet50 網絡,代碼如下:
# 加載ImageNet預訓練網絡模型,並卻掉最後一層
resnet=keras.applications.ResNet50(weights='imagenet',include_top=False)
resnet.summary()
# 測試網絡的輸出
x=tf.random.normal([4,224,224,3])
out=resnet(x) #獲得子網絡的輸出
out.shape
TensorShape([4, 7, 7, 2048])
上述代碼自動從服務器下載模型結構和在 ImageNet 數據集上預訓練好的網絡參數。通過設置 include_top
參數爲 False,可以選擇去掉 ResNet50 最後一層,此時網絡的輸出特徵圖大小爲[𝑏, 7,7,2048]。對於某個具體的任務,需要設置自定義的輸出節點數,以 100 類的分類任務爲例,我們在 ResNet50 基礎上重新構建新網絡。新建一個池化層(這裏的池化層暫時可以理解爲高、寬維度下采樣的功能),將特徵從[𝑏, 7,7,2048]降維到[𝑏, 2048]。代碼如下:
from tensorflow.keras import layers
# 新建池化層
global_average_layer=layers.GlobalAveragePooling2D()
# 利用上一層的輸出作爲本層的額輸入,測試其輸出
x=tf.random.normal([4,7,7,2048])
# 池化層降維,形狀由[4,7,7,2048]變爲[4,1,1,2048],刪減維度後變爲[4,2048]
out=global_average_layer(x)
out.shape
TensorShape([4, 2048])
最後新建一個全連接層,並設置輸出節點數爲 100,代碼如下:
#新建全連接層
fc=layers.Dense(100)
# 利用上一層的輸出[4,2048]作爲本層的輸入,測試其輸出
x=tf.random.normal([4,2048])
out=fc(x) #輸出層的輸出爲樣本屬於100分類的概率分佈
out.shape
TensorShape([4, 100])
在創建預訓練的 ResNet50 特徵子網絡、新建的池化層和全連接層後,我們重新利用Sequential 容器封裝成一個新的網絡:
# 重新包裹成我們的網絡模型
from tensorflow.keras import Sequential
mynet=Sequential([resnet,global_average_layer,fc])
mynet.summary()
可以看到新的網絡模型的結構信息爲:
Layer (type) Output Shape Param #
=================================================================
resnet50 (Model) (None, None, None, 2048) 23587712
_________________________________________________________________
global_average_pooling2d (Gl (None, 2048) 0
_________________________________________________________________
dense_4 (Dense) (None, 100) 204900
=================================================================
Total params: 23,792,612
Trainable params: 23,739,492
Non-trainable params: 53,120
通過設置 resnet.trainable = False
可以選擇凍結 ResNet 部分的網絡參數,只訓練新建的網絡層,從而快速、高效完成網絡模型的訓練。當然也可以在自定義任務上更新網絡的全部參數。
8.6 測量工具
在網絡的訓練過程中,經常需要統計準確率、召回率等測量指標,除了可以通過手動計算的方式獲取這些統計數據外,Keras 提供了一些常用的測量工具,位於 keras.metrics
模塊中,專門用於統計訓練過程中常用的指標數據。
Keras 的測量工具的使用方法一般有 4 個主要步驟:新建測量器,寫入數據,讀取統計數據和清零測量器。
8.6.1 新建測量器
在 keras.metrics
模塊中,提供了較多的常用測量器類,如統計平均值的 Mean
類,統計準確率的 Accuracy
類,統計餘弦相似度的 CosineSimilarity
類等。下面我們以統計誤差值爲例。在前向運算時,我們會得到每一個 Batch 的平均誤差,但是我們希望統計每個Step 的平均誤差,因此選擇使用 Mean 測量器。新建一個平均測量器,代碼如下:
# 新建平均測量器,適合 Loss 數據
loss_meter = metrics.Mean()
8.6.2 寫入數據
通過測量器的 update_state
函數可以寫入新的數據,測量器會根據自身邏輯記錄並處理採樣數據。例如,在每個 Step 結束時採集一次 loss 值,代碼如下:
# 記錄採樣的數據,通過 float()函數將張量轉換爲普通數值
loss_meter.update_state(float(loss))
上述採樣代碼放置在每個 Batch 運算結束後,測量器會自動根據採樣的數據來統計平均值。
8.6.3 讀取統計信息
在採樣多次數據後,可以選擇在需要的地方調用測量器的 result()
函數,來獲取統計值。例如,間隔性統計 loss 均值,代碼如下:
# 打印統計期間的平均 loss
print(step, 'loss:', loss_meter.result())
8.6.4 清除狀態
由於測量器會統計所有歷史記錄的數據,因此在啓動新一輪統計時,有必要清除歷史狀態。通過 reset_states()
即可實現清除狀態功能。例如,在每次讀取完平均誤差後,清零統計信息,以便下一輪統計的開始,代碼如下:
if step % 100 == 0:
# 打印統計的平均 loss
print(step, 'loss:', loss_meter.result())
loss_meter.reset_states() # 打印完後,清零測量器
8.6.5 準確率統計實戰
按照測量工具的使用方法,我們利用準確率測量器 Accuracy 類來統計訓練過程中的準確率。首先新建準確率測量器,代碼如下:
acc_meter = metrics.Accuracy() # 創建準確率測量器
在每次前向計算完成後,記錄訓練準確率數據。需要注意的是,Accuracy 類的 update_state
函數的參數爲預測值和真實值,而不是當前 Batch 的準確率。我們將當前 Batch 樣本的標籤和預測結果寫入測量器,代碼如下:
network = Sequential([
layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
x=tf.random.normal([4,784])
y=[6,6,8,2]
acc_meter=metrics.Accuracy()#創建準確率測量器
#[b,784]=>[b,10],網路輸出值
out=network(x)
# [b,10]=>[b],經過argmax 後計算預測值
pred=tf.argmax(out,axis=1)
pred=tf.cast(pred,dtype=tf.int32)
# 根據預測值與真實值寫入測量器
acc_meter.update_state(y,pred)
在統計完測試集所有 Batch 的預測值後,打印統計的平均準確率,並清零測量器,代碼如下:
# 讀取統計結果
print(step, 'Evaluate Acc:', acc_meter.result().numpy())
acc_meter.reset_states() # 清零測量器
8.7 可視化
在網絡訓練的過程中,通過 Web 端遠程監控網絡的訓練進度,可視化網絡的訓練結果,對於提高開發效率和實現遠程監控是非常重要的。TensorFlow 提供了一個專門的可視化工具,叫做 TensorBoard,它通過 TensorFlow 將監控數據寫入到文件系統,並利用 Web後端監控對應的文件目錄,從而可以允許用戶從遠程查看網絡的監控數據。
TensorBoard 的使用需要模型代碼和瀏覽器相互配合。在使用 TensorBoard 之前,需要安裝 TensorBoard 庫,安裝命令如下:
# 安裝 TensorBoard
pip install tensorboard
接下來我們分模型端和瀏覽器端介紹如何使用 TensorBoard 工具監控網絡訓練進度。
8.7.1 模型端
在模型端,需要創建寫入監控數據的 Summary 類,並在需要的時候寫入監控數據。首先通過 tf.summary.create_file_writer
創建監控對象類實例,並指定監控數據的寫入目錄,代碼如下:
# 創建監控類,監控數據將寫入 log_dir 目錄
summary_writer = tf.summary.create_file_writer(log_dir)
我們以監控誤差數據和可視化圖片數據爲例,介紹如何寫入監控數據。在前向計算完成後,對於誤差這種標量數據,我們通過 tf.summary.scalar
函數記錄監控數據,並指定時間戳 step
參數。這裏的 step 參數類似於每個數據對應的時間刻度信息,也可以理解爲數據曲線的𝑥座標,因此不宜重複。每類數據通過字符串名字來區分,同類的數據需要寫入相同名字的數據庫中。例如:
with summary_writer.as_default(): # 寫入環境
# 當前時間戳 step 上的數據爲 loss,寫入到名爲 train-loss 數據庫中
tf.summary.scalar('train-loss', float(loss), step=step)
TensorBoard 通過字符串 ID 來區分不同類別的監控數據,因此對於誤差數據,我們將它命名爲”train-loss”,其它類別的數據不可寫入,防止造成數據污染。
對於圖片類型的數據,可以通過 tf.summary.image
函數寫入監控圖片數據。例如,在訓練時,可以通過 tf.summary.image
函數可視化樣本圖片。由於 TensorFlow 中的張量一般包含了多個樣本,因此 tf.summary.image
函數接受多個圖片的張量數據,並通過設置max_outputs
參數來選擇最多顯示的圖片數量,代碼如下:
with summary_writer.as_default():
# 寫入環境
#寫入測試準確率
tf.summary.scalar('testacc',float(total_correct/total),step=step=)
# 可視化測試用的圖片,設置最多可視化9張圖片
tf.summary.image('val-onebyone-images:',val_images,max_outputs=9,step=step=)
運行模型程序,相應的數據將實時寫入到指定文件目錄中。
8.7.2 瀏覽器端
在運行程序時,監控數據被寫入到指定文件目錄中。如果要實時遠程查看、可視化這些數據,還需要藉助於瀏覽器和 Web 後端。首先是打開 Web 後端,通過在 cmd 終端運行tensorboard --logdir path
指定 Web 後端監控的文件目錄 path,即可打開 Web 後端監控進程,如圖 8.2 所示:
此時打開瀏覽器,並輸入網址 http://localhost:6006 (也可以通過 IP 地址遠程訪問,具體端口號可能會變動,可查看命令提示) 即可監控網絡訓練進度。TensorBoard 可以同時顯示多條監控記錄,在監控頁面的左側可以選擇監控記錄,如圖 8.3 所示:
在監控頁面的上端可以選擇不同類型數據的監控頁面,比如標量監控頁面SCALARS、圖片可視化頁面 IMAGES 等。對於這個例子,我們需要監控的訓練誤差和測試準確率爲標量類型數據,它的曲線在 SCALARS 頁面可以查看,如圖 8.4、圖 8.5 所示。
在 IMAGES 頁面,可以查看每個 Step 的圖片可視化效果,如圖 8.6 所示。
除了監控標量數據和圖片數據外,TensorBoard 還支持通過 tf.summary.histogram
查看張量數據的直方圖分佈,以及通過 tf.summary.text
打印文本信息等功能。例如:
with summary_writer.as_default():
# 當前時間戳 step 上的數據爲 loss,寫入到 ID 位 train-loss 對象中
tf.summary.scalar('train-loss', float(loss), step=step)
# 可視化真實標籤的直方圖分佈
tf.summary.histogram('y-hist',y, step=step)
# 查看文本信息
tf.summary.text('loss-text',str(float(loss)))
在 HISTOGRAMS 頁面即可查看張量的直方圖,如圖 8.7 所示,在 TEXT 頁面可以查看文本信息,如圖 8.8 所示。
實際上,除了 TensorBoard 工具可以無縫監控 TensorFlow 的模型數據外,Facebook 開發的 Visdom 工具同樣可以方便可視化數據,並且支持的可視化方式豐富,實時性高,使用起來較爲方便。圖 8.9 展示了 Visdom 數據的可視化方式。Visdom 可以直接接受PyTorch 的張量類型的數據,但不能直接接受 TensorFlow 的張量類型數據,需要轉換爲Numpy 數組。對於追求豐富可視化手段和實時性監控的讀者,Visdom 可能是更好的選擇。
參考文章
1.https://github.com/dragen1860/Deep-Learning-with-TensorFlow-book/tree/master/ch08