自动化超参数优化最强神器:Optuna

自动化超参数优化最强神器:Optuna

作为一个合格的调参大侠,这个神器必须收入麾下!本文中,云朵君将和大家一起学习这个超强调参神器:Optuna,它透过调整适当的超参数来提高模型预测能力,可以和多个常用的机器学习演算法整合!掌握它,比赛都有底气了~你是否曾经觉得模型有太多的超参数而感到厌烦吗?要从某一个演算法得到好的解必须要调整超参数,所谓的超参数就是控制训练模型的一组神秘数字,例如学习速率就是一种超参数。你永远不能事先知道 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,它可以帮助用户编写高度模块化的代码并动态构建超参数的搜索空间,我们将在本文后面学习。网格搜索、随机搜索、贝叶斯搜索和进化算法等不同的采样器,动找到最优超参数值。

Optuna 基础知识通过调整函数(x-1)^2 + (y+3)^2 参数 x,y,使得其取得最小值。通过数学计算,很容易得到该函数在x=1 和y=-3 处达到最小值。

代码语言:javascript复制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方法,该方法采用超参数的名称和范围来寻找其最佳值。换句话说

代码语言:javascript复制x = trial.suggest_float("x", -7, 7)

几乎和{"x": np.arange(-7, 7)}做 GridSearch 的时候一样。

为了开始优化,我们study从 Optuna 创建一个对象并将objective函数传递给它的optimize方法:

代码语言:javascript复制study = optuna.create_study()

study.optimize(objective, n_trials=100) # number of iterations

study.best_params

代码语言:javascript复制{'x': 0.9448382515046126,

'y': -3.074822812248314}

非常接近,但没有你想要的那么接近。在这里,我们只进行了 100 次试验,如下所示:

代码语言:javascript复制>>> len(study.trials)

100

# 继续训练

>>> study.optimize(objective, n_trials=100)

>>> study.best_params

{'x': 1.0212303395174502, 'y': -3.015575206335039}

Optuna 术语和约定的说明在 Optuna 中,整个优化过程称为Study。例如,使用对数损失作为指标来调整 XGBoost 参数是一项研究:

Trial通过指定超参数的一次试验来管理模型训练、评估和获得分数的所有单一执行。优化函数的一次执行称为试验。因此,这项研究是试验的集合。Study管理和记录所有已执行的试验。该记录有助于我们了解最佳超参数并建议要搜索的下一个参数空间。整个优化过程是基于一个目标函数,即研究需要一个可以优化的函数。代码语言:javascript复制study = optuna.create_study()

type(study)

代码语言:javascript复制optuna.study.Study

定义目标特征一项研究需要一个可以优化的功能。通常,此函数由用户定义,应命名objective并预期具有此签名:

Optuna 中的优化过程需要一个名为Objective的函数,完成的每个超参数调整,在这个目标函数中,我们必须决定优化所基于的指标。试验对象是'objective'方法的输入,并返回一个分数。

包括作为字典搜索的参数网格创建一个模型来尝试超参数组合集将模型拟合到具有单个候选集的数据使用此模型生成预测根据用户定义的指标对预测进行评分并返回研究中的每个试验都表示为optuna.Trial类。此类是 Optuna 如何找到参数最佳值的关键。

代码语言:javascript复制def objective(trial: optuna.Trial, ...):

# calculate score...

return score

定义搜索空间通常,在目标函数中做的第一件事是使用内置的 Optuna 方法创建搜索空间。

代码语言:javascript复制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有额外的steporlog参数。

创建研究对象要开始研究,创建一个研究对象direction。

代码语言:javascript复制study = optuna.create_study(direction="maximize")

如果我们想要优化的指标是像ROC AUC或准确性这样的性能分数,那么我们将设置direction='maximize'。否则,设置direction='maximize'来最小化诸如 RMSE、RMSLE、log损失等损失函数。

然后调用study.optimize的方法,传递目标函数名称和我们想要的试验次数:

代码语言:javascript复制study.optimize(objective, n_trials=100)

Optuna 采样参数Optuna 预设的超参数搜寻方法能有效地在短时间内往最佳的方向去寻找一组适合的参数。与 GridSearch 相比原本可能需要数小时的搜索空间在短短的几分钟内就可以获得不错的经果。并且有效的降低 loss。除了回归问题, Optuna 也能对分类问题进行超参数搜寻,官方的GitHub也有提供各种不同机器学习框架的写法。

TPESampler 为预设的超参数采样器。它试图透过提高最后一次试验的分数来对超参数候选者进行采样。除此之外 Optuna 提供了以下这几个参数采样的方式:

GridSampler: 与Sklearn 的GridSearch采样方式相同。使用此方法时建议不要设定太大的范围。RandomSampler: 与Sklearn 的RandomizedGridSearch采样方式相同。TPESampler: 全名Tree-structured Parzen Estimator sampler。预设采样方式。CmaEsSampler: 基于CMA ES 演算算法的采样器(不支援类别型的超参数).如果需要替换采样参数的方式可以参考以下程式。

