Blog
本記事は,2021年度PFN夏季インターンシップで勤務された青山裕良さんによる寄稿です.
はじめに
PFN2021 夏季インターンに参加した青山裕良です.現在は東京工業大学 工学院機械系 ライフエンジニアリングコースで只野研究室に所属し,手術ロボットに関する研究をしています.
今回のインターンでは,効率的な特定物体アノテーションを実現するWebアプリケーション開発に取り組みました.
背景
まず,機械学習の文脈における”アノテーション”とは,データに対してメタデータを注釈として付与することです. 以下のようなアノテーション済み教師データを目にする機会は多いかと思います.
https://projects.preferred.jp/tidying-up-robot/
広く普及している一般物体のアノテーションだけでなく,特定物体のアノテーションもリテールやロボット,農業などの分野で活用されています. しかし,特定物体はクラス数が多くなると,一般物体よりもアノテーションに時間がかかり非常に大変です. 上記の例であれば,”pen” よりも “PROCKEY_yellow” などと具体的に注釈をつける方が大変なことは想像がつくかと思います.
この特定物体アノテーションにかかる時間を短縮するために,アノテータが囲った物体と画像的に類似する物体を検索するアノテーションアプリケーションの開発に取り組みました. 従来,類似画像検索をロバストに行うのは難しかったですが,近年 Deep learning ベースの手法によって実用的になってきています.中でも今回は OpenAI から発表された高いZero-shot性能を持つ CLIP を画像特徴量抽出器として使用しました.
成果物
今回は kaggle の Retail Product Checkout Dataset を使用し,商品名のアノテーションを行いました.
以下は今回開発したアプリケーションのデモ動画で,この例では前述のデータセットに入っている200商品をマッチング対象としています.左側に表示されている5商品は類似画像商品トップ5で,該当商品が概ねそこに入っていることが分かります.
アノテータがバウンディングボックスで対象商品を囲むと,囲った領域の画像がサーバに送られます.サーバにはあらかじめマッチング対象の商品画像をエンコードしてまとめた行列を持っておき,クライアントから送られてきた画像を CLIP の Image Encoder でエンコードしたベクトルとの内積から類似度の計算を行います. 最後に類似度の上位5位の商品画像と商品名をクライアントに返し,表示します.
以上の処理を経て,100~400ms程度で類似画像商品がアプリ上に表示され,簡単に該当商品を見つけることができます. また,僕は中国語が読めないですが,中国語のみが書かれた商品のアノテーションができます.
アノテーションデータはJSON形式でエクスポートしてデータセットの作成等に活用できます.(インポートして修正なども可能です.)
もう少し実用的な実験として,約15000の製品をマッチング対象として,アノテーション時間を測定しました. これは,従来行われてきた商品名検索によるアノテーションよりも,今回開発した類似画像検索を用いたアノテーションの方が効率が良くなっていることを期待したものです. 実際に1枚の画像アノテーション (82商品) にかかる時間を測定したところ,従来手法と比べて 約2/3 に短縮できていることが分かりました.
① Conventional | ② Proposed | |
---|---|---|
Surround products with bboxes | 06m00s | |
Annotate product names | 18m36s | 10m03s |
Total | 24m36s | 16m03s |
技術的な小話
バックエンド
FastAPI を使ってます. Dockerfile も tiangolo/uvicorn-gunicorn-fastapi をベースイメージにすれば簡単に書けます.
そして FastAPI はデフォルトで吐く openapi.json を openapitools/openapi-generator-cli に投げると,RESTのAPIクライアントTypeScriptコードを自動生成できるのも便利です.(Python で定義した型を TypeScript で定義し直す時間とか何も生み出されてないので…)
フロントエンド
Next.js + TypeScript を使ってます.
状態管理は Redux Toolkit を使用して以下のように管理しています.(もっと良いストアの設計があったら教えて下さい)
export type AppState = { products: Res<Product[]>; keywordProducts: Res<Product[] | null>; tasks: Res<Task[]>; editingRegionId: RegionId | null; editingRegion: Res<RegionStruct | null>; selectedProduct: Res<Product | null>; }; export const initialState: AppState = { products: { loading: false, data: [] }, keywordProducts: { loading: false, data: null }, tasks: { loading: false, data, }, editingRegionId: null, editingRegion: { loading: false, data: null }, selectedProduct: { loading: false, data: null, }, };
スタイルは Tailwind CSS と Emotion を使用しています.以下のように className にすべてのスタイルが記載され,個人的には気に入ってます.(tailwindでパッと書けないスタイルは普通にcssで書いた方が早そう)
import { css, cx } from "@emotion/css"; import React, { FC } from "react"; type Props = { isOn: boolean; } & JSX.IntrinsicElements["div"]; export const ToggleButton: FC<Props> = ({ isOn, className, onClick, ...props }) => ( <div className={cx( `inline-block cursor-pointer relative w-[50px] h-[30px] max-w-full bg-white`, `transition ${className} ${isOn ? "on" : ""}`, css` vertical-align: middle; border-radius: 100em; box-shadow: inset 0px 1px 1px 1px #d6d6d6, inset 0 -1px 1px 1px #ececec, inset 0 0 0px 2px #f5f5f5; ::before { content: ""; display: block; position: absolute; left: 5%; top: 10%; width: 50%; height: 80%; background: white; border-radius: 100%; box-shadow: -2px 2px 2px 0 #bbbbbb, 0px 0px 10px 0 #e4e4e4; } &.on { background: #00de00; box-shadow: unset; transition: all 0s; } &.on::before { margin-left: 40%; box-shadow: 2px 2px 2px 0 #bbbbbb, 0px 0px 10px 0 #e4e4e4; } `, )} onClick={onClick} {...props} /> );
インフラ
PFN の Kubernetes クラスタにデプロイしました. k8s は全く触ったことがなかったのですが,どうやらdocker-compose.yml
から k8s の manifest を生成できるらしいというお話を聞き,やってみました. 具体的には,docker-compose.yml
から Kompose を使用して k8s resources manifest を生成し,そのファイル群を kustomize に食わせて manifest.yaml
を生成しました.(ただし,最後に直接 manifest の中身をいじって調整しました.) あとは,$ cat manifest.yaml | kubectl apply -f -
で簡単にデプロイできます.
フロントエンドとバックエンドの pod に加えて,nginx の pod を立て,各pod にアクセスを振り分けています. 前二者は予め自分の作成したdocker imageをコンテナレジストリに上げておき,それを pull して pod が起動しますが,nginx には以下のように ConfigMap を使って設定を渡します.
apiVersion: apps/v1 kind: Deployment metadata: annotations: kompose.cmd: kompose convert -f ../docker-compose.yml kompose.version: 1.23.0 (bc7d9f4f) creationTimestamp: null labels: io.kompose.service: nginx name: nginx spec: replicas: 1 selector: matchLabels: io.kompose.service: nginx strategy: {} template: metadata: annotations: kompose.cmd: kompose convert -f ../docker-compose.yml kompose.version: 1.23.0 (bc7d9f4f) creationTimestamp: null labels: io.kompose.service: nginx spec: volumes: - name: config configMap: name: reverse-proxy containers: - image: nginx volumeMounts: - name: config mountPath: "/etc/nginx/nginx.conf" subPath: nginx.conf name: nginx ports: - containerPort: 8000 resources: {} restartPolicy: Always status: {} --- apiVersion: v1 kind: ConfigMap metadata: name: reverse-proxy data: nginx.conf: | events { worker_connections 16; } http { server { listen 8000; server_name localhost; charset utf-8; client_max_body_size 10M; location /api/v1 { proxy_pass http://backend:8080; proxy_redirect off; } location / { proxy_pass http://frontend:3000; proxy_redirect off; } } }
まとめ・感想・謝辞
今回はフロントエンドでインターンに採用されましたが,実際にはバックエンドやインフラや学習周りも同時に実装を進め,あっという間に5週間が過ぎてしまいました. この短い期間の中で1つのアプリケーションを使える状態に仕上げるのは基本的には大変なことだと思いますが,今回それができたのは環境的な要因が大きかったように思います.
1つは自分の各領域の質問に対して,メンターの秋田さん松元さんをはじめとする社員の皆さん誰かが必ず明確な解決策を提案してくれたことです. 社員さんの時間を余計に取ってしまったような気もしていますが,今まで詰まっても基本的に自分で頑張って解決する環境で生きてきたので,様々な領域について社内の誰かに聞けば分かるという環境に居心地の良さを感じました.
また,あまり意識はしてませんでしたが,期間中毎日迷いなく進み続けることができていたように思います.これはミーティングの中で,次の方針についてこういう理由でここから手をつけた方が良いというメンターお二方の判断が非常に早かったためだと振り返り,普段の自分について反省する機会にもなりました.
GPUクラスタをいつでも使えることについても想像以上に良い影響がありました. 今回はWebアプリケーション開発をしていたので,周りの方よりは使用することが少なかったと思いますが,それでもデータセットの処理やモデルのFinetuneなどちょっとした作業をクラスタに投げつけて時間を短縮できた実感があります.
これから自分がどの分野・業界に進んだとしても,この5週間の経験が必ずどこかで役に立つと思えるほどに,濃く深い時間を過ごすことができました.大変お世話になりました.ありがとうございました.
Tag