在ResNet-50网络上应用二阶优化实践

    常见的优化算法可分为一阶优化算法和二阶优化算法。经典的一阶优化算法如SGD等,计算量小、计算速度快,但是收敛的速度慢,所需的迭代次数多。而二阶优化算法使用目标函数的二阶导数来加速收敛,能更快地收敛到模型最优值,所需要的迭代次数少,但由于二阶优化算法过高的计算成本,导致其总体执行时间仍然慢于一阶,故目前在深度神经网络训练中二阶优化算法的应用并不普遍。二阶优化算法的主要计算成本在于二阶信息矩阵(Hessian矩阵、等)的求逆运算,时间复杂度约为$O(n^3)$。

    MindSpore开发团队在现有的自然梯度算法的基础上,对FIM矩阵采用近似、切分等优化加速手段,极大的降低了逆矩阵的计算复杂度,开发出了可用的二阶优化器THOR。使用8块Ascend 910 AI处理器,THOR可以在72min内完成ResNet50-v1.5网络和ImageNet数据集的训练,相比于SGD+Momentum速度提升了近一倍。

    本篇教程将主要介绍如何在Ascend 910 以及GPU上,使用MindSpore提供的二阶优化器THOR训练ResNet50-v1.5网络和ImageNet数据集。

    示例代码目录结构

    整体执行流程如下:

    1. 准备ImageNet数据集,处理需要的数据集;

    2. 定义ResNet50网络;

    3. 定义损失函数和THOR优化器;

    4. 加载数据集并进行训练,训练完成后,查看结果及保存模型文件;

    5. 加载保存的模型,进行推理。

    准备环节

    实践前,确保已经正确安装MindSpore。如果没有,可以通过安装MindSpore。

    下载完整的ImageNet2012数据集,将数据集解压分别存放到本地工作区的ImageNet2012/ilsvrcImageNet2012/ilsvrc_eval路径下。

    目录结构如下:

    1. └─ImageNet2012
    2. ├─ilsvrc
    3. n03676483
    4. n04067472
    5. n01622779
    6. ......
    7. └─ilsvrc_eval
    8. n03018349
    9. n02504013
    10. n07871810
    11. ......

    配置分布式环境变量

    Ascend 910

    Ascend 910 AI处理器的分布式环境变量配置参考分布式并行训练 (Ascend)

    GPU

    GPU的分布式环境配置参考分布式并行训练 (GPU)

    分布式训练时,通过并行的方式加载数据集,同时通过MindSpore提供的数据增强接口对数据集进行处理。加载处理数据集的脚本在源码的src/dataset.py脚本中。

    1. import os
    2. import mindspore.common.dtype as mstype
    3. import mindspore.dataset.engine as de
    4. import mindspore.dataset.vision.c_transforms as C
    5. import mindspore.dataset.transforms.c_transforms as C2
    6. from mindspore.communication.management import init, get_rank, get_group_size
    7. def create_dataset(dataset_path, do_train, repeat_num=1, batch_size=32, target="Ascend"):
    8. if target == "Ascend":
    9. device_num, rank_id = _get_rank_info()
    10. else:
    11. init()
    12. rank_id = get_rank()
    13. device_num = get_group_size()
    14. if device_num == 1:
    15. ds = de.ImageFolderDataset(dataset_path, num_parallel_workers=8, shuffle=True)
    16. else:
    17. ds = de.ImageFolderDataset(dataset_path, num_parallel_workers=8, shuffle=True,
    18. num_shards=device_num, shard_id=rank_id)
    19. image_size = 224
    20. mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
    21. std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
    22. # define map operations
    23. if do_train:
    24. trans = [
    25. C.RandomCropDecodeResize(image_size, scale=(0.08, 1.0), ratio=(0.75, 1.333)),
    26. C.RandomHorizontalFlip(prob=0.5),
    27. C.Normalize(mean=mean, std=std),
    28. C.HWC2CHW()
    29. ]
    30. else:
    31. trans = [
    32. C.Decode(),
    33. C.Resize(256),
    34. C.CenterCrop(image_size),
    35. C.Normalize(mean=mean, std=std),
    36. C.HWC2CHW()
    37. ]
    38. type_cast_op = C2.TypeCast(mstype.int32)
    39. ds = ds.map(operations=trans, input_columns="image", num_parallel_workers=8)
    40. ds = ds.map(operations=type_cast_op, input_columns="label", num_parallel_workers=8)
    41. # apply batch operations
    42. ds = ds.batch(batch_size, drop_remainder=True)
    43. # apply dataset repeat operation
    44. ds = ds.repeat(repeat_num)
    45. return ds

    定义网络

    本示例中使用的网络模型为ResNet50-v1.5,先定义ResNet50网络,然后使用二阶优化器自定义的算子替换Conv2d和 和Dense算子。定义好的网络模型在在源码src/resnet_thor.py脚本中,自定义的算子Conv2d_thorDense_thorsrc/thor_layer.py脚本中。

    • 使用Conv2d_thor替换原网络模型中的Conv2d

    • 使用Dense_thor替换原网络模型中的Dense

    1. ...
    2. from src.resnet_thor import resnet50
    3. ...
    4. if __name__ == "__main__":
    5. ...
    6. net = resnet50(class_num=config.class_num, damping=damping, loss_scale=config.loss_scale,
    7. frequency=config.frequency, batch_size=config.batch_size)
    8. ...

    定义损失函数

    MindSpore支持的损失函数有SoftmaxCrossEntropyWithLogitsL1LossMSELoss等。THOR优化器需要使用SoftmaxCrossEntropyWithLogits损失函数。

    损失函数的实现步骤在src/crossentropy.py脚本中。这里使用了深度网络模型训练中的一个常用trick:label smoothing,通过对真实标签做平滑处理,提高模型对分类错误标签的容忍度,从而可以增加模型的泛化能力。

    1. class CrossEntropy(_Loss):
    2. """CrossEntropy"""
    3. def __init__(self, smooth_factor=0., num_classes=1000):
    4. super(CrossEntropy, self).__init__()
    5. self.onehot = P.OneHot()
    6. self.on_value = Tensor(1.0 - smooth_factor, mstype.float32)
    7. self.off_value = Tensor(1.0 * smooth_factor / (num_classes - 1), mstype.float32)
    8. self.ce = nn.SoftmaxCrossEntropyWithLogits()
    9. self.mean = P.ReduceMean(False)
    10. def construct(self, logit, label):
    11. one_hot_label = self.onehot(label, F.shape(logit)[1], self.on_value, self.off_value)
    12. loss = self.ce(logit, one_hot_label)
    13. loss = self.mean(loss, 0)
    14. return loss

    __main__函数中调用定义好的损失函数:

    1. ...
    2. from src.crossentropy import CrossEntropy
    3. ...
    4. if __name__ == "__main__":
    5. ...
    6. # define the loss function
    7. if not config.use_label_smooth:
    8. config.label_smooth_factor = 0.0
    9. loss = CrossEntropy(smooth_factor=config.label_smooth_factor, num_classes=config.class_num)
    10. ...

    THOR优化器的参数更新公式如下:

    \theta^{t+1} = \theta^t + \alpha F^{-1}\nabla E

    参数更新公式中各参数的含义如下:

    • $\theta$:网络中的可训参数;

    • $t$:迭代次数;

    • $\alpha$:学习率值,参数的更新步长;

    • $F^{-1}$:FIM矩阵,在网络中计算获得;

    • $\nabla E$:一阶梯度值。

    从参数更新公式中可以看出,THOR优化器需要额外计算的是每一层的FIM矩阵,每一层的FIM矩阵就是之前在自定义的网络模型中计算获得的。FIM矩阵可以对每一层参数更新的步长和方向进行自适应的调整,加速收敛的同时可以降低调参的复杂度。

    训练网络

    配置模型保存

    MindSpore提供了callback机制,可以在训练过程中执行自定义逻辑,这里使用框架提供的ModelCheckpoint函数。 ModelCheckpoint可以保存网络模型和参数,以便进行后续的fine-tuning操作。 TimeMonitorLossMonitor是MindSpore官方提供的callback函数,可以分别用于监控训练过程中单步迭代时间和loss值的变化。

    1. ...
    2. from mindspore.train.callback import ModelCheckpoint, CheckpointConfig, TimeMonitor, LossMonitor
    3. ...
    4. if __name__ == "__main__":
    5. ...
    6. # define callbacks
    7. time_cb = TimeMonitor(data_size=step_size)
    8. loss_cb = LossMonitor()
    9. cb = [time_cb, loss_cb]
    10. if config.save_checkpoint:
    11. config_ck = CheckpointConfig(save_checkpoint_steps=config.save_checkpoint_epochs * step_size,
    12. keep_checkpoint_max=config.keep_checkpoint_max)
    13. ckpt_cb = ModelCheckpoint(prefix="resnet", directory=ckpt_save_dir, config=config_ck)
    14. cb += [ckpt_cb]
    15. ...

    配置训练网络

    通过MindSpore提供的model.train接口可以方便地进行网络的训练。THOR优化器通过降低二阶矩阵更新频率,来减少计算量,提升计算速度,故重新定义一个Model_Thor类,继承MindSpore提供的Model类。在Model_Thor类中增加二阶矩阵更新频率控制参数,用户可以通过调整该参数,优化整体的性能。

    1. ...
    2. from mindspore.train.loss_scale_manager import FixedLossScaleManager
    3. from src.model_thor import Model_Thor as Model
    4. ...
    5. if __name__ == "__main__":
    6. ...
    7. loss_scale = FixedLossScaleManager(config.loss_scale, drop_overflow_update=False)
    8. if target == "Ascend":
    9. model = Model(net, loss_fn=loss, optimizer=opt, amp_level='O2', loss_scale_manager=loss_scale,
    10. keep_batchnorm_fp32=False, metrics={'acc'}, frequency=config.frequency)
    11. else:
    12. model = Model(net, loss_fn=loss, optimizer=opt, loss_scale_manager=loss_scale, metrics={'acc'},
    13. amp_level="O2", keep_batchnorm_fp32=True, frequency=config.frequency)
    14. ...

    训练脚本定义完成之后,调scripts目录下的shell脚本,启动分布式训练进程。

    Ascend 910

    目前MindSpore分布式在Ascend上执行采用单卡单进程运行方式,即每张卡上运行1个进程,进程数量与使用的卡的数量一致。其中,0卡在前台执行,其他卡放在后台执行。每个进程创建1个目录,目录名称为train_parallel+ device_id,用来保存日志信息,算子编译信息以及训练的checkpoint文件。下面以使用8张卡的分布式训练脚本为例,演示如何运行脚本:

    使用以下命令运行脚本:

    1. sh run_distribute_train.sh [RANK_TABLE_FILE] [DATASET_PATH] [DEVICE_NUM]

    脚本需要传入变量RANK_TABLE_FILEDATASET_PATHDEVICE_NUM,其中:

    • RANK_TABLE_FILE:组网信息文件的路径。(rank table文件的生成,参考HCCL_TOOL)

    • DEVICE_NUM: 实际的运行卡数。 其余环境变量请参考安装教程中的配置项。

    训练过程中loss打印示例如下:

    1. ...
    2. epoch: 1 step: 5004, loss is 4.4182425
    3. epoch: 2 step: 5004, loss is 3.740064
    4. epoch: 3 step: 5004, loss is 4.0546017
    5. epoch: 4 step: 5004, loss is 3.7598825
    6. epoch: 5 step: 5004, loss is 3.3744206
    7. ...
    8. epoch: 41 step: 5004, loss is 1.8217756
    9. epoch: 42 step: 5004, loss is 1.6453942
    10. ...

    训练完后,每张卡训练产生的checkpoint文件保存在各自训练目录下,device_0产生的checkpoint文件示例如下:

    1. └─train_parallel0
    2. ├─resnet-1_5004.ckpt
    3. ├─resnet-2_5004.ckpt
    4. ......
    5. ├─resnet-42_5004.ckpt
    6. ......

    GPU

    在GPU硬件平台上,MindSpore采用OpenMPI的mpirun进行分布式训练,进程创建1个目录,目录名称为train_parallel,用来保存日志信息和训练的checkpoint文件。下面以使用8张卡的分布式训练脚本为例,演示如何运行脚本:

    脚本需要传入变量DATASET_PATHDEVICE_NUM,其中:

    • DATASET_PATH:训练数据集路径。

    • DEVICE_NUM: 实际的运行卡数。

    在GPU训练时,无需设置DEVICE_ID环境变量,因此在主训练脚本中不需要调用int(os.getenv('DEVICE_ID'))来获取卡的物理序号,同时context中也无需传入device_id。我们需要将device_target设置为GPU,并需要调用init()来使能NCCL。

    训练过程中loss打印示例如下:

    1. ...
    2. epoch: 1 step: 5004, loss is 4.2546034
    3. epoch: 2 step: 5004, loss is 4.0819564
    4. epoch: 3 step: 5004, loss is 3.7005644
    5. epoch: 4 step: 5004, loss is 3.2668946
    6. epoch: 5 step: 5004, loss is 3.023509
    7. ...
    8. epoch: 36 step: 5004, loss is 1.645802
    9. ...

    训练完后,保存的模型文件示例如下:

    1. └─train_parallel
    2. ├─ckpt_0
    3. ├─resnet-1_5004.ckpt
    4. ├─resnet-2_5004.ckpt
    5. ......
    6. ├─resnet-36_5004.ckpt
    7. ......
    8. ......
    9. ├─ckpt_7
    10. ├─resnet-1_5004.ckpt
    11. ├─resnet-2_5004.ckpt
    12. ......
    13. ├─resnet-36_5004.ckpt
    14. ......

    使用训练过程中保存的checkpoint文件进行推理,验证模型的泛化能力。首先通过load_checkpoint接口加载模型文件,然后调用Modeleval接口对输入图片类别作出预测,再与输入图片的真实类别做比较,得出最终的预测精度值。

    定义推理网络

    1. 使用load_checkpoint接口加载模型文件。

    2. 使用model.eval接口读入测试数据集,进行推理。

    3. 计算得出预测精度值。

    1. ...
    2. from mindspore.train.serialization import load_checkpoint, load_param_into_net
    3. ...
    4. if __name__ == "__main__":
    5. ...
    6. # define net
    7. net = resnet(class_num=config.class_num)
    8. net.add_flags_recursive(thor=False)
    9. # load checkpoint
    10. param_dict = load_checkpoint(args_opt.checkpoint_path)
    11. keys = list(param_dict.keys())
    12. for key in keys:
    13. if "damping" in key:
    14. param_dict.pop(key)
    15. load_param_into_net(net, param_dict)
    16. net.set_train(False)
    17. # define model
    18. model = Model(net, loss_fn=loss, metrics={'top_1_accuracy', 'top_5_accuracy'})
    19. # eval model
    20. res = model.eval(dataset)
    21. print("result:", res, "ckpt=", args_opt.checkpoint_path)

    执行推理

    推理网络定义完成之后,调用scripts目录下的shell脚本,进行推理。

    Ascend 910

    在Ascend 910硬件平台上,推理的执行命令如下:

    1. sh run_eval.sh [DATASET_PATH] [CHECKPOINT_PATH]

    脚本需要传入变量DATASET_PATHCHECKPOINT_PATH,其中:

    • DATASET_PATH:推理数据集路径。

    • CHECKPOINT_PATH: 保存的checkpoint路径。

    目前推理使用的是单卡(默认device 0)进行推理,推理的结果如下:

    1. result: {'top_5_accuracy': 0.9295574583866837, 'top_1_accuracy': 0.761443661971831} ckpt=train_parallel0/resnet-42_5004.ckpt
    • top_5_accuracy:对于一个输入图片,如果预测概率排名前五的标签中包含真实标签,即认为分类正确;

    • top_1_accuracy:对于一个输入图片,如果预测概率最大的标签与真实标签相同,即认为分类正确。

    GPU

    在GPU硬件平台上,推理的执行命令如下:

    脚本需要传入变量DATASET_PATHCHECKPOINT_PATH,其中:

    • DATASET_PATH:推理数据集路径。

    • : 保存的checkpoint路径。

    1. result: {'top_5_accuracy': 0.9287972151088348, 'top_1_accuracy': 0.7597031049935979} ckpt=train_parallel/resnet-36_5004.ckpt