使用NNI,从此告别手动调参

一.前言

最近在朋友的介绍下,了解了一个神经网络的调参神器——微软开发的NNI (Neural Network Intelligence),在经过简单尝试之后,发现是真的香。倘若你也苦于每次炼丹都要手动设置超级参数,那你可以选择尝试一下NNI,从此告别繁杂重复的调参参数手动设置。话不多说,直接上干货。

二.NNI怎样帮助调参的?

对于人工智能从业者,炼丹可谓是一门看家本领,但是通常炼丹是一项十分痛苦的事情。一般来说,我们使用的模型都具有很多的超级参数,那究竟怎样的超级参数组合才能使得模型效果最佳呢?我们往往需要进行大量的尝试,对不同参数设置不同的值进行组合,对于每次组合进行一次实验,然后再设置另一组参数进行实验,如此反复直到找到符合理想的一组参数。

那么,我们能不能直接一次性设置多组参数,然后让程序自动跑完一组然后继续跑下一组呢?

当然可以,直接写个脚本也能做到。或者尝试使用NNI,通过NNI我们可以设置好超级参数的搜索空间,然后使用其实现的超级参数调优算法去探索一组理想的超参。此外,它还提供了可视化的WEB界面,能够让我们实时观测程序的运行状态和搜索空间的搜索状态等等。

2.1 NNI调参的工作流程

那么,NNI是怎样做到帮我们自动的探索超参的搜索空间呢,其工作流程大概如下:

Input: 搜索空间(search space), 实验代码(trial code), 配置文件(config file)
Output: 一组最优的超级参数(hyperparameter)配置

1: For t = 0, 1, 2, ..., maxTrialNum(配置文件设置的最大实验次数),
2:      hyperparameter = chose a set of parameter from search space //从搜索空间选择对应的一组参数
3:      final result = run_trial_and_evaluate(hyperparameter) //利用这组参数进行实验,并返回最终的结果
4:      report final result to NNI //将本组超参的最终结果报告给NNI
5:      If reach the upper limit time: // 达到了设置实验运行时间的上限,
6:          Stop the experiment //停止实验
7: return hyperparameter value with best final result // 返回最佳的一组超级参数

了解了其工作流程以后,接下来,博主将代领大家利用NNI进行实践了。

三.NNI调参实践

3.1 NNI的安装

NNI支持如下主流的操作系统:

Ubuntu >= 16.04
macOS >= 10.14.1
Windows 10 >= 1809

其安装命令如下:

# Linux或MacOS
python3 -m pip install --upgrade nni

# Windows
python -m pip install --upgrade nni

安装完成后,若想验证是否安装成功,可以参见官方的提供的示例:

git clone -b v2.6 https://github.com/Microsoft/nni.git

若git clone失败,可以直接把后面的链接贴到浏览器去手动下载。

然后,运行官网的例子:

# Linux或MacOS
nnictl create --config nni/examples/trials/mnist-pytorch/config.yml

# Windows
nnictl create --config nni\examples\trials\mnist-pytorch\config_windows.yml

即在--config后面加上trials目录下minst-pytorch目录下的相应配置文件即可。

若运行成功,可以看到命令行输出的Web UI URLs,选择本机IP:8080,即http://127.0.0.1:8080在浏览器打开就可看到Web界面。

3.2 实验代码说明

本次使用的实验代码是一个利用Resnet网络来对一个汽车分类数据集进行分类的Demo。因为只是尝试下NNI,在实验过程中只探索了最常用的三个超级参数:

batch size
epochs
learning rate

由于需要更新模型中的超级参数,因此引入可变参数

import argparse

def get_params():
    parser = argparse.ArgumentParser()
    parser.add_argument("--batch_size", type=int, default=32)
    parser.add_argument("--epochs", type=int, default=50)
    parser.add_argument("--lr", type=float, default=0.001)
    parser.add_argument("--use_cuda", action='store_true', default=False)
    parser.add_argument('--seed', type=int, default=1, metavar='S',
                        help='random seed (default: 1)')

    args, _ = parser.parse_known_args()
    
    return args

if __name__ == "__main__":
    pass

其实也可以直接简单定义一个以超参变量名为键的字典这里是因为笔者在已有代码上进行修改的缘故才如此)。由于使用NNI,因此只需要将NNI得到的参数名和参数值更新到args中,即:

tuner_params = nni.get_next_parameter()
params = vars(merge_parameter(get_params(), tuner_params))

上述代码中merge_parameter()方法可以将nni中的参数”添加“到args中去,然后通过vars()函数,可以使得我们在实验代码中像用字典一样取对应的超参,例如:

for epoch in range(args['epochs'])

根据2.1节,在使用NNI时,我们还需要向NNI报告当前结果以及报告最终结果,即:

nni.report_intermediate_result(test_acc)

nni.report_final_result(test_acc)

完成主程序的代码,仅供参考:

import nni
import torch
import torch.nn as nn
import torch.utils.data as data
import torch.optim as optim
from data_loader import CarDataset
from model.resnet18 import Resnet18
from configuration import get_params
from nni.utils import merge_parameter
import logging

logger = logging.getLogger('resnet_AutoML')

