基于MindSpore Serving部署推理服务
今天要分享的是服务化部署框架(MindSpore Serving)
具体要实现的就是一个可以在线识图的页面。
1.MindSpore Serving安装
MindSpore Serving目前只能通过指定whl包安装
pip install https://ms-release.obs.cn-north-4.myhuaweicloud.com/1.7.0/Serving/x86_64/mindspore_serving-1.7.0-cp39-cp39-linux_x86_64.whl
2.MindSpore Serving使用
查看相关文档
https://www.mindspore.cn/serving/docs/zh-CN/r1.7/serving_example.html
快速入门的例子有点不够形象化
于是去github的代码仓里面看看样例都有哪些
https://gitee.com/mindspore/serving/tree/r1.7/example/resnet
需要没有GPU, 就拿resnet50来跑跑看,cifa10的数据集也不大,cpu下训练也不费事。
如下是相关模型的训练代码:
from mindspore.train import Model
from mindvision.dataset import Cifar10
from mindvision.engine.callback import ValAccMonitor
from mindvision.classification.models.classifiers import BaseClassifier
from mindvision.classification.models.head import DenseHead
from mindvision.classification.models.neck import GlobalAvgPooling
from mindvision.classification.utils.model_urls import model_urls
from mindvision.utils.load_pretrained_model import LoadPretrainedModel
from typing import Type, Union, List, Optional
from mindvision.classification.models.blocks import ConvNormActivation
from mindspore import nn
class ResidualBlockBase(nn.Cell):
expansion: int = 1 # 最后一个卷积核数量与第一个卷积核数量相等
def __init__(self, in_channel: int, out_channel: int,
stride: int = 1, norm: Optional[nn.Cell] = None,
down_sample: Optional[nn.Cell] = None) -> None:
super(ResidualBlockBase, self).__init__()
if not norm:
norm = nn.BatchNorm2d
self.conv1 = ConvNormActivation(in_channel, out_channel,
kernel_size=3, stride=stride, norm=norm)
self.conv2 = ConvNormActivation(out_channel, out_channel,
kernel_size=3, norm=norm, activation=None)
self.relu = nn.ReLU()
self.down_sample = down_sample
def construct(self, x):
"""ResidualBlockBase construct."""
identity = x # shortcuts分支
out = self.conv1(x) # 主分支第一层:3*3卷积层
out = self.conv2(out) # 主分支第二层:3*3卷积层
if self.down_sample:
identity = self.down_sample(x)
out += identity # 输出为主分支与shortcuts之和
out = self.relu(out)
return out
class ResidualBlock(nn.Cell):
expansion = 4 # 最后一个卷积核的数量是第一个卷积核数量的4倍
def __init__(self, in_channel: int, out_channel: int,
stride: int = 1, norm: Optional[nn.Cell] = None,
down_sample: Optional[nn.Cell] = None) -> None:
super(ResidualBlock, self).__init__()
if not norm:
norm = nn.BatchNorm2d
self.conv1 = ConvNormActivation(in_channel, out_channel,
kernel_size=1, norm=norm)
self.conv2 = ConvNormActivation(out_channel, out_channel,
kernel_size=3, stride=stride, norm=norm)
self.conv3 = ConvNormActivation(out_channel, out_channel * self.expansion,
kernel_size=1, norm=norm, activation=None)
self.relu = nn.ReLU()
self.down_sample = down_sample
def construct(self, x):
identity = x # shortscuts分支
out = self.conv1(x) # 主分支第一层:1*1卷积层
out = self.conv2(out) # 主分支第二层:3*3卷积层
out = self.conv3(out) # 主分支第三层:1*1卷积层
if self.down_sample:
identity = self.down_sample(x)
out += identity # 输出为主分支与shortcuts之和
out = self.relu(out)
return out
def make_layer(last_out_channel, block: Type[Union[ResidualBlockBase, ResidualBlock]],
channel: int, block_nums: int, stride: int = 1):
down_sample = None # shortcuts分支
if stride != 1 or last_out_channel != channel * block.expansion:
down_sample = ConvNormActivation(last_out_channel, channel * block.expansion,
kernel_size=1, stride=stride, norm=nn.BatchNorm2d, activation=None)
layers = []
layers.append(block(last_out_channel, channel, stride=stride, down_sample=down_sample, norm=nn.BatchNorm2d))
in_channel = channel * block.expansion
# 堆叠残差网络
for _ in range(1, block_nums):
layers.append(block(in_channel, channel, norm=nn.BatchNorm2d))
return nn.SequentialCell(layers)
class ResNet(nn.Cell):
def __init__(self, block: Type[Union[ResidualBlockBase, ResidualBlock]],
layer_nums: List[int], norm: Optional[nn.Cell] = None) -> None:
super(ResNet, self).__init__()
if not norm:
norm = nn.BatchNorm2d
# 第一个卷积层,输入channel为3(彩色图像),输出channel为64
self.conv1 = ConvNormActivation(3, 64, kernel_size=7, stride=2, norm=norm)
# 最大池化层,缩小图片的尺寸
self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, pad_mode='same')
# 各个残差网络结构块定义,
self.layer1 = make_layer(64, block, 64, layer_nums[0])
self.layer2 = make_layer(64 * block.expansion, block, 128, layer_nums[1], stride=2)
self.layer3 = make_layer(128 * block.expansion, block, 256, layer_nums[2], stride=2)
self.layer4 = make_layer(256 * block.expansion, block, 512, layer_nums[3], stride=2)
def construct(self, x):
x = self.conv1(x)
x = self.max_pool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
return x
def _resnet(arch: str, block: Type[Union[ResidualBlockBase, ResidualBlock]],
layers: List[int], num_classes: int, pretrained: bool, input_channel: int):
backbone = ResNet(block, layers)
neck = GlobalAvgPooling() # 平均池化层
head = DenseHead(input_channel=input_channel, num_classes=num_classes) # 全连接层
model = BaseClassifier(backbone, neck, head) # 将backbone层、neck层和head层连接起来
if pretrained:
# 下载并加载预训练模型
LoadPretrainedModel(model, model_urls[arch]).run()
return model
def resnet50(num_classes: int = 1000, pretrained: bool = False):
"ResNet50模型"
return _resnet("resnet50", ResidualBlock, [3, 4, 6, 3], num_classes, pretrained, 2048)
# 数据集根目录
data_dir = "./data"
# 下载解压并加载CIFAR-10训练数据集
dataset_train = Cifar10(path=data_dir, split='train', batch_size=6, resize=32)
ds_train = dataset_train.run()
step_size = ds_train.get_dataset_size()
# 下载解压并加载CIFAR-10测试数据集
dataset_val = Cifar10(path=data_dir, split='test', batch_size=6, resize=32)
ds_val = dataset_val.run()
# 定义ResNet50网络
network = resnet50(pretrained=True)
# 全连接层输入层的大小
in_channel = network.head.dense.in_channels
head = DenseHead(input_channel=in_channel, num_classes=10)
# 重置全连接层
network.head = head
# 设置学习率
num_epochs = 40
lr = nn.cosine_decay_lr(min_lr=0.00001, max_lr=0.001, total_step=step_size * num_epochs,
step_per_epoch=step_size, decay_epoch=num_epochs)
# 定义优化器和损失函数
opt = nn.Momentum(params=network.trainable_params(), learning_rate=lr, momentum=0.9)
loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
# 实例化模型
model = Model(network, loss, opt, metrics={"Accuracy": nn.Accuracy()})
# 模型训练
model.train(num_epochs, ds_train, callbacks=[ValAccMonitor(model, ds_val, num_epochs)])
感觉最终的训练准确度不够高。十几个小时白费了。
训练出来的模型需要保存为mindir才能用。
执行example里面的export_resnet.py,将模型进行转换。但是export_resnet.py这个脚本是直接下载已经训练好的模型来转换的,
如果要转换自己训练出来的模型记得修改下文件。
执行export_resnet.py会报错,先是报缺少easydict模块。
执行pip install easydict, 安装报错。查询了一下,是setuptools版本过低造成。于是执行pip install -U setuptools进行升级。
然后再执行pip install easydict,顺利安装成功。
这个时候转换成功了。生成了mindir模型。
有了模型就可以运行server。
执行启动server后报错,提升缺少so文件。
通过sudo apt-get install libpython3.9进行库安装。
再次运行server。
依然报错,提示不支持的设备。
查看代码
config = server.ServableStartConfig(servable_directory=servable_dir, servable_name="resnet50", device_ids=0,
num_parallel_workers=4)
于是查询了下api,看可以设置device_type
于是增加了device_type=“CPU”
还是不行,于是看了下文档。
因为当前是CPU,因此推理后端只能是Mindspore Lite
只能安装Mindspore Lite了
推理/训练runtime、推理/训练jar包、以及benchmark/codegen/converter/cropper工具 | CPU | Linux-x86_64 | https://ms-release.obs.cn-north-4.myhuaweicloud.com/1.7.0/MindSpore/lite/release/linux/x86_64/mindspore-lite-1.7.0-linux-x64.tar.gz |
既然用Lite了,模型也要转换。通过如下命令将模型转换。
修改servable_config.py文件,把模型修改成mindspore lite模型
然后再执行
然后查看_servable_local.py在什么情况下会抛出异常。
看代码是执行了函数开头的命令后没有获取到相关设备信息才报的错。把这个命令单独拎出来执行下看看。
直接报so找不到。先设置下LD_LIBRARY_PATH, 首先是查看下相关的so文件位置在哪。然后再进行相关设置。
最新服务起来了。如果设置了127.0.0.1只能本机访问,因为使用的是虚拟机,外部访问需要修改ip为0.0.0.0。同时虚机的网络要设置为桥接模式才可以。
然后查看虚机ip
代码测试通过,grpc还有点问题,我也只用到了restfull,grpc就先不管了。这里用模型是我本地训练出来的,看来还是不行,于是就换了样例里面的模型文件。
3.页面访问
页面访问的话方案很多,可以是vue或是flask。不过都要写页面,html JavaScript等等。
在网上搜到一个纯python的库,streamlit, 看上去很不错。
Streamlit 是一个基于 Python 的 Web 应用程序框架,致力于以更高效、更灵活的方式可视化数据,并分析结果。
Streamlit是一个开源库,可以帮助数据科学家和学者在短时间内开发机器学习 (ML) 可视化仪表板。只需几行代码,我们就可以构建并部署强大的数据应用程序。
为什么选择Streamlit?
目前,应用程序需求量巨大,开发人员需要一直开发新的库和框架,帮助构建并部署快速上手的仪表板。Streamlit 是一个库,可将仪表板的开发时间从几天缩短至几小时。以下是选择 Streamlit 的原因:
1. Streamlit是一个免费的开源库。
2. 和安装其他python 包一样, Streamlit的安装非常简单。
3. Streamlit学起来很容易,无需要任何 Web 开发经验,只需对 Python 有基本的了解,就足以构建数据应用程序。
4. Streamlit与大部分机器学习框架兼容,包括 Tensorflow 和 Pytorch、Scikit-learn 和可视化库,如 Seaborn、Altair、Plotly 等。
#! -*- coding=utf-8 -*-
import cv2
import numpy as np
import streamlit as st
import base64
import requests
import json
st.title('图片识别演示')
labels = {'airplane':'飞机',
'automobile':'汽车',
'bird':'鸟',
'cat':'猫',
'deer':'鹿',
'dog':'狗',
'frog':'青蛙',
'horse':'马',
'ship':'船',
'truck':'卡车'}
uploaded_file = st.file_uploader("上传文件", type="jpg")
if uploaded_file is not None:
file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
opencv_image = cv2.imdecode(file_bytes, 1)
st.image(opencv_image, channels="BGR")
instances = []
base64_data = base64.b64encode(file_bytes).decode()
instances.append({"image": {"b64": base64_data}})
instances_map = {"instances": instances}
post_payload = json.dumps(instances_map)
ip = "192.168.0.225"
restful_port = 1500
servable_name = "resnet50"
method_name = "classify_top1"
result = requests.post(f"http://{ip}:{restful_port}/model/{servable_name}:{method_name}", data=post_payload)
result = json.loads(result.text)
label = result['instances'][0]['label']
st.text(f'图片识别为:{labels[label]}')
#cv2.imwrite('test.jpg',opencv_image)
执行如下命令启动服务器:
streamlit run D:\ai\0709\server.py
最终的效果如下:上传文件支持拖拽和选择上传。
最终效果如下:
这个青蛙有点离谱了。。。模型的准确性有点低。