代码语言:javascript复制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))

Optuna 中提供的不同采样器:网格搜索:将每个超参数的搜索空间离散化。优化器为每个超参数配置启动学习,并在最后选择最佳配置。随机搜索:对搜索空间进行随机采样,直到满足停止条件为止。贝叶斯搜索:寻找最佳超参数的基于概率模型的方法进化算法:利用适应度函数的值来寻找最佳超参数的元启发式方法。Optuna进行超参数调优的优势:①轻松集成且功能多:需要简单的安装,然后就可以开始使用了。可以处理广泛的任务并找到最佳调整的替代方案。

②即时动态搜索空间:熟悉的 Pythonic 语法,如条件和循环,用于自动搜索最佳超参数。

③最先进的算法:快速搜索大空间并更快地修剪没有希望的试验以获得更好和更快的结果。

④分布式优化:可以轻松并行化超参数搜索,而对原始代码几乎没有更改。

⑤良好的可视化:各种可视化功能也可用于直观地分析优化结果。

⑥ 支援大多数 ML 与 DL 的学习套件。例如: Sklearn、PyTorch、TensorFlow、 XGBoost、LightGBM、 CatBoost...等。

⑦ 高效的抽样和剪枝算法

通常,超参数优化框架的成本效益是通过搜索法寻找待评估参数和效率计算法从学习曲线计算待评估参数并识别待剔除参数的专长来衡量的。

对不利轨迹的消除表示为修剪或自动提前停止。

抽样方法有两种;(1) 关系抽样方法,处理参数之间的相互关系。(2) 独立抽样,单独采样每个参数,其中Optuna对两种抽样方法都是有效的。另外,Optuna 还有一个功能,允许用户部署自己定制的采样方法。剪枝算法对于确保成本效益的“成本”因素至关重要,它分为两部分操作:(1)定期监控中间目标值,以及(2)暂停不符合预定义可能性的实验。Optuna 应用实例代码语言:javascript复制# 导入相关包

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 为单位。更多数据介绍请见文末。

代码语言:javascript复制%%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'))

代码语言:javascript复制Wall time: 23.8 s

数据预处理我们本次比赛的目标是预测建筑物的能源消耗。我们必须预测的 4 种能量类型是:

标签

能量类型

0

1

冷冻水

2

蒸汽

3

热水

删除异常数据代码语言:javascript复制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()

日总使用量可视化代码语言:javascript复制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。合乎逻辑的做法是删除该时间段的数据。

site_id == 0 都有哪些楼栋代码语言:javascript复制building_meta_df[building_meta_df.site_id == 0]

从结果看,所有site_id == 0的数据都有building_id <= 104。

因此做个筛选:

代码语言:javascript复制train_df = train_df.query(

'not (building_id <= 104

& meter == 0

& timestamp <= "2016-05-20")')

时序特征处理本数据中有个重要的特征:timestamp,指定是测量仪表数据时的时间戳。

时间拆分代码语言:javascript复制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。

代码语言:javascript复制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

将统计表与原训练表合并,得到每栋楼的每个仪表所读取到能耗总值及其统计值。

代码语言:javascript复制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

首先看几个样本数据:

代码语言:javascript复制weather_train_df.sample(10)

统计缺失值总数代码语言:javascript复制weather_train_df.isna().sum()

代码语言:javascript复制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

气象数据训练和测试数据合并代码语言:javascript复制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

代码语言:javascript复制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

代码语言:javascript复制# 根据站点id,测量日期时间,计算天气24小时平均温度

df_2d = temp_skeleton.groupby(['site_id', temp_skeleton.timestamp.dt.hour]

)['temp_rank'].mean().unstack(level =1)

代码语言:javascript复制# 添加滞后信息

site_ids_offsets = pd.Series(df_2d.values.argmax(axis=1) - 14)

# 减去14 是因为最低为14,以0为基线更加方便计算

site_ids_offsets.index.name = 'site_id'

代码语言:javascript复制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

代码语言:javascript复制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()

代码语言:javascript复制770

代码语言:javascript复制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()

特征衍生通过滑动窗口,衍生一些时序特征。

代码语言:javascript复制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 列进行分类以减少合并时的内存。

代码语言:javascript复制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()

代码语言:javascript复制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 进行模型训练在本节中,我们将学习如何使用 Optuna。但首先,将列分为分类特征和数值特征。

代码语言:javascript复制上下滑动查看更多源码代码语言:javascript复制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模型现在训练电表的LightGBM模型,获得最佳验证分数,并将此分数作为最终分数返回。

代码语言:javascript复制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 类:

影响决策树结构和学习的参数影响训练速度的参数参数以获得更好的精度对抗过拟合的参数大多数时候,这些类别有很多重叠,提高一个类别的效率可能会降低另一个类别的效率。因此手动调参难以出色的完成这项任务。

