Hyperband是機(jī)器學(xué)習(xí)中一個(gè)相當(dāng)實(shí)用的超參數(shù)調(diào)優(yōu)算法,核心思路是用逐次減半來分配計(jì)算資源。說白了就是讓一堆配置先跑幾輪,表現(xiàn)差的直接踢掉,剩下的繼續(xù)訓(xùn)練更多輪次。
這個(gè)方法的巧妙之處在于平衡了探索和利用。你既要試足夠多的配置組合(探索),又要給有潛力的配置足夠的訓(xùn)練時(shí)間(利用)。傳統(tǒng)方法要么試得不夠多,要么每個(gè)都試要很久浪費(fèi)時(shí)間。
本文我們來通過調(diào)優(yōu)一個(gè)lstm來展示Hyperband的工作機(jī)制,并和貝葉斯優(yōu)化、隨機(jī)搜索、遺傳算法做了對比。結(jié)果挺有意思的。
Hyperband的工作原理
Hyperband結(jié)合了多臂老虎機(jī)策略和逐次減半算法(SHA)。多臂老虎機(jī)問題其實(shí)就是在探索新選擇和利用已知好選擇之間做權(quán)衡。
SHA則是具體的資源分配策略如下:給隨機(jī)采樣的配置分配固定預(yù)算(比如訓(xùn)練輪數(shù)),每輪評估后踢掉表現(xiàn)最差的,把剩余預(yù)算分給剩下的。Hyperband更進(jìn)一步,用不同的初始預(yù)算跑多次SHA,這樣既能快速篩選,又不會(huì)遺漏那些需要長時(shí)間訓(xùn)練才能顯現(xiàn)優(yōu)勢的配置。
![]()
相比其他調(diào)優(yōu)方法,Hyperband在處理大搜索空間時(shí)速度和效率優(yōu)勢明顯。
下圖展示了Hyperband如何逐步給獲勝配置(#4)分配更多資源,雖然最開始的預(yù)算分配是隨機(jī)的:
![]()
Hyperband工作流程
整個(gè)過程從Bracket 1開始,創(chuàng)建很多超參數(shù)配置,每個(gè)分配少量預(yù)算。然后逐步減少配置數(shù)量,同時(shí)增加幸存者的預(yù)算。到了Bracket 2,只給Bracket 1的幸存者(配置#1和#4)更多預(yù)算。最終在Bracket 3把全部預(yù)算給最優(yōu)配置#4。
這種做法能有效探索廣泛配置范圍,同時(shí)快速淘汰表現(xiàn)差的,在探索和利用間找到平衡。
算法的四個(gè)關(guān)鍵步驟
定義預(yù)算和減半因子
首先要定義最大資源預(yù)算R(單個(gè)模型能訓(xùn)練的總輪數(shù))和減半因子η(決定淘汰激進(jìn)程度的預(yù)設(shè)因子)。減半因子常用2、3或4。每步都用η來減少配置數(shù)量,用η來增加幸存者預(yù)算。
計(jì)算Bracket數(shù)量
算法跑一系列bracket,每個(gè)bracket是用不同起始預(yù)算的完整SHA運(yùn)行。最大bracket索引s_max的計(jì)算公式是:
其中η是減半因子,R是最大資源預(yù)算。算法從s_max個(gè)bracket迭代到零。
運(yùn)行逐次減半
對每個(gè)bracket s,Hyperband確定起始的超參數(shù)配置數(shù)量n_s。有意思的是,初始預(yù)算小的bracket配置數(shù)量大,初始預(yù)算大的bracket配置數(shù)量小。
配置數(shù)量的數(shù)學(xué)定義:
其中n_s是當(dāng)前bracket要評估的配置數(shù)量,R是最大資源預(yù)算,η是減半因子,s_max是最大bracket數(shù),s是當(dāng)前bracket索引。
每個(gè)bracket的初始預(yù)算r_s計(jì)算公式:
![]()
Hyperband先采樣n_s個(gè)隨機(jī)超參數(shù)配置,用初始預(yù)算r_s輪訓(xùn)練每個(gè)。然后根據(jù)性能選出前n_s/η個(gè)配置。這些"幸存者"繼續(xù)訓(xùn)練更多輪,總共r_s?η輪。
這個(gè)減半候選數(shù)量、增加預(yù)算的過程持續(xù)進(jìn)行,直到bracket中只剩一個(gè)配置或達(dá)到最大預(yù)算。
選擇最終配置
所有bracket跑完后,選擇表現(xiàn)最好的配置作為最終結(jié)果。Hyperband的效率就來自快速丟棄表現(xiàn)差的配置,把資源用來訓(xùn)練更有前景的配置。
演示:支持向量分類器
我們用SVC來演示具體工作過程,調(diào)優(yōu)正則化參數(shù)C和核系數(shù)gamma。
搜索空間:C取[0.1, 1, 10, 100],gamma取['scale', 'auto', 0.1, 1, 10]
設(shè)置最大預(yù)算R = 81,減半因子η = 3。
最大bracket索引計(jì)算得出:
所以Hyperband會(huì)為s = 4, 3, 2, 1, 0運(yùn)行bracket。每個(gè)bracket有不同的起始配置數(shù)量和初始預(yù)算:
- Bracket 1 (s = 4):1個(gè)配置,初始預(yù)算9
- Bracket 2 (s = 3):3個(gè)配置,初始預(yù)算3
- Bracket 3 (s = 2):9個(gè)配置,初始預(yù)算1
- Bracket 4 (s = 1):27個(gè)配置,初始預(yù)算1/3
- Bracket 5 (s = 0):81個(gè)配置,初始預(yù)算1/9
以Bracket 3為例說明SHA過程:
初始運(yùn)行時(shí),Hyperband隨機(jī)采樣9個(gè)超參數(shù)配置,用1輪小預(yù)算訓(xùn)練每個(gè),記錄性能,保留前3個(gè)最佳配置丟棄其余6個(gè)。
第二輪,3個(gè)幸存者用3輪更大預(yù)算訓(xùn)練,保留前1個(gè)最佳配置。
最終輪,剩余配置用9輪最終預(yù)算訓(xùn)練,記錄最終性能。
總預(yù)算R = 81就這樣分布在各個(gè)bracket中,高效找到最佳配置。
實(shí)際用例:LSTM股價(jià)預(yù)測實(shí)驗(yàn)
我們用更復(fù)雜的LSTM網(wǎng)絡(luò)來驗(yàn)證Hyperband效果,目標(biāo)是預(yù)測NV股票收盤價(jià)。
從Alpha Vantage API獲取歷史日線數(shù)據(jù),加載到Pandas DataFrame并預(yù)處理。訓(xùn)練集用于模型訓(xùn)練和驗(yàn)證,測試集單獨(dú)保存避免數(shù)據(jù)泄漏。
import torch
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
# create target and input vals
target_col = 'close'
y = df.copy()[target_col].shift(-1) # avoid data leakage
y = y.iloc[:-1] # drop the last row (as y = nan)
input_cols = [col for col in df.columns if col not in [target_col, 'dt']] # drop dt as year, month, date can capture sequence
X = df.copy()[input_cols]
X = X.iloc[:-1] # drop the last row
# create trainning and test dataset (trianing will split into train and val for wfv)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=800, shuffle=False, random_state=42
)
# preprocess
cat_cols = ['year', 'month', 'date']
num_cols = list(set(input_cols) - set(cat_cols))
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), num_cols),
('cat', OneHotEncoder(handle_unknown='ignore'), cat_cols)
]
)
X_train = preprocessor.fit_transform(X_train)
X_test = preprocessor.transform(X_test)
# convert the dense numpy arrays to pytorch tensors
X_train = torch.from_numpy(X_train.toarray()).float()
y_train = torch.from_numpy(y_train.values).float().unsqueeze(1)
X_test = torch.from_numpy(X_test.toarray()).float()
y_test = torch.from_numpy(y_test.values).float().unsqueeze(1)
原始數(shù)據(jù)包含6,501個(gè)NV歷史股價(jià)記錄樣本:
RangeIndex: 6501 entries, 0 to 6500
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 dt 6501 non-null datetime64[ns]
1 open 6501 non-null float32
2 high 6501 non-null float32
3 low 6501 non-null float32
4 close 6501 non-null float32
5 volume 6501 non-null int32
6 ave_open 6501 non-null float32
7 ave_high 6501 non-null float32
8 ave_low 6501 non-null float32
9 ave_close 6501 non-null float32
10 total_volume 6501 non-null int32
11 30_day_ma_close 6501 non-null float32
12 year 6501 non-null object
13 month 6501 non-null object
14 date 6501 non-null object
dtypes: datetime64[ns](1), float32(9), int32(2), object(3)
memory usage: 482.6+ KB
基于多對一架構(gòu)在PyTorch上定義LSTMModel類:
import torch
import torch.nn as nn
class LSTMModel(nn.Module):
def __init__(self, input_dim, hidden_dim, layer_dim, output_dim, dropout):
super(LSTMModel, self).__init__()
self.hidden_dim = hidden_dim
self.layer_dim = layer_dim
self.dropout = dropout
self.lstm = nn.LSTM(
input_dim, hidden_dim, layer_dim, batch_first=True, dropout=dropout
)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
h0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim).to(x.device)
c0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim).to(x.device)
o_t, _ = self.lstm(x, (h0.detach(), c0.detach()))
o_final = self.fc(o_t[:, -1, :])
return o_final
Hyperband在更廣搜索空間中表現(xiàn)更好,定義以下搜索空間:
import random
def search_space():
return {
'lr': 10**random.uniform(-6, -1),
'hidden_dim': random.choice([16, 32, 64, 128, 256]),
'layer_dim': random.choice([1, 2, 3, 4, 5]),
'dropout': random.uniform(0.1, 0.6),
'batch_size': random.choice([16, 32, 64, 128, 256])
}
為時(shí)間序列數(shù)據(jù)定義滑動(dòng)窗口驗(yàn)證的train_and_val_wfv函數(shù):
def train_and_val_wfv(hyperparams, budget, X, y, train_window, val_window):
total_val_loss = 0
all_loss_histories = []
num_folds = (X.size(0) - train_window - val_window) // val_window + 1
for i in range(num_folds):
train_start = i * val_window
train_end = train_start + train_window
val_start = train_end
val_end = val_start + val_window
# ensure not to go past the end of the dataset
if val_end > X.size(0):
break
# create folds
X_train_fold = X[train_start:train_end]
y_train_fold = y[train_start:train_end]
X_val_fold = X[val_start:val_end]
y_val_fold = y[val_start:val_end]
# train and validate on the current fold
fold_val_loss, fold_loss_history = train_and_val(
hyperparams=hyperparams,
budget=budget,
X_train=X_train_fold,
y_train=y_train_fold,
X_val=X_val_fold,
y_val=y_val_fold
)
total_val_loss += fold_val_loss
all_loss_histories.append(fold_loss_history)
# compute ave. loss
avg_val_loss = total_val_loss / num_folds
return avg_val_loss, all_loss_histories
run_hyperband函數(shù)接受搜索空間函數(shù)、驗(yàn)證函數(shù)、總預(yù)算R和減半因子eta四個(gè)參數(shù)。代碼中R設(shè)為100,eta為3,滑動(dòng)窗口交叉驗(yàn)證的訓(xùn)練和驗(yàn)證窗口分別為3,000和500。
def run_hyperband(search_space_fn, val_fn, R, eta):
s_max = int(log(R, eta))
overall_best_config = None
overall_best_loss = float('inf')
all_loss_histories = []
# outer loop: iterate through all brackets
for s in range(s_max, -1, -1):
n = int(R / eta**s)
r = int(R / n)
main_logger.info(f'... running bracket s={s}: {n} configurations, initial budget={r} ...')
# geerate n random hyperparameter configurations
configs = [get_hparams_fn() for _ in range(n)]
# successive halving
for i in range(s + 1):
budget = r * (eta**i)
main_logger.info(f'... training {len(configs)} configurations for budget {budget} epochs ...')
evaluated_results = []
for config in configs:
loss, loss_history = train_val_fn(config, budget)
evaluated_results.append((config, loss, loss_history))
# record loss histories for plotting
all_loss_histories.append((evaluated_results, budget))
# sort and select top configurations
evaluated_results.sort(key=lambda x: x[1])
# keep track of the best configuration found so far
if evaluated_results and evaluated_results[0][1] < overall_best_loss:
overall_best_loss = evaluated_results[0][1]
overall_best_config = evaluated_results[0][0]
num_to_keep = floor(len(configs) / eta)
configs = [result[0] for result in evaluated_results[:num_to_keep]]
if not configs:
break
return overall_best_config, overall_best_loss, all_loss_histories, s_max
# define budget, halving factor
R = 100
eta = 3
# wfv setting
train_window = 3000
val_window = 500
# run sha
best_config, best_loss, all_loss_histories, s_max = run_hyperband(
search_space_fn=search_space,
val_fn=lambda h, b: train_and_val_wfv(h, b, X_train, y_train, train_window=train_window, val_window=val_window),
R=R,
eta=eta
)
實(shí)驗(yàn)結(jié)果
最佳超參數(shù)配置:
- lr: 0.0001614172022855225
- hidden_dim: 128
- layer_dim: 3
- dropout: 0.5825758700895215
- batch_size: 16
最佳驗(yàn)證損失(MSE):0.0519
下圖的實(shí)線跟蹤訓(xùn)練過程中的平均驗(yàn)證損失,垂直虛線表示Hyperband算法修剪表現(xiàn)差模型的時(shí)點(diǎn):
![]()
早期停止的線條(主要是紫色)是表現(xiàn)差的配置,因損失過高被修剪掉。少數(shù)持續(xù)到100輪的線條(主要是青綠色和藍(lán)色)是最成功的配置,損失開始時(shí)快速下降然后穩(wěn)定在很低值,說明性能優(yōu)異。這就是Hyperband的高效之處:快速淘汰差配置,不用浪費(fèi)時(shí)間長期訓(xùn)練它們。
與其他調(diào)優(yōu)方法的對比
為了客觀比較,這里用相同搜索空間、模型和訓(xùn)練驗(yàn)證窗口,對貝葉斯優(yōu)化、隨機(jī)搜索、遺傳算法各跑了20次試驗(yàn)。
貝葉斯優(yōu)化
貝葉斯優(yōu)化用概率模型(如高斯過程)建模驗(yàn)證誤差,選擇下一個(gè)最優(yōu)超參數(shù)配置評估。
最佳配置:lr 0.00016768631941614767, hidden_dim 256, layer_dim 3, dropout 0.3932769195043036, batch_size 64
最佳驗(yàn)證損失(MSE):0.0428
![]()
貝葉斯優(yōu)化損失歷史
隨機(jī)搜索
隨機(jī)搜索從搜索空間隨機(jī)采樣固定數(shù)量配置,不利用過去試驗(yàn)結(jié)果。
最佳配置:lr 0.0004941205117774383, hidden_dim 128, layer_dim 2, dropout 0.3398469430820351, batch_size 64
最佳驗(yàn)證損失(MSE):0.03620
![]()
隨機(jī)搜索損失歷史
遺傳算法
受生物進(jìn)化啟發(fā),遺傳算法維護(hù)超參數(shù)配置群體,用變異和交叉概念生成新的潛在更優(yōu)配置。
最佳配置:lr 0.006441170552290832, hidden_dim 128, layer_dim 3, dropout 0.2052570911345997, batch_size 128
最佳驗(yàn)證損失(MSE):0.1321
![]()
遺傳算法損失歷史
結(jié)果分析
有意思的是,隨機(jī)搜索(0.0362)和貝葉斯優(yōu)化(0.0428)在最終驗(yàn)證損失上略優(yōu)于Hyperband(0.0519)。這說明效率和找到全局最優(yōu)間存在權(quán)衡。
Hyperband的效率來自早期積極修剪表現(xiàn)差配置,這樣能節(jié)省大量時(shí)間,但風(fēng)險(xiǎn)是可能意外淘汰"大器晚成"的配置,也就是那些需要長時(shí)間訓(xùn)練才能顯現(xiàn)優(yōu)勢的配置。
在這個(gè)案例中,隨機(jī)搜索和貝葉斯優(yōu)化更成功。隨機(jī)搜索給每個(gè)模型完整訓(xùn)練預(yù)算,讓高性能配置達(dá)到全部潛力。貝葉斯優(yōu)化的智能搜索在找最佳超參數(shù)集方面也比Hyperband的早停方法更有效。
改進(jìn)Hyperband性能的策略
想要改善Hyperband性能,可以調(diào)整其參數(shù)或與其他調(diào)優(yōu)方法結(jié)合。
調(diào)整關(guān)鍵參數(shù)方面,設(shè)置大的R(總預(yù)算)能讓更多"大器晚成"模型證明價(jià)值,減少過早修剪好配置的機(jī)會(huì)。設(shè)置小的eta(減半因子)允許更溫和的修剪過程,讓更多配置進(jìn)入下一bracket(eta=3丟棄三個(gè)配置,eta=1只丟棄一個(gè))。
而更有前景的是將Hyperband與貝葉斯優(yōu)化結(jié)合。BOHB(Bayesian Optimization and HyperBand)是這樣的混合方法,用Hyperband的逐次減半作框架,但用貝葉斯優(yōu)化的概率模型替換隨機(jī)采樣。BOHB用貝葉斯優(yōu)化選擇最有前景的候選者輸入Hyperband的bracket中。
這種方法結(jié)合了兩者優(yōu)點(diǎn):Hyperband的快速結(jié)果加上貝葉斯優(yōu)化的強(qiáng)最終性能。
總結(jié)
Hyperband是個(gè)挺實(shí)用的超參數(shù)優(yōu)化算法,能有效平衡廣泛搜索空間的探索和有前景配置的利用。其快速修剪差配置的能力使其比傳統(tǒng)網(wǎng)格搜索和隨機(jī)搜索明顯更快更可擴(kuò)展。
雖然貝葉斯優(yōu)化等方法可能在樣本效率上更高,但Hyperband的簡單性和可并行性讓它成為很多機(jī)器學(xué)習(xí)任務(wù)的有力選擇,特別是訓(xùn)練成本昂貴時(shí)。
還是那句話沒有銀彈。選擇哪種調(diào)優(yōu)方法還得看具體場景:如果你有足夠計(jì)算資源且更在乎最終性能,貝葉斯優(yōu)化可能更合適;如果你需要快速得到不錯(cuò)結(jié)果,Hyperband是個(gè)好選擇;如果預(yù)算有限,隨機(jī)搜索也不失為簡單有效的baseline。
關(guān)鍵是理解每種方法的權(quán)衡,根據(jù)實(shí)際需求做選擇。
https://avoid.overfit.cn/post/08d708548fdd4c19b4d9ff7973e9e612
作者:Kuriko IWAI
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲(chǔ)服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.