摘要:用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 。