Categories
程式開發

TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優


TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優 1

「導語」 當我們創建了Keras 模型並開始進行訓練時,一般都會指定一些超參數(如學習率)的值來對訓練的過程進行調控,而這些超參數的取值會對模型訓練的結果產生很大的影響,因此在機器學習工作流程中一項十分重要的步驟就是要確定模型超參數的最佳取值,亦即超參數調優。在TensorFlow 中,我們可以使用HParams 插件很方便地完成這一調優過程。

什麼是超參數

機器學習和深度學習的模型中往往包含成千上萬個參數,其中有的參數可以通過模型訓練並利用反向傳播算法來進行優化,比如模型中的權重(weights) 和偏差(bias) 等,我們稱之為參數(parameters)。還有一些參數不能通過模型訓練來進行優化,比如學習率(learning rate) 、深度神經網絡中的隱含層(hidden layers) 的個數以及隱含層的神經元(hidden units) 個數等,我們稱之為超參數(hyper parameters)。

超參數是用來調節整個模型的訓練過程的,它們只是配置變量,並不直接參與到訓練的過程中,因此需要我們不斷調整與嘗試,以使得模型效果達到最優。需要注意,在一次訓練迭代的過程中,參數是不斷進行更新的,而超參數是恆定不變的。

一般而言,我們會根據模型在訓練集和驗證集上的損失大小,以及預定義的評估指標來選擇最優超參數組合,並最終將該組超參數應用於正式訓練以及線上的Serving 服務。

超參數調優策略

模型的超參數一般都有很多,而且每個超參數也會有眾多的候選值,形成的參數空間較大,如果僅依賴人工逐項測試,時間和精力成本無疑是巨大的,因此有許多自動調參算法被提出。

目前比較常用的超參數自動調優策略包括網格搜索(grid search) 和隨機搜索(random search) 等。

網格搜索

網格搜索是指在所有候選的超參數取值中,嘗試所有的超參數組合,並選取其中使模型效果達到最優的超參數組合作為最終解。

比如模型有2 個超參數,每個超參數有3 個候選值,那麼所有的超參數組合就有3*3=9 種,網格搜索會嘗試所有9 組超參數並從中選擇最佳組合。

網格搜索的缺點是,模型訓練評估的次數會隨超參數數量以及超參數候選值數量的增加而呈指數級增長,從而產生很高的算力和時間成本,因此它並不適用於超參數的數量及候選值數量較多的情況。

隨機搜索

隨機搜索是指每次選取隨機的超參數組合進行嘗試,它可以手動設置嘗試的次數,避免遍歷整個超參數空間,從而可以減少嘗試的成本。

相比網格搜索,隨機搜索能夠更高效地進行超參數調優,但是隨機搜索並不能保證一定能選取到最優的超參數取值,具有不確定性。

HParams 超參數調優步驟

TensorFlow 提供了HParams 插件來輔助我們進行超參數調優, HParams 插件支持網格搜索和隨機搜索兩種策略。下面以Keras 模型訓練為例,介紹使用HParams 進行超參數調優的步驟。

定義超參數

使用HParam 類初始化定義所有的超參數,並指定超參數的取值域(Domain)。 Domain 有三種類型,一種為IntInterval 表示連續的整數取值, Discrete 表示離散取值,可以是整數,浮點數以及字符串等, RealInterval 表示連續的浮點數取值。

例HP_DEEP_LAYERS = hp.HParam(“deep_layers”, hp.IntInterval(1, 3)) 表示連續的整數取值的超參數,1 為最小值, 3 為最大值,取值範圍是 [1, 3] 。

例HP_DEEP_LAYER_SIZE = hp.HParam(“ deep_layer_size”,hp.Discrete([32, 64, 128])) 表示離散取值的超參數,可以取參數列表(list) 中的任一元素。

例HP_LEARNING_RATE = hp.HParam(“learning_rate”, hp.RealInterval(0.001, 0.1)) 表示連續的浮點數取值的超參數, 0.001 為最小值, 0.1 為最大值,取值範圍是 [0.001, 0.1] 。

定義評估指標

使用Metric 類定義我們要用到的評估指標,後面會根據這些指標來選取最優的超參數組合。

例hp.Metric(“epoch_auc”, group=”validation”, display_name=”auc (val.)”) 表示要用到的評估指標為epoch_auc 。指標必須是被Tensorboard 回調函數記錄的或者自定義的標量(scalar) ,一般存儲在日誌文件中,可視化展示時會被HPARAMS 面板調用並顯示。

