BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Yahoo!の近傍探索ツールNGTを使って類似商品APIをつくる

はじめまして、BASEビール部部長の氏原です。BASEのData Strategy Groupで機械学習エンジニアをしています。 今回初登場ということで、暑いときにいいサワーエールのお話でも......といきたいところですが、ここは開発ブログということなので仕方ありません。開発のお話をしましょう。

現在私は商品の画像に基づいて、その商品に似た商品を類似商品として提示するAPIの開発を行なっています。今回はこのAPIをYahoo!さんのNGT(Neighborhood Graph and Tree for Indexing)を使って作成したことについて書いてみようと思います。

f:id:beerbierbear:20180814180938p:plain:w800

背景

BASE株式会社はネットショップ作成サービス「BASE」を運営しています。ここで作成されたショップはそれぞれ別のWEBサイトとして公開されていますが、ショッピングアプリ「BASE」では作成されたショップを横断して商品を検索し購入することができます。しかしショップの数は公称で50万店舗、非常に多くの商品がありますのでユーザーさんが興味を持つであろう商品をいかにして探しやすくするかはサービスにとって喫緊の課題と言えます。その取り組みの一環として、私は商品詳細ページにある関連商品の改善を目的に類似商品APIの作成を行っています。

全体の概観

類似商品APIは以下の3つの構成要素で成り立っています。

  1. 画像の特徴量抽出
    • 新しい商品画像がサービスに登録された際に特徴量を計算して保存する
  2. 特徴量のindexing
    • 関連商品として出す商品の画像を選別して近傍探索用のindexを作成する
  3. API
    • 検索元商品の画像から近傍の画像を探索しその画像に対応する商品を類似商品として返す

ではそれぞれについて解説していきましょう。

画像の特徴量抽出

BASEには毎日結構な量の商品が新しく登録されていきます。既存の商品の画像を差し替えたりすることもあります。それらの新しい画像の特徴量は随時計算しておかないといけません。

特徴量抽出の全体構成

BASEでは商品画像をS3に保存しています。S3ではファイルが登録されると、そのことをイベントとして通知できます。特徴量抽出ではそれを利用して以下のようにシステムを組んでいます。

f:id:beerbierbear:20180814175019p:plain:h300

S3, SNS, SQS

S3で画像が登録されると、そのイベントをまずSNSに投げます。そしてSNSはそのイベントをそのままSQSに投げます。

一旦SNSを通しているのは、画像登録を契機に何かしらの処理を行いたいという要望は今後他にも出てくることを想定しているためです。SNSであればsubscriberを増やせばイベントをbroadcastできます。

こうしてS3に登録された画像はイベントとしてSQSに溜まっていきます。

ECS

SQSに溜まったイベントを取りに行くのがECS配下で稼働しているServiceです。ここではSQSからイベントをpollingして、取得したイベントから画像を取得して一枚づつ特徴量を計算してDBに保存していきます。ECSはAuto Scalingと組み合わせればSQSが溜まってきたときにServiceを増やすのが簡単です。

画像の特徴量

みなさん、画像の特徴量といえば何を思いつきますか?SIFTとかHOGでしょうか。最近ですとDeepLearningでしょうか。今回、画像の特徴量の抽出にはMobileNetを利用しました。

いちからMoblieNetを作るのではなく学習済みモデルをそのまま利用しました。Kerasのやつですね。ホント楽になりましたね。

from keras.applications.mobilenet import MobileNet

model = MobileNet(weights='imagenet',
                  include_top=False,
                  input_shape=(224, 224, 3),
                  pooling="max")

include_topはFalseにしてクラス分類のネットワークは外して特徴量を抽出する部分だけ使います。画像はもう単純に224×224にリサイズして使います。これで224×224のRGB画像からfloat32の1024次元のベクトルが得られます。

from keras.applications.mobilenet import preprocess_input
from keras.preprocessing import image

import boto3
import keras.applications.mobilenet
import numpy as np

import io

