Optuna:超参优化框架

概述

Optuna (KDD19) 是一个超参搜索工具,通过在代码中定义超参空间,然后执行一定次数的实验寻找最优组合。
搜索过程的实验数据会保存到数据库里(如sqlite),支持并行调参,支持数据可视化。

相关内容

目前在比较多的科研代码中可见到一些实验数据管理工具,如CometML、Wandb和Neptune。
这些工具可在代码中上传实验数据到云,并在云端对提供实验数据管理、可视化和协作等功能,也提供超参搜索功能。

Optuna强在超参搜索,弱在数据管理、可视化,不上云所以没有协作,因此也有将Optuna和Wandb搭配使用的。
类似超参优化框架还有Hyperopt等,Github上Optuna星多所以学习Optuna。

超参优化方法

1. 人工调参(manual tuning)

for alpha in 0.3 0.5 0.7; do
    for beta in 0 0.1 0.5 0.9; do
        python3 main.py --alpha $alpha --beta $beta | tee -a main.log
    done
done

好写脚本,顺便做了ablation study。

比grid search覆盖空间更广(同一超参在每个trial中值都不一样)

4. 贝叶斯优化(Bayesian Optimization, NIPS11)

对历史数据建模(常用GPR或TPE),兼顾好优化(Exploitation)和大方差(Exploration)。
CometML这些都提供贝叶斯优化帮助调参。

Optuna

提供三大功能:

  1. 通过Sampler(默认用TPE)根据历史测试结果做贝叶斯优化,减少测试数
  2. 通过Pruner根据测试中间结果指导早停,缩短测试时间
  3. 通过Optuna Dashboard对调参过程可视化

基本用法:调参

在脚本中定义本次study,其中objective是每次测试都调用获取优化指标的函数。

# 只要study_name和storage一样,就可以多个脚本一起并行调参
study = optuna.create_study(study_name='test1', storage='sqlite:///xx.db', load_if_exists=True)
# 可以存储自定义数据
study.set_user_attr('contributors', ['Yi', 'Luo'])
# 也可以先跑n_trials次测试,视情况重复运行脚本、接着再跑n_trials次
study.optimize(objective, direction='minimize', n_trials=3)

一个study中每次测试都叫一个trial。
通过trial获取本次测试超参的值,进行测试,然后返回优化指标。

def objective(trial):
    # 常用3种限定超参的方法,其它的也都可以用这三种表示
    dist = trial.suggest_categorical('dist', [1, False, 'opt'])
    # step是步长,在suggest_float中也可以用
    epochs = trial.suggest_int('epoch', low=100, high=200, step=10)
    # log指在对数域采样
    lr = trial.suggest_float('lr', low=0.0001, high=0.1, log=True)
    # 每个trial可以存储自定义数据
    trial.set_user_attr('acc', 0.98)
    # 自定义数据可以调用
    print(trial.study.user_attr('contributors'))
    # 如果自己本来的脚本不好搭配optuna,可以写成子进程
    p = subprocess.Popen(
        'python3 main.py --dist %s --epochs %d --lr %.3f' % (dist, epochs, lr),
        shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    # 这种方法需要从标准输出中解析出指标(如果报告中间值还可以激活pruning)
    while 1:
        lines = p.stdout.readlines()
        for line in lines:
            line = line.strip().decode()
            if line.startswith('score: '):
                acc = float(line.split('score: ', 1)[1])
        if (not lines) and type(p.poll()) == int:
            break
    # 需要返回一个评估指标,多个也支持,外面direction换成directions(列表)
    return accuracy

Optuna可以依据历史trials信息基于贝叶斯优化高效地选择超参,在我试用时相比Grid Search能减少90%以上的trial数。

进阶用法:Ablation Study

由于Study在创建后,suggest_categorical等超参设置便不能再改,而调参轨迹可能无法覆盖ablation study的各种情况(即使覆盖也有数据点不均衡的问题),因此无法保障在调参的同时能利用数据点做完ablation study。

此时可利用study.enqueue_trial接口预约一定数量的测试,这些测试使用指定的超参。如

# 先随便调20次,累积一些测试信息
study.optimize(objective, n_trials=20)
# 每类情况调10次
for _ in range(10):
    # 第一类情况
    study.enqueue_trial({'use_feature_1': True})
    # enqueue_trial只是“预约”,所以下面要将预约执行
    study.optimize(objective, n_trials=1)
    # 另一类情况
    study.enqueue_trial({'use_feature_2': False})
    study.optimize(objective, n_trials=1)

用法2:早停

在减少trial数之外,Optuna还提供一些Pruner算法,以提前结束预期效果不好的trial(需要在脚本过程中报告中间值,如每个epoch或run的结果),进一步节省调参时间。

以下两个Pruner在深度学习中比较有用:

1. SuccessiveHalvingPruner

每个trial都是一次完整的模型训练(我们通常称为run),根据每个epoch的效果决定该run是否早停。

SuccessiveHalvingPruner在不同epoch处设置有“关卡”,每个关卡只放一定比例的trial过去,其它的停掉。
以默认参数为例,第一个trial不停,以此测算出max_epoch如1000,进而设置minimal_resource为1%即10,默认的通过比例为1/4,所以关卡设置在第10、40、160、640 epoch处。

类似效果的还有MedianPruner(停掉表现在中位数以下的,没那么复杂的关卡设置)等等。

2. WilcoxonPruner

每个trial包含k个run(如k折验证),根据已知runs的效果提前结束最终平均效果可能较差的trial。

用法3:可视化

需要python3 -m pip install optuna-dashboard,然后optuna-dashboard --storage sqlite:///xx.db就可以运行一个前端服务,查看实验状态、每个实验的数据、各个参数的重要性等等。
另外还有个纯前端页面,将sqlite数据库丢上去就可以可视化,但功能比本地服务少。

Page Not Found

Try to search through the entire repo.