所见即搜,3分钟教你搭建一个服装搜索系统!
摘要:用MindSpore+Jina,基于Fashion-MNIST Dataset搭建的服装搜索系统。
引言
各位算法萌新们,是不是经常训练了模型却不知道如何部署和应用?或者只会调参但不会前端后端所以没法向老板们解释这个模型可以做啥?如果有一种非常简单的方式,让你在3分钟内就能建立一个以深度学习为支撑的搜索系统,并能在前端展示出来show给各位老板们看?想不想尝试呢?本文来自MindSpore社区技术治理委员会(TSC)的成员肖涵博士——Jina的创始人,用MindSpore+Jina,基于Fashion-MNIST Dataset搭建的服装搜索系统。
[本文目录]
- 如何用Jina①步搞定?
- Jina的hello-world是如何运行的?
- 如何使用MindSpore+Jina来搭建搜索系统?
- 创建一个MindSpore Executor
- 修改MindSpore的Encoder和网络代码
- 写一个单元测试
- 准备Dockerfile
- 最后一步:终于可以Build了!
- 来看MindSpore的成品吧!
- 总结
喜欢逛淘宝或者各大海淘网站的各位程序员(的女朋友们),你们在浏览服装的时候,是不是会经常看见模特们身上的衣服,全!都!想!要!但是,不知道从哪儿买,货号是什么?就算从各大穿搭博主那儿知道货号了,也懒得一一去搜索。现在,完全不需要这么麻烦,只要你花3分钟建立这个服装搜索系统,当你的女朋友再看到模特身上的衣服,就可以搜索出最相似的衣服,是不是很赞!
图1 Shop the look
在做之前,先了解一下我们今天需要使用的两个框架:MindSpore和Jina
- MindSpore是2020年3月28日华为开源的深度学习框架,它能原生支持自家的昇腾芯片,极大的提升了运行性能!
- Jina是一个由最先进的AI和深度学习驱动的云端神经搜索框架,可以在多个平台和架构上实现任何类型的大规模索引和查询。无论你搜索图片、视频片段还是音频片段,Jina都能处理。
这里使用的数据集是Fashion-MNIST dataset。它包含70,000张图片,其中60,000张为训练集,10,000张为测试集。每张图片都是28x28的灰度图像,一共10个类别。下面我们正式开始吧!
如何用Jina①步搞定?
首先你需要一台电脑,确认一下环境是否ok:
- Mac OS or Linux
- Python 3.7, 3.8
- Docker
然后执行以下一行命令即可:
pip install jina && jina hello-world
或者直接用docker:
docker run -v "$(pwd)/j:/j" jinaai/jina hello-world --workdir /j && open j/hello-world.html # replace "open" with "xdg-open" on Linux
现在开始运行程序,就可以看到运行结果了:
图3 Jina hello-world运行结果
是不是很神奇?那么Jina是如何实现的呢?可以先花1分钟时间了解Jina的十个基本组件,在本文中最重要的三个信息分别是:
- YAML config:让用户可以自定义的描述对象的属性。
- Executor:代表了Jina中的算法单元。譬如把图像编码成向量、对结果进行排序等算法等都可以用Executor来表述。我们可以用Crafter来把制作/分割和转化要搜索的内容,然后用 Encoder来将制作好的搜索对象表示为向量,再用Indexer 保存和检索搜索的向量和键值信息,最后用Ranker来对搜索出的结果排序。
- Flow:表示一个高阶的任务, 譬如我们所说的索引(index)、搜索(search)、训练(train),都属于一个flow。
Jina的hello-world是如何运行的?
想知道hello-world运行的细节嘛?其实在很简单,在hello-world里,我们使用YAML文件来描述index和search的flow,可以导入YAML文件,并通过.plot() 命令来可视化:
from pkg_resources import resource_filename from jina.flow import Flow f = Flow.load_config(resource_filename('jina', '/'.join(('resources', 'helloworld.flow.index.yml')))).plot()
图4 hello-world YAML文件流程图
YAML文件里的信息是如何表示成图的呢?下面可以看看直观的对比:
图5 YAML文件信息
其实,这个flow中包含了两步(在Jina中也可以叫两个Pod):第一步它将数据通过并行的方式喂给encoder,输出向量和meta信息分片存储在索引器中。查询flow也是以同样的方式运行,只不过在参数上有些小变化。
既然原理这么简单,如果我们自己训练的模型,是不是也可以替换呢?下面我们来手把手教大家如何只用4步,就可以用MindSpore+Jina来搭建服装搜索系统。
如何使用MindSpore+Jina来搭建搜索系统?
创建一个MindSpore Executor
MindSpore的ModelZoo里有很多深度学习模型,本文使用的是最经典的CV网络:LeNet。我们可以通过jina hub来创建一个新的MindSpore Executor,本文使用的Jina Hub版本是v0.7的,可以输入以下命令安装:
pip install "jina[hub]"
安装好后,如果你想创建一个新的executor,可以直接输入:
jina hub new
执行这个命令后会弹出一下指导命令,按照下面的要求输入即可,有些设置直接用默认的就行,直接按Enter键就可以啦。
比较重要的是这几个命令:
所有命令输入完成后,你会看到MindSporeLeNet这个文件夹已经创建成功了。然后下载MindSpore 的LeNet代码库和Fashion MNIST的训练数据,按照下面的方式把它们放到MindSporeLeNet模块下即可:
图7 MindSporeLeNet代码结构
修改MindSpore的Encoder和网络结构代码
- 1.修改__init__.py
这是原始的__init__.py 代码,有一个基础类BaseEncoder ,我们要改变一下encode的方式,把它变成 BaseMindsporeEncoder。
from jina.executors.encoders import BaseEncoder class MindsporeLeNet(BaseEncoder): """ :class:`MindsporeLeNet` What does this executor do?. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # your customized __init__ below raise NotImplementedError def encode(self, data, *args, **kwargs): raise NotImplementedError
BaseMindsporeEncoder 是Jina中的抽象类,它在__init__构造函数中会导入MindSpore模型的checkpoint。此外,它还能通过self.model提供MindSpore模型的属性接口。下面这张表显示了MindSporeLeNet通过构造函数继承的类。
图8 MindSporeLeNet中继承的类
修改完以后如下所示:
from jina.executors.encoders.frameworks import BaseMindsporeEncoder class MindsporeLeNet(BaseMindsporeEncoder): """ :class:`MindsporeLeNet` Encoding image into vectors using mindspore. """ def encode(self, data, *args, **kwargs): # do something with `self.model` raise NotImplementedError def get_cell(self): raise NotImplementedError
- 2.执行 encode() 方法。
给定一堆batch size为B的图像数据(用的numpy 的ndarray来表示,shape为[B, H, W]),encode() 把图像数据转换成向量的embeddings(shape为[B, D])。通过self.model 导入MindSpore LeNet 模型后,我们可以通过self.model(Tensor(data)).asnumpy()来进行转换即可。
注意:self.model的输入shape很容易出错。原始的LeNet模型的输入是三通道的图片,shape是32x32,所以输入必须是[B, 3, 32, 32]。然而Fashion-MNIST是灰度图片,单通道,图像的shape是28x28,所以我们要么调整图片的尺寸,要么给图片补零。这里我们就用简单的补零操作了。最终的encode()函数如下所示:
def encode(self, data, *args, **kwargs): # LeNet only accepts BCHW format where H=W=32 # hence we need to do some simple padding data = numpy.pad(data.reshape([-1, 1, 28, 28]), [(0, 0), (0, 0), (0, 4), (0, 4)]).astype('float32') return self.model(Tensor(data)).asnumpy()
- 3.执行get_cell()方法。
在MindSpore中,我们通常把神经网络中的层叫做『cell』,它可以是一个单独的神经网络层(譬如conv2d, relu, batch_norm)。为了得到向量的embedding,我们只需要从LeNet中移除classification head 即可(譬如最后一个softmax层)。这个很好实现,只需要从原始的LeNet5类中继承,然后重写construct() 函数即可。
def get_cell(self): from .lenet.src.lenet import LeNet5 class LeNet5Embed(LeNet5): def construct(self, x): x = self.conv1(x) x = self.relu(x) x = self.max_pool2d(x) x = self.conv2(x) x = self.relu(x) x = self.max_pool2d(x) x = self.flatten(x) x = self.fc1(x) x = self.relu(x) x = self.fc2(x) x = self.relu(x) return x return LeNet5Embed()
写一个单元测试
当你在创建一个Jina executor 的时候,一定不要忘了写单元测试,如果在executor里没有单元测试的话,是无法通过 Jina Hub API来创建的哦~
在这个样例中已经生成了一个测试模板,你可以在tests 文件夹里面找到test_mindsporelenet.py 文件。先检查下MindSpore是否运行,如果可以运行的话,看看输出的shape是否是我们所希望的。
import numpy as np from .. import MindsporeLeNet def test_mindsporelenet(): """here is my test code https://docs.pytest.org/en/stable/getting-started.html#create-your-first-test """ mln = MindsporeLeNet(model_path='lenet/ckpt/checkpoint_lenet-1_1875.ckpt') tmp = np.random.random([4, 28 * 28]) # The sixth layer is a fully connected layer (F6) with 84 units. # it is the last layer before the output assert mln.encode(tmp).shape == (4, 84)
准备Dockerfile
python层面的准备工作已经完成了,下面我们准备Docker image。我们可以基于已有的Dockerfile来创建,只需要加一行运行train.py代码来生成checkpoint文件的代码即可。
FROM mindspore/mindspore-cpu:1.0.0 # setup the workspace COPY . /workspace WORKDIR /workspace # install the third-party requirements RUN pip install --user -r requirements.txt + RUN cd lenet && \ + python train.py --data_path data/fashion/ --ckpt_path ckpt --device_target="CPU" && \ + cd - # for testing the image RUN pip install --user pytest && pytest -s ENTRYPOINT ["jina", "pod", "--uses", "config.yml"]
这一行使用了MindSpore LeNet代码库里的train.py来生成训练的checkpoint。我们在测试和部署的时候会用到这个checkpoint。在config.yml文件中,需要把checkpoint的文件地址放在model_path这个参数里。requests.on定义了MindSporeLeNet在index和search的request下应该如何执行。如果上面这些内容不理解也没关系,其实都是从helloworld.encoder.yml这个文件里复制和改动的。
!MindsporeLeNet with: model_path: lenet/ckpt/checkpoint_lenet-1_1875.ckpt metas: py_modules: - __init__.py # - You can put more dependencies here requests: on: [IndexRequest, SearchRequest]: - !Blob2PngURI {} - !EncodeDriver {} - !ExcludeQL with: fields: - buffer - chunks
最后一步:终于可以Build了!
终于可以把MindSporeLeNet build成Docker镜像了!!执行以下命令:
jina hub build MindsporeLeNet/ --pull --test-uses
- --pull :当你的图片数据集不在本地时,这个命令会告诉 Hub builder 来下载数据集
- --test-uses :增加一个额外的测试来检查创建的镜像是否可以通过 Jina Flow API试运行成功。
现在终端已经开始打印日志了,如果时间太久的话,可以在MindsporeLeNet/lenet/src/config.py 中将epoch_size调小。
最后成功的信息:
HubIO@51772[I]:Successfully built cfa38dcfc1f9 HubIO@51772[I]:Successfully tagged jinahub/pod.encoder.mindsporelenet:0.0.1 HubIO@51772[I]:building MindsporeLeNet/ takes 57 seconds (57.86s) HubIO@51772[S]: built jinahub/pod.encoder.mindsporelenet:0.0.1 (sha256:cfa38dcfc1) uncompressed size: 1.1 GB
现在你可以通过下面的命令将它作为一个Pod来使用了:
jina pod --uses jinahub/pod.encoder.mindsporelenet:0.0.1
对比jina pod --uses abc.yml, 我们会发现jinahub/pod.encoder.mindsporelenet:0.0.1的日志信息的开头处有一个docker容器。这些log日志是从Docker的container传输到host端的,下面描述了两者具体的差异。
图9 差异对比
当然,你也可以上传这个镜像到Docker仓库里:
jina hub build MindsporeLeNet/ --pull --test-uses --repository YOUR_NAMESPACE --push
来看MindSpore的成品吧!
最后,直接在index和search的flow中来使用新创建的MindSpore Executor吧,很简单,只需要替换pods.encode.uses这行代码就行:
图10 index与query的YAML文件差异
jina hello-world 的参数可以自定义,只要指定我们刚刚编写好的index和query的YAML文件,输入以下命令即可:
jina hello-world --uses-index helloworld.flow.index.yml --uses-query helloworld.flow.query.yml
哈哈,完成了!几分钟之内你就可以看到开头动图显示的查询结果了!
图11 最终输出结果
总结
本文中使用了MindSpore+Jina来共同搭建一个服装搜索系统,代码非常简单,其实只要学会修改encode的代码,根据需要构建网络层,然后打包成docker的image,修改YAML文件就可以用Jina来实现最终的展示效果了,这样大家只要可以根据自己的需求,修改少量的代码,即可自行搭建一个基于MindSpore的搜索系统,是不是非常简单呢~感兴趣的同学可以直接点击以下链接,就可以直接运行:https://gitee.com/mindspore/community/tree/master/mindspore-jina
本文分享自华为云社区《3分钟教你用MindSpore和Jina搭建一个服装搜索系统!》,原文作者:chengxiaoli 。