基于Mindspore框架与ModelArts 平台的MNIST手写体识别实验

背景简介/问题描述

该实验包含了2部分内容:

1.基于Mindspore框架的模型本地训练及预测

2.基于Modelarts平台和Tensorflow框架的模型训练及部署

1.1 基于Mindspore框架的模型本地训练及预测

要求实现一个简单的图片分类的功能,整体流程如下:

1、处理需要的数据集,这里使用了MNIST数据集。

2、定义一个网络,这里我们使用LeNet网络。

3、定义损失函数和优化器。

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

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

验证模型,加载测试数据集和训练后的模型,验证结果精度。

1.2 基于Modelarts平台和Tensorflow框架的模型训练及部署

要求将本地的自定义算法通过简单的代码适配,实现在ModelArts上进行模型训练与部署的全流程。

要求使用PyTorch1.8实现手写数字图像识别,示例采用的数据集为MNIST官方数据集。

通过案例,要求了解在ModelArts平台上训练作业、部署推理模型并预测的完整流程。

操作样例前流程如下:

  1. 准备训练数据:下载MNIST数据集。
  2. 准备训练文件和推理文件:编写训练与推理代码。
  3. 创建OBS桶并上传文件:创建OBS桶和文件夹,并将数据集和训练脚本,推理脚本,推理配置文件上传到OBS中。
  4. 创建训练作业:进行模型训练。
  5. 推理部署:训练结束后,将生成的模型导入ModelArts用于创建 AI 应用,并将AI应用部署为在线服务。
  6. 预测结果:上传一张手写数字图片,发起预测请求获取预测结果。
  7. 清除资源:运行完成后,停止服务并删除OBS中的数据,避免不必要的扣费。

算法介绍

2.1 本地训练实现

2.1.1 实验准备

2.1.1.1 MindSpore准备

在动手进行实践之前安装相应版本的MindSpore到电脑,并进行好环境配置。

根据电脑配置,我选择安装的是1.10.0的windows的python3.9的CPU版本。

另外,还需要在运行时进行相应缺失的库的安装。由于我是在anaconda环境下安装的,该有的库基本都装过都有,所以这里省去了一些安装库的步骤;但我第一次尝试的时候用的是本地的python环境,运行时就出现了很多报错,因为很多库没有装。

同时,安装mindstore的时候还应该注意安装到了哪个环境里,比如我曾经装过anaconda和anaconda3两个环境,在直接用pip指令装的时候由于路径优先级,优先装在anaconda的环境而非其他,因此在使用anaconda3时应该将anaconda prompt的目标路径设置成anaconda3之后再使用conda指令进行安装。

2.1.1.2 数据准备

示例中用到的MNIST数据集是由10类28*28的灰度图片组成,训练数据集包含60000张图片,测试数据集包含10000张图片。

MNIST数据集下载页面如下:http://yann.lecun.com/exdb/mnist/。页面提供4个数据集下载链接,其中前2个文件是训练数据需要,后2个文件是测试结果需要。

将数据集下载并解压到本地路径下,这里将数据集解压分别存放到工作区的./MNIST_Data/train、./MNIST_Data/test路径下。

目录结构如下:

└─MNIST_Data

    ├─  test

    │      t10k-images.idx3-ubyte

    │      t10k-labels.idx1-ubyte

    │

    └─  train

            train-images.idx3-ubyte

            train-labels.idx1-ubyte

为了方便样例使用,我们在样例脚本中添加了自动下载数据集的功能。

注意,此处必须是把文件直接存在test或train文件夹下,而非文件夹。

2.1.2 实现思路与步骤

此次实验的本地阶段主要分为以下几个步骤:导入Python库和模块并配置运行信息,数据处理,网络定义,损失函数及优化器定义,以及训练和验证。

2.1.2.1 导入Python库和模块并配置运行信息

在使用前,先提前导入一些明面上需要的Python库,例如目前使用到os库等。

我们是通过context.set_context来配置运行需要的信息的,譬如运行模式、后端信息、硬件等信息。因此导入context模块来配置运行需要的信息,其代码如下。

parser.add_argument('--device_target', type=str, default=" Ascend", choices=['Ascend', 'GPU', 'CPU'], help='device where the code will be implemented (default: Ascend)')

在样例中我们配置样例运行使用图模式。再train.py和eval.py根据实际情况配置硬件信息,譬如代码运行在Ascend AI处理器上,则device_target选择Ascend,代码运行在CPU、GPU同理。

