模型越大,优化超参数越困难。
什么是超参数优化?
假设存在一个机器学习项目的处理流程:拿到数据集,直接输入给模型,得到输出。这个流程里控制的参数被称为模型超参数,即用于控制模型的训练、使用过程的参数。
如果我们训练一个 SGD 线性回归模型,模型的参数是系数和偏置,模型的超参数是学习率。
假设模型有超参数 a、b、c,都是 0-10 之间的整数。一个正确的参数组合会产生最好的模型效果。这相识某种三位密码锁,然而三位密码锁只有一个正解,模型的超参数组合可以有多个最优解。
那么如何寻找最优的超参数组合?
一种简单的思路是,对所有可能的组合,计算该组合下模型的性能指标。
这种方法非常耗时,实际问题中该方法不可行,因为超参数经常是实数,组合是无穷多的。
让我们看一下随机森林模型的定义。
RandomForestClassifier(
n_estimators=100,
criterion='gini',
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
max_features='auto',
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
bootstrap=True,
oob_score=False,
n_jobs=None,
random_state=None,
verbose=0,
warm_start=False,
class_weight=None,
ccp_alpha=0.0,
max_samples=None,
)
其中有 9 个参数,所有参数的组合是无限多,我们不可能枚举各组合。
因此,我们设置参数网格,在网格上搜索模型参数的组合,该方法被称为 grid search。
假设 n_estimators 可以取 [100, 200, 250, 300, 400, 500] 。max_depth 可以取 [1, 2, 5, 7, 11] 。criterion 可以取 ['gini', 'entropy'],这样似乎超参数组合不太多了,但仍需要花费大量时间进行计算。
我们可以在上述条件下进行 grid search,统计验证集上模型的得分。当我们做了 k-折交叉验证时,我们在计算中引入了新的循环,需要更多的计算时间。
因此,Grid Search 并不流行。
下边通过预测手机价格范围这个例子来演示 Grid Search。
数据下载地址
代码如下。
import numpy as np
import pandas as pd
from sklearn import ensemble, metrics, model_selection
df = pd.read_csv('mobile-phone-price/train.csv')
x = df.drop('price_range', axis=1).values
y = df.price_range.values
classifier = ensemble.RandomForestClassifier(n_jobs=-1)
param_grid = {
'n_estimators': [100, 200, 250, 300, 400, 500],
'max_depth': [1, 2, 5, 7, 11, 15],
'criterion': ['gini', 'entropy']
}
model = model_selection.GridSearchCV(
estimator=classifier,
param_grid=param_grid,
scoring='accuracy',
verbose=10,
n_jobs=1,
cv=5
)
model.fit(x, y)
print(f'Best Score: {model.best_score_}')
print('Best Param Set:')
best_params = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print(f'{param_name}: {best_params[param_name]}')
计算后,我们得到了该 Grid 下的最优参数。
下一个模型选择方法是随机选择 Random Search。
随机挑选超参数组合并计算交叉验证得分,由于不枚举所有组合,因此耗时小于 Grid Search。
在使用时通常需要人为指定计算多少次组合,指定的计算次数决定耗时长短。
代码如下。
import numpy as np
import pandas as pd
from sklearn import ensemble, metrics, model_selection
df = pd.read_csv('mobile-phone-price/train.csv')
x = df.drop('price_range', axis=1).values
y = df.price_range.values
classifier = ensemble.RandomForestClassifier(n_jobs=-1)
param_grid = {
'n_estimators': np.arange(100, 1500, 100),
'max_depth': np.arange(1, 31),
'criterion': ['gini', 'entropy']
}
model = model_selection.RandomizedSearchCV(
estimator=classifier,
param_distributions=param_grid,
n_iter=20,
scoring='accuracy',
verbose=10,
n_jobs=1,
cv=5
)
model.fit(x, y)
print(f'Best Score: {model.best_score_}')
print('Best Param Set:')
best_params = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print(f'{param_name}: {best_params[param_name]}')
与 Grid Search 相比,Random Search 对超参数的搜索范围更广,而且迭代次数更少,得到的参数效果更好。
通过使用这两种超参数搜索方法,可以对任何模型搜索最优(?)的超参数。
有时你需要使用流水线处理方法,例如,我们要处理一个多分类任务,其中训练数据由两列文本构成,现在我们要构建模型预测目标类型。
假设我们的处理流水线是这样的,首先对文本做 tf-idf ,接着使用带有 SVM 分类器的 SVD 分解方法,产生多个特征。
现在问题是,我们如何同时选择 SVM 的超参数和 SVD 的超参数。
实现代码如下(该代码没有提供数据来源,只能看看逻辑)。
import numpy as np
import pandas as pd
from sklearn import metrics, model_selection, pipeline
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def quadratic_weighted_kappa(y_true, y_pred):
return metrics.cohen_kappa_score(y_true, y_pred, weights='quadratic')
train = pd.read_csv('../input/train.csv')
test = pd.read_csv('../input/test.csv')
idx = test.id.values.astype(int)
train = train.drop('id', axis=1)
test = test.drop('id', axis=1)
y = train.relevance.values
traindata = list(
train.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1)
)
testdata = list(
test.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1)
)
tfv = TfidfVectorizer(
min_df=3,
max_features=None,
strip_accents='unicode',
analyzer='word',
token_pattern=r'\w{1,}',
ngram_range=(1, 3),
use_idf=1,
smooth_idf=1,
sublinear_tf=1,
stop_words='english'
)
tfv.fit(traindata)
X = tfv.transform(traindata)
X_test = tfv.transform(testdata)
svd = TruncatedSVD()
scl = StandardScaler()
svm_model = SVC()
clf = pipeline.Pipeline([
('svd', svd),
('scl', scl),
('svm', svm_model)
])
param_grid = {
'svd__n_components' : [200, 300],
'svm__C': [10, 12]
}
kappa_scorer = metrics.make_scorer(
quadratic_weighted_kappa,
greater_is_better=True
)
model = model_selection.GridSearchCV(
estimator=clf,
param_grid=param_grid,
scoring=kappa_scorer,
verbose=10,
n_jobs=-1,
refit=True,
cv=5
)
model.fit(x, y)
print(f'Best Score: {model.best_score_}')
print('Best Param Set:')
best_params = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
print(f'{param_name}: {best_params[param_name]}')
接下来介绍几个高级超参数优化技术,首先看一下最小功能方法 minimization of functions,该方法使用不同种最小化算法,如 downhill simplex algorithm、Nelder-Mead optimization、Bayesian with Gaussian process, 来寻找最优超参数。
下边看一下 Gaussian Process 时如何用在超参数优化上的。这类方法需要一个优化的目标函数,因此,很像最小化误差的处理方式 minimize loss。
假设我们的目标函数是 accuracy,accuracy 越高越好,这样我们无法直接对 accuracy 做最小化算法,但可以将其乘以 -1,使其值越小越好。
Scikit-learn 提供的 gp_minimize 方法执行的是 Bayesian optimization with gaussian process。
import numpy as np
import pandas as pd
from sklearn import metrics, ensemble, model_selection
from functools import partial
from skopt import gp_minimize, space
np.int = np.int64
def optimize(params, param_names, x, y):
params = dict(zip(param_names, params))
model = ensemble.RandomForestClassifier(**params)
kf = model_selection.StratifiedKFold(n_splits=5)
accuracies = []
for train_idx, test_idx in kf.split(X=x, y=y):
xtrain = x[train_idx]
ytrain = y[train_idx]
xtest = x[test_idx]
ytest = y[test_idx]
model.fit(xtrain, ytrain)
preds = model.predict(xtest)
accuracies.append(
metrics.accuracy_score(ytest, preds)
)
return np.mean(accuracies) * -1
df = pd.read_csv('mobile-phone-price/train.csv')
x = df.drop("price_range", axis=1).values
y = df.price_range.values
param_space = [
space.Integer(3, 15, name="max_depth"),
space.Integer(100, 1500, name="n_estimators"),
space.Categorical(["gini", "entropy"], name="criterion"),
space.Real(0.01, 1, prior="uniform", name="max_features")
]
param_names = [
"max_depth",
"n_estimators",
"criterion",
"max_features"
]
# 包装 optimize 函数,固定后三项参数,有科理化内味
optimization_function = partial(
optimize,
param_names=param_names,
x=x,
y=y
)
result = gp_minimize(
optimization_function,
dimensions=param_space,
n_calls=15,
n_random_starts=10,
verbose=10
)
best_params = dict(zip(param_names, result.x))
print(best_params)
接下来,我们可以绘制出该计算收敛的过程。
from skopt.plots import plot_convergence
plot_convergence(result)
有许多库可以用来做超参数优化,这里使用的 scikit-optimize 只是其中之一 (注:该库已经不再维护😑)。
另一个好用的库是 hyperopt,该库使用 Tree-structured Parzen Estimator, TPE 方法来寻找最优超参数。
import numpy as np
import pandas as pd
from functools import partial
from sklearn import ensemble, metrics, model_selection
from hyperopt import hp, fmin, tpe, Trials
from hyperopt.pyll.base import scope
def optimize(params, x, y):
model = ensemble.RandomForestClassifier(**params)
kf = model_selection.StratifiedKFold(n_splits=5)
accuracies = []
for train_idx, test_idx in kf.split(X=x, y=y):
xtrain = x[train_idx]
ytrain = y[train_idx]
xtest = x[test_idx]
ytest = y[test_idx]
model.fit(xtrain, ytrain)
preds = model.predict(xtest)
accuracies.append(
metrics.accuracy_score(ytest, preds)
)
return np.mean(accuracies) * -1
df = pd.read_csv('mobile-phone-price/train.csv')
x = df.drop("price_range", axis=1).values
y = df.price_range.values
param_space = {
'max_depth': scope.int(
hp.quniform('max_depth', 1, 15, 1)
),
'n_estimators': scope.int(
hp.quniform('n_estimators', 100, 1500, 1)
),
'criterion': hp.choice('criterion', ['gini', 'entropy']),
'max_features': hp.uniform('max_features', 0, 1)
}
optimition_function = partial(
optimize,
x=x,
y=y
)
trials = Trials()
hopt = fmin(
fn=optimition_function,
space=param_space,
algo=tpe.suggest,
max_evals=15,
trials=trials
)
print(hopt)
TPE 的实现和 GP 的实现差不多,都需要定义参数空间,定义得分函数等。
运行上述代码可以得到调优等准确率和与之对应的一组超参数。
结果中 criterion 的值为 1 ,表示选择了 entropy。
上述调整超参数的方法是最常见的方法,可以用在几乎所有模型上:线性回归、逻辑回归、基于树的模型、梯度增强模型、神经网络。
尽管有上述方法库的存在,初学者应该从手动调整超参数开始学习此概念。例如,在梯度增强模型中,增加或减少 depth 参数,或者调整学习率,以此增强对超参数调优的理解。如果直接使用自动化工具,你肯定掌握其中细节。
推荐使用下表来学习超参数调整,其中 RS* 表示使用 Random Search 方法更好。
一旦你可以通过手动调整超参数实现模型效果提升,你甚至不需要使用自动化超参数调整工具,你会熟悉哪些参数对哪些模型有效。
当你构建了一个复杂模型,其中使用了大量的特征时,你的模型可能对数据训练时的过拟合敏感。为了防止过拟合发生,你需要往训练数据里添加噪声数据,或者惩罚损失函数,该惩罚手段被称为正则化,能够帮助模型提升泛化性能。
在线性模型中,经常使用的正则化方式是 L1 和 L2,L1 被称为 Lasso Regression,L2 被称为 Ridge Regression。
在神经网络中,使用 dropout 来添加模型噪声。
| Model | Optimize | RangeOfValues |
|---|---|---|
| Linear Regression | - fit_intercept - normalize | - T/F - T/F |
| Ridge | - alpha - fit_intercept - normalize | - 0.01,0.1,1,10,100 - T/F - T/F |
| KNN | - n_neighbors - p | - 2,4,8,16,... -2,3 |
| SVM | - C - gamma - class_weight | - 0.001,0.01,..,1000 - 'auto', RS* - 'balanced', None |
| Logistic Regression | - Penalty - C | - l1/l2 - 0.001,0.01,...,100 |
| Lasso | - alpha - normalize | - 0.1,1,10 - T/F |
| Random Forest | - n_estimitors - max_depth - max_fs - min_x_split - min_x_leaf | - 120,300,..,1200 - 5,8,15,25,30 - log2,sqrt,None - 1,2,5,10,15,100 - 1,2,5,10 |
| XGBoost | - eta - gamma - max_depth - min_chd_w - subx - colx - lambda - alpha | - 0.01,0.015.. - 0.05,...,1 - 3,..,25 - 1,3,5,7 - 0.6,..,1 - 0.6,..,1 - 0.01,..,1,RS* - 0,..,1,RS* |