华为昇腾AI暑期培训总结
前言
以培训最后的水果图片分类小项目为例,介绍基于昇腾产品,利用mindspore开发框架和昇腾AI计算平台CANN进行开发,训练,部署AI算法。
本次暑期实习涉及许多深度学习的基础知识,这里也不过多介绍了,由于最后给的项目实现时间有限,有很多东西没考虑周到,但总的流程还是完成了,这里分享一些开发中关键步骤结合理论分享一下,还有一些心得和开发中遇到问题解决方案记录一下。
一、引言
1.1 Atlas 200I DK A2 产品简介(本次主角):
Atlas 200I DK A2 开发者套件(以下简称开发者套件)是一款高性能的 AI 开发者套件,可提供
8TOPS INT8 的计算能力。
开发者套件集成了昇腾 310B 处理器,可以实现图像、视频等多种数据分析与推理计算,可广
泛用于智能监控、机器人、无人机等场景,开发者套件外观如下图。
1.2 CANN 简介
CANN (Compute Architecture for Neural Networks)是华为公司针对 AI 场景推出的异构计算架
构,通过提供多层次的编程接口,支持用户快速构建基于昇腾平台的 AI 应用和业务。包括:
⚫ AscendCL:昇腾硬件的统一编程接口,包含了编程模型、硬件资源抽象、AI 任务及内核管
理、内存管理、模型和算子调用、媒体预处理接口、加速库调用等一系列功能,充分释放
昇腾系统多样化算力,使能开发者快速开发 AI 应用。
⚫ TBE 算子开发工具:预置丰富 API 接口,支持用户自定义算子开发和自动化调优,缩短工
期,节省人力。
⚫ 算子库:基于昇腾处理器,深度协同优化的高性能算子库。
当进行开发板推理实验时,需要安装 CANN 软件使用 ATC 工具进行模型转换,然后使用
AsencdCL 进行推理应用的开发。
二、Atlas 200I DK A2 开发环境配置
1.配件准备
1.开发者套件(主板,电源线,电源适配器)---->型号:Atlas 200I DK A2
2.Micro SD卡—>推荐使用 SD 3.0 接口标准的Micro SD 卡,容量不小于64GB。
3.读卡器(需使用支持 Micro SD 卡的读卡器)
4.PC
5.连接线(Type-C数据线/RJ45网线) 这里使用的是Type-C数据线
2.制卡-镜像恢复的方式
昇腾官方提供 Ascend-ai-devkit-imager 工具,将装好开发环境的镜像恢复至 SD卡。
把 SD卡插入读卡器,读卡器插入 PC 机上,选择将要烧写的 SD 卡后烧入镜像(在线烧入看网速):
烧好镜像后先将SD卡插入开发版,再接通电源,接入电源后 D3、LED1、LED3 指示灯会依次绿色常亮,表示启动正常。效果图如下:
采用Type-C远程登入,官方文档已经很详尽了,这里不再累述啦
链接: Type-C接口远程登录
前期准备就这些啦,本次培训能体验到开发板的使用,是课内很难体验到的一次经历。
三、模型训练
这里使用的是华为的ModelArts管理控制台里的环境
镜像:mindspore1.7.0-cann5.1.0-py3.7-euler2.8.3
规格:Ascend: 4*Ascend 910|CPU: 96核 384GB
其实我们在实验的过程中使用的是环境中的python内核
为什么呢?虽然说这次培训提供了最稳定的版本mindspore1.7,但是1.7官方提供的案例太少,而且官方案例中使用了mindvision这个类,但这个已经弃用了,下载后会出先cv库版本问题,连原有的mindspore1.7都用不了了,果断选择mindspore2.0版本。使用2.0的版本的缺点就是原有的NPU就用不了了。(有舍才有得,大不了用CPU,反正不是自己的CPU 😃)
3.1案例复现 and 数据集替换
在实际项目开发过程中是不会从0开始,复现一个已有的案例比从零开始开发一个项目更加高效。特别是有一个紧迫的时间表或者预算限制时,借鉴现有案例可以帮助我们更快地推进项目。
在mindspore官网有以下几个经典案例可供参考借鉴:
这里我们选择第一个Resnet50图像分类任务作为我们项目的基础,具体实现如官网总体来说还是比较顺利的:
链接: 基于mindsporeResNet50图像分类
介绍一下我的数据集:
一个文件夹为一个类别,每个文件夹下有多张图片,将数据分为训练集和测试集,
训练集是用于训练机器学习模型,测试集是用于评估模型性能和泛化能力。
与官方案例不同的是官方使用的是二进制版本的CIFAR-10文件,有专门的mindspore.dataset.Cifar10Dataset()接口来读取数据,因此需要自行写一段数据加载接口,以下是我使用的以供参考:
def my_create_dataset(dataset_dir, usage, resize, batch_size):
data_set = ds.ImageFolderDataset(dataset_dir=dataset_dir,shuffle=True)
trans = [vision.RandomCropDecodeResize(size=(32,32),
scale=(0.08, 1.0),
ratio=(0.75, 1.333)),]
if usage == "train":
trans += [
vision.RandomHorizontalFlip(prob=0.5)
]
trans += [
vision.Rescale(1.0 / 255.0, 0.0),
vision.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]),
vision.HWC2CHW()
]
target_trans = transforms.TypeCast(mstype.int32)
# 数据映射操作
data_set = data_set.map(operations=trans,
input_columns='image',
num_parallel_workers=4)
data_set = data_set.map(operations=target_trans,
input_columns='label',
num_parallel_workers=4)
# 批量操作
data_set = data_set.batch(batch_size)
return data_set
# 获取处理后的训练与测试数据集
dataset_train = my_create_dataset(dataset_dir=data_dir_train,
usage="train",
resize=image_size,
batch_size=batch_size)
step_size_train = dataset_train.get_dataset_size()
dataset_val = my_create_dataset(dataset_dir=data_dir_test,
usage="test",
resize=image_size,
batch_size=batch_size)
step_size_val = dataset_val.get_dataset_size()
3.2 模型优化
对原案例进行模型优化有几个主要的原因:提升性能,适应特定需求,节约资源等等,模型优化过程考验自己对深度学习神经网络知识的理解与运用(卷积,池化,批量归一化,全连接等等,特定的网络有自己的改进思想,比如本次使用的ResNet就应用了残差块,来解决深度神经网络中的梯度消失和梯度爆炸等问题),这次模型优化我们引入了多尺度融合,接下来简单介绍一下多尺度融合:
首先为什么引入多尺度呢??这是我们的想法:在图像分类任务中,物体的尺度可能因为距离、视角或者图像分辨率等因素而变化。使用单一尺度的特征可能导致模型对特定尺度的物体具有较好的分类能力,但对于其他尺度的物体可能效果不佳。多尺度融合技术可以帮助模型同时关注多个尺度的特征,从而提升分类性能。
多尺度融合关键步骤:
1.提取特征图构造不同尺度的特征金字塔
2.对不同尺度的特征图进行上采样(统一到特定图片大小)
3.对统一后的特征图进行融合得到具有不同尺度特征图信息的特征图
在我们对resnet50改造过程中大致如下图(很草率的一张图。。。):
具体修改代码如下:
class ResNet(nn.Cell):
def __init__(self, block: Type[Union[ResidualBlockBase, ResidualBlock]],
layer_nums: List[int], num_classes: int, input_channel: int) -> None:
super(ResNet, self).__init__()
self.relu = nn.ReLU()
# 第一个卷积层,输入channel为3(彩色图像),输出channel为64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, weight_init=weight_init)
self.norm = nn.BatchNorm2d(64)
self.norm2=nn.BatchNorm2d(512)
# 最大池化层,缩小图片的尺寸
self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, pad_mode='same')
# 各个残差网络结构块定义
self.layer1 = make_layer(64, block, 64, layer_nums[0])#3
#512
self.layer2 = make_layer(64 * block.expansion, block, 128, layer_nums[1], stride=2)#4
#1024
self.layer3 = make_layer(128 * block.expansion, block, 256, layer_nums[2], stride=2)#6
#2048
self.layer4 = make_layer(256 * block.expansion, block, 512, layer_nums[3], stride=2)#3
# 平均池化层
self.avg_pool = nn.AvgPool2d()
# flattern层
self.flatten = nn.Flatten()
self.conv2=nn.Conv2d(512,256,1)
self.conv3=nn.Conv2d(1024,256,1)
self.conv4=nn.Conv2d(2048,256,1)
self.conv5 = nn.Conv2d(768, 512, 1)
self.fc = nn.Dense(in_channels=8192, out_channels=2048)
self.fc2= nn.Dense(in_channels=2048, out_channels=30)
def construct(self, x):
features=[]
x = self.conv1(x)
x = self.norm(x)
x = self.relu(x)
x = self.max_pool(x)
x = self.layer1(x)
x = self.layer2(x)
features.append(self.conv2(x))
x = self.layer3(x)
x_t=self.conv3(x)
features.append(ms.ops.interpolate(x_t,
size=(x_t.shape[2]*2,x_t.shape[3]*2),
mode='bilinear'))
x = self.layer4(x)
x_tt=self.conv4(x)
features.append(ms.ops.interpolate(x_tt,
size=(x_tt.shape[2]*4,x_tt.shape[3]*4),
mode='bilinear'))
x_new=ms.ops.cat((features),1)
x_new=self.conv5(x_new)
x_new=self.norm2(x_new)
#维度512
x_new = self.avg_pool(x_new)
x_new = self.flatten(x_new)
x_new = self.fc(x_new)
x_new = self.fc2(x_new)
return x_new
然后就是进行训练了,通过对神经网络模型进行反向传播算法来不断调整模型参数,使其逐渐逼近目标函数的最优解。
在没有使用预预训练模型的情况下(大家平起平坐 😃)改良后的模型比原模型精度上提高了5%,但是模型的复杂度增加了,训练时间也随之增加,这点在实际项目中也是要考虑权衡的。
训练完成后就得得到了我们想要的模型权重文件:以.ckpt结尾
炼丹嘛,经过几个小时的模型训练,得到的就是模型的参数,现如今大模型也是这样,耗费巨资进行训练,最后得到的也是参数。
四、模型转化
开源框架训练出来的网络模型,要想运行在基于达芬奇架构的昇腾芯片上,需将其适配并转换成昇腾芯片支持的格式,华为昇腾软件栈 CANN 提供 ATC(Ascend Tensor Compile)工具用于模型转换并提供模型优化、压缩等其他功能。可以使用ATC 命令对开源 MindSpore(.air)、TensorFlow(.pb)、Caffe(.pro
to,.caffemodel )、ONNX(.onnx) 框架的模型进行转换。
4.1 模型转化 .ckpt—>.air
训练出来的模型权重文件ckpt,而官方给个的ATC没有此类型的示例,故先将ckpt转为Ascend模型的中间表示格式air,运行以下命令将ckpt转为air
import mindspore as ms
import numpy as np
from mindspore import Tensor
net = resnet50()
input_tensor = Tensor(np.ones([1, 3, 32, 32]).astype(np.float32))
ms.export(net, input_tensor, file_name='resnet50', file_format='AIR')
这里简单介绍一下 mindspore.export(net, *inputs, file_name, file_format)的使用
----net (Union[Cell, function]) - MindSpore网络结构。
----inputs (Union[Tensor, Dataset, List, Tuple, Number, Bool]) - 网络的输入,如果网络有多个输入,需要一同传入。当传入的类型为 Dataset 时,将会把数据预处理行为同步保存起来。需要手动调整batch的大小,当前仅支持获取 Dataset 的 image 列。
—-file_name (str) - 导出模型的文件名称。
----file_format (str) - MindSpore目前支持导出”AIR”,”ONNX”和”MINDIR”格式的模型。
4.2 CANN-ATC模型转换 .air—>.om
昇腾张量编译器(Ascend Tensor Compiler,简称ATC)是异构计算架构CANN体系下的模型转换工具:
它可以将开源框架的网络模型(如Caffe、TensorFlow等)以及Ascend IR定义的单算子描述文件转换为昇腾AI处理器支持的离线模型。
模型转换过程中,ATC会进行算子调度优化、权重数据重排、内存使用优化等具体操作,对原始的深度学习模型进行进一步的调优,从而满足部署场景下的高性能需求,使其能够高效执行在昇腾AI处理器上。
当前在开发者套件上,使用 ATC 工具转换模型时会出现转换过程很慢或卡住不动的问题。该问题可能由于内存不足,导致进程卡死。可通过创建交换分区的方式以分担开发者套件的内存压力。
⚫ 创建一个大小为 4G 的 swap 分区。
fallocate -l 4G /swapfile
⚫ 修改文件权限。
( chmod 命令就是用来修改文件权限的命令。在命令中,数字 “600” 表示要为文件设置的权限,其中第一位 “6” 表示文件所有者的权限,第二位 “0” 表示文件所属组的权限,第三位 “0” 表示其他用户的权限。
数字权限表示方式中,每个数字都是由三个位组成,每个位代表一种权限,从左到右分别是读(r)、写(w)、执行(x)权限。每种权限有不同的数值表示:
“0” 表示没有该权限。
“1” 表示执行权限。
“2” 表示写权限。
“4” 表示读权限。
所以 “600” 表示文件所有者具有读和写权限,而文件所属组和其他用户没有任何权限。)
chmod 600 /swapfile
⚫创建 swap 分区。
mkswap /swapfile
⚫挂载 swap 分区。(!!!每次开发板断电后都要重新挂载!!!)
swapon /swapfile
执行 cd 命令,进入 mindspore 模型.air所在的文件目录下执行:
atc --model=./resnet.air --framework=1 --output=resnet --soc_version=Ascend310B1 --input_shape="x:1,3,32,32" --input_format=NCHW
等待片刻后提示"ATC run success"表示运行成功。
ATC基础功能参数:
–model:待转化模型路径
–weight:caffeine权重文件路径
–framework:原始框架类型 0:Caffe 1:Mindspore 3:TensorFlow
–input_shape:模型输入数据的shape
–output:转换完成后OM离线模型的路径
–soc_version:模型转换时指定芯片版本,这里是Ascend310
到这里模型基本就转化完成了,可以在Ascend上运行
五、模型推理优化
5.1 AIPP使能
什么是AIPP呢?AIPP(Artificial Intelligence Pre-Processing)人工智能预处理,用于在AI Core上完成对输入的JPEG/PNG等图片进行预处理,为了提升推理时图片预处理的性能,可以通过AIPP配置使其在AI Core上实现。
该模块功能与DVPP相似,都可以实现媒体数据处理的功能,两者的功能范围有重合,比如改变图像尺寸、转换图像格式,但功能范围也有不同的,比如DVPP可以做图像编解码、视频编解码,AIPP可以做归一化配置。与DVPP不同的是,AIPP主要用于在AI Core上完成数据预处理,DVPP是昇腾AI处理器内置的图像处理单元,通过AscendCL媒体数据处理接口提供强大的媒体处理硬加速能力。
下图是Ascend 310处理器硬件架构:可以清楚的看到AI Core 和 DVPP位置
注:dvpp模块即将废弃,建议使用ImageProcessor类。(from—官方文档)
5.2 AIPP配置
–aipp_mode:指定了AIPP的模式,必须配置,dynamic 表示动态,static表示静态
–ralated_input_rank:参数可选,表识对模型第几个输入做AIPP处理
–input_format:输入图像类型
–crop:AIPP处理图片时是否支持抠图
–色域转换系数,有模版无需修改
通过txt文档编写后保存为后缀名为.cfg,例如:insert_op_yuv.cfg
重新将文件转化 air–>om,在转换命令后加上:–insert_op_conf=./insert_op_yuv.cfg
atc --model=./resnet.air --framework=1 --output=resnet --soc_version=Ascend310B1 --insert_op_conf=./insert_op_yuv.cfg --input_shape="x:1,3,32,32" --input_format=NCHW
5.3 推理部分
这里就直接上代码了哈
import cv2 # 图片处理三方库,用于对图片进行前后处理
import numpy as np # 用于对多维数组进行计算
from mindx.sdk import Tensor # mxVision 中的 Tensor 数据结构
from mindx.sdk import base # mxVision 推理接口
from mindx.sdk.base import ImageProcessor,Size # mxVision 图像预处理接口
from PIL import Image, ImageDraw, ImageFont # 可视化相关接口
# 初始化昇腾相关资源和变量
base.mx_init() # 初始化 mxVision 资源
DEVICE_ID = 0 # 设备id
# 初始化模型相关参数和变量
MODEL_WIDTH = 32 # 模型接受的输入图片宽度
MODEL_HEIGHT = 32 # 模型接受的输入图片高度
MODEL_PATH = "resnet_fruit1.om" # 模型路径
# 标签名列表
image_net_classes = [。。。] #这里省略啦
# 初始化ImageProcessor对象
imageProcessor = ImageProcessor(DEVICE_ID)
# 图片路径
image_path = "100.jpg"
# 读取图片路径进行解码,解码格式为nv12(YUV_SP_420)
decoded_image = imageProcessor.decode(image_path, base.nv12)
print('decoded_image width:{}'.format(decoded_image.original_width))
print('decoded_image height:{}'.format(decoded_image.original_height))
print('==========图片解码完成=========')
# 图像缩放
# 缩放尺寸
size_para = Size(MODEL_WIDTH, MODEL_HEIGHT)
# 读取将解码后的Image类按尺寸进行缩放,缩放方式为华为自研的高阶滤波算法(huaweiu_high_order_filter)
resized_image = imageProcessor.resize(decoded_image, size_para, base.huaweiu_high_order_filter)
print('resized_image width: ', resized_image.original_width)
print('resized_image height: ', resized_image.original_height)
print('==========图片缩放完成=========')
# 转换tensor
input_tensors = [resized_image.to_tensor()]
print(input_tensors[0].shape)
print('========tensor转换完成=========')
# 模型推理, 得到模型输出
model = base.model(modelPath=MODEL_PATH, deviceId=DEVICE_ID) # 初始化 base.model 类
output = model.infer(input_tensors)[0] # 执行推理。输入数据类型:List[base.Tensor], 返回模型推理输出的 List[base.Tensor]
output.to_host() # 将Tensor数据转移到内存
output = np.array(output) # 将数据转为 numpy array 类型
class_id = output.argmax()
object_class = image_net_classes[class_id]# 映射为标签名
print()
# 将标签显示在原图左上角位置
image = Image.open(image_path)
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", size=20)
font.size =50
draw.text((10, 50), object_class, font=font, fill=255)
image.show()
结果展示:
在做推理的时候已经接近培训的尾巴了,时间有限,有许多欠考虑的地方,比如说已经通过配置AIPP对图像进行尺寸修改了,然而仍用ImageProcessor类对图像又进行了一次尺寸修改;没有对比使用AIPP的性能,应该加上使用Opencv对图像进行预处理操作,两者进行比较。
另附上Opencv DVPP AIPP 三者的比较:
名称 | OpenCV | DVPP | AIPP |
---|---|---|---|
硬件资源 | CPU | DVPP | AICore |
功能 | 功能最全 | 视频图片编解码,抠图,缩放,叠加,粘黏,格式转换 | 色域转化,格式转化归一化 |
六、 问题及解决方案
6.1 有缘遇到的
一些在开发中遇到的问题,完成这个项目过程总是伴随着磕磕绊绊,解决问题,让自己更进一步。
1.读取图片问题:
修改后(问题出在是否要对图片进行解码Decode())
2.模型优化出现的问题
采用多尺度融合,需要对高纬度的数据进行上采样,参考了torch函数nn.UpsamplingBilinear2d()在mindspore中无相应函数,查阅相关文档找到相应功能函数ms.ops.interpolate(),根据需求进行实现
3.对预测图片编码的要求图片的编码格式需要(YUV_SP_420)格式,否则在用dvpp处理图片时会报错
4.最后的图像类比预测,不管输入什么图片就只有一个类结果
原因:.cfg文件中归一化操作没有保留,在推过程中模型对图像中的像素值范围敏感,引起模型的不稳定性和预测结果的不准确性。
解决方案:加上上图归一化操作;
6.2 庆幸没遇到的
ATC模型转化问题
算子不支持!!
查看算子信息库,可以查看是否支持此算子,如果没有,需要自定义算子来实现不支持的算子。
(为什么说是庆幸没遇到呢?在本次培训中也简单的教了如何使用CANN-自定义算子开发DSL方式,以Add算子为例,老师说即使是他自己,让他开发一个算子,也需要一天时间来调通,在时间有限的情况下,这无疑是判了新手死刑T_T)
七、最后的碎碎念
这次小项目是对本次培训内容的一次疏通,
7.1 方向与展望
1.考虑迁移更有难度的项目,比如目标检测,目标跟踪等,在开发板上运行,本次选择图像分类时间上可以说刚刚好。。
2.模型改进方面,虽然提高精度,但是模型复杂度增加了,训练时间也增加了,要追求高性能轻量级网络。
3.在训练时有无使用预训练模型效果差太多了。如果在实际项目中,预训练模型很重要
4.数据增强与泛化能力: 研究数据增强方法,增加训练集的多样性,提高模型的泛化能力。
7.2 一些遗憾和不足
- 在模型训练上没有保存改进前后模型的详细变化,最后的结果也不够直观
- 时间不够充裕,开发板几乎没有外设,如果能接入摄像头效果会更好
- 培训中也有体验安卓开发,但并没有深入探究,就实现了个demo
- 没有体验自定义算子开发工具TBE。