在本地实验中,应当按自己的电脑配置进行选择,比如我就选的是CPU模式,要将代码中一部分改为:...default="CPU"...。

2.1.2.2 数据处理

此处的代码都已经融入在算法实现处的代码之中,因此仅介绍数据处理包含的一些功能。数据集对于训练非常重要,好的数据集可以有效提高训练精度和效率。在加载数据集前,我们通常会对数据集进行一些处理。

首先我们要定义一个函数create_dataset来创建数据集。

在这个函数中,我们定义好需要进行的数据增强和处理操作:

1. 定义数据集

2. 定义进行数据增强和处理所需要的一些参数

3. 根据参数,生成对应的数据增强操作

4. 使用map映射函数,将数据操作应用到数据集

5. 对生成的数据集进行处理

其次我们根据数据集存储地址,生成数据集:

def create_dataset(data_dir, training=True, batch_size=32, resize=(32, 32), rescale=1/(255*0.3081), shift=-0.1307/0.3081, buffer_size=64):

之后生成训练集和测试集的路径:

data_train = os.path.join(data_dir, 'train') # train set

data_test = os.path.join(data_dir, 'test') # test set

然后利用MnistDataset方法读取mnist数据集,如果training是True则读取训练集,否则读取测试集:

ds = ms.dataset.MnistDataset(data_train if training else data_test)

下面是一些对数据集的简单处理操作,此处介绍map和shuffle两种:

map方法是非常有效的方法,可以整体对数据集进行处理,resize改变数据形状,rescale进行归一化,HWC2CHW改变图像通道:

ds = ds.map(input_columns=["image"], operations=[CV.Resize(resize), CV.Rescale(rescale, shift), CV.HWC2CHW()])

我们可以利用map方法改变数据集标签的数据类型,方法如下:

ds = ds.map(input_columns=["label"], operations=C.TypeCast(ms.int32))

而shuffle是打乱操作,同时设定了batchsize的大小,并将最后不足一个batch的数据抛弃掉:

ds.shuffle(buffer_size=buffer_size).batch(batch_size, drop_remainder=True)

return ds

其中,batch_size:每组包含的数据个数,现设置每组包含32个数据。

在进行数据集处理时,一般先进行修改图片尺寸,归一化,修改图像频道数等工作,再修改标签的数据类型。最后进行shuffle操作,同时设定batch_size,设置drop_remainder为True,则数据集中不足最后一个batch的数据会被抛弃。

MindSpore支持进行多种数据处理和增强的操作,各种操作往往组合使用。所有的默认的数据集创建和处理都在dataset.py中,不需自己进行修正。

当然,最后我们还需要根据数据集存储地址,生成数据集,构建训练和验证函数:

ds_train = create_dataset(os.path.join(args.data_path, "train"), cfg.batch_size)

ds_eval = create_dataset(os.path.join(args.data_path, "test"), cfg.batch_size,1)

2.1.2.3 网络定义

我们选择相对简单的LeNet网络。LeNet网络不包括输入层的情况下,共有7层:2个卷积层、2个下采样层(池化层)、3个全连接层。每层都包含不同数量的训练参数,如下图所示:

LeNet-5

 

使用MindSpore定义神经网络需要继承mindspore.nn.cell.Cell。Cell是所有神经网络(Conv2d等)的基类。

神经网络的各层需要预先在__init__方法中定义,然后通过定义construct方法来完成神经网络的前向构造。按照LeNet的网络结构,定义网络各层如下:

首先定义模型结构,MindSpore中的模型时通过construct定义模型结构,在__init__中初始化各层的对象:

class LeNet5(nn.Cell):

    def __init__(self, num_class=10, num_channel=1):

        super(LeNet5, self).__init__()

其次定义卷积层,ReLU激活函数,平坦层和全连接层

其中conv2d的输入通道为1维,输出为6维,卷积核尺寸为5*5,步长为1,不适用padding:

self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')

self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')

self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))

self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))

self.fc3 = nn.Dense(84, num_class, weight_init=Normal(0.02))

self.relu = nn.ReLU()

self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)

self.flatten = nn.Flatten()

    最后构建Lenet5架构,x代表网络的输入:

def construct(self, x):

        x = self.max_pool2d(self.relu(self.conv1(x)))

        x = self.max_pool2d(self.relu(self.conv2(x)))

        x = self.flatten(x)

        x = self.relu(self.fc1(x))

        x = self.relu(self.fc2(x))

        x = self.fc3(x)

        return x

