[python]使用NNI调参

overview

pros:

  • 内置多种调参算法可以选择,而不是无脑grid search
  • 可以方便的进行可视化,了解比如模型是对哪些参数是不敏感的(使用哪个值效果都差不多),哪些是敏感的。
  • 有一个调度器,如果模型的初期效果很差,调度器会自动终止模型,将计算资源分配给可能有更好参数的模型。
  • 通过ssh通信,可以在多服务器上调参。

cons:

  • 感觉这个包还是适合一些能够快速运行完成,或者在真正用于实战的模型(比如比赛或者是公司的产品)以及对好几个参数都很敏感的模型使用,毕竟在学术圈通过大力调参达到好的效果是为人所不齿的。

主要概念

Assessor

  • assessor就是我们熟知的early stopping,这一模块作用是判断当前尝试的超参是否有前途,如果否那么提前终止,以节约计算资源。
  • 下面介绍NNI中已有的几种assessor算法
median stop
  • 在当前尝试的超参训练的过程中,如果出现最新的step的结果比之前所有的step的结果的均值还要低,那么就终止这个训练。(很简单,就是模型已经不能保持一个上升的趋势了,那么肯定要停止了)
    • 优点:计算简单,运算速度快。
    • 缺点:有个很强的假设,就是模型结果波动不大,但是如果模型的解结果从开始阶段就有很大的波动,那么显然是不适用的。
curvefitting
  • 使用之前的n个样本点来拟合学习曲线,该算法引入了马尔可夫蒙特卡洛(Markov Chain Monte Carlo),这使得算法能够更加充分利用之前的样本点中的信息,以更好的预测当前训练的最终结果。该算法的停止标准为,当预计当前算法的最优结果低于之前的最优结果,则决定提前停止当前尝试。
    • 优点:能够更好的学习到前几次的尝试样本,能更准确的判断是否该提前停止
    • 缺点:该算法需要冷启动,需要设置较多超参

Tuner

  • 调参器:即使用什么策略进行调参
  • TPE:目前AutoML/NAS领域较常用的一种Tuner,适用于评估函数开销较大、搜索空间较大的场景,相比Random/Grid Search等暴力搜索方法,能够在搜索空间较大的应用场景下搜索出较为理想的参数。其缺点是需要寻找合适的概率模型。
  • Random Search:随机搜索,适用于对参数先验信息未知,且搜索空间较小的场景,算法简单方便,能够在一些较为简单的任务上得到比较理想的结果,缺点也十分明显,无法改进之前的搜索结果,得到更理想的参数。
  • Anneal:与Random Search一样是随机化搜索,不同的是在时间上较近的搜索结果和历次最好的搜索结果的基础上进行采样。其优点是能够改进Random Search的结果,比Random Search更快得到理想的参数。
  • Evolution:遗传算法,由于其缓慢的参数步进过程,适用于搜索空间较小的场景,但通常能达到较理想的参数。
  • SMAC: 同样也是SMBO算法的一种,在序列模型上引入了高斯随机过程的模型和基于随机森林的模型,能够同时处理连续数值型参数和离散类别型参数。
  • Batch Tuner: 只能对搜索空间里显式定义的参数进行逐个遍历搜索。适用于搜索空间极小的场景,不适用于连续数值型分布的参数。
  • Grid Search: 穷举法,只能搜索离散类别型参数,适用于搜索空间较小,模型训练/评估开销较小的场景。
  • Hyperband: 在训练的时候只迭代一定的次数,新的一轮训练在上一轮较为理想的参数上进一步训练。其设计是为了能够尽可能地遍历所有参数组合,适用于搜索空间较大但时间开销较为有限的场景。
  • Network Morphism: 类似于遗传算法的思想,每次基于父代产生一组子代,然后基于历次搜索过的网络结构和对应的评估值来预测子代的评估值,选出最理想的一个子代进行训练。适用于DL场景,能够较为稳定地搜索得到一组理想的参数,由于训练之前会进行一轮较为仔细的预测选择,避免了多次训练DL模型较大的开销。

trial

  • NNI中而trial就是一组需要调节的超参数
  • NNI中有两种方式来调节超参数一种是通过NNI API,一种是通过NNI Python annotation
NNI API
  • 第一步定义超参搜索空间,存储于一个json文件
{
    "dropout_rate":{"_type":"uniform","_value":[0.1,0.5]},
    "conv_size":{"_type":"choice","_value":[2,3,5,7]},
    "hidden_size":{"_type":"choice","_value":[124, 512, 1024]},
    "learning_rate":{"_type":"uniform","_value":[0.0001, 0.1]}
}
  • 更新模型代码
import NNI
RECEIVED_PARAMS = nni.get_next_parameter()
# RECEIVED_PARAMS = {"conv_size": 2, "hidden_size": 124, "learning_rate": 0.0307, "dropout_rate": 0.2029}
  • 周期性的回报测试结果(dev集)(可选),这个值是被用于报告给assessor的,metrics可以是一个值(float,int)或者是一个有键名default键值是数字的一个字典。
nni.report_intermediate_result(metrics)
  • 汇报最终的测试结果(test集),汇报给tuner
nni.report_final_result(metrics)
  • 在配置文件中激活NNI API
