跳转至

超参数优化

有了优秀的模型,就有了优化超参数以获得最佳得分模型的难题。那么,什么是超参数优化呢?假设您的机器学习项目有一个简单的流程。有一个数据集,你直接应用一个模型,然后得到结果。模型在这里的参数被称为超参数,即控制模型训练/拟合过程的参数。如果我们用 SGD 训练线性回归,模型的参数是斜率和偏差,超参数是学习率。你会发现我在本章和本书中交替使用这些术语。假设模型中有三个参数 a、b、c,所有这些参数都可以是 1 到 10 之间的整数。这些参数的 "正确 "组合将为您提供最佳结果。因此,这就有点像一个装有三拨密码锁的手提箱。不过,三拨密码锁只有一个正确答案。而模型有很多正确答案。那么,如何找到最佳参数呢?一种方法是对所有组合进行评估,看哪种组合能提高指标。让我们看看如何做到这一点。

# 初始化最佳准确度
best_accuracy = 0
# 初始化最佳参数的字典
best_parameters = {"a": 0, "b": 0, "c": 0} 
# 循环遍历 a 的取值范围 1~10
for a in range(1, 11):
    # 循环遍历 b 的取值范围 1~10
    for b in range(1, 11):
        # 循环遍历 c 的取值范围 1~10
        for c in range(1, 11):
            # 创建模型,使用 a、b、c 参数
            model = MODEL(a, b, c)
            # 使用训练数据拟合模型
            model.fit(training_data)
            # 使用模型对验证数据进行预测
            preds = model.predict(validation_data)
            # 计算预测的准确度
            accuracy = metrics.accuracy_score(targets, preds)
             # 如果当前准确度优于之前的最佳准确度,则更新最佳准确度和最佳参数
            if accuracy > best_accuracy:
                best_accuracy = accuracy 
                best_parameters["a"] = a
                best_parameters["b"] = b 
                best_parameters["c"] = c

在上述代码中,我们从 1 到 10 对所有参数进行了拟合。因此,我们总共要对模型进行 1000 次(10 x 10 x 10)拟合。这可能会很昂贵,因为模型的训练需要很长时间。不过,在这种情况下应该没问题,但在现实世界中,并不是只有三个参数,每个参数也不是只有十个值。 大多数模型参数都是实数,不同参数的组合可以是无限的。

让我们看看 scikit-learn 的随机森林模型。

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,
)

有 19 个参数,而所有这些参数的所有组合,以及它们可以承担的所有值,都将是无穷无尽的。通常情况下,我们没有足够的资源和时间来做这件事。因此,我们指定了一个参数网格。在这个网格上寻找最佳参数组合的搜索称为网格搜索。我们可以说,n_estimators 可以是 100、200、250、300、400、500;max_depth 可以是 1、2、5、7、11、15;criterion 可以是 gini 或 entropy。这些参数看起来并不多,但如果数据集过大,计算起来会耗费大量时间。我们可以像之前一样创建三个 for 循环,并在验证集上计算得分,这样就能实现网格搜索。还必须注意的是,如果要进行 k 折交叉验证,则需要更多的循环,这意味着需要更多的时间来找到完美的参数。因此,网格搜索并不流行。让我们以根据手机配置预测手机价格范围数据集为例,看看它是如何实现的。

图 1:手机配置预测手机价格范围数据集展示

训练集中只有 2000 个样本。我们可以轻松地使用分层 kfold 和准确率作为评估指标。我们将使用具有上述参数范围的随机森林模型,并在下面的示例中了解如何进行网格搜索。