网络的构建也是已经给出的,我们要做的就是把lenet/src/lenet.py中的代码改成这个网络架构的代码即可。

2.1.2.4 损失函数及优化器定义

在进行定义之前,先简单介绍损失函数及优化器的概念。

损失函数:又叫目标函数,用于衡量预测值与实际值差异的程度。深度学习通过不停地迭代来缩小损失函数的值。定义一个好的损失函数,可以有效提高模型的性能。

优化器:用于最小化损失函数,从而在训练过程中改进模型。

定义了损失函数后,可以得到损失函数关于权重的梯度。梯度用于指示优化器优化权重的方向,以提高模型性能。

MindSpore支持的损失函数有SoftmaxCrossEntropyWithLogits、L1Loss、MSELoss等。这里使用SoftmaxCrossEntropyWithLogits损失函数。

MindSpore提供了callback机制,可以在训练过程中执行自定义逻辑,这里使用框架提供的ModelCheckpoint为例。 ModelCheckpoint可以保存网络模型和参数,以便进行后续的fine-tuning(微调)操作。

因此我们这次实验就使用这个框架进行优化和损失函数定义即可,即,将这里的代码相应的复制到train函数和eval函数相应的部分,

#设定loss函数

net_loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction="mean")

#设定优化器

net_opt = nn.Momentum(network.trainable_params(), cfg.lr, cfg.momentum)

    #编译形成模型

model = Model(network, net_loss, net_opt, metrics={"Accuracy": Accuracy()})

# 训练网络 train.py

model.train(cfg['epoch_size'], ds_train, callbacks=[time_cb, ckpoint_cb, LossMonitor()], dataset_sink_mode=args.dataset_sink_mode)

2.1.2.5 其余代码修改总结

在train.py中,如果是如数据准备中路径设置,则和函数中默认值一样,不需修改。在eval.py中,需要写清一个具体的.ckpt文件路径和文件名,例如下列代码中标红部分:

parser.add_argument('--ckpt_path', type=str, default="./ckpt/checkpoint_lenet-1_1875.ckpt", help='if mode is test, must be the path where the trained ckpt file')

2.1.2.6 训练和验证过程

编译运行train.py和eval.py完成模型的训练及验证过程。

训练过程中会打印loss值。loss值会波动,但总体来说train.py在epoch变多时打印的loss值会逐步减小,eval.py根据其结果的.ckpt文件打印出的精度逐步提高。

2.2 平台训练实现

2.2.1 准备工作

2.2.1.1 前提条件

已注册华为云帐号,且在使用ModelArts前检查帐号状态,帐号不能处于欠费或冻结状态。

2.2.1.2 准备训练数据

本案例使用的数据是MNIST数据集,从MNIST官网下载四个数据集至本地。四个数据集具体情况如下:

“train-images-idx3-ubyte.gz”:训练集的压缩包文件。训练集,共包含60000个样本。

“train-labels-idx1-ubyte.gz”:训练集标签的压缩包文件。训练集标签,共包含60000个样本的类别标签。

“t10k-images-idx3-ubyte.gz”:验证集的压缩包文件。验证集,共包含10000个样本。

“t10k-labels-idx1-ubyte.gz”:验证集标签的压缩包文件。验证集标签,共包含10000个样本的类别标签。

注意,这里准备的训练数据都是压缩包形式,而不是解压缩后的文件!

2.2.1.3 准备训练文件和推理文件

需要准备train.py,customize_service.py以及config.json三个文件,第一个为训练文件,后两个为推理文件。

此三个文件样例中都有提供,只需创建相应的空白文件并将代码复制进去保存上传即可。同时如果有需要还可以按需进行修改。

2.2.2 创建OBS桶和训练作业

2.2.2.1 创建OBS桶

将训练使用的数据和代码文件、推理代码文件与推理配置文件,上传到OBS桶中。在 ModelArts 上运行训练作业时,需要从OBS桶中读取数据和代码文件。

创建OBS桶并上传数据的步骤如下:

  • 登录OBS管理控制台,按示例创建OBS桶和文件夹。

  • 上传文件

上传Step1 准备训练数据下载的MNIST数据集压缩包文件到OBS中。上传数据集到“mnist-data”文件夹中。

注意:

创建的OBS桶所在区域和后续使用ModelArts必须在同一个区域Region,否则会导致训练时找不到OBS桶。创建OBS桶时,桶的存储类别请勿选择“归档存储”,归档存储的OBS桶会导致模型训练失败。

