使用MindSpore进行模型训练需要做以下的准备工作
MindSpore提供了一个MindVision的开源网络工具箱,其中包含了很多的网络结构以及预训练数据,但在云端的NPU环境中 (当时使用的是mindspore_1.5.1-cann_5.0.3-py_3.7-euler_2.8.3-aarch64) ,会直接报错,因此需要我们自己编写网络结构。
编写网络结构可以参考MindSpore的官网,以下是一个简单的ResNet18的网络结构。
编写网络结构的要点在于,在 __init__
方法中定义网络中不同层的结构,在 construct
方法中设置不同层之间的连接关系。
import mindspore.nn as nn
class ResBlock2RT(nn.Cell):
def __init__(self, in_channel):
super(ResBlock2RT, self).__init__()
self.seq = nn.SequentialCell(
[
nn.Conv2d(in_channel, in_channel, kernel_size=3, stride=1),
nn.BatchNorm2d(in_channel),
nn.ReLU(),
nn.Conv2d(in_channel, in_channel, kernel_size=3, stride=1),
nn.BatchNorm2d(in_channel),
])
self.relu = nn.ReLU()
def construct(self, x):
x = self.seq(x) + x
return self.relu(x)
class ResBlock2DS(nn.Cell):
def __init__(self, in_channel, out_channel, stride):
super(ResBlock2DS, self).__init__()
self.seq = nn.SequentialCell(
[
nn.Conv2d(in_channel, out_channel, 3, stride=stride),
nn.BatchNorm2d(out_channel),
nn.ReLU(),
nn.Conv2d(out_channel, out_channel, 3, stride=1),
nn.BatchNorm2d(out_channel),
])
self.shortup = nn.SequentialCell(
[
nn.Conv2d(in_channel, out_channel, 1, stride=stride),
nn.BatchNorm2d(out_channel),
])
self.relu = nn.ReLU()
def construct(self, x):
y = self.seq(x) + self.shortup(x)
return self.relu(y)
class ResNet18(nn.Cell):
def __init__(self, class_num=10, input_size=224):
super(ResNet18, self).__init__()
self.PreProcess = nn.SequentialCell([
nn.Conv2d(3, 64, 7, stride=2),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3, stride=2, pad_mode='same'),
])
self.ResBlock = nn.SequentialCell([
ResBlock2RT(64),
ResBlock2RT(64),
ResBlock2DS(64, 128, 2),
ResBlock2RT(128),
ResBlock2DS(128, 256, 2),
ResBlock2RT(256),
ResBlock2DS(256, 512, 2),
ResBlock2RT(512),
])
self.LastProcess = nn.SequentialCell([
nn.AvgPool2d(input_size // 32),
nn.Flatten(),
nn.Dense(512, 1024),
nn.ReLU(),
nn.Dense(1024, 512),
nn.ReLU(),
nn.Dropout(keep_prob=0.5),
nn.Dense(512, class_num),
nn.Softmax(),
])
def construct(self, x):
x = self.PreProcess(x)
x = self.ResBlock(x)
y = self.LastProcess(x)
return y
既然使用云端NPU进行训练,固然要将数据上传到云端。MindSpore可以使用华为云的对象存储服务存储需要训练的数据;在启动训练后,MindSpore可以挂载Obs桶内的一个文件夹到本地的一个文件夹中,这样用户就可以通过Obs桶进行加载训练数据。
如果我们的训练数据是由大量的小文件组织起来,上传会比较麻烦。所以我们可以将训练数据进行压缩,然后可以使用python自带的 zipfile
库进行文件解压,使用起来比较简单,解压代码只有一行,其余的代码是用于判断文件存在性以及输出日志。
import os
import zipfile
import logging
def unzip_file(file_name, target):
logging.info('unzip file {}'.format(file_name))
with zipfile.ZipFile(file_name) as zf:
zf.extractall(path=target)
logging.info('file {} unzipped.'.format(file_name))
对分类模型,其数据格式是一张图片对应一个标签。我们可以使用一个类将硬盘数据转换为MindSpore模型,也可以使用列表、迭代器,其比较重要的是 __len__
以及 __getitem__
这两个方法,作用是计算数据个数以及使用索引的方式获取数据。
其大概的格式如下,注意 __getitem__
方法中需要同时返回图片的数据以及标签。
class DatasetGenerator:
def __init__(self, data_path: str, label_path: str, resize=None):
self.data = []
self.label = []
...
def __getitem__(self, item):
...
return img, label
def __len__(self):
return len(self.label)
如果不是所有数据都会用于训练,就要进行数据分割。数据分割可以手工分割,也可以通过代码实现。以下是实现数据分割的简单的代码。
import time
import numpy as np
class SubSet:
def __init__(self, dataset, ls):
self.dataset = dataset
self.ls = ls
def __getitem__(self, item):
return self.dataset[self.ls[item]]
def __str__(self):
return str(self.ls)
def __len__(self):
return len(self.ls)
def random_split(dataset, target_size, seed=int(time.time())):
data_length = len(dataset)
combine_ls = list(range(data_length)) # get the dataset index
out_ls = []
np.random.seed(seed)
for item in target_size:
x = np.random.choice(combine_ls, size=int(data_length * item), replace=False)
tuple(map(combine_ls.remove, x))
out_ls.append(SubSet(dataset, x))
return out_ls
具体的使用方式是这样的, random_split
会使用列表保存几个分割后的子集。
x = [i for i in range(100)]
y = random_split(x, (0.1, 0.2, 0.3, 0.4))
使用 mindspore.dataset.GeneratorDataset
将数据转换为MindSpore的数据格式,以下代码包含了数据加载以及数据分割,结果是将数据导入为训练以及测试数据集。
import mindspore.dataset as ds
ds_gen = DatasetGenerator(data_path=data_path,
label_path=label_path,
# resize=resize
)
# split dataset to train dataset and valid dataset
train_gen, valid_gen = random_split(ds_gen, (0.75, 0.25))
train_ds = ds.GeneratorDataset(train_gen, ["image", "label"], shuffle=True)
valid_ds = ds.GeneratorDataset(valid_gen, ["image", "label"], shuffle=True)
仅仅如此还是不够的,我们还需要进行下一步的处理,即需要
其中(1),(5)是数据格式变换,为必选项;(2)-(4)为数据增强,是可选项。具体的代码如下:
import mindspore.dataset.vision.c_transforms as cv_trans
import mindspore.dataset.transforms.c_transforms as c_trans
resize_op = cv_trans.Resize(size=(resize, resize), interpolation=Inter.BICUBIC)
random_hor_flip_op = cv_trans.RandomHorizontalFlip()
random_rotation_op = cv_trans.RandomRotation(10, resample=Inter.BICUBIC)
type_cast_op_image = c_trans.TypeCast(dtype.float32)
type_cast_op_label = c_trans.TypeCast(dtype.int32)
HWC2CHW = cv_trans.HWC2CHW()
preprocess_operation = [resize_op,
random_hor_flip_op,
random_rotation_op,
type_cast_op_image,
HWC2CHW]
train_ds = train_ds.map(operations=preprocess_operation,
input_columns="image")
train_ds = train_ds.map(operations=type_cast_op_label, input_columns="label")
train_ds = train_ds.batch(batch_size)
valid_ds = valid_ds.map(operations=preprocess_operation,
input_columns="image")
valid_ds = valid_ds.map(operations=type_cast_op_label, input_columns="label")
valid_ds = valid_ds.batch(batch_size)
训练时如果需要查看模型的精度,以及保存模型训练过程中迭代最优的结果,需要重载 mindspore.train.callback.Callback
类,以下是回调类的样式。
import logging
import mindspore
import mindspore.nn as nn
from mindspore.train.callback import Callback
def get_accuracy(network, datasets):
metric = nn.Accuracy('classification')
for x, y in datasets:
y_hat = network(x)
metric.update(y_hat, y)
return metric.eval()
def get_loss(network, loss_f, dataset):
loss = 0
for x, y in dataset:
y_hat = network(x)
loss += loss_f(y_hat, y)
return loss
class AccuracyMonitor(Callback):
def __init__(self, save_url, valid_data, export):
self.save_file_pth = save_url + 'auto-save.ckpt'
self.best_file_pth = save_url + 'best.ckpt'
self.local_best_pt = './best.ckpt'
self.valid_data = valid_data
self.export = export
self.max_valid_acc = 0
def epoch_end(self, run_context):
"""Called after each epoch finished."""
callback_params = run_context.original_args()
cur_epoch_num = callback_params.cur_epoch_num
epoch_num = callback_params.epoch_num
network = callback_params.network
train_data = callback_params.train_dataset
train_accu = get_accuracy(network, train_data)
valid_accu = get_accuracy(network, self.valid_data)
loss = get_loss(network, callback_params.loss_fn, train_data)
loss /= callback_params.batch_num
if valid_accu > self.max_valid_acc:
self.max_valid_acc = valid_accu
mindspore.save_checkpoint(network, self.local_best_pt)
mindspore.save_checkpoint(network, self.best_file_pth)
logging.info('Save the best state to {}'.format(self.local_best_pt))
elif not cur_epoch_num % 10 and cur_epoch_num:
mindspore.save_checkpoint(network, self.save_file_pth)
logging.debug('Auto-save state to {}'.format(self.save_file_pth))
print('epoch:[{}/{}] Loss:{} Train Accuracy:{} Valid Accuracy:{}'.format(cur_epoch_num, epoch_num,
loss, train_accu, valid_accu))
MindSpore封装了高阶的训练接口,训练时只需要定义好训练网络(net)、损失函数(loss)、优化器(optim)、训练轮次(args.epochs)、训练数据(t_dt)、回调列表(callback_list)即可,如下所示。
loss = ...
optim = ...
callback_list = [...]
model = Model(net, loss_fn=loss, optimizer=optim)
model.train(args.epochs, t_dt, callback_list, False)
使用云端NPU进行训练,需要使用命令行进行交互,可以使用python的 argparse
的库进行操作,具体可以参考https://docs.python.org/3/library/argparse.html。
由于Modelarts进行云端训练是通过Shell的方式进行启动的,点击新建训练能看到如下所示的启动代码
因此,我们要在训练脚本中设置解析参数。
Modelarts是华为提供的一个PaaS,使用Modelarts进行训练,需要进行以下工作。
在对象存储服务中新建一个桶,在桶的对象中建立如下目录
resnet/
|-code/
|-data/
|-images/
|-log/
|-output/
其中,resnet是我们的项目工程的总目录,属于可选项;code目录用于存放代码; data目录用于存放数据信息,其中的images目录存放图片数据,dataset.txt存放图片的数据的标签; log目录用于存放输出的日志信息;output目录用于存放输出的数据信息。
具体是将数据上传到obs桶中的data/目录中,可以使用obsutil进行上传;也可以压缩后打包上传,再进行解压缩。
将所有的算法文件上传到obs桶的/resnet/code/目录下。
进入Modelarts界面,点击算法管理,创建,选择AI引擎为Ascend-Powered-Engine,mindspore,设置代码目录为/resnet/code/,选择启动文件。
然后在输入/输出数据配置,设置启动脚本中的路径名称,并设置超参,如下所示。
进入Modelarts界面,点击训练管理-训练作业,创建训练作业,输入名称,配置输入文件路径、输出文件路径、超参数、日志,选择资源为Ascend,点击提交,即可开始训练。
具体代码可以查看 ResNet: 基于MindSpore的残差网络