MindSpore端侧应用实战:实现迁移学习的垃圾分类应用
看了大佬的文章分享,就用昇腾环境复现了端侧的应用开发实战,收获满满呀!本案例是基于MindSpore框架预置的图像分类预训练模型,用垃圾分类数据集在昇腾芯片上进行迁移学习训练;然后用训练好的模型文件基于MindSpore Lite部署到手机上实现垃圾分类应用,将分类图片进一步分类成可回收物、干垃圾、有害垃圾和湿垃圾四大类。通过这个例子,能快速了解如何利用MindSpore框架进行端侧个性化应用开发的端对端过程。
1. PC侧迁移学习步骤
在深度学习中,大部分任务的数据和网络模型规模较大,训练网络模型时,如果不使用预训练模型,从头开始训练,需要消耗大量的时间。因此,大部分任务都会选择预训练模型,在其上做微调。本端侧垃圾分类应用是基于MobileNetV2预训练模型,在其上进行迁移学习。
环境安装
-
安装MindSpore框架
本用例选用Ascend对应的MindSpore版本。当然,用户可根据个人系统和处理器架构安装对应版本MindSpore框架。 -
下载代码
在Gitee中克隆MindSpore开源项目仓库,进入./model_zoo/official/cv/mobilenetv2/。
在这里可以找到完整可运行的样例代码:https://gitee.com/mindspore/mindspore/tree/master/model_zoo/official/cv/mobilenetv2
git clone https://gitee.com/mindspore/mindspore.git
cd ./mindspore/model_zoo/official/cv/mobilenetv2
预训练模型准备
用户可以下载提前训练好的预训练模型, 当然也可以参照model zoo自行训练预训练模型。 为快速进行迁移学习,本用例直接选用提前训练好的预训练模型。
数据集准备
MobileNetV2的代码默认使用ImageFolder格式管理数据集,每一类图片整理成单独的一个文件夹, 数据集结构如下:
└─ImageFolder
├─train
│ class1Folder
│ ......
└─eval
class1Folder
也可以参考MindSpore支持图像领域常用的数据集, 修改src/dataset.py换成适合的数据集接口。src/dataset.py中默认使用随机裁剪,随机水平翻转,随机色彩增强,正则化等基本的数据增强方法, 也可以参考数据增强添加新的数据处理算子。
模型训练和推理
一般一个分类网络包含两部分:backbone和head,其中backbone部分通常是一系列卷积层,负责提取图片特征;head部分通常是一组全连接层,用于分类,一般最后一个全连接层的输出对应数据集的分类数。这里我们选择冻结backbone部分的参数,只训练head的参数进行微调。
定义网络模型及加载预训练参数
首先按照代码第1行,构建MobileNetV2的backbone网络,head网络,并且构建包含这两个子网络的MobileNetV2网络。 代码第3-10行展示了如何定义backbone_net与head_net,以及将两个子网络置入mobilenet_v2中。 代码第12-23行,展示了在微调训练模式下,需要将预训练模型加载入backbone_net子网络,并且冻结backbone_net中的参数,不参与训练。 代码第21-23行展示了如何冻结网络参数。
1: backbone_net, head_net, net = define_net(args_opt, config)
2: ...
3: def define_net(config, is_training):
4: backbone_net = MobileNetV2Backbone()
5: activation = config.activation if not is_training else "None"
6: head_net = MobileNetV2Head(input_channel=backbone_net.out_channels,
7: num_classes=config.num_classes,
8: activation=activation)
9: net = mobilenet_v2(backbone_net, head_net)
10: return backbone_net, head_net, net
11: ...
12: if args_opt.pretrain_ckpt and args_opt.freeze_layer == "backbone":
13: load_ckpt(backbone_net, args_opt.pretrain_ckpt, trainable=False)
14: ...
15: def load_ckpt(network, pretrain_ckpt_path, trainable=True):
16: """
17: train the param weight or not
18: """
19: param_dict = load_checkpoint(pretrain_ckpt_path)
20: load_param_into_net(network, param_dict)
21: if not trainable:
22: for param in network.get_parameters():
23: param.requires_grad = False
执行训练
训练时,运行train.py时需要传入dataset_path、platform、pretrain_ckpt与freeze_layer四个参数。 验证时,运行eval.py并且传入dataset_path、platform、pretrain_ckpt三个参数。
# train with Python file
python train.py --platform=Ascend --dataset_path [DATASET_PATH] --pretrain_ckpt [PRETRAIN_CHECKPOINT_PATH] --freeze_layer=backbone]
# eval with Python file
python eval.py --platform=Ascend --dataset_path [DATASET_PATH] --pretrain_ckpt [PRETRAIN_CHECKPOINT_PATH]
-
--dataset_path:训练与验证数据集路径,无默认值,用户训练/验证时必须输入。
-
--platform:处理器类型,默认为“Ascend”,也可以设置为“CPU”或"GPU"。
-
--pretrain_ckpt:迁移训练或调优时,需要传入的预训练模型文件。
-
--freeze_layer:冻结网络层,输入“none"、"backbone"其中一个。
结果展示
-
运行py文件, 输出训练结果
epoch[1/15], iter[15] cost: 4300.820, per step time: 286.721, avg loss: 2.354
epoch[2/15], iter[15] cost: 4010.630, per step time: 267.375, avg loss: 1.601
epoch[3/15], iter[15] cost: 4009.706, per step time: 267.314, avg loss: 1.353
epoch[4/15], iter[15] cost: 3991.812, per step time: 266.121, avg loss: 1.208
epoch[5/15], iter[15] cost: 4007.363, per step time: 267.158, avg loss: 1.125
epoch[6/15], iter[15] cost: 3995.016, per step time: 266.334, avg loss: 1.068
epoch[7/15], iter[15] cost: 4017.721, per step time: 267.848, avg loss: 1.038
epoch[8/15], iter[15] cost: 4016.534, per step time: 267.769, avg loss: 0.994
epoch[9/15], iter[15] cost: 4006.452, per step time: 267.097, avg loss: 0.986
epoch[10/15], iter[15] cost: 3992.108, per step time: 266.141, avg loss: 0.993
epoch[11/15], iter[15] cost: 4006.859, per step time: 267.124, avg loss: 0.959
epoch[12/15], iter[15] cost: 4042.366, per step time: 269.491, avg loss: 0.938
epoch[13/15], iter[15] cost: 4010.737, per step time: 267.382, avg loss: 0.921
epoch[14/15], iter[15] cost: 4010.041, per step time: 267.336, avg loss: 0.919
epoch[15/15], iter[15] cost: 4003.595, per step time: 266.906, avg loss: 0.941
total cost 173.1637 s
-
运行py文件, 输出推理结果
result:{'acc': 0.86}
pretrain_ckpt=ckpt_0\mobilenetv2_15.ckpt
模型导出
当有了训练好的CheckPoint文件后,想接着在MindSpore Lite端侧做推理,需要通过网络和CheckPoint生成对应的MINDIR格式模型文件。代码如下:
import argparse
import numpy as np
from mindspore import Tensor
from mindspore.train.serialization import export
from src.config import set_config
from src.models import define_net, load_ckpt
parser = argparse.ArgumentParser(description='MobilenetV2 export')
parser.add_argument('--platform', type=str, default="Ascend", choices=("Ascend", "GPU", "CPU"), \
help='run platform, only support GPU, CPU and Ascend')
parser.add_argument('--pretrain_ckpt', type=str, required=True, help='Pretrained checkpoint path \
for fine tune or incremental learning')
args_opt = parser.parse_args()
args_opt.is_training = False
args_opt.run_distribute = False
config = set_config(args_opt)
backbone_net, head_net, net = define_net(config, args_opt.is_training)
load_ckpt(net, args_opt.pretrain_ckpt)
input = np.random.uniform(0.0, 1.0, size=[1, 3, 224, 224]).astype(np.float32)
export(net, Tensor(input), file_name='mobilenetv2.mindir', file_format='MINDIR')
执行命令:
python export.py --platform=Ascend --pretrain_ckpt [PRETRAIN_CHECKPOINT_PATH]
最后会在执行命令同目录下生成 mobilenetv2.mindir 文件。
2. 端侧推理步骤
在端侧利用MindSpore Lite C++ API和通过迁移学习训练好的模型完成端侧推理,实现对摄像头设备捕获的内容进行分类,并在APP图像预览界面中,显示出分类结果。
转换模型
端侧推理框架使用的模型是.ms格式,所以这里需要对模型做进一步转换。 这里基于上述导出的.mindir格式的模型文件,使用MindSpore Lite模型转换工具将其转换成.ms格式文件。
以MobileNetV2模型为例,如下脚本将其转换为MindSpore Lite模型用于端侧推理。
./converter_lite --fmk=MINDIR --modelFile=mobilenetv2.mindir --outputFile=garbage_mobilenetv2.ms
部署应用
将模型转换为端侧的.ms格式文件后,想进一步将其部署到手机进行推理应用,需安装对应的端侧开发环境Android Studio。
运行依赖
-
Android Studio >= 3.2 (推荐0以上版本)
-
NDK 21.3
-
CMake10.2
-
Android SDK >= 26
-
JDK >= 1.8
构建与运行
-
下载对应版本Android Studio安装包进行安装,然后加载示例代码并安装相应的SDK(参考官网教程部署应用下的构建与运行的步骤1)
-
连接Android设备,运行示例代码的应用程序。
通过USB连接Android设备调试,点击Run 'app'即可在你的设备上运行本示例项目(参考部署应用下的构建与运行的步骤2) -
在Android设备上,点击“继续安装”,即可在手机上安装进行迁移学习后的垃圾分类应用,打开对应安装的app,即可通过摄像头设备捕获相应的物体,来查看具体的垃圾分类结果。
核心代码
配置MindSpore Lite依赖项
Android JNI层调用MindSpore C++ API时,需要相关库文件支持。可通过MindSpore Lite源码编译生成mindspore-lite-{version}-minddata-{os}-{device}.tar.gz库文件包并解压缩(包含libmindspore-lite.so库文件和相关头文件)。 本示例中,build过程由app/download.gradle文件自动下载MindSpore Lite版本文件,并放置在相关目录下,可直接官网下载获取。
在app/CMakeLists.txt文件中建立.so库文件链接,可查看源码。
部署模型文件
本示例中使用的端侧垃圾分类模型文件为garbage_mobilenetv2.ms,放置在app/src/main/assets/model目录下。
编写端侧推理代码
在JNI层调用MindSpore Lite C++ API实现端测推理。
推理代码流程如下,完整代码请参见src/cpp/GarbageMindSporeNetnative.cpp。
重训推理流程
如果想用其他数据集,以实现自己的图像分类任务,步骤如下:
1.将重新转换生成的.ms模型放置在app/src/main/assets/model目录下,替换原有模型。
2.并根据重训后的标签分类,更改src/cpp/GarbageMindSporeNetnative.cpp代码中的分类数组,标签数组,或修改其他文件的相应代码。比如:垃圾分类新模型分为4大类别,共对应26种小类别垃圾,代码如下:
static const int RET_GARBAGE_SORT_SUM = 4;
static const char *labels_name_grbage_sort_map[RET_GARBAGE_SORT_SUM] = {{"可回收物"}, {"干垃圾"}, {"有害垃圾"}, {"湿垃圾"}};
static const int RET_GARBAGE_DETAILED_SUM = 26;
static const char *labels_name_grbage_detailed_map[RET_GARBAGE_DETAILED_SUM] = { {"塑料瓶"},{"帽子"},{"报纸"},{"易拉罐"},{"玻璃制品"},{"玻璃瓶"}, {"硬纸板"}, {"篮球"},
{"纸张"},{"金属制品"},{"一次性筷子"},{"打火机"},{"扫把"},{"旧镜子"},{"牙刷"},{"脏污衣服"},{"贝壳"},{"陶瓷碗"},
{"油漆桶"},{"电池"},{"荧光灯"},{"药片胶囊"},{"橙皮"},{"菜叶"},{"蛋壳"},{"香蕉皮"}};
3.根据输出结果的最大值,获取对应标签数组的垃圾类别。
std::string ProcessRunnetResult(const int RET_CATEGORY_SUM, const char *const labels_name_map[],
std::unordered_map<std::string, mindspore::tensor::MSTensor *> msOutputs) {
// Get the branch of the model output.
std::unordered_map<std::string, mindspore::tensor::MSTensor *>::iterator iter;
iter = msOutputs.begin();
// The mobilenetv2.ms model output just one branch.
auto outputTensor = iter->second;
// Get a pointer to the first score.
float *temp_scores = static_cast<float *>(outputTensor->MutableData());
float max = 0.0;
int maxIndex = 0;
for (int i = 0; i < RET_CATEGORY_SUM; ++i) {
if (temp_scores > max) {
max = temp_scores;
maxIndex = i;
}
}
if(maxIndex >= 0 && maxIndex <= 9){
categoryScore += labels_name_grbage_sort_map[0];
categoryScore += ":";
}else if(maxIndex > 9 && maxIndex <= 17){
categoryScore += labels_name_grbage_sort_map[1];
categoryScore += ":";
}else if(maxIndex > 17 && maxIndex <= 21){
categoryScore += labels_name_grbage_sort_map[2];
categoryScore += ":";
}else if(maxIndex > 21 && maxIndex <= 25){
categoryScore += labels_name_grbage_sort_map[3];
categoryScore += ":";
}
categoryScore += labels_name_map[maxIndex];
return categoryScore;
}