同时,由于要免费使用,要选择华北-北京四通道,且选择单用户访问,低频存储。

2.2.2.2 创建训练作业

登录ModelArts管理控制台,选择和OBS桶相同的区域。

在“全局配置”中检查当前帐号是否已完成访问授权的配置。如未完成,请参考使用委托授权。针对之前使用访问密钥授权的用户,建议清空授权,然后使用委托进行授权。

在左侧导航栏的“训练管理”-> “训练作业”中,单击“创建训练作业”。填写创建训练作业相关信息。

“创建方式”:选择“自定义算法”。

“启动方式”:选择“预置框架”,下拉框中选择PyTorch,pytorch_1.8.0-cuda_10.2-py_3.7-ubuntu_18.04-x86_64。

“代码目录”:选择已创建的代码目录路径“/test-modelarts-xx/pytorch/mnist-code/”。

“启动文件”:选择代码目录下上传的训练脚本“train.py”。

“输入”:单击“添加”,设置训练输入的“参数名称”为“data_url”。设置数据存储位置为 “/test-modelarts-xx/pytorch/mnist-data/”。

“输出”:单击“添加”,设置训练输出的“参数名称”为“train_url”。设置数据存储位置为 “/test-modelarts-xx/pytorch/mnist-output/”

“资源类型”:选择 GPU 单卡的规格,如“GPU: 1*NVIDIA-V100(16GB) | CPU: 8 核 64GB 780GB”。

其他参数保持默认即可。同时本样例代码为单机单卡场景,选择GPU多卡规格会导致训练失败。

之后就可以开始训练了。

2.2.3 推理部署

模型训练完成后,可以创建AI应用,将AI应用部署为在线服务。

在ModelArts管理控制台,单击左侧导航栏中的“AI应用管理>AI应用”,进入“我的AI应用”页面,单击“创建”。

在“创建AI应用”页面,填写相关参数,然后单击“立即创建”。

在“元模型来源”中,选择“从训练中选择>训练作业(New)”页签,选择Step4 创建训练作业中完成的训练作业,勾选“动态加载”。AI引擎的值是系统自动写入的,无需设置。

在AI应用列表页面,当AI应用状态变为“正常”时,表示AI应用创建成功。

单击AI应用名称左侧的小三角,打开此AI应用下的所有版本。在对应版本所在行,单击操作列“部署>在线服务”,将AI应用部署为在线服务,填写参数并根据界面提示完成在线服务创建,节点规格需选择CPU。

完成服务部署后,返回在线服务页面列表页,等待服务部署完成,当服务状态显示为“运行中”,表示服务已部署成功。

2.2.4 预测结果与资源释放

在“在线服务”页面,单击在线服务名称,进入服务详情页面。

单击“预测”页签,请求类型选择“multipart/form-data”,请求参数填写“image”,单击“上传”按钮上传示例图片,然后单击“预测”。

预测完成后,预测结果显示区域将展示预测结果,根据预测结果内容,可识别出此图片的数字具体为多少。

在运行结束后,如不再需要资源,则要手动进行删除,释放资源。

算法实现

3.1 本地代码实现

环境为python3.9, pycharm 2022.2.1, cuda 11.6, anaconda环境编译 。

代码思路见上,具体主要代码如下:(其他需要的话可私信发压缩包)

3.1.1 train.py

# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""
######################## train lenet example ########################
train lenet and get network model files(.ckpt) :
python train.py --data_path /YourDataPath
"""

import os
import ast
import argparse
from src.config import mnist_cfg as cfg
from src.dataset import create_dataset
from src.lenet import LeNet5
import mindspore.nn as nn
from mindspore import context
from mindspore.train.callback import ModelCheckpoint, CheckpointConfig, LossMonitor, TimeMonitor
from mindspore.train import Model
from mindspore.nn.metrics import Accuracy
from mindspore.common import set_seed

set_seed(1)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='MindSpore Lenet Example')
    # 设备设置
    parser.add_argument('--device_target', type=str, default="CPU", choices=['Ascend', 'GPU', 'CPU'],
                        help='default: CPU')
    parser.add_argument('--data_path', type=str, default="./MNIST_Data",
                        help='./MNIST_Data') #path where the dataset is saved
    parser.add_argument('--ckpt_path', type=str, default="./ckpt", help='./MNIST_Data/train') #if is test, must provide\path where the trained ckpt file
    parser.add_argument('--dataset_sink_mode', type=ast.literal_eval, default=True,
                        help='dataset_sink_mode is False or True') #dataset_sink_mode is False or True

    args = parser.parse_args()


    context.set_context(mode=context.GRAPH_MODE, device_target=args.device_target)
    ds_train = create_dataset(os.path.join(args.data_path, "train"), cfg.batch_size)
    network = LeNet5(cfg.num_classes)
    # 设定loss函数
    net_loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction="mean")
    # 设定优化器
    net_opt = nn.Momentum(network.trainable_params(), cfg.lr, cfg.momentum)

    time_cb = TimeMonitor(data_size=ds_train.get_dataset_size())
    config_ck = CheckpointConfig(save_checkpoint_steps=cfg.save_checkpoint_steps,
                                 keep_checkpoint_max=cfg.keep_checkpoint_max)
    ckpoint_cb = ModelCheckpoint(prefix="checkpoint_lenet", directory=args.ckpt_path, config=config_ck)
    #编译形成模型
    model = Model(network, net_loss, net_opt, metrics={"Accuracy": Accuracy()})

    print("============== Starting Training ==============")
    # 训练网络 train.py 
    model.train(cfg['epoch_size'], ds_train, callbacks=[time_cb, ckpoint_cb, LossMonitor()],
                dataset_sink_mode=args.dataset_sink_mode)

 

3.1.2 eval.py

 

# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""
######################## eval lenet example ########################
eval lenet according to model file:
python eval.py --data_path /YourDataPath --ckpt_path Your.ckpt
"""

import os
import ast
import argparse
import mindspore.nn as nn
from mindspore import context
from mindspore.train.serialization import load_checkpoint, load_param_into_net
from mindspore.train import Model
from mindspore.nn.metrics import Accuracy
from src.dataset import create_dataset
from src.config import mnist_cfg as cfg
from src.lenet import LeNet5

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='MindSpore Lenet Example')
    # 设备设置
    parser.add_argument('--device_target', type=str, default="CPU", choices=['Ascend', 'GPU', 'CPU'],
                        help='device where the code will be implemented (default: CPU)')
    parser.add_argument('--data_path', type=str, default="./MNIST_Data",
                        help='path where the dataset is saved')
    parser.add_argument('--ckpt_path', type=str, default="./ckpt/checkpoint_lenet-10_1875.ckpt", help='if mode is test, must provide\
                        path where the trained ckpt file')
    parser.add_argument('--dataset_sink_mode', type=ast.literal_eval,
                        default=False, help='dataset_sink_mode is False or True')

    args = parser.parse_args()

    context.set_context(mode=context.GRAPH_MODE, device_target=args.device_target)

    network = LeNet5(cfg.num_classes)
    #设定loss函数
    net_loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction="mean")
    repeat_size = cfg.epoch_size
    #设定优化器
    net_opt = nn.Momentum(network.trainable_params(), cfg.lr, cfg.momentum)
    #编译形成模型
    model = Model(network, net_loss, net_opt, metrics={"Accuracy": Accuracy()})

    print("============== Starting Testing ==============")
    param_dict = load_checkpoint(args.ckpt_path)
    load_param_into_net(network, param_dict)
    ds_eval = create_dataset(os.path.join(args.data_path, "test"),
                             cfg.batch_size,
                             1)
    acc = model.eval(ds_eval, dataset_sink_mode=args.dataset_sink_mode)
    print("============== {} ==============".format(acc))

 

3.1.3 lenet.py

 

# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""LeNet."""
import mindspore.nn as nn
from mindspore.common.initializer import Normal


class LeNet5(nn.Cell):
    """
    Lenet network

    Args:
        num_class (int): Number of classes. Default: 10.
        num_channel (int): Number of channels. Default: 1.

    Returns:
        Tensor, output tensor
    Examples:
        >>> LeNet(num_class=10)

    """



    def __init__(self, num_class=10, num_channel=1):
        super(LeNet5, self).__init__()
        #定义卷积层,ReLU激活函数,平坦层和全连接层
        #conv2d的输入通道为1维,输出为6维,卷积核尺寸为5*5,步长为1,不适用padding
        self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')
        self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')
        self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))
        self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))
        self.fc3 = nn.Dense(84, num_class, weight_init=Normal(0.02))
        self.relu = nn.ReLU()
        self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)
        self.flatten = nn.Flatten()

    def construct(self, x):
        #构建Lenet5架构,x代表网络的输入
        x = self.max_pool2d(self.relu(self.conv1(x)))
        x = self.max_pool2d(self.relu(self.conv2(x)))
        x = self.flatten(x)
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x)
        return x

 

