【课程作业经验】使用NPU进行MindSpore模型训练

准备工作

使用MindSpore进行模型训练需要做以下的准备工作

  1. 训练的网络模型
  2. 训练数据以及数据处理
  3. 训练回调函数
  4. 启动训练的python脚本

网络模型

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. 图片大小放缩
  2. 随机水平翻转
  3. 随机角度旋转
  4. 数据类型转换
  5. 将(h,w,c)通道的格式转换为(c,h,w)格式

其中(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的方式进行启动的,点击新建训练能看到如下所示的启动代码

【课程作业经验】使用NPU进行MindSpore模型训练_第1张图片

因此,我们要在训练脚本中设置解析参数。

使用云端NPU进行训练

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-Enginemindspore,设置代码目录为/resnet/code/,选择启动文件。

然后在输入/输出数据配置,设置启动脚本中的路径名称,并设置超参,如下所示。

【课程作业经验】使用NPU进行MindSpore模型训练_第2张图片

创建训练

进入Modelarts界面,点击训练管理-训练作业,创建训练作业,输入名称,配置输入文件路径、输出文件路径、超参数、日志,选择资源为Ascend,点击提交,即可开始训练。

【课程作业经验】使用NPU进行MindSpore模型训练_第3张图片

总结

具体代码可以查看 ResNet: 基于MindSpore的残差网络

你可能感兴趣的:(python,深度学习,开发语言)