前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自动化超参数优化最强神器:Optuna

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

作者头像
数据STUDIO
发布2022-05-24 14:41:15
15K1
发布2022-05-24 14:41:15
举报
文章被收录于专栏:数据STUDIO
作为一个合格的调参大侠,这个神器必须收入麾下!本文中,云朵君将和大家一起学习这个超强调参神器: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=1y=-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,定义一个目标,返回想要最小化的函数。

在目标的主体中,我们定义要优化的参数,在这种情况下,简单xy。参数trialoptuna的一个特殊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_floatsuggest_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. 抽样方法有两种;(1) 关系抽样方法,处理参数之间的相互关系。(2) 独立抽样,单独采样每个参数,其中Optuna对两种抽样方法都是有效的。另外,Optuna 还有一个功能,允许用户部署自己定制的采样方法。
  2. 剪枝算法对于确保成本效益的“成本”因素至关重要,它分为两部分操作:(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 类:

  1. 影响决策树结构和学习的参数
  2. 影响训练速度的参数
  3. 参数以获得更好的精度
  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_score

fit_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(<class 'collections.OrderedDict'>, 
{'training': OrderedDict([('l2', 19.23687900448755)]), 
'valid_1': OrderedDict([('l2', 18.314557112625117)])})

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

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

修剪以便更快地搜索

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

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

  • XGBoost:optuna.integration.XGBoostPruningCallback
  • LightGBM:optuna.integration.LightGBMPruningCallback
  • Chainer:optuna.integration.ChainerPruningExtension
  • Keras:optuna.integration.KerasPruningCallback
  • TensorFlowoptuna.integration.TensorFlowPruningHook
  • tf.kerasoptuna.integration.TFKerasPruningCallback
  • MXNetoptuna.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(<class 'collections.OrderedDict'>, 
{'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_policylambda对减少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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据STUDIO 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 超参数优化器
  • Optuna 基础知识
    • Optuna 术语和约定的说明
      • 定义目标特征
        • 定义搜索空间
          • 创建研究对象
            • Optuna 采样参数
              • Optuna 中提供的不同采样器:
                • Optuna进行超参数调优的优势:
                • Optuna 应用实例
                • 读取数据
                • 数据预处理
                  • 删除异常数据
                    • 日总使用量可视化
                      • site_id == 0 都有哪些楼栋
                    • 时序特征处理
                      • 时间拆分
                      • 描述性统计
                    • 缺失值处理
                      • 气象数据缺失值处理
                      • 统计缺失值总数
                      • 气象数据训练和测试数据合并
                    • 特征衍生
                    • 使用 Optuna 进行模型训练
                      • 训练LightGBM模型
                        • 最重要参数概述
                        • 使用trial模块动态定义超参数
                        • 创建 Lightgbm 模型
                        • 两行代码完成调优
                          • 进行"Study"和优化
                          • 修剪以便更快地搜索
                          • Optuna 可视化搜索过程
                            • 调优的历史
                              • 切片图
                                • 等高线图
                                  • 平行坐标图
                                  • 另一种简单模板
                                    • 参考资料
                                    相关产品与服务
                                    GPU 云服务器
                                    GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档