3.1.4 本地运行示例结果

 

 

上图为train.py成功运行的截图,其会训练生成11个ckpt文件存在.ckpt文件夹里。

下图为文件夹中文件展示。

 

以下分别为取其中1~10编号的文件进行评估(eval)得到的accuracy值:(此处仅取第一个文件的运行结果截图,其余见下一部分)

 

 

3.2 在线训练代码

3.2.1 train.py

# coding=gbk
# base on https://github.com/pytorch/examples/blob/main/mnist/main.py

from __future__ import print_function

import os
import gzip
import codecs
import argparse
from typing import IO, Union

import numpy as np

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.optim.lr_scheduler import StepLR

import shutil


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output


# 模型训练,设置模型为训练模式,加载训练数据,计算损失函数,执行梯度下降
def train(args, model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
            if args.dry_run:
                break


# 模型验证,设置模型为验证模式,加载验证数据,计算损失函数和准确率
def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


# pytorch mnist
# https://github.com/pytorch/vision/blob/v0.9.0/torchvision/datasets/mnist.py
def get_int(b: bytes) -> int:
    return int(codecs.encode(b, 'hex'), 16)


def open_maybe_compressed_file(path: Union[str, IO]) -> Union[IO, gzip.GzipFile]:
    """Return a file object that possibly decompresses 'path' on the fly.
       Decompression occurs when argument `path` is a string and ends with '.gz' or '.xz'.
    """
    if not isinstance(path, torch._six.string_classes):
        return path
    if path.endswith('.gz'):
        return gzip.open(path, 'rb')
    if path.endswith('.xz'):
        return lzma.open(path, 'rb')
    return open(path, 'rb')


SN3_PASCALVINCENT_TYPEMAP = {
    8: (torch.uint8, np.uint8, np.uint8),
    9: (torch.int8, np.int8, np.int8),
    11: (torch.int16, np.dtype('>i2'), 'i2'),
    12: (torch.int32, np.dtype('>i4'), 'i4'),
    13: (torch.float32, np.dtype('>f4'), 'f4'),
    14: (torch.float64, np.dtype('>f8'), 'f8')
}


def read_sn3_pascalvincent_tensor(path: Union[str, IO], strict: bool = True) -> torch.Tensor:
    """Read a SN3 file in "Pascal Vincent" format (Lush file 'libidx/idx-io.lsh').
       Argument may be a filename, compressed filename, or file object.
    """
    # read
    with open_maybe_compressed_file(path) as f:
        data = f.read()
    # parse
    magic = get_int(data[0:4])
    nd = magic % 256
    ty = magic // 256
    assert 1 <= nd <= 3
    assert 8 <= ty <= 14
    m = SN3_PASCALVINCENT_TYPEMAP[ty]
    s = [get_int(data[4 * (i + 1): 4 * (i + 2)]) for i in range(nd)]
    parsed = np.frombuffer(data, dtype=m[1], offset=(4 * (nd + 1)))
    assert parsed.shape[0] == np.prod(s) or not strict
    return torch.from_numpy(parsed.astype(m[2], copy=False)).view(*s)


def read_label_file(path: str) -> torch.Tensor:
    with open(path, 'rb') as f:
        x = read_sn3_pascalvincent_tensor(f, strict=False)
    assert(x.dtype == torch.uint8)
    assert(x.ndimension() == 1)
    return x.long()


def read_image_file(path: str) -> torch.Tensor:
    with open(path, 'rb') as f:
        x = read_sn3_pascalvincent_tensor(f, strict=False)
    assert(x.dtype == torch.uint8)
    assert(x.ndimension() == 3)
    return x


def extract_archive(from_path, to_path):
    to_path = os.path.join(to_path, os.path.splitext(os.path.basename(from_path))[0])
    with open(to_path, "wb") as out_f, gzip.GzipFile(from_path) as zip_f:
        out_f.write(zip_f.read())
# --- pytorch mnist
# --- end


# raw mnist 数据处理
def convert_raw_mnist_dataset_to_pytorch_mnist_dataset(data_url):
    """
    raw

    {data_url}/
        train-images-idx3-ubyte.gz
        train-labels-idx1-ubyte.gz
        t10k-images-idx3-ubyte.gz
        t10k-labels-idx1-ubyte.gz

    processed

    {data_url}/
        train-images-idx3-ubyte.gz
        train-labels-idx1-ubyte.gz
        t10k-images-idx3-ubyte.gz
        t10k-labels-idx1-ubyte.gz
        MNIST/raw
            train-images-idx3-ubyte
            train-labels-idx1-ubyte
            t10k-images-idx3-ubyte
            t10k-labels-idx1-ubyte
        MNIST/processed
            training.pt
            test.pt
    """
    resources = [
        "train-images-idx3-ubyte.gz",
        "train-labels-idx1-ubyte.gz",
        "t10k-images-idx3-ubyte.gz",
        "t10k-labels-idx1-ubyte.gz"
    ]

    pytorch_mnist_dataset = os.path.join(data_url, 'MNIST')

    raw_folder = os.path.join(pytorch_mnist_dataset, 'raw')
    processed_folder = os.path.join(pytorch_mnist_dataset, 'processed')

    os.makedirs(raw_folder, exist_ok=True)
    os.makedirs(processed_folder, exist_ok=True)

    print('Processing...')

    for f in resources:
        extract_archive(os.path.join(data_url, f), raw_folder)

    training_set = (
        read_image_file(os.path.join(raw_folder, 'train-images-idx3-ubyte')),
        read_label_file(os.path.join(raw_folder, 'train-labels-idx1-ubyte'))
    )
    test_set = (
        read_image_file(os.path.join(raw_folder, 't10k-images-idx3-ubyte')),
        read_label_file(os.path.join(raw_folder, 't10k-labels-idx1-ubyte'))
    )
    with open(os.path.join(processed_folder, 'training.pt'), 'wb') as f:
        torch.save(training_set, f)
    with open(os.path.join(processed_folder, 'test.pt'), 'wb') as f:
        torch.save(test_set, f)

    print('Done!')


def main():
    # 定义可以接收的训练作业运行参数
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')

    parser.add_argument('--data_url', type=str, default=False,
                        help='mnist dataset path')
    parser.add_argument('--train_url', type=str, default=False,
                        help='mnist model path')

    parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                        help='input batch size for training (default: 64)')
    parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                        help='input batch size for testing (default: 1000)')
    parser.add_argument('--epochs', type=int, default=14, metavar='N',
                        help='number of epochs to train (default: 14)')
    parser.add_argument('--lr', type=float, default=1.0, metavar='LR',
                        help='learning rate (default: 1.0)')
    parser.add_argument('--gamma', type=float, default=0.7, metavar='M',
                        help='Learning rate step gamma (default: 0.7)')
    parser.add_argument('--no-cuda', action='store_true', default=False,
                        help='disables CUDA training')
    parser.add_argument('--dry-run', action='store_true', default=False,
                        help='quickly check a single pass')
    parser.add_argument('--seed', type=int, default=1, metavar='S',
                        help='random seed (default: 1)')
    parser.add_argument('--log-interval', type=int, default=10, metavar='N',
                        help='how many batches to wait before logging training status')
    parser.add_argument('--save-model', action='store_true', default=True,
                        help='For Saving the current Model')
    args = parser.parse_args()

    use_cuda = not args.no_cuda and torch.cuda.is_available()

    torch.manual_seed(args.seed)

    # 设置使用 GPU 还是 CPU 来运行算法
    device = torch.device("cuda" if use_cuda else "cpu")

    train_kwargs = {'batch_size': args.batch_size}
    test_kwargs = {'batch_size': args.test_batch_size}
    if use_cuda:
        cuda_kwargs = {'num_workers': 1,
                       'pin_memory': True,
                       'shuffle': True}
        train_kwargs.update(cuda_kwargs)
        test_kwargs.update(cuda_kwargs)

    # 定义数据预处理方法
    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
        ])

    # 将 raw mnist 数据集转换为 pytorch mnist 数据集
    convert_raw_mnist_dataset_to_pytorch_mnist_dataset(args.data_url)

    # 分别创建训练和验证数据集
    dataset1 = datasets.MNIST(args.data_url, train=True, download=False,
                       transform=transform)
    dataset2 = datasets.MNIST(args.data_url, train=False, download=False,
                       transform=transform)

    # 分别构建训练和验证数据迭代器
    train_loader = torch.utils.data.DataLoader(dataset1, **train_kwargs)
    test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)

    # 初始化神经网络模型并拷贝模型到计算设备上
    model = Net().to(device)
    # 定义训练优化器和学习率策略,用于梯度下降计算
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)
    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)

    # 训练神经网络,每一轮进行一次验证
    for epoch in range(1, args.epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
        scheduler.step()

    # 保存模型与适配 ModelArts 推理模型包规范
    if args.save_model:

        # 在 train_url 训练参数对应的路径内创建 model 目录
        model_path = os.path.join(args.train_url, 'model')
        os.makedirs(model_path, exist_ok = True)

        # 按 ModelArts 推理模型包规范,保存模型到 model 目录内
        torch.save(model.state_dict(), os.path.join(model_path, 'mnist_cnn.pt'))

        # 拷贝推理代码与配置文件到 model 目录内
        the_path_of_current_file = os.path.dirname(__file__)
        shutil.copyfile(os.path.join(the_path_of_current_file, 'infer/customize_service.py'), os.path.join(model_path, 'customize_service.py'))
        shutil.copyfile(os.path.join(the_path_of_current_file, 'infer/config.json'), os.path.join(model_path, 'config.json'))

if __name__ == '__main__':
    main()

3.2.2 customize_service.py

# coding=gbk

import os
import log
import json

import torch.nn.functional as F
import torch.nn as nn
import torch
import torchvision.transforms as transforms

import numpy as np
from PIL import Image

from model_service.pytorch_model_service import PTServingBaseService

logger = log.getLogger(__name__)

# 定义模型预处理
infer_transformation = transforms.Compose([
    transforms.Resize(28),
    transforms.CenterCrop(28),
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])


class PTVisionService(PTServingBaseService):

    def __init__(self, model_name, model_path):
        # 调用父类构造方法
        super(PTVisionService, self).__init__(model_name, model_path)

        # 调用自定义函数加载模型
        self.model = Mnist(model_path)

        # 加载标签
        self.label = [0,1,2,3,4,5,6,7,8,9]

    def _preprocess(self, data):
        preprocessed_data = {}
        for k, v in data.items():
            input_batch = []
            for file_name, file_content in v.items():
                with Image.open(file_content) as image1:
                    # 灰度处理
                    image1 = image1.convert("L")
                    if torch.cuda.is_available():
                        input_batch.append(infer_transformation(image1).cuda())
                    else:
                        input_batch.append(infer_transformation(image1))
            input_batch_var = torch.autograd.Variable(torch.stack(input_batch, dim=0), volatile=True)
            print(input_batch_var.shape)
            preprocessed_data[k] = input_batch_var

        return preprocessed_data

    def _postprocess(self, data):
        results = []
        for k, v in data.items():
            result = torch.argmax(v[0])
            result = {k: self.label[result]}
            results.append(result)
        return results

    def _inference(self, data):

        result = {}
        for k, v in data.items():
            result[k] = self.model(v)

        return result


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output


def Mnist(model_path, **kwargs):
    # 生成网络
    model = Net()

    # 加载模型
    if torch.cuda.is_available():
        device = torch.device('cuda')
        model.load_state_dict(torch.load(model_path, map_location="cuda:0"))
    else:
        device = torch.device('cpu')
        model.load_state_dict(torch.load(model_path, map_location=device))

    # CPU 或者 GPU 映射
    model.to(device)

    # 声明为推理模式
    model.eval()

    return model

3.2.3 config.json

{
    "model_algorithm": "image_classification",
    "model_type": "PyTorch",
    "runtime": "pytorch_1.8.0-cuda_10.2-py_3.7-ubuntu_18.04-x86_64"
}

 

讨论及结论

4.1 本地运行结果

本地运行train.py后生成ckpt文件夹,其中包含各个epoch生成的.ckpt文件。之后运行eval.py即可输出accuracy。运行截图如下:

 

 

 

 

 

4.2 在线运行结果

在线服务部署完成后,便可以在“在线服务”页面,单击在线服务名称,进入服务详情页面,单击“预测”页签,请求类型选择“multipart/form-data”,请求参数填写“image”,单击“上传”按钮上传示例图片如下,然后单击“预测”。

 

 预测完成后,预测结果显示区域将展示预测结果如下图。(左下区域应该显示输入的图片,这里我没加载出来)

 

根据预测结果内容,可识别出此图片的数字是“2”。

 

参考文献

[1] 华为云.使用自定义算法构建模型.2023-05-11.[DB/OL].使用自定义算法构建模型(手写数字识别)_AI开发平台ModelArts_最佳实践_模型训练(自定义算法-新版训练)_华为云 (huaweicloud.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值