代码语言:javascript复制上下滑动查看更多源码代码语言:javascript复制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_scorefit_lgbm函数具有核心训练代码并定义超参数。

接下来,我们将熟悉 “trial” 模块的内部工作原理。

使用trial模块动态定义超参数这是使用 Optuna 与传统的定义并运行代码之间的比较:

这就是按运行定义的优势。这使得用户更容易编写直观的代码来获取超参数,而不是预先定义整个搜索空间。

可以使用这些方法来获取超参数:

代码语言:javascript复制# 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)

创建 Lightgbm 模型创建我们的主要训练函数了,我们放置了所有可能的超参数范围,并且该函数采用了一个试验对象,并且应该返回一个分数值。

代码语言:javascript复制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两行代码完成调优进行"Study"和优化在定义了目标函数并使用“trail”模块找到超参数后,就可以开始调优了。只需2行代码完成所有超参数调优。

代码语言:javascript复制study = optuna.create_study()

study.optimize(objective, n_trials=10)

代码语言:javascript复制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(,

{'training': OrderedDict([('l2', 19.23687900448755)]),

'valid_1': OrderedDict([('l2', 18.314557112625117)])})

由于n_trials的值是10,因此输出相当大。因此如下截图是最后一次trail的结果记录打印。

到目前为止,就完成了超参数调优了。

修剪以便更快地搜索对无用的Trial进行剪枝是在optuna中一种先进而实用的技术。如果你不熟悉修剪是什么,可以理解为它是一种压缩ML搜索算法中的数据的技术,通过消除冗余和不重要的数据来分类实例,从而减少决策树的大小。

因此,剪枝可以提高最终分类器的复杂度,防止过拟合。Optuna提供了对多个流行ML框架的集成,用户可以使用它在超参数训练期间尝试修剪。例子:

XGBoost:optuna.integration.XGBoostPruningCallbackLightGBM:optuna.integration.LightGBMPruningCallbackChainer:optuna.integration.ChainerPruningExtensionKeras:optuna.integration.KerasPruningCallbackTensorFlowoptuna.integration.TensorFlowPruningHooktf.kerasoptuna.integration.TFKerasPruningCallbackMXNetoptuna.integration.MXNetPruningCallback这里可以详细了解这些集成的剪枝方法:optuna.integration[3]。

列举一个使用修剪创建目标函数的简单示例:

代码语言:javascript复制上下滑动查看更源码代码语言:javascript复制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基础上修改了模型训练参数:

代码语言:javascript复制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')

])

代码语言:javascript复制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(,

{'training': OrderedDict([('l2', 19.23687900448755)]),

'valid_1': OrderedDict([('l2', 18.314557112625117)])})

Optuna 可视化搜索过程Optuna 为我们提供了可视化训练和学习历史的选项,以确定具有最佳性能的超参数。最棒的是,所有这些可视化只需要 1 行代码!!!

Optuna 在同时也提供了可视化的套件:

plot_optimization_history (可视化优化的过程)plot_intermediate_values (可视化学习的曲线)plot_slice (可视化个别参数)plot_contour (可视化参数间的彼此关系)plot_parallel_coordinate (可视化高维度中参数间的彼此关系)plot_param_importances (可视化参数对模型的重要程度)plot_edf (可视化验分布函数)代码语言:javascript复制optuna.visualization.plot_optimization_history(study)

调优的历史可视化搜索历史让我们更好地了解超参数对我们模型的影响。我们还可以通过使用更窄的参数空间来进一步缩小搜索范围。

代码语言:javascript复制optuna.visualization.plot_intermediate_values(study)

所有不同的颜色显示每个试验的损失曲线。

切片图代码语言:javascript复制optuna.visualization.plot_slice(study)

等高线图以目标值作为轮廓绘制参数对。

代码语言:javascript复制optuna.visualization.plot_contour(study)

平行坐标图

代码语言:javascript复制optuna.visualization.plot_parallel_coordinate(study)

代码语言:javascript复制optuna.visualization.plot_param_importances(study)

fig.show(config=plotly_config)

绘制超参数重要性。从这张图我们可以发现eta(learning_rate) 学习速率是最为重要的。此外grow_policy与lambda对减少loss上无太大帮助。因此在下一次执行试验的时候可以考虑将无用的参数移除,并将重要的超参数范围加大取得更好的搜索结果。

另一种简单模板创建 Optuna 研究并运行trial完整代码。

代码语言:javascript复制上下滑动查看更多源码代码语言:javascript复制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

相关推荐

365bet官方网站下载 用c实现继承

用c实现继承

📅 11-02 👁️ 6611
365在线体育app下载 平安福和金无忧哪个好

平安福和金无忧哪个好

📅 09-06 👁️ 4914
365bet官方网站下载 会计上的核销是什么意思?有什么目的?

会计上的核销是什么意思?有什么目的?

📅 01-09 👁️ 9692
365bet官方网站下载 美国人是怎么治疗痔疮的?

美国人是怎么治疗痔疮的?

📅 09-27 👁️ 1265