最近在朋友的介绍下,了解了一个神经网络的调参神器——微软开发的NNI (Neural Network Intelligence),在经过简单尝试之后,发现是真的香。倘若你也苦于每次炼丹都要手动设置超级参数,那你可以选择尝试一下NNI,从此告别繁杂重复的调参参数手动设置。话不多说,直接上干货。
对于人工智能从业者,炼丹可谓是一门看家本领,但是通常炼丹是一项十分痛苦的事情。一般来说,我们使用的模型都具有很多的超级参数,那究竟怎样的超级参数组合才能使得模型效果最佳呢?我们往往需要进行大量的尝试,对不同参数设置不同的值进行组合,对于每次组合进行一次实验,然后再设置另一组参数进行实验,如此反复直到找到符合理想的一组参数。
那么,我们能不能直接一次性设置多组参数,然后让程序自动跑完一组然后继续跑下一组呢?
当然可以,直接写个脚本也能做到。或者尝试使用NNI,通过NNI我们可以设置好超级参数的搜索空间,然后使用其实现的超级参数调优算法去探索一组理想的超参。此外,它还提供了可视化的WEB界面,能够让我们实时观测程序的运行状态和搜索空间的搜索状态等等。
那么,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支持如下主流的操作系统:
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界面。
本次使用的实验代码是一个利用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
使用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]}
}
除了搜索空间外,还可以指定实验的关键信息,例如实验文件、调参算法等,配置可以采用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
进入命令行,切换到实验代码所在的目录后输入如下命令:
nnictl create --config exp_config.yaml
在浏览器输入localhost:8080
便可以看到如下主界面:
点击Trials detail
即可看到具体的实验细节,包括有实验参数的超参组别:
以及各个实验报的中间结果(这里为测试集上的准确率):
本文只是NNI调参的入门,具体细节请参见如下资源:
项目的完整源码地址:NNI自动调参示例程序 (有条件的支持一下)
以上便是本文的全部内容,要是觉得不错的话,可以点个赞或关注一下博主,后续还会持续带来各种干货,当然要是有问题的话也请批评指正!!!