# rf_grid_search.py 
import numpy as np 
import pandas as pd
from sklearn import ensemble
from sklearn import metrics
from sklearn import model_selection
if __name__ == "__main__": 
    # 读取数据
    df = pd.read_csv("../input/mobile_train.csv")
    # 删除 price_range 列
    X = df.drop("price_range", axis=1).values
    # 取目标变量 y("price_range"列)
    y = df.price_range.values
    # 创建随机森林分类器,使用所有可用的 CPU 核心进行训练
    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"] 
    }
    # 创建 GridSearchCV 对象 model,用于在参数网格上进行网格搜索
    model = model_selection.GridSearchCV( 
        estimator=classifier,
        param_grid=param_grid, 
        scoring="accuracy", 
        verbose=10,
        n_jobs=1, 
        cv=5
    )
    # 使用网格搜索对象 model 拟合数据,寻找最佳参数组合
    model.fit(X, y)
    # 打印出最佳模型的最佳准确度分数
    print(f"Best score: {model.best_score_}") 
    # 打印最佳参数集合
    print("Best parameters set:")
    best_parameters = model.best_estimator_.get_params() 
    for param_name in sorted(param_grid.keys()):
        print(f"\t{param_name}: {best_parameters[param_name]}")

这里打印了很多内容,让我们看看最后几行。

