本篇文章是随机森林气温预测任务的最后一篇文章啦,本文我们的主要内容就是 调参。前面在介绍随机森林算法的时候,我们知道在建立树模型的时候,我们通常会使用预剪枝策略,边建立决策树边限制树的复杂度,以防止树模型出现过拟合的现象。
接下来,我们调参的参数大部分都是和预剪枝策略相关的,比如树的深度、叶子节点的个数、切分的最小样本数、叶子节点最小样本个数等等。话不多说,一起来开始今天的学习吧~
数据集读取、预处理和切分
读取数据集:
import pandas as pd
import os
df = pd.read_csv("data" + os.sep + "temps_extended.csv")
df.head()
数据特征这里就不介绍了,前面的文章我们已经详细地讨论过特征啦~
进行度热编码、数据集切分:
import numpy as np
from sklearn.model_selection import train_test_split
def get_train_test(df):
data = pd.get_dummies(df)
y = data.actual
X = data.drop('actual',axis=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
return X_train, X_test, y_train, y_test
X_train, X_test, y_train, y_test = get_train_test(df)
训练集和测试集样本情况:
>> X_train.shape, X_test.shape
((1643, 17), (548, 17))
在开始调参前,我们需要先看看随机森林 RandomForestRegressor
有哪些参数:
>> from sklearn.ensemble import RandomForestRegressor
>> rfr = RandomForestRegressor(random_state=0)
>> rfr.get_params()
{'bootstrap': True,
'ccp_alpha': 0.0,
'criterion': 'squared_error',
'max_depth': None,
'max_features': 'auto',
'max_leaf_nodes': None,
'max_samples': None,
'min_impurity_decrease': 0.0,
'min_samples_leaf': 1,
'min_samples_split': 2,
'min_weight_fraction_leaf': 0.0,
'n_estimators': 100,
'n_jobs': None,
'oob_score': False,
'random_state': 0,
'verbose': 0,
'warm_start': False}
我们接下来的调参主要选择其中比较重要的几个:
-
max_depth
:树的深度 -
max_features
:选取的特征个数 -
min_samples_leaf
:叶子节点包含的最少样本数 -
min_samples_split
:切分节点时的最少样本数 -
n_estimators
:随机森林中树的个数
下面的调参工作,小鱼也将围绕这 5 个特征展开。
RandomizedSearchCV 随机搜索
在开始任务之前,首先选择一个大致的合适区间作为参数空间:
n_estimators = [int(x) for x in np.linspace(50, 1000, 5)]
max_depth = [int(x) for x in np.linspace(10, 30, 5)]
max_features = [6, 8, 10, 12, 16]
min_samples_split = [2, 5, 9, 13, 17]
min_samples_leaf = [1, 4, 7, 10, 13]
random_grid = {
"n_estimators": n_estimators,
"max_depth": max_depth,
"min_samples_split": min_samples_split,
"min_samples_leaf": min_samples_leaf,
"max_features": max_features
}
输出:
>> random_grid
{'n_estimators': [50, 287, 525, 762, 1000],
'max_depth': [10, 15, 20, 25, 30],
'min_samples_split': [2, 5, 9, 13, 17],
'min_samples_leaf': [1, 4, 7, 10, 13],
'max_features': [6, 8, 10, 12, 16]}
对于每个参数,小鱼都列出了 5 个取值,参数有 5 个,一共会产生 5**5 = 3125
种组合,再加上个 3 折的交叉验证,那么一共就需要训练 9375 个模型,这样我们调参的效率也就太低了。
那有什么办法可以解决效率问题呢?
答案就是 RandomizedSearchCV
,这个函数可以帮助我们在候选集组合中,随机地选择几组合适的参数来建模,并且求其交叉验证后的评估结果。以此来大致地找到一个最合适的参数。
这样不我们就不需要训练 9375 个模型,找到最优的参数组合了。比如随机选择其中的 200 组参数,并进行 3 折交叉验证,那么一共只需要训练 600 个模型。
下面是代码部分:
from sklearn.model_selection import RandomizedSearchCV
# 随机选择最合适的参数组合
rf = RandomForestRegressor(random_state=0)
rf_random = RandomizedSearchCV(
estimator=rf,
param_distributions=random_grid,
n_iter=100,
scoring="neg_mean_absolute_error",
cv=3,
verbose=2,
random_state=0,
n_jobs=-1
)
rf_random.fit(X_train, y_train)
RandomizedSearchCV
参数说明:
-
estimator
:RandomizedSearchCV
这个方法是一个通用的,并不是专为随机森林设计的,所以我们需要指定选择的算法模型是什么。 -
distributions
:参数的候选空间,就是我们用字典格式给出的参数候选值。 -
n_iter
:随机寻找参数组合的个数,比如在这里我们赋值了 100,代表接下来要随机找 100 组参数的组合,然后在其中找到最好的一组。 -
scoring
:模型的评估方法,即按照该方法去找到最好的参数组合。 -
cv
:几折交叉验证。 -
random_state
:随机种子,为了使得咱们的结果能够一致,排除掉随机成分的干扰,一般我们都会指定成一个值。 -
n_jobs
:多进程训练,如果是 -1 就会用所有的 CPU。
输出:
Fitting 3 folds for each of 100 candidates, totalling 300 fits
RandomizedSearchCV(cv=3, estimator=RandomForestRegressor(random_state=0),
n_iter=100, n_jobs=-1,
param_distributions={'max_depth': [10, 15, 20, 25, 30],
'max_features': [6, 8, 10, 12, 16],
'min_samples_leaf': [1, 4, 7, 10, 13],
'min_samples_split': [2, 5, 9, 13, 17],
'n_estimators': [50, 287, 525, 762,
1000]},
random_state=0, scoring='neg_mean_absolute_error',
verbose=2)
获取最优的参数组合:
>> rf_random.best_params_
{'n_estimators': 50,
'min_samples_split': 5,
'min_samples_leaf': 10,
'max_features': 16,
'max_depth': 25}
接下来,我们使用测试集来实际看看调参的结果如何。定义相关函数:
def predict_and_evaluate(model, x_test, y_test, print_mape=True):
predictions = model.predict(x_test)
# 计算误差 MAPE (Mean Absolute Percentage Error)
errors = abs(predictions - y_test)
mape = errors / y_test
if print_mape:
print(f'MAPE:{mape.mean():.2%}')
return mape.mean()
使用默认参数建模:
>> base_model = RandomForestRegressor(random_state=0)
>> base_model.fit(X_train, y_train)
>> predict_and_evaluate(base_model, X_test, y_test)
MAPE:6.65%
使用 RandomizedSearchCV
为我们找到的最优参数组合:
>> best_random = rf_random.best_estimator_
>> predict_and_evaluate(best_random, X_test, y_test)
MAPE:6.60%
可以看到模型的效果提升了一些,但是这已经是上限了吗?
GridSearchCV 网格搜索
RandomizedSearchCV 在随机搜索时,相当于为我们锁定了大致的范围。接下来就要靠 GridSearchCV 在缩小后的范围内开展地毯式搜索了,这就是网络搜索,说白了就是将所有的参数组合都进行一遍。
在随机搜索时,我们得到的最优参数组合为:
{'n_estimators': 50,
'min_samples_split': 5,
'min_samples_leaf': 10,
'max_features': 16,
'max_depth': 25}
在参数的附近,定义网格搜索的参数空间:
# 网络搜索
param_grid = {
'n_estimators': [50, 100, 200],
'min_samples_split': [4, 5, 6],
'min_samples_leaf': [9, 10, 11],
'max_features': [15, 16, 17],
'max_depth': [24, 25, 26]
}
进行网格搜索和交叉验证:
from sklearn.model_selection import GridSearchCV
rf = RandomForestRegressor(random_state=0)
grid_search = GridSearchCV(
estimator=rf,
param_grid=param_grid,
scoring='neg_mean_absolute_error',
cv=3,
n_jobs=-1,
verbose=2
)
grid_search.fit(X_train, y_train)
输出:
Fitting 3 folds for each of 243 candidates, totalling 729 fits
GridSearchCV(cv=3, estimator=RandomForestRegressor(random_state=0), n_jobs=-1,
param_grid={'max_depth': [24, 25, 26],
'max_features': [15, 16, 17],
'min_samples_leaf': [9, 10, 11],
'min_samples_split': [4, 5, 6],
'n_estimators': [50, 100, 200]},
scoring='neg_mean_absolute_error', verbose=2)
网格搜索最佳参数组合:
>> grid_search.best_params_
{'max_depth': 24,
'max_features': 15,
'min_samples_leaf': 10,
'min_samples_split': 4,
'n_estimators': 50}
评估:
>> predict_and_evaluate(grid_search.best_estimator_, X_test, y_test)
MAPE:6.59%
误差下降了 0.01 个百分点,改善效果不是很明显。可以多来几组地毯式搜索找到最优的参数组合。
总结
随机搜索可以更节约时间,尤其是在任务开始阶段,我们并不知道哪一个参数在哪一个位置效果能更好,这样我们可以把参数间隔设置的更大一些,先用随机搜索确定一些大致位置。
网络搜索相当于地毯式搜索,当我们得到了大致位置之后,想在这里寻找到最优参数的时候就派上用场了。可以把随机和网络搜索当做一套组合拳,搭配使用。
调参的方法其实还有很多,比如贝叶斯优化。上述的调参方式,每一个都是独立的进行不会对之后的结果产生任何影响,贝叶斯优化的基本思想在于每一个优化都是在不断积累经验,这样我会慢慢得到最终的解应当在的位置,相当于前一步结果会对后面产生影响了。