Metric 構造函數的第一個參數tag 表示指標的名稱,對於使用Tensorboard 回調函數(callbacks) 記錄的指標,其名稱一般為epoch_tag 或batch_tag ,如epoch_auc 。對於自定義的指標, tag 則為tf.summary.scalar(“test_auc”, auc, step=1) 中設定的名稱,這里為test_auc 。

Metric 構造函數的第二個參數group 表示指標存儲的路徑,比如訓練的指標存儲在train 目錄下,驗證的指標存儲在validation 目錄下,自定義的指標可以存儲在test 目錄下等,詳見示例程序。

Metric 構造函數的第三個參數display_name 表示指標在HPARAMS 面板中顯示的名稱。

配置HParams

通過hp.hparams_config(hparams=HPARAMS, metrics=METRICS) 可以按需設置將要選取的超參數以及用於評估的指標。

hparams_config 方法有兩個參數,它們分別表示所有待選超參數HParam 的列表(list) 和所有評估指標Metric 的列表(list) 。

如果不進行此項全局設置, HParams 默認會記錄所有在模型中使用到的超參數以及模型輸出的所有指標值並在HPARAMS 面板中顯示。

構建超參數模型

一般是是將超參數以字典(dict) 的形式傳遞給模型構造函數以完成模型的構建。

模型構造函數的代碼如下所示:

def model_fn(hparams):
model = keras.models.Sequential()

for _ in range(hparams[HP_DEEP_LAYERS]):
model.add(
keras.layers.Dense(
units=hparams[HP_DEEP_LAYER_SIZE],
activation="relu",
use_bias=True,
))
model.add(keras.layers.Dense(units=1, activation="sigmoid"))
model.compile(
optimizer=tf.keras.optimizers.Adam(
learning_rate=hparams[HP_LEARNING_RATE]),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=["AUC"],
)

return model

超參數字典的key 為上面定義的HParam 對象, value 為基本數據類型的值。

模型的構建過程與一般模型構建相同,只不過將原來固定的參數換成超參數字典中的value 值。當然,也可以預先定義好一個可配置的subclass 模型,然後將超參數傳入該模型,即可更加方便地完成模型的構建。

模型訓練

模型訓練時需要指定fit 方法的callback 參數,不僅需要包含Tensorboard 回調函數,還需要包括hp.KerasCallback 回調函數。

其中第一個回調函數用於記錄損失(loss) 以及指標(metrics) 的值,第二個回調函數用來記錄本次訓練使用的超參數組合以及計算最後的損失值以及指標值。

hp.KerasCallback(logdir, hparams) 中第一個參數為記錄HParams 日誌的目錄,第二個參數為超參數字典,與傳遞給模型構造函數的字典相同。

如果使用某組超參數執行了多次訓練,那麼最終HPARAMS 面板的顯示結果為多次評估結果的平均值。

可視化調參結果

如果進行2 次超參數選擇,其日誌根目錄mlp 的結構如下所示:

mlp
├── 0
│   ├── events.out.tfevents.1589257272.alexander.4918.34.v2
│   ├── test
│   │   └── events.out.tfevents.1589257274.alexander.4918.2418.v2
│   ├── train
│   │   └── events.out.tfevents.1589257272.alexander.4918.95.v2
│   └── validation
│   └── events.out.tfevents.1589257273.alexander.4918.1622.v2
├── 1
│   ├── events.out.tfevents.1589257274.alexander.4918.2575.v2
│   ├── test
│   │   └── events.out.tfevents.1589257275.alexander.4918.4958.v2
│   ├── train
│   │   └── events.out.tfevents.1589257274.alexander.4918.2636.v2
│   └── validation
│   └── events.out.tfevents.1589257274.alexander.4918.4162.v2
└── events.out.tfevents.1589257272.alexander.4918.5.v2

其中0 和1 目錄分別存儲了一組超參數訓練與驗證後的結果數據。

模型的訓練和驗證結果(包括loss 以及metrics) 會以events.out.tfevents 文件的形式保存在Tensorboard 回調函數指定的目錄下,本示例中為0 或1 目錄下的train 和validation 目錄。

HParams 記錄的日誌會保存在0 或1 根目錄下的events.out.tfevents 文件中。