def train(model,train_iter,loss_fn,optimizer,device):
    """
    功能:训练模型
    """
    model.train()
    train_l_sum, train_acc_sum, n, c = 0,0,0,0
    for x,y in train_iter:
        x,y = x.float().to(device),y.to(device)
        y_hat = model(x)
        l = loss_fn(y_hat,y)
        # 梯度清零
        optimizer.zero_grad()
        # 反向传播
        l.backward()
        # 更新参数
        optimizer.step()
        train_l_sum += l.item()
        train_acc_sum += (y_hat.argmax(dim = 1) == y).sum().item()
        n += y.shape[0]
        c += 1

    return train_acc_sum / n, train_l_sum / c

def evaluate_accuracy(model,test_iter,loss_fn,device):
    """
    功能:在测试集上进行测评
    """
    model.eval()
    test_l_sum,test_acc_sum,n,c = 0.0,0,0,0
    with torch.no_grad():
        for x,y in test_iter:
            x,y = x.float().to(device),y.to(device)
            y_hat = model(x)
            test_l_sum += loss_fn(y_hat,y)
            test_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
            c += 1
    
    return test_acc_sum / n, test_l_sum / c


def main(args):
    # 是否启用GPU加速
    use_cuda = args['use_cuda'] and torch.cuda.is_available()
    
    torch.manual_seed(args['seed'])
    # 定义GPU设备
    device = torch.device("cuda:0" if use_cuda else "cpu")
    
    # 加载数据 
    train_dataset = CarDataset(data_path="car3/train")
    test_dataset = CarDataset(data_path="car3/test")
    train_iter = data.DataLoader(
        dataset=train_dataset,
        batch_size=args['batch_size'],
        shuffle=True,
        num_workers=4
    )
    test_iter = data.DataLoader(
        dataset=test_dataset,
        batch_size=args['batch_size'],
        shuffle=False,
        num_workers=4
    )

    # 加载Resnet模型    
    model = Resnet18(class_num=3).to(device)
    
    # 设定交叉熵作为损失函数
    loss_fn = nn.CrossEntropyLoss()
    
    # 选择Adam优化器
    optimizer = optim.Adam(params=model.parameters(), lr=args['lr'])
    
    for epoch in range(args['epochs']):
        train_acc, train_loss = train(model, train_iter, loss_fn, optimizer, device)
        test_acc, test_loss = evaluate_accuracy(model, test_iter, loss_fn, device)
        print("Epoch {}: train_acc: {:.6} train_loss: {:.4} test_acc: {:.6} test_loss: {:.4}".format(
            epoch, train_acc, train_loss, test_acc, test_loss))
        nni.report_intermediate_result(test_acc)
        logger.debug('test accuracy {:.4f}'.format(test_acc))
        logger.debug('Pipe send intermediate result done.')
    
    # 报告最终结果
    nni.report_final_result(test_acc)
    logger.debug('Final result is %g', test_acc)
    logger.debug('Send final result done.')

if __name__ == "__main__":
    try:
        tuner_params = nni.get_next_parameter()
        logger.debug(tuner_params)
        params = vars(merge_parameter(get_params(), tuner_params))
        # print(type(params))
        main(params)
    except Exception as exception:
        logger.exception(exception)
        raise

3.4 搜索空间配置

使用NNI时,我们需要创建一个搜索空间配置文件,可以为YAML文件或JSON文件,在其中我们可以设置我们使用的超级参数的取值,展示采用JSON格式定义的搜索空间:

{
    "batch_size": {"_type":"choice", "_value": [16, 32, 64]},
    "epochs":{"_type":"choice","_value":[20, 30]},
    "lr":{"_type":"choice","_value":[0.0001, 0.001, 0.01]}
}

3.5 实验配置

除了搜索空间外,还可以指定实验的关键信息,例如实验文件、调参算法等,配置可以采用YAML文件,对应的示例如下:

experimentName: CIFAR10
searchSpaceFile: search_space.json # 指定搜索空间文件
trialCommand: python main.py --use_cuda # 实验运行shell命令
experimentWorkingDirectory: nni-experiments # NNI记录实验日志的目录
trialConcurrency: 1
tuner: # 指定调参算法
  name: TPE
  classArgs:
    optimize_mode: maximize
trainingService: # 本地运行
  platform: local

3.5 运行结果展示

进入命令行,切换到实验代码所在的目录后输入如下命令:

nnictl create --config exp_config.yaml

便可看到命令行的输出如下:
nni_run

在浏览器输入localhost:8080便可以看到如下主界面:

使用NNI,从此告别手动调参_第1张图片

点击Trials detail即可看到具体的实验细节,包括有实验参数的超参组别:

使用NNI,从此告别手动调参_第2张图片

以及各个实验报的中间结果(这里为测试集上的准确率):

使用NNI,从此告别手动调参_第3张图片

四.结语

本文只是NNI调参的入门,具体细节请参见如下资源:

  • NNI 微软官方Github地址
  • NNI使用官方教程

项目的完整源码地址:NNI自动调参示例程序 (有条件的支持一下)
以上便是本文的全部内容,要是觉得不错的话,可以点个赞或关注一下博主,后续还会持续带来各种干货,当然要是有问题的话也请批评指正!!!

你可能感兴趣的:(深度学习实战,pytorch,深度学习,torch_geometric)