MindSpore实现ResNet50详解(附单机+集群代码)
计算机视觉(Compute Vision,CV)给计算机装上了“眼睛”,让计算机像人类一样也有“视觉”能力,能够“看”懂图片里的内容。
作为深度学习领域的最重要的应用场景之一,在手机拍照、智能安防、自动驾驶等场景均有广泛的应用。同时也出现了一批经典的网络,如AlexNet、ResNet等。本文以典型的图片分类网络ResNet50为例,介绍一下如何使用MindSpore来完成一个CV应用的开发及部署。
卷积神经网络能够很好的提取图片中的特征,并且能够通过降维来减少计算量,在CV领域应用的非常广泛。在早期的卷积神经网络中,往往通过增加网络的深度来获取更好的算法效果,然而随着网络深度的加深,梯度的反向传播路径会越长,由链式法则的原理可以看出,反向传播的路径越长,得到的梯度值越趋近于0,从而导致梯度消失。
残差网络(如图x所示)的出现解决这个问题。通过不同的残差网络的组合,构成了一系列的ResNet网络结构,如表x所示。
这里以ResNet50为例,介绍一下如何使用MindSpore来完成一个CV的典型算法的编码、训练、部署等环节。
在此仅列出了部分重要代码片段,完整代码请参考:
https://gitee.com/mindspore/mindspore/blob/master/model_zoo/resnet
1. 网络定义
通过MindSpore的提供的接口可以很方便的构建所需要的网络,如ResNet50所使用的残差结构定义如下:
class ResidualBlock(nn.Cell):
"""
ResNet V1 residual block definition.
Args:
in_channel (int): Input channel.
out_channel (int): Output channel.
stride (int): Stride size for the first convolutional layer. Default: 1.
Returns:
Tensor, output tensor.
Examples:
>>> ResidualBlock(3, 256, stride=2)
"""
expansion = 4
def __init__(self,
in_channel,
out_channel,
stride=1):
super(ResidualBlock, self).__init__()
channel = out_channel // self.expansion
self.conv1 = _conv1x1(in_channel, channel, stride=1)
self.bn1 = _bn(channel)
self.conv2 = _conv3x3(channel, channel, stride=stride)
self.bn2 = _bn(channel)
self.conv3 = _conv1x1(channel, out_channel, stride=1)
self.bn3 = _bn_last(out_channel)
self.relu = nn.ReLU()
self.down_sample = False
if stride != 1 or in_channel != out_channel:
self.down_sample = True
self.down_sample_layer = None
if self.down_sample:
self.down_sample_layer = nn.SequentialCell([_conv1x1(in_channel, out_channel, stride),
_bn(out_channel)])
self.add = P.TensorAdd()
def construct(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.down_sample:
identity = self.down_sample_layer(identity)
out = self.add(out, identity)
out = self.relu(out)
return out
2. 数据集
我们使用开源数据集ImageNet2012作为训练集,使用MindSpore提供的dataset接口构建一个训练集。MindSpore支持多种数据集的格式,这里准备的ImageNet2012是以原始图片的格式存储的,因此我们使用如下接口先将数据集打开:
import mindspore.dataset.engine as de
ds = de.ImageFolderDatasetV2(dataset_path, num_parallel_workers=8, shuffle=True)
随后需要定义数据增强的过程,并将其应用到数据集的特定列上:
trans = [
C.RandomCropDecodeResize(image_size, scale=(0.08, 1.0), ratio=(0.75, 1.333)),
C.RandomHorizontalFlip(prob=0.5),
C.Normalize(mean=mean, std=std),
C.HWC2CHW()
]
ds = ds.map(input_columns="image", num_parallel_workers=12, operations=trans)
最后需要定义数据集需要重复多少次,即训练的时候需要遍历多少遍数据集(epoch),以及每次从数据集中取出的数据的大小(batch size):
ds = ds.repeat(repeat_num)
ds = ds.batch(batch_size, drop_remainder=True)
3. 训练
MindSpore支持多种后端作为加速器来进行网络训练,这里我们使用Ascend 910来训练。在脚本上只需要将device_target设置为”Ascend”即可
context.set_context(mode=context.GRAPH_MODE, device_target="Ascend", save_graphs=False)
之后调用步骤1和2中的接口创建数据集和网络模型:
dataset = create_dataset(dataset_path=args_opt.dataset_path, do_train=True, repeat_num=config.epoch_size, batch_size=config.batch_size, target=target)
net = resnet(class_num=config.class_num)
因为要进行训练,所以还需要定义一个损失函数和优化器来进行参数更新,这里我们选用常用的交叉熵损失函数和动量优化器:
loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
opt = Momentum(filter(lambda x: x.requires_grad, net.get_parameters()), lr, config.momentum, config.weight_decay, config.loss_scale)
同时,为了后续的精度验证,我们需要把过程中的权值保存到文件,即checkpoint文件,MindSpore的checkpoint的文件是通过Callback来完成的,因此我们需要定义如下的checkpoint保存的Callback:
config_ck = CheckpointConfig(save_checkpoint_steps=config.save_checkpoint_epochs * step_size, keep_checkpoint_max=config.keep_checkpoint_max)
ckpt_cb = ModelCheckpoint(prefix="resnet", directory=ckpt_save_dir, config=config_ck)
最后,将网络模型、损失函数、优化器组合在一起构建一个model,并调用model的train接口来启动训练过程:
model = Model(net, loss_fn=loss, optimizer=opt)
model.train(config.epoch_size, dataset, callbacks=ckpt_cb)
4. 推理/验证
训练结束后,我们希望验证一下训练的效果如何,即使用所保存的checkpoint文件用来做推理,精度如何。MindSpore同时提供了eval接口可以很方便的进行推理验证。
推理的流程和训练基本一致,需要创建验证数据集、定义网络模型并将checkpoint里的权值load到网络模型里进行初始化。由于推理仅需要执行正向流程,因此推理不需要定义优化器。相关接口如下:
dataset = create_dataset(dataset_path=dataset_path, do_train=False, batch_size=config.batch_size,target="Ascend")
net = resnet(class_num=config.class_num)
param_dict = load_checkpoint(args_opt.checkpoint_path)
load_param_into_net(net, param_dict)
net.set_train(False)
loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
model = Model(net, loss_fn=loss, metrics={'top_1_accuracy', 'top_5_accuracy'})
res = model.eval(dataset)
如下图所示,经过90个epoch的训练,最终精度收敛到了76.76%
经过上面的流程,我们已经可以将ResNet50的网络成功的训练起来了,但对于像ImageNet这样的大型数据集,使用单个加速器去训练会耗时非常久。
在单个Ascend910上,ResNet50的吞吐率只有2115images/sec,而ImageNet一共约有128万张图片,也就是说使用单个Ascend910训练一个epoch需要约10分钟,完整训练90个epoch则需要900分钟。如果将加速器换成GPU V100,完整训练则需要耗时约1500分钟。因此,通常情况下我们会考虑使用集群来加速训练过程。
我们回顾一下网络的训练流程,它是将数据集中的数据输入到网络中,网络通过正反向计算得到梯度,随后优化器将梯度应用到权值上进行权值更新。从这个流程可以看出,在集群上,我们可以通过将数据集划分成多块,从而使用数据并行来执行训练。然而,得益于MindSpore的自动并行机制,这些过程在MindSpore中几乎是一键式的。
首先,我们只需要在创建数据集的时候传入期望切分的个数以及当前设备期望拿到哪一份数据。
通常会将这两个值设置为加速器的个数(device_num)以及当前加速器的id(rank_id):
ds = de.ImageFolderDatasetV2(dataset_path, num_parallel_workers=8, shuffle=True,
num_shards=device_num, shard_id=rank_id)
然后,在训练脚本中,通过context,打开自动并行的开关,就可以很方便的将训练任务在多个加速器间并行执行起来。
context.set_auto_parallel_context(device_num=args_opt.device_num, parallel_mode=ParallelMode.DATA_PARALLEL, mirror_mean=True)
最后,如果对分布式训练以及集合通信有所了解的话,可以通过调整集合通信数据的切分点来进行性能的进一步调优。
auto_parallel_context().set_all_reduce_fusion_split_indices([107, 160])
我们分别测试了MindSpore+Ascend910以及TensorFlow+GPU V100在集群下的性能表现,结果如下:
从图中可以看到,MindSpore+Ascend910在16pcs 时,性能是TensorFlow+GPU V100的1.9倍!性能无敌了!
CV作为人工智能的最重要的应用之一,极大的提高了生产场景中的效率以及智能化程度。通过使用MindSpore可以很方便、高效地完成CV典型应用的构建、训练、验证、部署等过程。并且通过MindSpore的自动并行机制,可以实现CV应用在生产环节中快速适应硬件资源的动态扩展及性能调优。
- 点赞
- 收藏
- 关注作者
评论(0)