啟動Tensorboard 並指定其logdir 參數為mlp ,然後選擇HPARAMS 面板即可看到可視化的調參結果。

完整超參數調優示例

網格搜索示例

網格搜索需要遍歷所有的超參數組合,所以此時在初始化HParam 超參數對象時應該盡量使用Discrete 域類型,方便數據遍歷。當然IntInterval 和RealInterval 域類型的數據也可以通過指定步長的方式來進行遍歷。

如果超參數對象的域類型是IntInterval 和RealInterval 時,可以通過該對象的domain.min_value 和domain.max_value 屬性獲取超參數候選值的最小值和最大值。如果是Discrete 類型,可以通過domain.values 屬性獲取到該超參數所有候選值的列表(list)。

網格搜索的示例代碼如下所示(搜索步驟參見run_all 函數):

import os
import tensorflow as tf
from tensorflow import keras
from tensorboard.plugins.hparams import api as hp
from absl import app, flags
import shutil
import numpy as np

FLAGS = flags.FLAGS
flags.DEFINE_string("logdir", "mlp", "logs dir")

HP_DEEP_LAYERS = hp.HParam("deep_layers", hp.IntInterval(1, 3))
HP_DEEP_LAYER_SIZE = hp.HParam("deep_layer_size", hp.Discrete([32, 64, 128]))
HP_LEARNING_RATE = hp.HParam("learning_rate", hp.RealInterval(0.001, 0.1))

HPARAMS = [
HP_DEEP_LAYERS,
HP_DEEP_LAYER_SIZE,
HP_LEARNING_RATE,
]

METRICS = [
hp.Metric(
"epoch_auc",
group="validation",
display_name="auc (val.)",
),
hp.Metric(
"epoch_loss",
group="validation",
display_name="loss (val.)",
),
hp.Metric(
"batch_auc",
group="train",
display_name="auc (train)",
),
hp.Metric(
"batch_loss",
group="train",
display_name="loss (train)",
),
hp.Metric(
"test_auc",
group="test",
display_name="auc (test)",
),
]

def model_fn(hparams):
model = keras.models.Sequential()

for _ in range(hparams[HP_DEEP_LAYERS]):
model.add(
keras.layers.Dense(
units=hparams[HP_DEEP_LAYER_SIZE],
activation="relu",
use_bias=True,
))
model.add(keras.layers.Dense(units=1, activation="sigmoid"))

model.compile(
optimizer=tf.keras.optimizers.Adam(
learning_rate=hparams[HP_LEARNING_RATE]),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=["AUC"],
)

return model

def run(data, hparams, base_logdir, session_id):
model = model_fn(hparams)
logdir = os.path.join(base_logdir, session_id)

tensorboard_callback = tf.keras.callbacks.TensorBoard(
log_dir=logdir,
update_freq=10,
profile_batch=0,
)
hparams_callback = hp.KerasCallback(logdir, hparams)

((x_train, y_train), (x_val, y_val), (x_test, y_test)) = data

model.fit(
x=x_train,
y=y_train,
epochs=2,
batch_size=128,
validation_data=(x_val, y_val),
callbacks=[tensorboard_callback, hparams_callback],
)

test_dir = os.path.join(logdir, "test")
with tf.summary.create_file_writer(test_dir).as_default():
_, auc = model.evaluate(x_test, y_test)
tf.summary.scalar("test_auc", auc, step=1)

def prepare_data():
x_train, y_train = (
np.random.rand(6000, 32),
np.random.randint(2, size=(6000, 1)),
)

x_val, y_val = (
np.random.rand(1000, 32),
np.random.randint(2, size=(1000, 1)),
)

x_test, y_test = (
np.random.rand(1000, 32),
np.random.randint(2, size=(1000, 1)),
)

return ((x_train, y_train), (x_val, y_val), (x_test, y_test))

def run_all(logdir):
data = prepare_data()
with tf.summary.create_file_writer(logdir).as_default():
hp.hparams_config(hparams=HPARAMS, metrics=METRICS)

session_index = 0
for deep_layers in range(HP_DEEP_LAYERS.domain.min_value,
HP_DEEP_LAYERS.domain.max_value):
for deep_layer_size in HP_DEEP_LAYER_SIZE.domain.values:
for learning_rate in np.arange(HP_LEARNING_RATE.domain.min_value,
HP_LEARNING_RATE.domain.max_value,
0.01):
hparams = {
HP_DEEP_LAYERS: deep_layers,
HP_DEEP_LAYER_SIZE: deep_layer_size,
HP_LEARNING_RATE: learning_rate,
}
session_id = str(session_index)
session_index += 1
print("--- Running training session %d" % (session_index))
hparams_string = str(hparams)
print(hparams_string)
run(
data=data,
hparams=hparams,
base_logdir=logdir,
session_id=session_id,
)

