超參數優化
Bayesian Optimization使用Hyperopt進行參數調優
1. 前言
本文将介紹一種快速有效的方法用于實作機器學習模型的調參。有兩種常用的調參方法:網格搜尋和随機搜尋。每一種都有自己的優點和缺點。網格搜尋速度慢,但在搜尋整個搜尋空間方面效果很好,而随機搜尋很快,但可能會錯過搜尋空間中的重要點。幸運的是,還有第三種選擇:貝葉斯優化。本文我們将重點介紹貝葉斯優化的一個實作,一個名為hyperopt的Python子產品。
使用貝葉斯優化進行調參可以讓我們獲得給定模型的最佳參數,例如邏輯回歸模型。這也使我們能夠執行最佳的模型選擇。通常機器學習工程師或資料科學家将為少數模型(如決策樹,支援向量機和K近鄰)執行某種形式(網格搜尋或随機搜尋)的手動調參,然後比較準确率并選擇最佳的一個來使用。該方法可能比較的是次優模型。也許資料科學家找到了決策樹的最優參數,但卻錯過了SVM的最優參數。這意味着他們的模型比較是有缺陷的。如果SVM參數調整得很差,K 近鄰可能每次都會擊敗SVM。貝葉斯優化允許資料科學家找到所有模型的最佳參數,并是以比較最佳模型。這會得到更好的模型選擇,因為你比較的是最佳的k近鄰和最佳的決策樹。隻有這樣你才能非常自信地進行模型選擇,確定選擇并使用的是實際最佳的模型。
本文涵蓋的主題有:
- 目标函數
- 搜尋空間
- 存儲評估試驗
- 可視化
- 經典資料集上的完整示例:Iris
2. 目标函數 - 一個啟發性例子
假設你有一個定義在某個範圍内的函數,并且想把它最小化。也就是說,你想找到産生最低輸出值的輸入值。下面的簡單例子找到\(x\)的值用于最小化線性函數\(y(x)=x\)
from hyperopt import fmin, tpe, hp
best = fmin(
fn=lambda x: x,
space=hp.uniform('x', 0, 1),
algo=tpe.suggest,
max_evals=100)
print(best)
輸出結果:
{'x': 0.000269455723739237}
- \(fmin\)首先接受一個函數來最小化,記為\(fn\),在這裡用一個匿名函數\(lambda \ x:x\)來指定。該函數可以是任何有效的值傳回函數,例如回歸中的平均絕對誤差。
- \(space\)是指定搜尋空間,在本例中,它是0到1之間的連續數字範圍,\(hp.uniform('x', 0, 1)\)指定。\(hp.uniform\)是一個内置的hyperopt函數,它有三個參數:名稱\(x\),範圍的下限和上限0和1。
- \(algo\)參數指定搜尋算法,本例中tpe表示tree of Parzen estimators。該主題超出了本文的範圍,但有數學背景的讀者可以細讀這篇文章。algo參數也可以設定為\(hyperopt.random\),但是這裡我們沒有涉及,因為它是衆所周知的搜尋政策。但在未來的文章中我們可能會涉及。
- 最後\(max\_evals\)是最大評估次數。這個fmin函數将傳回一個python字典。
2.1 稍微複雜的例子
這有一個更複雜的目标函數:
\(lambda\ x: (x-1)^2\)。這次我們試圖最小化一個二次方程\(y(x)=(x-1)^2\)。是以我們改變搜尋空間以包括我們已知的最優值\((x=1)\)加上兩邊的一些次優範圍:\(hp.uniform('x', -2, 2)\)。
best = fmin(
fn=lambda x: (x-1)**2,
space=hp.uniform('x', -2, 2),
algo=tpe.suggest,
max_evals=100)
print(best)
{'x': 0.997369045274755}
3. 搜尋空間
hyperopt子產品包含一些友善的函數來指定輸入參數的範圍。我們已經見過\(hp.uniform\)。最初,這些是随機搜尋空間,但随着hyperopt更多的學習(因為它從目标函數獲得更多回報),通過它認為提供給它最有意義的回報,會調整并采樣初始搜尋空間的不同部分。
以下内容将在本文中使用:
- \(hp.choice(label, options)\)其中options應是python清單或元組。
- \(hp.normal(label, mu, sigma)\)其中mu和sigma分别是均值和标準差。
- \(hp.uniform(label, low, high)\)其中low和high是範圍的下限和上限。
import hyperopt.pyll.stochastic
space = {
'x': hp.uniform('x', 0, 1),
'y': hp.normal('y', 0, 1),
'name': hp.choice('name', ['alice', 'bob']),
}
print(hyperopt.pyll.stochastic.sample(space))
{'y': -1.4012610048810574, 'x': 0.7258615424906184, 'name': 'alice'}
4. Trials捕獲資訊
如果能看到hyperopt黑匣子内發生了什麼是極好的。Trials對象使我們能夠做到這一點。我們隻需要導入一些東西。
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
fspace = {
'x': hp.uniform('x', -5, 5)
}
def f(params):
x = params['x']
val = x**2
return {'loss': val, 'status': STATUS_OK}
trials = Trials()
best = fmin(fn=f, space=fspace, algo=tpe.suggest, max_evals=50, trials=trials)
print('best:', best)
print 'trials:'
for trial in trials.trials[:2]:
print(trial)
\(STATUS_OK\)和Trials是新導入的。Trials對象允許我們在每個時間步存儲資訊。然後我們可以将它們列印出來,并在給定的時間步檢視給定參數的函數評估值。
best: {'x': 0.014420181637303776}
trials:
{'refresh_time': None, 'book_time': None, 'misc': {'tid': 0, 'idxs': {'x': [0]}, 'cmd': ('domain_attachment', 'FMinIter_Domain'), 'vals': {'x': [1.9646918559786162]}, 'workdir': None}, 'state': 2, 'tid': 0, 'exp_key': None, 'version': 0, 'result': {'status': 'ok', 'loss': 3.8600140889486996}, 'owner': None, 'spec': None}
{'refresh_time': None, 'book_time': None, 'misc': {'tid': 1, 'idxs': {'x': [1]}, 'cmd': ('domain_attachment', 'FMinIter_Domain'), 'vals': {'x': [-3.9393509404526728]}, 'workdir': None}, 'state': 2, 'tid': 1, 'exp_key': None, 'version': 0, 'result': {'status': 'ok', 'loss': 15.518485832045357}, 'owner': None, 'spec': None}
Trials對象将資料存儲為BSON對象,其工作方式與JSON對象相同。BSON來自pymongo子產品。我們不會在這裡讨論細節,這是對于需要使用MongoDB進行分布式計算的hyperopt的進階選項,是以需要導入pymongo。回到上面的輸出。
- tid是時間 id,即時間步,其值從0到\(max\_evals-1\)。它随着疊代次數遞增。
- \('x'\)是鍵\('vals'\)的值,其中存儲的是每次疊代參數的值。
- \('loss'\)是鍵\('result'\)的值,其給出了該次疊代目标函數的值。
4.1 可視化
我們看看損失vs值的圖
f, ax = plt.subplots(1)
xs = [t['misc']['vals']['x'] for t in trials.trials]
ys = [t['result']['loss'] for t in trials.trials]
ax.scatter(xs, ys, s=20, linewidth=0.01, alpha=0.75)
ax.set_title('$val$ $vs$ $x$ ', fontsize=18)
ax.set_xlabel('$x$', fontsize=16)
ax.set_ylabel('$val$', fontsize=16)
5. Iris 資料集
在本節中,我們将介紹4個使用hyperopt在經典資料集Iris上調參的完整示例。我們将涵蓋K近鄰(KNN),支援向量機(SVM),決策樹和随機森林。需要注意的是,由于我們試圖最大化交叉驗證的準确率(acc請參見下面的代碼),而hyperopt隻知道如何最小化函數,是以必須對準确率取負。最小化函數f與最大化f的負數是相等的。
對于這項任務,我們将使用經典的Iris資料集,并進行一些有監督的機器學習。資料集有有4個輸入特征和3個輸出類别。資料被标記為屬于類别0,1或2,其映射到不同種類的鸢尾花。輸入有4列:萼片長度,萼片寬度,花瓣長度和花瓣寬度。輸入的機關是厘米。我們将使用這4個特征來學習模型,預測三種輸出類别之一。因為資料由sklearn提供,它有一個很好的DESCR屬性,可以提供有關資料集的詳細資訊。嘗試以下代碼以獲得更多細節資訊。
5.1 K近鄰
我們現在将使用hyperopt來找到K近鄰(KNN)機器學習模型的最佳參數。KNN模型是基于訓練資料集中k個最近資料點的大多數類别對來自測試集的資料點進行分類。下面的代碼結合了我們所涵蓋的一切。
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,100))
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print('best:',best)
現在讓我們看看輸出結果的圖。y軸是交叉驗證分數,x軸是k近鄰個數。下面是代碼和它的圖像:
f, ax = plt.subplots(1)#, figsize=(10,10))
xs = [t['misc']['vals']['n'] for t in trials.trials]
ys = [-t['result']['loss'] for t in trials.trials]
ax.scatter(xs, ys, s=20, linewidth=0.01, alpha=0.5)
ax.set_title('Iris Dataset - KNN', fontsize=18)
ax.set_xlabel('n_neighbors', fontsize=12)
ax.set_ylabel('cross validation accuracy', fontsize=12)
k 大于63後,準确率急劇下降。這是因為資料集中每個類的數量。這三個類中每個類隻有50個執行個體。是以讓我們将\('n\_neighbors'\)的值限制為較小的值來進一步探索。
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,50))
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print('best:',best)
現在我們可以清楚地看到k有一個最佳值,k=4。
上面的模型沒有做任何預處理。是以我們來歸一化和縮放特征,看看是否有幫助。用如下代碼:
# now with scaling as an option
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = KNeighborsClassifier(**params)
return cross_val_score(clf, X_, y).mean()
space4knn = {
'n_neighbors': hp.choice('n_neighbors', range(1,50)),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4knn, algo=tpe.suggest, max_evals=100, trials=trials)
print('best:',best)
并像這樣繪制參數:
parameters = ['n_neighbors', 'scale', 'normalize']
cols = len(parameters)
f, axes = plt.subplots(nrows=1, ncols=cols, figsize=(15,5))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
ys = np.array(ys)
axes[i].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.75, c=cmap(float(i)/len(parameters)))
axes[i].set_title(val)
我們看到縮放和/或歸一化資料并不會提高預測準确率。k的最佳值仍然為4,這得到98.6%的準确率。
是以這對于簡單模型 KNN 調參很有用。讓我們看看用支援向量機(SVM)能做什麼。
5.2 支援向量機(SVM)
由于這是一個分類任務,我們将使用sklearn的SVC類。代碼如下:
iris = datasets.load_iris()
X = iris.data
y = iris.target
def hyperopt_train_test(params):
X_ = X[:]
if 'normalize' in params:
if params['normalize'] == 1:
X_ = normalize(X_)
del params['normalize']
if 'scale' in params:
if params['scale'] == 1:
X_ = scale(X_)
del params['scale']
clf = SVC(**params)
return cross_val_score(clf, X_, y).mean()
space4svm = {
'C': hp.uniform('C', 0, 20),
'kernel': hp.choice('kernel', ['linear', 'sigmoid', 'poly', 'rbf']),
'gamma': hp.uniform('gamma', 0, 20),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
}
def f(params):
acc = hyperopt_train_test(params)
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space4svm, algo=tpe.suggest, max_evals=100, trials=trials)
print('best:',best)
parameters = ['C', 'kernel', 'gamma', 'scale', 'normalize']
cols = len(parameters)
f, axes = plt.subplots(nrows=1, ncols=cols, figsize=(20,5))
cmap = plt.cm.jet
for i, val in enumerate(parameters):
xs = np.array([t['misc']['vals'][val] for t in trials.trials]).ravel()
ys = [-t['result']['loss'] for t in trials.trials]
xs, ys = zip(\*sorted(zip(xs, ys)))
axes[i].scatter(xs, ys, s=20, linewidth=0.01, alpha=0.25, c=cmap(float(i)/len(parameters)))
axes[i].set_title(val)
axes[i].set_ylim([0.9, 1.0])
我們得到結果:
同樣,縮放和歸一化也沒有幫助。核函數的首選是(linear),C的最佳值是1.4168540399911616,gamma的最佳值是15.04230279483486。這組參數得到了99.3%的分類準确率。
5.3 是時候把所有東西合為一體了
自動調整一個模型的參數(如SVM或KNN)非常有趣并且具有啟發性,但同時調整它們并取得全局最佳模型則更有用。這使我們能夠一次比較所有參數和所有模型,是以為我們提供了最佳模型。代碼如下:
digits = datasets.load_digits()
X = digits.data
y = digits.target
print X.shape, y.shape
def hyperopt_train_test(params):
t = params['type']
del params['type']
if t == 'naive_bayes':
clf = BernoulliNB(**params)
elif t == 'svm':
clf = SVC(**params)
elif t == 'dtree':
clf = DecisionTreeClassifier(**params)
elif t == 'knn':
clf = KNeighborsClassifier(**params)
else:
return 0
return cross_val_score(clf, X, y).mean()
space = hp.choice('classifier_type', [
{
'type': 'naive_bayes',
'alpha': hp.uniform('alpha', 0.0, 2.0)
},
{
'type': 'svm',
'C': hp.uniform('C', 0, 10.0),
'kernel': hp.choice('kernel', ['linear', 'rbf']),
'gamma': hp.uniform('gamma', 0, 20.0)
},
{
'type': 'randomforest',
'max_depth': hp.choice('max_depth', range(1,20)),
'max_features': hp.choice('max_features', range(1,5)),
'n_estimators': hp.choice('n_estimators', range(1,20)),
'criterion': hp.choice('criterion', ["gini", "entropy"]),
'scale': hp.choice('scale', [0, 1]),
'normalize': hp.choice('normalize', [0, 1])
},
{
'type': 'knn',
'n_neighbors': hp.choice('knn_n_neighbors', range(1,50))
}
])
count = 0
best = 0
def f(params):
global best, count
count += 1
acc = hyperopt_train_test(params.copy())
if acc > best:
print 'new best:', acc, 'using', params['type']
best = acc
if count % 50 == 0:
print 'iters:', count, ', acc:', acc, 'using', params
return {'loss': -acc, 'status': STATUS_OK}
trials = Trials()
best = fmin(f, space, algo=tpe.suggest, max_evals=1500, trials=trials)
print('best:',best)
由于我們增加了評估數量,此代碼需要一段時間才能運作完:\(max\_evals=1500\)。當找到新的最佳準确率時,它還會添加到輸出用于更新。好奇為什麼使用這種方法沒有找到前面的最佳模型:參數為kernel=linear,C=1.416,gamma=15.042的SVM。
6. 總結
我們已經介紹了簡單的例子,如最小化确定的線性函數,以及複雜的例子,如調整SVM的參數。後面讀者需要根據自己的需求再去調整選擇的參數,也可以基于深度學習模型進行調參。
轉載至https://www.jianshu.com/p/35eed1567463