[CV]  criterion=entropy, max_depth=15, n_estimators=500, score=0.895, 
total=  1.0s
[CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 
[CV]  criterion=entropy, max_depth=15, n_estimators=500, score=0.890, 
total=  1.1s
[CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 
[CV]  criterion=entropy, max_depth=15, n_estimators=500, score=0.910, 
total=  1.1s
[CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 
[CV]  criterion=entropy, max_depth=15, n_estimators=500, score=0.880, 
total=  1.1s
[CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 
[CV]  criterion=entropy, max_depth=15, n_estimators=500, score=0.870, 
total=  1.1s
[Parallel(n_jobs=1)]: Done 360 out of 360 | elapsed: 3.7min finished 
Best score: 0.889
Best parameters set: 
    criterion: 'entropy' 
    max_depth: 15
    n_estimators: 500

最后,我们可以看到,5折交叉检验最佳得分是 0.889,我们的网格搜索得到了最佳参数。我们可以使用的下一个最佳方法是随机搜索。在随机搜索中,我们随机选择一个参数组合,然后计算交叉验证得分。这里消耗的时间比网格搜索少,因为我们不对所有不同的参数组合进行评估。我们选择要对模型进行多少次评估,这就决定了搜索所需的时间。代码与上面的差别不大。除 GridSearchCV 外,我们使用 RandomizedSearchCV。

if __name__ == "__main__":
    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 拟合数据,寻找最佳参数组合
    model.fit(X, y)
    print(f"Best score: {model.best_score_}") 
    print("Best parameters set:")
    best_parameters = model.best_estimator_.get_params() 
    for param_name in sorted(param_grid.keys()):
        print(f"\t{param_name}: {best_parameters[param_name]}")

我们更改了随机搜索的参数网格,结果似乎有了些许改进。

Best score: 0.8905
Best parameters set:
    criterion: entropy 
    max_depth: 25
    n_estimators: 300

如果迭代次数较少,随机搜索比网格搜索更快。使用这两种方法,你可以为各种模型找到最优参数,只要它们有拟合和预测功能,这也是 scikit-learn 的标准。有时,你可能想使用管道。例如,假设我们正在处理一个多类分类问题。在这个问题中,训练数据由两列文本组成,你需要建立一个模型来预测类别。让我们假设你选择的管道是首先以半监督的方式应用 tf-idf,然后使用 SVD 和 SVM 分类器。现在的问题是,我们必须选择 SVD 的成分,还需要调整 SVM 的参数。下面的代码段展示了如何做到这一点。

import numpy as np 
import pandas as pd
from sklearn import metrics
from sklearn import model_selection
from sklearn import pipeline
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
# 计算加权二次 Kappa 分数
def quadratic_weighted_kappa(y_true, y_pred): 
    return metrics.cohen_kappa_score( 
        y_true,
        y_pred,
        weights="quadratic" 
    )

if __name__ == '__main__':
    # 读取训练集
    train = pd.read_csv('../input/train.csv')
    # 从测试数据中提取 id 列的值,并将其转换为整数类型,存储在变量 idx 中
    idx = test.id.values.astype(int) 

    # 从训练数据中删除 'id' 列
    train = train.drop('id', axis=1) 
    # 从测试数据中删除 'id' 列
    test = test.drop('id', axis=1)
    # 从训练数据中提取目标变量 'relevance' ,存储在变量 y 中
    y = train.relevance.values

    # 将训练数据中的文本特征 'text1' 和 'text2' 合并成一个新的特征列,并存储在列表 traindata 中
    traindata = list(train.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1))
    # 将测试数据中的文本特征 'text1' 和 'text2' 合并成一个新的特征列,并存储在列表 testdata 中
    testdata = list(test.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1))

    # 创建一个 TfidfVectorizer 对象 tfv,用于将文本数据转换为 TF-IDF 特征
    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'
    )

    # 使用训练数据拟合 TfidfVectorizer,将文本特征转换为 TF-IDF 特征
    tfv.fit(traindata)
    # 将训练数据中的文本特征转换为 TF-IDF 特征矩阵 X
    X =  tfv.transform(traindata) 
    # 将测试数据中的文本特征转换为 TF-IDF 特征矩阵 X_test
    X_test = tfv.transform(testdata)
    # 创建 TruncatedSVD 对象 svd,用于进行奇异值分解
    svd = TruncatedSVD()
    # 创建 StandardScaler 对象 scl,用于进行特征缩放
    scl = StandardScaler()
    # 创建支持向量机分类器对象 svm_model
    svm_model = SVC()

    # 创建机器学习管道 clf,包含奇异值分解、特征缩放和支持向量机分类器
    clf = pipeline.Pipeline(
        [
            ('svd', svd), 
            ('scl', scl), 
            ('svm', svm_model)
        ] 
    )

    # 定义要进行网格搜索的参数网格 param_grid
    param_grid = {
        'svd__n_components' : [200, 300], 
        'svm__C': [10, 12]
    }

    # 创建自定义的评分函数 kappa_scorer,用于评估模型性能
    kappa_scorer = metrics.make_scorer( 
        quadratic_weighted_kappa, 
        greater_is_better=True
    )

    # 创建 GridSearchCV 对象 model,用于在参数网格上进行网格搜索,寻找最佳参数组合
    model = model_selection.GridSearchCV( 
        estimator=clf,
        param_grid=param_grid, 
        scoring=kappa_scorer, 
        verbose=10,
        n_jobs=-1, 
        refit=True, 
        cv=5
    )

     # 使用 GridSearchCV 对象 model 拟合数据,寻找最佳参数组合
    model.fit(X, y)
    # 打印出最佳模型的最佳准确度分数
    print("Best score: %0.3f" % model.best_score_)
    # 打印最佳参数集合
    print("Best parameters set:")
    best_parameters = model.best_estimator_.get_params() 
    for param_name in sorted(param_grid.keys()):
        print("\t%s: %r" % (param_name, best_parameters[param_name])) 

    # 获取最佳模型
    best_model = model.best_estimator_
    best_model.fit(X, y)
    # 使用最佳模型进行预测
    preds = best_model.predict(...)

这里显示的管道包括 SVD(奇异值分解)、标准缩放和 SVM(支持向量机)模型。请注意,由于没有训练数据,您无法按原样运行上述代码。当我们进入高级超参数优化技术时,我们可以使用不同类型的最小化算法来研究函数的最小化。这可以通过使用多种最小化函数来实现,如下坡单纯形算法、内尔德-梅德优化算法、使用贝叶斯技术和高斯过程寻找最优参数或使用遗传算法。我将在 "集合与堆叠(ensembling and stacking) "一章中详细介绍下坡单纯形算法和 Nelder-Mead 算法的应用。首先,让我们看看高斯过程如何用于超参数优化。这类算法需要一个可以优化的函数。大多数情况下,都是最小化这个函数,就像我们最小化损失一样。

因此,比方说,你想找到最佳参数以获得最佳准确度,显然,准确度越高越好。现在,我们不能最小化精确度,但我们可以将精确度乘以-1。这样,我们是在最小化精确度的负值,但事实上,我们是在最大化精确度。 在高斯过程中使用贝叶斯优化,可以使用 scikit-optimize (skopt) 库中的 gp_minimize 函数。让我们看看如何使用该函数调整随机森林模型的参数。

import numpy as np 
import pandas as pd
from functools import partial
from sklearn import ensemble
from sklearn import metrics
from sklearn import model_selection
from skopt import gp_minimize
from skopt import space
def optimize(params, param_names, x, y): 
    # 将参数名称和对应的值打包成字典
    params = dict(zip(param_names, params))
    # 创建随机森林分类器模型,使用传入的参数配置
    model = ensemble.RandomForestClassifier(**params)
    # 创建 StratifiedKFold 交叉验证对象,将数据分为 5 折
    kf = model_selection.StratifiedKFold(n_splits=5)

    # 初始化用于存储每个折叠的准确度的列表
    accuracies = []

     # 循环遍历每个折叠的训练和测试数据
    for idx in kf.split(X=x, y=y):
        train_idx, test_idx = idx[0], idx[1] 

        xtrain = x[train_idx]
        ytrain = y[train_idx] 

        xtest = x[test_idx]
        ytest = y[test_idx]

        # 在训练数据上拟合模型
        model.fit(xtrain, ytrain)

        # 使用模型对测试数据进行预测
        preds = model.predict(xtest)

        # 计算折叠的准确度
        fold_accuracy = metrics.accuracy_score(ytest, preds)
        accuracies.append(fold_accuracy)

    # 返回平均准确度的负数(因为 skopt 使用负数来最小化目标函数)
    return -1 * np.mean(accuracies)

if __name__ == "__main__":
    # 读取数据
    df = pd.read_csv("../input/mobile_train.csv") 
    # 取特征矩阵 X(去掉"price_range"列)
    X = df.drop("price_range", axis=1).values 
    # 目标变量 y("price_range"列)
    y = df.price_range.values

    # 定义超参数搜索空间 param_space
    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
    param_names = [ 
        "max_depth", 
        "n_estimators", 
        "criterion", 
        "max_features"
    ]

    # 创建函数 optimization_function,用于传递给 gp_minimize
    optimization_function = partial(
        optimize,
        param_names=param_names, 
        x=X,
        y=y 
    )

    # 使用 Bayesian Optimization(基于贝叶斯优化)来搜索最佳超参数
    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)