# S3から画像を取ってくる
s3 = boto3.resource('s3')
img_object = s3.Object(bucket, object_key)
response = img_object.get()
# 画像を読み込む
img_data = io.BytesIO(response["Body"].read())
img = image.load_img(img_data, target_size=(224, 224))
# numpyのarrayにしてMobileNetの前処理をする
x = image.img_to_array(img)
xs = preprocess_input(np.array([x])
# 特徴量を計算する
vec = model.predict(xs).flatten()

特徴量を保存するDB

上で得られた特徴量はDBに保存しておきます。 今回特徴量を保存するのにはAuroraを利用しました。1024次元のfloat32のベクトルを保存するのに容量あまり気にしないでもいい場所が欲しかったためです。でもRDSとか単にベクトル保存する場所としては機能過多ではあります。必要な機能を考えると単なるKey-Value Storeでいいんですが、ここは今後も要検討です。

特徴量のindexing

画像登録に連動してDBに特徴量を保存できるようになりましたが、サービスで使うにはある画像の特徴量vectorの近傍にあるvectorがどれなのかを知ることができるようにしなくてはいけません。これを実現するために今回はNGTを利用しました。

NGTとは

Yahoo!さんの説明をそのまま引用させていただきます。

NGTは任意の密ベクトルに対して事前に登録した(同次元の)ベクトルから最も距離が近いベクトルの上位数件(k件)を高速に近似k最近傍探索(k-Nearest Neighbor Search)するためのソフトウエアです。 高次元ベクトルデータ検索技術「NGT」の性能と使い方の紹介

超高速に近傍のベクトルを探せるソフトです。本当に超高速です。350万件の1024次元のベクトルを登録してみたところ、メモリを15G程度食いますが近傍1000件取ってくるのに数十msくらいしかかかりません。これならキャッシュを併用すれば十分使えると判断しました。 python wrapperもあるので使うのも簡単です。

index作成の全体構成

ECS Taskを利用したバッチ処理でNGTのindexを作成しています。

f:id:beerbierbear:20180814175146p:plain:h300

ECS

indexの作成は日次バッチで行うのでCloudWatch Eventをdailyで投げるようにして、ECSがそれを受け取ってTaskを実行するようにしました。 TaskはAuroraからindexingする特徴量を取得してNGTに登録します。このとき全部の特徴量を使うのではなく、古い商品は登録しないなどある程度の取捨選択をしています。

NGTはpython wrapperを使ってこんな感じです。(適当に簡略化してます)

from ngt import base as ngt

ngt_index = ngt.Index.create(b"any/where/you/want/to/save/index", 1024)

# 結果は大きいのでサーバーサイドCursorつかう
conn = MySQLdb.connect(..., cursorclass=MySQLdb.cursors.SSCursor)
cursor = conn.cursor()
cursor.execute(......) # 画像の特徴量とかとってくる

# 大きくて全部持ってこれないから一個づつ処理
for row in cursor:
    image_id, vec = row
    oid = ngt_index.insert_object(vec)
    # NGT内でのobject idと画像のIDの紐付けは自分で覚えておく必要あり
    ...

# indexの作成
ngt_index.build_index(num_threads=8)

S3

作成されたNGTのindexはS3に保存しています。indexは毎日作成され、ある程度の期間保存して古いものは捨ててます。

API

NGTのindexが日々作成されるようになりましたので、今度はそれを利用する部分を用意しましょう。

API提供部分の全体構成

f:id:beerbierbear:20180814175216p:plain:h150

構成としてはAPI Gatewayを入り口としたECSの二段構えになっています。 これは類似商品を取得するという機能と類似画像を取得するという機能を分離させておくことで、類似画像APIを利用した他の機能の開発を簡単にするためです。例えば、現在BASEの商品の画像検索APIも開発中ですが、これはこの仕組みにそのまま乗っかっています。

f:id:beerbierbear:20180814175234p:plain:h300

類似商品API

ここは以下のような役割を受け持ちます。

  1. API Gatewayから商品IDを受け取る
  2. 商品IDを対応する画像IDに変換する
  3. 画像IDを類似画像APIに投げ、類似画像のIDを受け取る
  4. 類似画像のIDを対応する商品IDに変換する
  5. 類似商品のIDをAPI Gatewayに返す

やってることはシンプルです。 構成のところには出ていませんが、毎度類似画像APIを呼ばなくてもいいようにElastiCacheを設置して、類似画像APIを呼ぶ前にそっちを確認しています。

類似画像API

ここはNGTの薄いwrapperになってます。やっているのは画像IDや画像の特徴量そのものを受け取り、近傍の画像のIDを返すだけです。 ここはindex作成が終わったときに新しいindexを読むように再起動されます。この再起動はECSの仕組みを使って複数動いているECS Serviceを少しづつ再起動させていくことでサービスとしては無停止で行われるようになっています。

実際にどう変わるか

では、構築したAPIで類似商品がどの程度改善したかみてみましょう。

f:id:beerbierbear:20180814175959j:plain:h600

この洋服の関連商品は以前のロジックだと以下のようになってました。

f:id:beerbierbear:20180814180047j:plain:h600

うん、服ですらないですね。 正直どうしてこれらが関連商品に上がってきたのか謎です。名前だろうか。

で、それが今回のAPIで以下のように改善しました。

f:id:beerbierbear:20180814180241j:plain:h600

おお!服だ! ......まあそれだけで感動できるほど前のが悪すぎたという話もあります。 雰囲気もなんとなく似てますかね。ファッション系は概ね良い感じに類似商品を出せるようになりました。

まとめ

私の所属しているData Strategyチームは最近できたばかりで、まっさらなAWS環境を渡してもらってインフラ含めて一から類似商品APIを構築するというなかなか楽しい経験をさせてもらいました。まだまだ精度を上げたいところではありますが、そこそこ良いものができたのではないかと思っています。 実際関連商品のタップされる率は今までの2倍ほどに伸びており一安心というところです。

今回構築した類似商品APIでは今のところ画像しか見ていません。そのため少々コンテキストから外れたものが出てくることもあります。そこで現在商品のタイトルや説明も考慮するようにAPIをアップデートしようとしているところです。もっと精度を上げて、私がいいビールを探せるように......いえ、ユーザーさんがいい商品をみつけられるようにこれからも頑張ります。

開発した成果は、ショッピングアプリ「BASE」で見ることが出来ます。個別の商品を見るビューをスクロールして「関連する商品」をぜひご覧下さい。

P.S.

長くなるんで省きましたが、実は今回作ったAPIのAWSの構成管理はTerraformで書いて1発で構築できるようにしています。その話はまたの機会にしたいと思います。