useAnnotation: false
searchSpacePath: /path/to/your/search_space.json
NNI Python Annotation
  • Python中使用NNI's的语法,就像普通的注释一样,不会对现有的代码进行改动,就像没有使用NNI的时候进行调参就可以了。
  • 可以指定要调的参数,以及对应的调参方法,范围。
  • 选择是否报告给assessor以及tuner
  • 可以发现这种方式更加的方便,以及可读性也更强了。
  • @nni.variable调节参数。
  • @nni.report_intermediate_result/@nni.report_final_result进行结果汇报。
  • @nni.report_intermediate_result/@nni.report_final_result进行汇报。
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
   """@nni.variable(nni.choice(50, 250, 500), name=batch_size)"""
    batch_size = 128
    for i in range(10000):
        batch = mnist.train.next_batch(batch_size)
       """@nni.variable(nni.choice(0.1, 0.5), name=dropout_rate)"""
        dropout_rate = 0.5
        mnist_network.train_step.run(feed_dict={mnist_network.images: batch[0],
                                                mnist_network.labels: batch[1],
                                                mnist_network.keep_prob: dropout_rate})
        if i % 100 == 0:
            test_acc = mnist_network.accuracy.eval(
                feed_dict={mnist_network.images: mnist.test.images,
                            mnist_network.labels: mnist.test.labels,
                            mnist_network.keep_prob: 1.0})
          """@nni.report_intermediate_result(test_acc)"""

    test_acc = mnist_network.accuracy.eval(
        feed_dict={mnist_network.images: mnist.test.images,
                    mnist_network.labels: mnist.test.labels,
                    mnist_network.keep_prob: 1.0})
  """@nni.report_final_result(test_acc)"""
  • YAML配置文件中,设置:
useAnnotation: true

启发式调参

  • 可以提高调参效率,一开始我们可能只会给一个很小的超参数搜索空间,比如ab。如果观察到大多数好的trail都很靠近b,那么我们可以把b进行松弛到c,随后继续调参。

例子

  • 第一步定义参数的搜索空间,通过写JSON文件
{
    "C": {"_type":"uniform","_value":[0.1, 1]},
    "keral": {"_type":"choice","_value":["linear", "rbf", "poly", "sigmoid"]},
    "degree": {"_type":"choice","_value":[1, 2, 3, 4]},
    "gamma": {"_type":"uniform","_value":[0.01, 0.1]},
    "coef0 ": {"_type":"uniform","_value":[0.01, 0.1]}
}

  • 第二步:在config.yml中配置,设定了运行时的任务标记,如作者名字,实验名字(会在UI上进行标示),尝试训练的并发数,单任务最长执行时间,最多尝试多少次,训练的平台时本地还是远程,搜索空间配置文件路径,调参使用的搜索算法,是否使用GPU等参数(gpuNum: 0表示不使用)。
authorName: default
experimentName: example_sklearn-classification
trialConcurrency: 1
maxExecDuration: 1h
maxTrialNum: 100
#choice: local, remote
trainingServicePlatform: local
searchSpacePath: search_space.json
#choice: true, false
useAnnotation: false
tuner:
  #choice: TPE, Random, Anneal, Evolution
  builtinTunerName: TPE
  classArgs:
    #choice: maximize, minimize
    optimize_mode: maximize
trial:
  command: python3 main.py
  codeDir: .
  gpuNum: 0
  • 在原有代码中加入NNI自动训练框架
1.引入nni包
import nni
2.获取参数配置
RECEIVED_PARAMS = nni.get_next_parameter()
3.向NNI报告训练的中间指标metric(可选)
nni.report_intermediate_result(metrics)
4.向NNI报告训练的最终结果指标metric,例如准确率,loss等
nni.report_final_result(metrics)
5.在主函数中更新模型使用的参数
RECEIVED_PARAMS = nni.get_next_parameter()
PARAMS = get_default_parameters()
PARAMS.update(RECEIVED_PARAMS)
  • 启动训练:
nnictl create --config ./config.yml
  • 我们可以通过输出的Web UI链接地址来访问,IP:8080,在训练过程中UI界面是一直动态更新的,最终我们可以看到训练了100次,也就是尝试了100种不同的参数组合。
import nni
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
import logging
import numpy as np


LOG = logging.getLogger('sklearn_classification')

def load_data():
    '''Load dataset, use 20newsgroups dataset'''
    digits = load_digits()
    X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, random_state=99, test_size=0.25)

    ss = StandardScaler()
    X_train = ss.fit_transform(X_train)
    X_test = ss.transform(X_test)

    return X_train, X_test, y_train, y_test

def get_default_parameters():
    '''get default parameters'''
    params = {
        'C': 1.0,
        'keral': 'linear',
        'degree': 3,
        'gamma': 0.01,
        'coef0': 0.01
    }
    return params

def get_model(PARAMS):
    '''Get model according to parameters'''
    model = SVC()
    model.C = PARAMS.get('C')
    model.keral = PARAMS.get('keral')
    model.degree = PARAMS.get('degree')
    model.gamma = PARAMS.get('gamma')
    model.coef0 = PARAMS.get('coef0')
    
    return model

def run(X_train, X_test, y_train, y_test, PARAMS):
    '''Train model and predict result'''
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)
    LOG.debug('score: %s' % score)
    nni.report_final_result(score)

if __name__ == '__main__':
    X_train, X_test, y_train, y_test = load_data()

    try:
        # get parameters from tuner
        RECEIVED_PARAMS = nni.get_next_parameter()
        LOG.debug(RECEIVED_PARAMS)
        PARAMS = get_default_parameters()
        PARAMS.update(RECEIVED_PARAMS)
        LOG.debug(PARAMS)
        model = get_model(PARAMS)
        run(X_train, X_test, y_train, y_test, model)
    except Exception as exception:
        LOG.exception(exception)
        raise

你可能感兴趣的:([python]使用NNI调参)