你是否曾经觉得模型有太多的超参数而感到厌烦吗?要从某一个演算法得到好的解必须要调整超参数,所谓的超参数就是控制训练模型的一组神秘数字,例如学习速率就是一种超参数。你永远不能事先知道 0~1 之间哪一个数字是最适合的,唯一的方法就是试错(trial and error)。那万一模型有多个超参数可以控制,岂不是就有成千上万种组合要慢慢尝试吗?
如果你有也这个问题,看这篇就对了!虽然你可能听过 Sklearn 的 GridSearchCV 网格搜索同样也是暴力的找出最佳参数,或是使用 RandomizedSearchCV 随机搜索指定超参数的范围并随机的抽取参数进⾏训练,其它们的共同缺点是非常耗时与占用机器资源。
这里我们要来介绍 Optuna 这个自动找超参数的方便工具,并且可以和多个常用的机器学习演算法整合。
Optuna 透过调整适当的超参数来提高模型预测能力,此方法最初于2019 发表于arxiv 的一篇论文Optuna: A Next-generation Hyperparameter Optimization Framework[1] 同时开源在GitHub[2]上免费提供大家使用。同时 Optuna 也是 2021 年 Kaggle 竞赛中最常见的模型调参工具。
下图所示是超参数优化器在整个算法学习过程中的位置。
如上图所示,超参数调优器在模型外部,调优是在模型训练之前完成的。调整过程的结果是超参数的最佳值,然后将其馈送到模型训练阶段。
Optuna 是一个完全用 Python 编写的最先进的自动超参数调整框架。专为机器学习而设计,可以与 PyTorch、TensorFlow、Keras、SKlearn 等其他框架一起使用。
Optuna 使用一种称为运行时定义的 API,它可以帮助用户编写高度模块化的代码并动态构建超参数的搜索空间,我们将在本文后面学习。网格搜索、随机搜索、贝叶斯搜索和进化算法等不同的采样器,动找到最优超参数值。
通过调整函数(x-1)^2 + (y+3)^2 参数 x,y,使得其取得最小值。通过数学计算,很容易得到该函数在x=1 和y=-3 处达到最小值。
import optuna # pip install optuna
def objective(trial):
x = trial.suggest_float("x", -7, 7)
y = trial.suggest_float("y", -7, 7)
return (x - 1) ** 2 + (y + 3) ** 2
导入后optuna
,定义一个目标,返回想要最小化的函数。
在目标的主体中,我们定义要优化的参数,在这种情况下,简单x
和y
。参数trial
是optuna的一个特殊Trial对象,它对每个超参数进行优化。
其中,它有一种suggest_float
方法,该方法采用超参数的名称和范围来寻找其最佳值。换句话说
x = trial.suggest_float("x", -7, 7)
几乎和{"x": np.arange(-7, 7)}
做 GridSearch 的时候一样。
为了开始优化,我们study
从 Optuna 创建一个对象并将objective
函数传递给它的optimize
方法:
study = optuna.create_study()
study.optimize(objective, n_trials=100) # number of iterations
study.best_params
{'x': 0.9448382515046126,
'y': -3.074822812248314}
非常接近,但没有你想要的那么接近。在这里,我们只进行了 100 次试验,如下所示:
>>> len(study.trials)
100
# 继续训练
>>> study.optimize(objective, n_trials=100)
>>> study.best_params
{'x': 1.0212303395174502, 'y': -3.015575206335039}
在 Optuna 中,整个优化过程称为Study
。例如,使用对数损失作为指标来调整 XGBoost 参数是一项研究:
Trial
通过指定超参数的一次试验来管理模型训练、评估和获得分数的所有单一执行。优化函数的一次执行称为试验。因此,这项研究是试验的集合。Study
管理和记录所有已执行的试验。该记录有助于我们了解最佳超参数并建议要搜索的下一个参数空间。整个优化过程是基于一个目标函数,即研究需要一个可以优化的函数。study = optuna.create_study()
type(study)
optuna.study.Study
一项研究需要一个可以优化的功能。通常,此函数由用户定义,应命名objective
并预期具有此签名:
Optuna 中的优化过程需要一个名为Objective
的函数,完成的每个超参数调整,在这个目标函数中,我们必须决定优化所基于的指标。试验对象是'objective'
方法的输入,并返回一个分数。
研究中的每个试验都表示为optuna.Trial
类。此类是 Optuna 如何找到参数最佳值的关键。
def objective(trial: optuna.Trial, ...):
# calculate score...
return score
通常,在目标函数中做的第一件事是使用内置的 Optuna 方法创建搜索空间。
def objective(trial):
rf_params = {
"n_estimators": trial.suggest_integer(name="n_estimators", low=100, high=2000),
"max_depth": trial.suggest_float("max_depth", 3, 8),
"max_features": trial.suggest_categorical(
"max_features", choices=["auto", "sqrt", "log2"]
),
"n_jobs": -1,
"random_state": 1121218,
}
rf = RandomForestRegressor(**rf_params)
...
在上述目标函数中,我们创建了一个随机森林超参数的小型搜索空间。搜索空间是一个普通的字典。要创建可能的值进行搜索,必须使用试验对象的suggest_*
函数。这些函数至少需要范围的超参数名称、最小值和最大值,以搜索分类超参数或可能的类别。为了使空间更小,suggest_float
并suggest_int
有额外的step
orlog
参数。
要开始研究,创建一个研究对象direction
。
study = optuna.create_study(direction="maximize")
如果我们想要优化的指标是像ROC AUC或准确性这样的性能分数,那么我们将设置direction='maximize'
。否则,设置direction='maximize'
来最小化诸如 RMSE、RMSLE、log损失等损失函数。
然后调用study.optimize
的方法,传递目标函数名称和我们想要的试验次数:
study.optimize(objective, n_trials=100)
Optuna 预设的超参数搜寻方法能有效地在短时间内往最佳的方向去寻找一组适合的参数。与 GridSearch 相比原本可能需要数小时的搜索空间在短短的几分钟内就可以获得不错的经果。并且有效的降低 loss。除了回归问题, Optuna 也能对分类问题进行超参数搜寻,官方的GitHub也有提供各种不同机器学习框架的写法。
TPESampler 为预设的超参数采样器。它试图透过提高最后一次试验的分数来对超参数候选者进行采样。除此之外 Optuna 提供了以下这几个参数采样的方式:
GridSampler
: 与Sklearn 的GridSearch
采样方式相同。使用此方法时建议不要设定太大的范围。RandomSampler
: 与Sklearn 的RandomizedGridSearch
采样方式相同。TPESampler
: 全名Tree-structured Parzen Estimator sampler。预设采样方式。CmaEsSampler
: 基于CMA ES 演算算法的采样器(不支援类别型的超参数).如果需要替换采样参数的方式可以参考以下程式。
from optuna.samplers import CmaEsSampler, RandomSampler
# Study with a random sampler
study = optuna.create_study(sampler=RandomSampler(seed=42))
# Study with a CMA ES sampler
study = optuna.create_study(sampler=CmaEsSampler(seed=42))
①轻松集成且功能多:需要简单的安装,然后就可以开始使用了。可以处理广泛的任务并找到最佳调整的替代方案。
②即时动态搜索空间:熟悉的 Pythonic 语法,如条件和循环,用于自动搜索最佳超参数。
③最先进的算法:快速搜索大空间并更快地修剪没有希望的试验以获得更好和更快的结果。
④分布式优化:可以轻松并行化超参数搜索,而对原始代码几乎没有更改。
⑤良好的可视化:各种可视化功能也可用于直观地分析优化结果。
⑥ 支援大多数 ML 与 DL 的学习套件。例如: Sklearn、PyTorch、TensorFlow、 XGBoost、LightGBM、 CatBoost...等。
⑦ 高效的抽样和剪枝算法 通常,超参数优化框架的成本效益是通过搜索法寻找待评估参数和效率计算法从学习曲线计算待评估参数并识别待剔除参数的专长来衡量的。 对不利轨迹的消除表示为修剪或自动提前停止。
# 导入相关包
import gc
import os
from pathlib import Path
import random
import sys
from tqdm import tqdm_notebook as tqdm
import numpy as np # linear algebra
import pandas as pd # data processing
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.core.display import display, HTML
# --- plotly ---
from plotly import tools, subplots
import plotly.offline as py
py.init_notebook_mode(connected=True)
import plotly.graph_objs as go
import plotly.express as px
import plotly.figure_factory as ff
# --- models ---
from sklearn import preprocessing
from sklearn.model_selection import StratifiedKFold
import lightgbm as lgb
import xgboost as xgb
import catboost as cb
上下滑动查看更多源码
使用kaggle比赛数据集。
本次竞赛根据历史使用率和观测到的天气,跨四种能源类型构建这些反事实模型。该数据集包括三年来来自全球多个不同地点的一千多座建筑物的每小时仪表读数。
train.csv
building_id
-- 建筑物元数据的外键。meter
-- 仪表 ID 代码。读作{0: electricity, 1: chilledwater, 2: steam, 3: hotwater}
。并非每座建筑都有所有仪表类型。timestamp
-- 测量时间meter_reading
-- 目标变量。以千瓦时(或等价物)为单位的能耗。请注意,这是具有测量误差的真实数据,我们预计这将施加基线水平的建模误差。站点 0 的电表读数以 kBTU 为单位。更多数据介绍请见文末。
%%time
# 读取数据...
root = '../input/ashrae-energy-prediction'
train_df = pd.read_csv(os.path.join(root, 'train.csv'))
weather_train_df = pd.read_csv( os.path.join(root, 'weather_train.csv'))
test_df = pd.read_csv(os.path.join(root, 'test.csv'))
weather_test_df = pd.read_csv(os.path.join(root, 'weather_test.csv'))
building_meta_df = pd.read_csv(os.path.join(root, 'building_metadata.csv'))
sample_submission = pd.read_csv(os.path.join(root, 'sample_submission.csv'))
Wall time: 23.8 s
我们本次比赛的目标是预测建筑物的能源消耗。我们必须预测的 4 种能量类型是:
标签 | 能量类型 |
---|---|
0 | 电 |
1 | 冷冻水 |
2 | 蒸汽 |
3 | 热水 |
train_df['timestamp'] = pd.to_datetime(train_df['timestamp'])
train_df['date'] = train_df['timestamp'].dt.date
train_df['meter_reading_log1p'] = np.log1p(train_df['meter_reading'])
train_df.head()
def plot_date_usage(train_df,meter=0, building_id=0):
# meter:仪表id
# 建筑id
# 筛选
train_temp_df = train_df[train_df['meter'] == meter]
train_temp_df = train_temp_df[train_temp_df['building_id'] == building_id]
# 以日期聚合,对仪表读书求和
train_temp_df_meter = train_temp_df.groupby('date')['meter_reading_log1p'].sum()
# 转成pd.DataFrame()
train_temp_df_meter = train_temp_df_meter.to_frame().reset_index()
# 绘制折线图 plotly.express
fig = px.line(train_temp_df_meter, x='date', y='meter_reading_log1p')
fig.show()
plot_date_usage(train_df,meter=0, building_id=0)
由图所示,2016 年 5 月中旬之前的数据看起来很奇怪,因为它位于图表的底部。究其原因是在 5 月 20 日之前site_id == 0
的所有电表读数均为 0。合乎逻辑的做法是删除该时间段的数据。
building_meta_df[building_meta_df.site_id == 0]
从结果看,所有site_id == 0
的数据都有building_id <= 104
。
因此做个筛选:
train_df = train_df.query(
'not (building_id <= 104
& meter == 0
& timestamp <= "2016-05-20")')
本数据中有个重要的特征:timestamp,指定是测量仪表数据时的时间戳。
def preprocess(df):
"""
时间戳处理,将其拆分为小时、月份、星期及周末
"""
df["hour"] = df["timestamp"].dt.hour
df["month"] = df["timestamp"].dt.month
df["dayofweek"] = df["timestamp"].dt.dayofweek
df["weekend"] = df["dayofweek"] >= 5
preprocess(train_df)
首先根据建筑id,仪表id聚合,对仪表读书求和。计算每栋楼的能耗的各个统计值:均值、中值、最大值、最小值及方差等,并将所有统计值concat
成一个总的统计表building_stats_df
。
df_group = train_df.groupby(['building_id', 'meter'])['meter_reading_log1p']
building_mean = df_group.mean().astype(np.float16)
building_median = df_group.median().astype(np.float16)
building_min = df_group.min().astype(np.float16)
building_max = df_group.max().astype(np.float16)
building_std = df_group.std().astype(np.float16)
building_stats_df = pd.concat([building_mean,
building_median,
building_min,
building_max,
building_std], axis=1,
keys=['building_mean',
'building_median',
'building_min',
'building_max',
'building_std']
).reset_index()
building_stats_df
将统计表与原训练表合并,得到每栋楼的每个仪表所读取到能耗总值及其统计值。
train_df = pd.merge(train_df, building_stats_df,
on=['building_id', 'meter'],
how='left', copy=False) # 不复制,直接覆盖原表
train_df.head()
气象数据(weather_train_df,来自尽可能靠近该站点的气象站的天气数据),表中有很多NaN 值,所以我们不能只去掉这些条目。我们将尝试通过插值数据来填充这些值。
在数值分析的数学领域,插值是一种估计,一种基于一组离散的已知数据点的范围构造(查找)新数据点的方法。 -- https://en.wikipedia.org/wiki/Interpolation
首先看几个样本数据:
weather_train_df.sample(10)
weather_train_df.isna().sum()
site_id 0
timestamp 0
air_temperature 55
cloud_coverage 69173
dew_temperature 113
precip_depth_1_hr 50289
sea_level_pressure 10618
wind_direction 6268
wind_speed 304
dtype: int64
weather = pd.concat([weather_train_df, weather_test_df],
ignore_index=True)
weather['timestamp'] = pd.to_datetime(weather['timestamp'])
del weather_test_df # 合并完成后删除数据,节约存储空间
weather_key = ['site_id', 'timestamp']
weather
temp_skeleton = weather[weather_key + ['air_temperature'] #筛选字段
].drop_duplicates(subset=weather_key # 删除 weather_key 中重复信息
).sort_values(by=weather_key).copy()
# 并根据 weather_key 排序
# 根据站点id,测量日期时间,计算天气日均温度
temp_skeleton['temp_rank'] = temp_skeleton.groupby(
['site_id', temp_skeleton.timestamp.dt.date]
)['air_temperature'].rank('average')
temp_skeleton
# 根据站点id,测量日期时间,计算天气24小时平均温度
df_2d = temp_skeleton.groupby(['site_id', temp_skeleton.timestamp.dt.hour]
)['temp_rank'].mean().unstack(level =1)
# 添加滞后信息
site_ids_offsets = pd.Series(df_2d.values.argmax(axis=1) - 14)
# 减去14 是因为最低为14,以0为基线更加方便计算
site_ids_offsets.index.name = 'site_id'
site_id
0 5
1 0
2 9
3 6
4 8
5 0
6 6
7 6
8 5
9 7
10 8
11 6
12 0
13 7
14 6
15 6
dtype: int64
def timestamp_align(df):
df['offset'] = df.site_id.map( site_ids_offsets)
df['timestamp_aligned'] = (df.timestamp - pd.to_timedelta(df.offset, unit='H'))
df['timestamp'] = df['timestamp_aligned']
del df['timestamp_aligned']
return df
del weather
del temp_skeleton
gc.collect()
770
weather_train_df = timestamp_align(weather_train_df)
weather_train_df = weather_train_df.groupby('site_id').apply(
lambda group: group.interpolate(method= "ffill", limit_direction= "forward"))
weather_train_df.head()
通过滑动窗口,衍生一些时序特征。
def add_lag_feature(weather_df, window=3):
# 滑窗求一些统计值
group_df = weather_df.groupby('site_id')
cols = ['dew_temperature', 'cloud_coverage',
'precip_depth_1_hr', 'air_temperature',
'sea_level_pressure', 'wind_direction', 'wind_speed' ]
croll = group_df[cols].rolling(window=window, min_periods=0)
lag_mean = croll.mean().reset_index().astype(np.float16)
lag_max = croll.max().reset_index().astype(np.float16)
lag_min = croll.min().reset_index().astype(np.float16)
lag_std = croll.std().reset_index().astype(np.float16)
for col in cols:
weather_df[f'{col}_mean_lag{window}'] = lag_mean[col]
weather_df[f'{col}_max_lag{window}'] = lag_max[col]
weather_df[f'{col}_min_lag{window}'] = lag_min[col]
weather_df[f'{col}_std_lag{window}'] = lag_std[col]
add_lag_feature(weather_train_df, window=3)
add_lag_feature(weather_train_df, window=72)
现在添加了滞后,我们将对 primary_use 列进行分类以减少合并时的内存。
primary_use_list = building_meta_df['primary_use'].unique()
primary_use_dict = {key:value for value, key in enumerate(primary_use_list)}
print('primary_use_dict: ', primary_use_dict)
building_meta_df['primary_use'] = building_meta_df['primary_use'].map(primary_use_dict)
gc.collect()
primary_use_dict: {'Education': 0,
'Lodging/residential': 1,
'Office': 2,
'Entertainment/public assembly': 3,
'Other': 4, 'Retail': 5, 'Parking': 6,
'Public services': 7,
'Warehouse/storage': 8,
'Food sales and service': 9,
'Religious worship': 10,
'Healthcare': 11, 'Utility': 12,
'Technology/science': 13,
'Manufacturing/industrial': 14,
'Services': 15}
349
在本节中,我们将学习如何使用 Optuna。但首先,将列分为分类特征和数值特征。
上下滑动查看更多源码
category_cols = ['building_id', 'site_id', 'primary_use']
weather_cols = [
'air_temperature', 'cloud_coverage',
'dew_temperature', 'precip_depth_1_hr', 'sea_level_pressure',
'wind_direction', 'wind_speed', 'air_temperature_mean_lag72',
'air_temperature_max_lag72', 'air_temperature_min_lag72',
'air_temperature_std_lag72', 'cloud_coverage_mean_lag72',
'dew_temperature_mean_lag72', 'precip_depth_1_hr_mean_lag72',
'sea_level_pressure_mean_lag72', 'wind_direction_mean_lag72',
'wind_speed_mean_lag72', 'air_temperature_mean_lag3',
'air_temperature_max_lag3',
'air_temperature_min_lag3', 'cloud_coverage_mean_lag3',
'dew_temperature_mean_lag3',
'precip_depth_1_hr_mean_lag3', 'sea_level_pressure_mean_lag3',
'wind_direction_mean_lag3', 'wind_speed_mean_lag3']
feature_cols = ['square_feet', 'year_built'] + [
'hour', 'weekend', 'dayofweek', # 'month'
'building_median'] + weather_cols
def create_X_y(train_df, target_meter):
target_train_df = train_df[train_df['meter'] == target_meter]
target_train_df = target_train_df.merge(building_meta_df,
on='building_id',
how='left')
target_train_df = target_train_df.merge(weather_train_df,
on=['site_id', 'timestamp'],
how='left')
X_train = target_train_df[feature_cols + category_cols]
y_train = target_train_df['meter_reading_log1p'].values
del target_train_df
return X_train, y_train
现在训练电表的LightGBM模型,获得最佳验证分数,并将此分数作为最终分数返回。
import optuna
from optuna import Trial
debug = False
train_df_original = train_df
# Only using 10000 data,,, for fast computation for debugging.
train_df = train_df.sample(10000)
通常,大多数基于树的模型的超参数可以分为 4 类:
大多数时候,这些类别有很多重叠,提高一个类别的效率可能会降低另一个类别的效率。因此手动调参难以出色的完成这项任务。
上下滑动查看更多源码
def objective(trial: optuna.Trial, fast_check=True, target_meter=0, return_info=False):
folds = 5
seed = 666
shuffle = True
# 定义k折交叉验证
kf = StratifiedKFold(n_splits=folds, shuffle=shuffle, random_state=seed)
# 创建X_train, y_train
X_train, y_train = create_X_y(train_df, target_meter=target_meter)
y_valid_pred_total = np.zeros(X_train.shape[0])
gc.collect()
print('target_meter', target_meter, X_train.shape)
L = [X_train.columns.get_loc(cat_col) for cat_col in category_cols]
categorical_features = L
print('cat_features', categorical_features)
models = []
valid_score = 0
for train_idx, valid_idx in kf.split(X_train, y_train):
train_data = X_train.iloc[train_idx,:], y_train[train_idx]
valid_data = X_train.iloc[valid_idx,:], y_train[valid_idx]
print('train', len(train_idx), 'valid', len(valid_idx))
a, b, c = fit_lgbm(trial, train_data, valid_data,
cat_features=category_cols,
num_rounds=1000)
model, y_pred_valid, log = a, b, c
y_valid_pred_total[valid_idx] = y_pred_valid
models.append(model)
gc.collect()
valid_score += log["valid/l2"]
if fast_check:
break
valid_score /= len(models)
if return_info:
return valid_score, models, y_pred_valid, y_train
else:
return valid_score
fit_lgbm
函数具有核心训练代码并定义超参数。
接下来,我们将熟悉 “trial” 模块的内部工作原理。
这是使用 Optuna 与传统的定义并运行代码之间的比较:
这就是按运行定义的优势。这使得用户更容易编写直观的代码来获取超参数,而不是预先定义整个搜索空间。
可以使用这些方法来获取超参数:
# Categorical parameter
optimizer = trial.suggest_categorical('optimizer', ['MomentumSGD', 'Adam'])
# Int parameter
num_layers = trial.suggest_int('num_layers', 1, 3)
# Uniform parameter
dropout_rate = trial.suggest_uniform('dropout_rate', 0.0, 1.0)
# Loguniform parameter
learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-2)
# Discrete-uniform parameter
drop_path_rate = trial.suggest_discrete_uniform('drop_path_rate', 0.0, 1.0, 0.1)
创建我们的主要训练函数了,我们放置了所有可能的超参数范围,并且该函数采用了一个试验对象,并且应该返回一个分数值。
def fit_lgbm(trial, train, val, devices=(-1,), seed=None,
cat_features=None,
num_rounds=1500):
"""Train Light GBM model"""
X_train, y_train = train
X_valid, y_valid = val
metric = 'l2'
params = {
"n_estimators": trial.suggest_categorical("n_estimators", [10000]),
"learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3),
"num_leaves": trial.suggest_int("num_leaves", 20, 3000, step=20),
"max_depth": trial.suggest_int("max_depth", 3, 12),
"min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 200, 10000, step=100),
"max_bin": trial.suggest_int("max_bin", 200, 300),
'lambda_l1': trial.suggest_loguniform('lambda_l1', 1e-8, 10.0),
'lambda_l2': trial.suggest_loguniform('lambda_l2', 1e-8, 10.0),
"min_gain_to_split": trial.suggest_float("min_gain_to_split", 0, 15),
"bagging_fraction": trial.suggest_float(
"bagging_fraction", 0.2, 0.95, step=0.1
),
"bagging_freq": trial.suggest_categorical("bagging_freq", [1]),
"feature_fraction": trial.suggest_float(
"feature_fraction", 0.2, 0.95, step=0.1
),
}
# 是否使用GPU
device = devices[0]
if device == -1:
# use cpu
pass
else:
# use gpu
print(f'using gpu device_id {device}...')
params.update({'device': 'gpu', 'gpu_device_id': device})
params["seed"] = seed
params.update({"objective": "regression",
"learning_rate": 0.1,
"boosting": "gbdt",
"metric": metric,
"verbosity": -1,})
early_stop = 20
verbose_eval = 20
# LGBM 数据准备
d_train = lgb.Dataset(X_train, label=y_train, categorical_feature=cat_features)
d_valid = lgb.Dataset(X_valid, label=y_valid, categorical_feature=cat_features)
watchlist = [d_train, d_valid]
print('training LGB:')
model = lgb.train(params,
train_set=d_train,
num_boost_round=num_rounds,
valid_sets=watchlist,
verbose_eval=verbose_eval,
early_stopping_rounds=early_stop)
# predictions
y_pred_valid = model.predict(X_valid,
num_iteration=model.best_iteration)
print('best_score', model.best_score)
log = {'train/l2': model.best_score['training']['l2'],
'valid/l2': model.best_score['valid_1']['l2']}
return model, y_pred_valid, log
在定义了目标函数并使用“trail
”模块找到超参数后,就可以开始调优了。只需2行代码完成所有超参数调优。
study = optuna.create_study()
study.optimize(objective, n_trials=10)
target_meter 0 (5846, 35)
cat_features [32, 33, 34]
train 4676 valid 1170
training LGB:
[LightGBM] [Warning] Unknown parameter: shuffle
Training until validation scores don't improve for 20 rounds
[20] training's l2: 19.2369 valid_1's l2: 18.3146
Early stopping, best iteration is:
[1] training's l2: 19.2369 valid_1's l2: 18.3146
best_score defaultdict(<class 'collections.OrderedDict'>,
{'training': OrderedDict([('l2', 19.23687900448755)]),
'valid_1': OrderedDict([('l2', 18.314557112625117)])})
由于n_trials
的值是10,因此输出相当大。因此如下截图是最后一次trail的结果记录打印。
到目前为止,就完成了超参数调优了。
对无用的Trial进行剪枝是在optuna中一种先进而实用的技术。如果你不熟悉修剪是什么,可以理解为它是一种压缩ML搜索算法中的数据的技术,通过消除冗余和不重要的数据来分类实例,从而减少决策树的大小。
因此,剪枝可以提高最终分类器的复杂度,防止过拟合。Optuna提供了对多个流行ML框架的集成,用户可以使用它在超参数训练期间尝试修剪。例子:
optuna.integration.XGBoostPruningCallback
optuna.integration.LightGBMPruningCallback
optuna.integration.ChainerPruningExtension
optuna.integration.KerasPruningCallback
optuna.integration.TensorFlowPruningHook
optuna.integration.TFKerasPruningCallback
optuna.integration.MXNetPruningCallback
这里可以详细了解这些集成的剪枝方法:optuna.integration[3]。
列举一个使用修剪创建目标函数的简单示例:
上下滑动查看更源码
def objective_with_prune(trial: Trial, fast_check=True, target_meter=0):
folds = 5
seed = 666
shuffle = False
kf = KFold(n_splits=folds, shuffle=shuffle, random_state=seed)
X_train, y_train = create_X_y(train_df, target_meter=target_meter)
y_valid_pred_total = np.zeros(X_train.shape[0])
gc.collect()
print('target_meter', target_meter, X_train.shape)
x = [X_train.columns.get_loc(cat_col) for cat_col in category_cols]
cat_features = x
print('cat_features', cat_features)
models0 = []
valid_score = 0
for train_idx, valid_idx in kf.split(X_train, y_train):
train_data = X_train.iloc[train_idx,:], y_train[train_idx]
valid_data = X_train.iloc[valid_idx,:], y_train[valid_idx]
print('train', len(train_idx), 'valid', len(valid_idx))
model, y_pred_valid, log = fit_lgbm_with_pruning(
trial, train_data, valid_data,
cat_features=category_cols,
num_rounds=1000)
y_valid_pred_total[valid_idx] = y_pred_valid
models0.append(model)
gc.collect()
valid_score += log["valid/l2"]
if fast_check:
break
valid_score /= len(models0)
return valid_score
其中fit_lgbm_with_pruning
函数在函数fit_lgbm
基础上修改了模型训练参数:
from optuna.integration import LightGBMPruningCallback
model = lgb.train(params,
train_set=d_train,
num_boost_round=num_rounds,
valid_sets=watchlist,
verbose_eval=verbose_eval,
early_stopping_rounds=early_stop,
callbacks=[
LightGBMPruningCallback(trial, metric,valid_name='valid_1')
])
target_meter 0 (5846, 35)
cat_features [32, 33, 34]
train 4676 valid 1170
training LGB:
Training until validation scores don't improve for 20 rounds
[20] training's l2: 19.2369 valid_1's l2: 18.3146
Early stopping, best iteration is:
[1] training's l2: 19.2369 valid_1's l2: 18.3146
best_score defaultdict(<class 'collections.OrderedDict'>,
{'training': OrderedDict([('l2', 19.23687900448755)]),
'valid_1': OrderedDict([('l2', 18.314557112625117)])})
Optuna 为我们提供了可视化训练和学习历史的选项,以确定具有最佳性能的超参数。最棒的是,所有这些可视化只需要 1 行代码!!!
Optuna 在同时也提供了可视化的套件:
optuna.visualization.plot_optimization_history(study)
可视化搜索历史让我们更好地了解超参数对我们模型的影响。我们还可以通过使用更窄的参数空间来进一步缩小搜索范围。
optuna.visualization.plot_intermediate_values(study)
所有不同的颜色显示每个试验的损失曲线。
optuna.visualization.plot_slice(study)
以目标值作为轮廓绘制参数对。
optuna.visualization.plot_contour(study)
optuna.visualization.plot_parallel_coordinate(study)
optuna.visualization.plot_param_importances(study)
fig.show(config=plotly_config)
绘制超参数重要性。从这张图我们可以发现eta(learning_rate) 学习速率是最为重要的。此外grow_policy
与lambda
对减少loss
上无太大帮助。因此在下一次执行试验的时候可以考虑将无用的参数移除,并将重要的超参数范围加大取得更好的搜索结果。
创建 Optuna 研究并运行trial完整代码。
上下滑动查看更多源码
def objective(trial, X, y):
param_grid = {
# "device_type": trial.suggest_categorical("device_type", ['gpu']),
"n_estimators": trial.suggest_categorical("n_estimators", [10000]),
"learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3),
"num_leaves": trial.suggest_int("num_leaves", 20, 3000, step=20),
"max_depth": trial.suggest_int("max_depth", 3, 12),
"min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 200, 10000, step=100),
"lambda_l1": trial.suggest_int("lambda_l1", 0, 100, step=5),
"lambda_l2": trial.suggest_int("lambda_l2", 0, 100, step=5),
"min_gain_to_split": trial.suggest_float("min_gain_to_split", 0, 15),
"bagging_fraction": trial.suggest_float(
"bagging_fraction", 0.2, 0.95, step=0.1
),
"bagging_freq": trial.suggest_categorical("bagging_freq", [1]),
"feature_fraction": trial.suggest_float(
"feature_fraction", 0.2, 0.95, step=0.1
),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1121218)
cv_scores = np.empty(5)
for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
model = lgbm.LGBMClassifier(objective="binary", **param_grid)
model.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
eval_metric="binary_logloss",
early_stopping_rounds=100,
callbacks=[
LightGBMPruningCallback(trial, "binary_logloss")
], # Add a pruning callback
)
preds = model.predict_proba(X_test)
cv_scores[idx] = log_loss(y_test, preds)
return np.mean(cv_scores)
本文只是熟悉Optuna的开始,涵盖了有关如何调整 ML 模型的超参数的大部分基础知识。我们学习了 Optuna 库中使用的术语,例如trail和study。我们还学习了如何定义使用 Optuna 调整所必需的目标函数。
[1]
Optuna: A Next-generation Hyperparameter Optimization Framework: https://arxiv.org/abs/1907.10902
[2]
GitHub: https://github.com/optuna/optuna
[3]
optuna.integration: https://optuna.readthedocs.io/en/stable/reference/integration.html