这同样会产生大量输出,最后一部分如下所示。

Iteration No: 14 started. Searching for the next optimal point. 
Iteration No: 14 ended. Search finished for the next optimal point. 
Time taken: 4.7793
Function value obtained: -0.9075 
Current minimum: -0.9075
Iteration No: 15 started. Searching for the next optimal point. 
Iteration No: 15 ended. Search finished for the next optimal point. 
Time taken: 49.4186
Function value obtained: -0.9075 
Current minimum: -0.9075
{'max_depth': 12, 'n_estimators': 100, 'criterion': 'entropy', 
'max_features': 1.0}

看来我们已经成功突破了 0.90 的准确率。这真是太神奇了! 我们还可以通过以下代码段查看(绘制)我们是如何实现收敛的。

from skopt.plots import plot_convergence
plot_convergence(result)

收敛图如图 2 所示。

图 2:随机森林参数优化的收敛图

Scikit- optimize 就是这样一个库。 hyperopt 使用树状结构贝叶斯估计器(TPE)来找到最优参数。请看下面的代码片段,我在使用 hyperopt 时对之前的代码做了最小的改动。

import numpy as np 
import pandas as pd
from functools import partial
from sklearn import ensemble
from sklearn import metrics
from sklearn import 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)
    ...
    return -1 * np.mean(accuracies)