def main(argv):
del argv # Unused args
logdir = FLAGS.logdir
shutil.rmtree(logdir, ignore_errors=True)
print("Saving output to %s." % logdir)
run_all(logdir=logdir)
print("Done. Output saved to %s." % logdir)

if __name__ == "__main__":
app.run(main)

隨機搜索示例

通過調用超參數對象的域(domain) 屬性的sample_uniform() 方法可以從該超參數的候選值中隨機選取一個值,然後就可以使用隨機生成的超參數組合進行訓練了。

sample_uniform 方法還可以接收一個帶有種子(seed) 的偽隨機數生成器,如random.Random(seed),這在分佈式訓練的超參數調優過程中十分重要,通過指定同一個偽隨機數生成器,可以保證所有worker 節點每次獲取到的超參數組合都是一致的,從而確保分佈式訓練能夠正常進行。

隨機搜索run_all 部分的代碼如下所示,其它部分的代碼與網格搜索相同。

def run_all(logdir):
data = prepare_data()
with tf.summary.create_file_writer(logdir).as_default():
hp.hparams_config(hparams=HPARAMS, metrics=METRICS)

session_index = 0
for _ in range(8):
hparams = {h: h.domain.sample_uniform() for h in HPARAMS}
hparams_string = str(hparams)
session_id = str(session_index)
session_index += 1
print("--- Running training session %d" % (session_index))
print(hparams_string)
run(
data=data,
hparams=hparams,
base_logdir=logdir,
session_id=session_id,
)

HPARAMS 面板

啟動Tensorboard 後,即可在頁面的上方看到HPARAMS 選項,點擊該選項就能夠看到HPARAMS 面板(Dashboard) 了。

HPARAMS 面板提供了左右兩個窗格,左邊的窗格提供了篩選功能,右邊的窗格提供了可視化評估結果的功能,下面來分別對它們的功用進行說明。

篩選窗格

篩選窗格提供了篩選功能,以控制右邊窗格的可視化渲染。它可以選擇用於展示的超參數以及指標,可以篩選被展示的超參數以及指標的值,還可以對可視化的結果進行排序等。如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優 2

可視化窗格

可視化窗格包含三個視圖(view) ,分別包含不同的信息。

表格視圖(Table View) 以表格的形式列出了所有的超參數組合以及對應的各項指標的值,還可以通過點擊Show Metrics 來展示指標隨batch 或epoch 的變化趨勢圖。如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優 3

平行坐標視圖(Parallel Coordinates View) 由一系列表示超參數和指標的豎向坐標軸組成,對於每一超參數的取值以及對應的指標值,都會通過一條線連接起來。點擊任意一條線都會對該組取值進行高亮顯示,可以在坐標軸上用鼠標標記一個區域,這時只會顯示在該區域內的取值,這對於判斷哪組超參數更為重要十分有幫助。如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優 4

散點圖視圖(Scatter Plot View) 由一系列超參數與指標之間關聯的散點圖組成,它可以幫助發現超參數之間或超參數與指標之間的潛在關聯。如下圖所示:

TensorFlow 篇| TensorFlow 2.x 基於HParams 的超參數調優 5

注意事項

RealInterval 的取值如果從0 開始,則要以浮點數0. 形式表示。

每一組超參數都需要獨立的訓練過程,所以要將不同組超參數訓練的日誌文件寫到不同的目錄下。

在進行超參數調優時,模型訓練方法fit 中的metrics 參數要設置為字符串類型或者一個全局的Metric 對象。這樣Tensorboard 記錄的metrics 名稱才能在多次訓練中保持一致如epoch_auc,而不會出現epoch_auc_1, epoch_auc_2 這種情況,從而使得HPARAMS 面板能夠正常獲取到metrics 的值並進行展示。

在分佈式訓練中進行超參數隨機搜索時,需要指定一個帶種子的偽隨機數生成器,使得每個worker 節點選取到的隨機值都一致,從而確保分佈式訓練能夠正常進行。

參考資料

使用HParams儀表板進行超參數調整Hparams演示