Hyperopt 入门指南
Hyperopt:是進行超參數優化的一個類庫。有了它我們就可以拜托手動調參的煩惱,并且往往能夠在相對較短的時間內獲取原優于手動調參的最終結果。
一般而言,使用hyperopt的方式的過程可以總結為:
- 用于最小化的目標函數
- 搜索空間
- 存儲搜索過程中所有點組合以及效果的方法
- 要使用的搜索算法
目標函數
一個簡單的例子
這里是一個簡單的例子,用來展示函數問題
from hyperopt import fmin, tpe, hp best = fmin(fn=lambda x: x,space=hp.uniform('x', -2.5, 2.5),algo=tpe.suggest,max_evals=100) print(best) {'x': -2.4988538280500032}我們來分解一下這個例子。
函數fmin首先接受一個函數來最小化,記為fn,在這里用一個匿名函數lambda x: x來指定。該函數可以是任何有效的值返回函數,例如回歸中的平均絕對誤差。
下一個參數指定搜索空間,在本例中,它是0到1之間的連續數字范圍,由hp.uniform(‘x’, 0, 1)指定。hp.uniform是一個內置的hyperopt函數,它有三個參數:名稱x,范圍的下限和上限0和1。
algo參數指定搜索算法,本例中tpe表示 tree of Parzen estimators。該主題超出了本文的范圍,但有數學背景的讀者可以細讀這篇文章。algo參數也可以設置為hyperopt.random,但是這里我們沒有涉及,因為它是眾所周知的搜索策略。但在未來的文章中我們可能會涉及。
最后,我們指定fmin函數將執行的最大評估次數max_evals。這個fmin函數將返回一個python字典。
上述函數的一個輸出示例是{‘x’: 0.0011494865292422545}。
以下是該函數的圖。紅點是我們試圖找到的點。
import matplotlib.pyplot as plt %matplotlib inline import numpy as np # 為隨機數固定一個數值 np.random.RandomState(seed=42)plt.rcParams["font.sans-serif"] = ['SimHei'] # 用于正常顯示中文標簽 plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負號x = np.linspace(-2.5, 2.5, 256, endpoint=True) # 繪制X軸(-2.5,2)的圖像f = x # y值plt.plot(x, f, "g-", lw=2.5, label="f(x)") plt.scatter(best['x'], best['x'], 50, color='blue') plt.title('f(x) = x 函數圖') plt.legend() plt.show()更復雜的例子-單獨定義目標函數
我們這里展示一個更為復雜的例子——f(x)=sin2(x?2)e?x2f(x) = sin^2(x-2)e^{-x^2}f(x)=sin2(x?2)e?x2,并且將fn定義為python中的函數。同時這里我們還需要通過負號將最大化問題轉化為最小化問題。
from hyperopt import fmin, tpe, hpdef fun_change(x):y = (np.sin(x - 2))**2 * (np.e)**(-x**2)return -ybest = fmin(fn=fun_change,space=hp.uniform('x', -2.5, 2.5),algo=tpe.suggest,max_evals=100) print(best)x = np.linspace(-2.5, 2.5, 256, endpoint=True) # 繪制X軸(-2.5,2)的圖像f = (np.sin(x - 2))**2 * (np.e)**(-x**2) # y值plt.plot(x, f, "g-", lw=2.5, label="f(x)") plt.scatter(best['x'], -fun_change(best['x']), 50, color='blue') plt.title('$f(x) = sin^2(x-2)e^{-x^2}$函數圖') plt.legend() plt.show() {'x': 0.21602283343494763}使用上面的方法,我們就可以傳入更加復雜的函數,或者某個其他類庫中單獨定義的函數,如sklearn中的SVC,RF,亦或是XGBoost或者LightGBM,并在對該函數進行搜索。不過假如真的使用sklearn中的機器學習模型的話,那就會存在另外一個問題,很明顯,一般的機器學習模型都不止一個超參數。那么我們又應該怎么傳入更多的超參數呢。
(Note:不過假如想要完美的使用xgb或者lgb還存在其他問題,這個我們會在其他文章里詳細描述使用方法)
參數空間
如何定義
想要同時傳入兩個超參數,我們首先要了解參數空間的概念。在Hyperopt中的參數空間需要使用hyperopt.hp下的函數進行定義。
hyperopt的hp包含很多用來定義參數空間的函數。之前我們已經使用過其中hp.uniform,該函數是返回位于[low,hight]之間的均勻分布的值。除此之外比較常用的還有randint:返回0到參數之間的整數,choice,從列表中選擇,該函數可以用于傳入字符串參數。
常用函數如下:
-
hp.choice(label, options)
- 返回傳入的列表或者數組其中的一個選項。
-
hp.randint(label, upper)
- 返回范圍:[0,upper]中的隨機整數。
-
hp.uniform(label, low, high)
- 返回位于[low,hight]之間的均勻分布的值。
- 在優化時,這個變量被限制為一個雙邊區間。
-
hp.normal(label, mu, sigma)
- 返回正態分布的實數值,其平均值為 mu ,標準偏差為 σ。優化時,這是一個無邊界的變量。
除此之外其實還有很多其他方法可以使用。這里不再做更多介紹。詳細內容可以參考本人翻譯的Hyperopt中文文檔。(已通過issues獲得授權)
import hyperopt.pyll.stochasticspace = {'x': hp.uniform('x', 0, 1),'y': hp.normal('y', 0, 1),'name': hp.choice('name', ['alice', 'bob']), }print(hyperopt.pyll.stochastic.sample(space)) {'name': 'alice', 'x': 0.5383149551265306, 'y': 0.05463630170554273}代碼實現與具體實現
而對于數個傳入的超參數,很明顯假設這也是一個函數的話,其極大概率是非凸的,而如貝葉斯優化等現代優化算法則是專門處理此類問題的。在搜索一開始的時候,hyperopt會默認進行隨機搜索,但是Hyperopt會在搜索過程中對函數的輸出進行預估,然后不斷地根據之前的結果,調整搜索空間。
之前說過為了向fmin中傳入更多的超參數,我們還可以對參數空間進行擴展,而下面就是一個同時對兩個參數進行優化的例子。其使用十分簡單
def fun_change(space):f = space['x'] + space['y']return -fspace = {"x": hp.uniform('x', -2.5, 2.5),"y": hp.uniform('y', -2.5, 2.5), }best = fmin(fn=fun_change, space=space, algo=tpe.suggest, max_evals=1000) print(best)x = np.linspace(-2.5, 2.5, 256, endpoint=True) # 繪制X軸(-2.5,2.5)的圖像 y = np.linspace(-2.5, 2.5, 256, endpoint=True) # 繪制X軸(-2.5,2.5)的圖像f = x + y # y值plt.plot(x, f, "g-", lw=2.5, label="f(x)") plt.scatter(best['x'], -fun_change(best), 50, color='blue') plt.scatter(best['x'], -fun_change(best), 50, color='red') plt.title('$f(x) = sin^2(x-2)e^{-x^2}$函數圖') plt.legend() plt.show() {'x': 2.498647148053222, 'y': 2.4999021654689324}與sklearn結合
在做到這一步的時候,我們已經可以通過將hyperopt和sklearn結合以獲取我們想要的超參數組合。這里我們首先來獲取數據集,這里使用的是非常經典的iris數據集。
import seaborn as sns import pandas as pd sns.set(style="whitegrid", palette="husl")iris = sns.load_dataset("iris") print(iris.head())iris = pd.melt(iris, "species", var_name="measurement") print(iris.head())f, ax = plt.subplots(1, figsize=(15,10)) sns.stripplot(x="measurement", y="value", hue="species", data=iris, jitter=True, edgecolor="white", ax=ax) sepal_length sepal_width petal_length petal_width species 0 5.1 3.5 1.4 0.2 setosa 1 4.9 3.0 1.4 0.2 setosa 2 4.7 3.2 1.3 0.2 setosa 3 4.6 3.1 1.5 0.2 setosa 4 5.0 3.6 1.4 0.2 setosaspecies measurement value 0 setosa sepal_length 5.1 1 setosa sepal_length 4.9 2 setosa sepal_length 4.7 3 setosa sepal_length 4.6 4 setosa sepal_length 5.0<matplotlib.axes._subplots.AxesSubplot at 0x7fd81b8bf588>上面的代碼展示了iris數據集,而后邊的代碼則是Hyperopt對sklearn中分類決策樹的調參實例。
from sklearn import datasets from sklearn.preprocessing import normalize, scale from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import cross_val_score # 這里的warnings實在太多了,我們加入代碼不再讓其顯示 import warnings warnings.filterwarnings("ignore")from hyperopt import Trials iris = datasets.load_iris() X = iris.data y = iris.targetdef hyperopt_model_score_dtree(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 = DecisionTreeClassifier(**params)return cross_val_score(clf, X_, y).mean()space_dtree = {'max_depth': hp.choice('max_depth', range(1, 20)),'max_features': hp.choice('max_features', range(1, 5)),'criterion': hp.choice('criterion', ["gini", "entropy"]),'scale': hp.choice('scale', [0, 1]),'normalize': hp.choice('normalize', [0, 1]) }def fn_dtree(params):acc = hyperopt_model_score_dtree(params)return -acc# 為可視化做準備 trials = Trials() best = fmin(fn=fn_dtree, space=space_dtree, algo=tpe.suggest, max_evals=1000, trials=trials) print('best:') print(best) best: {'criterion': 0, 'max_depth': 17, 'max_features': 1, 'normalize': 0, 'scale': 0} params = {'criterion': "gini", 'max_depth': 18, 'max_features': 2} clf = DecisionTreeClassifier(**params) print(cross_val_score(clf, X, y).mean()) 0.9669117647058822解析上面的例子
首先最關鍵的還是最小化的目標函數,我們這里構建的目標函數中測試的是sklearn中的分類決策樹,而目標函數返回的結果則是交叉驗證的平均損失。
然后是參數空間的問題,如之前所寫,這里只需要定義一個space然后穿入fmin即可。對于字符串的超參數,我們這里使用hp.choice解決該問題,同時要注意的是:假如構建參數空間使用’hp.choice’函數的,那么在模型中返回的將是傳入列表或者數組的index(索引),而且是從0開始的,在測試最終結果時候一定要注意這一點。為了檢測標準化和歸一化時候有用,我們這里額外添加了兩個參數"scale"和"normalize",而其對與分類決策樹的影響則定義于目標函數部分。
但是hyperopt進行的參數選擇到底是怎么樣影響我們的模型的呢?
可視化
目前看,hyperopt對于我們已經是個黑箱。但是我們也可以通過傳入Trials來獲取搜索過程中的結果。而通過可視化該結果,我們也可以對參數和模型的關系有更好地了解。
parameters = ['max_depth', 'max_features', 'criterion', 'scale','normalize'] # decision tree 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]ys = np.array(ys)axes[i].scatter(xs,ys,s=20,linewidth=0.01,alpha=0.5,c=cmap(float(i) / len(parameters)))axes[i].set_title(val)通過上面的例子我們可以看出scale和normalize在這里基本沒有什么影響。而其他參數的影響也可以比較好的看出。這在進行進一步的搜索的時候,將會非常有用。因為我們可以通過圖像獲得一個更小的參數搜索范圍。然后同樣的事情,我們再做一次,模型替代為SVM。
def hyperopt_model_score_svm(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()space_svm = {'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_svm(params):acc = hyperopt_model_score_svm(params)return -acctrials = Trials() best = fmin(f_svm, space_svm, algo=tpe.suggest, max_evals=1000, trials=trials) print('best:') print(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]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]) best: {'C': 3.383042170158757, 'gamma': 2.4082659264264454, 'kernel': 3, 'normalize': 1, 'scale': 0} params = {'C': 3.383042170158757, 'gamma': 2.4082659264264454, 'kernel': "poly"} clf = SVC(**params) print(cross_val_score(clf, normalize(X), y).mean()) 0.9643956205831742將一切融合為一體
將一切融合為一體,也就是說將模型本身也作為超參數進行優化,返回的結果將是最好的那個模型以及其最佳參數組合。但是這樣做存在一個問題。例子如下。
digits = datasets.load_digits() X = digits.data y = digits.target print(X.shape, y.shape)from sklearn.svm import SVC from sklearn.naive_bayes import BernoulliNB from sklearn.neighbors import KNeighborsClassifierdef 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 0return 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': 'dtree','max_depth': hp.choice('max_depth', range(1, 20)),'max_features': hp.choice('max_features', range(1, 5)),'criterion': hp.choice('criterion', ["gini", "entropy"]),},{'type': 'knn','n_neighbors': hp.choice('knn_n_neighbors', range(1,50))} ])count = 0 best = 0 def f(params):global best, countcount += 1acc = hyperopt_train_test(params.copy())if acc > best:print('new best:', acc, 'using', params['type'])print('iters:', count, ', acc:', acc, 'using', params)best = accif count % 50 == 0:print('iters:', count, ', acc:', acc, 'using', params)return -acctrials = Trials() best = fmin(f, space, algo=tpe.suggest, max_evals=1500, trials=trials) print('best:') print(best) (1797, 64) (1797,) new best: 0.9437997276286755 using svm iters: 1 , acc: 0.9437997276286755 using {'C': 9.103691443113167, 'gamma': 16.473379775097474, 'kernel': 'linear', 'type': 'svm'} new best: 0.9438221642590143 using knn iters: 4 , acc: 0.9438221642590143 using {'n_neighbors': 21, 'type': 'knn'} new best: 0.9644039680147021 using knn iters: 14 , acc: 0.9644039680147021 using {'n_neighbors': 4, 'type': 'knn'} iters: 50 , acc: 0.4696816789034243 using {'criterion': 'gini', 'max_depth': 6, 'max_features': 1, 'type': 'dtree'} new best: 0.968293886616605 using knn iters: 92 , acc: 0.968293886616605 using {'n_neighbors': 3, 'type': 'knn'} iters: 100 , acc: 0.968293886616605 using {'n_neighbors': 3, 'type': 'knn'} iters: 150 , acc: 0.928771856885554 using {'n_neighbors': 39, 'type': 'knn'} iters: 200 , acc: 0.6162381887566258 using {'criterion': 'entropy', 'max_depth': 12, 'max_features': 1, 'type': 'dtree'} iters: 250 , acc: 0.8241514006071918 using {'alpha': 1.1302495129317371, 'type': 'naive_bayes'} iters: 300 , acc: 0.9493786750179943 using {'n_neighbors': 14, 'type': 'knn'} iters: 350 , acc: 0.9560677594549536 using {'n_neighbors': 9, 'type': 'knn'} iters: 400 , acc: 0.5237318439952089 using {'criterion': 'gini', 'max_depth': 6, 'max_features': 2, 'type': 'dtree'} iters: 450 , acc: 0.968293886616605 using {'n_neighbors': 3, 'type': 'knn'} iters: 500 , acc: 0.9410369480336125 using {'n_neighbors': 23, 'type': 'knn'} iters: 550 , acc: 0.9404776918351127 using {'n_neighbors': 25, 'type': 'knn'} iters: 600 , acc: 0.9265458689408209 using {'n_neighbors': 42, 'type': 'knn'} iters: 650 , acc: 0.9215402390309818 using {'n_neighbors': 45, 'type': 'knn'} iters: 700 , acc: 0.9437997276286755 using {'C': 8.307346790972709, 'gamma': 14.31613674780396, 'kernel': 'linear', 'type': 'svm'} iters: 750 , acc: 0.9387859177942958 using {'n_neighbors': 27, 'type': 'knn'} iters: 800 , acc: 0.9215402390309818 using {'n_neighbors': 45, 'type': 'knn'} iters: 850 , acc: 0.9437997276286755 using {'C': 0.803748055109546, 'gamma': 13.091257036777954, 'kernel': 'linear', 'type': 'svm'} iters: 900 , acc: 0.927105153057152 using {'n_neighbors': 41, 'type': 'knn'} iters: 950 , acc: 0.951042605675164 using {'n_neighbors': 13, 'type': 'knn'} iters: 1000 , acc: 0.9371108386165347 using {'n_neighbors': 29, 'type': 'knn'} iters: 1050 , acc: 0.9465990330528881 using {'n_neighbors': 19, 'type': 'knn'} iters: 1100 , acc: 0.1012788128613552 using {'C': 2.811062086826674, 'gamma': 15.682420324900988, 'kernel': 'rbf', 'type': 'svm'} iters: 1150 , acc: 0.968293886616605 using {'n_neighbors': 3, 'type': 'knn'} iters: 1200 , acc: 0.8202448429778956 using {'alpha': 1.945569326498969, 'type': 'naive_bayes'} iters: 1250 , acc: 0.9332042809872384 using {'n_neighbors': 33, 'type': 'knn'} iters: 1300 , acc: 0.968293886616605 using {'n_neighbors': 3, 'type': 'knn'} iters: 1350 , acc: 0.9438138168274864 using {'n_neighbors': 22, 'type': 'knn'} iters: 1400 , acc: 0.9437997276286755 using {'C': 9.27573146818064, 'gamma': 17.45431916873462, 'kernel': 'linear', 'type': 'svm'} iters: 1450 , acc: 0.42834011383005555 using {'criterion': 'entropy', 'max_depth': 5, 'max_features': 1, 'type': 'dtree'} iters: 1500 , acc: 0.10183809697768631 using {'C': 1.2560544954464077, 'gamma': 9.055318830818816, 'kernel': 'rbf', 'type': 'svm'} best: {'classifier_type': 3, 'knn_n_neighbors': 2}在這次測試中,我們明顯的看到了一個問題。因為我們給出的不同模型的參數空間很寬泛,所以也就很可能會出現單個模型的初始的隨機搜索效果會很差。而因為Hyperopt自身的運行機制,其之后的注意力很有可能更多的放在第一個獲得較好優化結果的模型上,而這樣很有可能會錯過真正的最佳模型。那么有什么辦法可以解決這個問題嗎?
params = {'n_neighbors': 3} clf = KNeighborsClassifier(**params) print("best score",cross_val_score(clf, X, y).mean()) best score 0.968293886616605選擇搜索算法
之前我們選擇的優化參數都是tpe。這是一種帶有啟發性的搜索算法。類似于遺傳算法,粒子群算法。有時候算法可能在一開始就陷入局部最優的區域之中。一種比較好的解決方案就是選擇其他搜索函數-隨機搜索。但是也正是因為隨機搜索缺乏啟發性,所以隨機搜索想要獲得較好的結果也往往需要更多的搜索次數,因此上搜索算法的具體使用,請按照實際情況選擇。
隨機搜索:algo=hyperopt.random.suggest
def fn_dtree(params):acc = hyperopt_model_score_dtree(params)return -acc# 為可視化做準備 trials = Trials() from hyperopt import rand best = fmin(fn=fn_dtree, space=space_dtree, algo=rand.suggest, max_evals=1000, trials=trials) print('best:') print(best) best: {'criterion': 1, 'max_depth': 8, 'max_features': 1, 'normalize': 1, 'scale': 0} params = {'criterion': "entropy", 'max_depth': 9, 'max_features': 2} clf = DecisionTreeClassifier(**params) print(cross_val_score(clf, normalize(X), y).mean()) 0.9268790849673203更多的內容請參考:
編寫本篇教程中,參考了該文章,對于原文章和翻譯,我十分感謝。(但是除了感謝壞話也還是要說的,這篇文章代碼問題很多,不完整,錯誤很多,部分細節也不是很好)
其他教程:
總結
以上是生活随笔為你收集整理的Hyperopt 入门指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Hive 快速上手
- 下一篇: 虚拟机 centos 6.5 扩展根目录