if __name__ == "__main__": 
    df = pd.read_csv("../input/mobile_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) 
    }

    # 包装函数
    optimization_function = partial( 
        optimize,
        x=X, 
        y=y
    )

    # 开始训练
    trials = Trials()

    # 最小化目标值
    hopt = fmin(
        fn=optimization_function, 
        space=param_space,
        algo=tpe.suggest, 
        max_evals=15, 
        trials=trials
    )
    #打印最佳参数
    print(hopt)

正如你所看到的,这与之前的代码并无太大区别。你必须以不同的格式定义参数空间,还需要改变实际优化部分,用 hyperopt 代替 gp_minimize。结果相当不错!

 python rf_hyperopt.py
100%|██████████████████| 15/15 [04:38<00:00, 18.57s/trial, best loss: - 
0.9095000000000001]
{'criterion': 1, 'max_depth': 11.0, 'max_features': 0.821163568049807, 
'n_estimators': 806.0}

我们得到了比以前更好的准确度和一组可以使用的参数。请注意,最终结果中的标准是 1。这意味着选择了 1,即熵。 上述调整超参数的方法是最常见的,几乎适用于所有模型:线性回归、逻辑回归、基于树的方法、梯度提升模型(如 xgboost、lightgbm),甚至神经网络!

虽然这些方法已经存在,但学习时必须从手动调整超参数开始,即手工调整。手动调整可以帮助你学习基础知识,例如,在梯度提升中,当你增加深度时,你应该降低学习率。如果使用自动工具,就无法学习到这一点。请参考下表,了解应如何调整。RS* 表示随机搜索应该更好。

一旦你能更好地手动调整参数,你甚至可能不需要任何自动超参数调整。创建大型模型或引入大量特征时,也容易造成训练数据的过度拟合。为避免过度拟合,需要在训练数据特征中引入噪声或对代价函数进行惩罚。这种惩罚称为正则化,有助于泛化模型。在线性模型中,最常见的正则化类型是 L1 和 L2。L1 也称为 Lasso 回归,L2 称为 Ridge 回归。说到神经网络,我们会使用dropout、添加增强、噪声等方法对模型进行正则化。利用超参数优化,还可以找到正确的惩罚方法。

Model Optimize Range of values
Linear Regression - fit_intercept
- normalize
- True/False
- True/False
Ridge - alpha
- fit_intercept
- normalize
- 0.01, 0.1, 1.0, 10, 100
- True/False
- True/False
k-neighbors - n_neighbors
- p
- 2, 4, 8, 16, ...
- 2, 3, ...
SVM - C
- gamma
- class_weight
- 0.001, 0.01, ...,10, 100, 1000
- 'auto', RS*
- 'balanced', None
Logistic Regression - Penalyt
- C
- L1 or L2
- 0.001, 0.01, ..., 10, ..., 100
Lasso - Alpha
- Normalize
- 0.1, 1.0, 10
- True/False
Random Forest - n_estimators
- max_depth
- min_samples_split
- min_samples_leaf
- max features
- 120, 300, 500, 800, 1200
- 5, 8, 15, 25, 30, None
- 1, 2, 5, 10, 15, 100
- log2, sqrt, None
XGBoost - eta
- gamma
- max_depth
- min_child_weight
- subsample
- colsample_bytree
- lambda
- alpha
- 0.01, 0.015, 0.025, 0.05, 0.1
- 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0
- 3, 5, 7, 9, 12, 15, 17, 25
- 1, 3, 5, 7
- 0.6, 0.7, 0.8, 0.9, 1.0
- 0.6, 0.7, 0.8, 0.9, 1.0
- 0.01, 0.1, 1.0, RS
- 0, 0.1, 0.5, 1.0, RS
Back to top