Amazonのデータセットで始める商品検索
この記事は 情報検索・検索技術 Advent Calendar 2022 の7日目の記事です。
はじめに
今年の夏、Amazonが多言語 (英語、スペイン語、そして日本語) のラベル付きの商品検索のデータセットを公開しました。
情報検索において商品検索は、ウェブ検索を作りたいという企業より商品検索を作りたい企業の方が多いという意味で、ポピュラーなトピックだと思います。ところが公開データで実験を行おうとするとドメインが違うウェブ検索のデータセットか、ラベルのない商品カタログか、ラベルはあるけど小規模なデータセットかという限られた選択肢しかなく、仕方がないので非公開の独自データセットを作って実験を行うという状況でした。
しかし、それがAmazonのデータセット公開により状況が大きく変わりました。日本語を含む多言語での大規模ラベル付き商品検索のデータセットはおそらく世界初で、論文でもこのデータがこれからのゴールドスタンダードになるだろうと述べていて、来年以降はこのデータセットを使った提案手法がたくさん世に出てくると思います。
来る日に備えて、商品検索の諸問題に対する各手法の比較を行えるように、商品検索をゼロから作っているので、本記事ではAmazonのデータセットと私が実装を行っているリポジトリの紹介をします。なお、この記事では全体の流れに焦点を当てるために個別のアルゴリズムについては議論しません。
リポジトリについて
こちらがそのリポジトリです: https://github.com/rejasupotaro/amazon-product-search これまでは新しい手法を見つけるとアドホックにJupyter Notebookを書いて検証していたのですが、それだと過去のノートブックに書かれた他の手法との比較が難しいとか、そもそも後日コードを見て何がやりたかったのか分からなくなるといった致命的な問題があったので wiki を作って各手法に対してメンテナンス可能な形で実装、実験、考察のセットで置こうとしています。最初はprivateでやろうとしていたのですが、wikiがpublicなリポジトリにしか作れないようなので、せっかくなら公開しようと思ったのがこの記事を書き始めた背景です。
その検索システムの現在の全体像は以下のようになっています。
一般的な検索システムのようにこのプロジェクトもIndexing (データソースから検索インデックスを作る側、Ingestion PipelineやETL Pipelineなどと呼ばれることがある) とRetrieval (クエリを受け取って検索エンジンに問い合わせる側、いわゆる検索アプリ) の2つの主要なコンポーネントからなります。
まずpipelineに商品のデータを流してElasticsearchのインデックスを作って、Streamlitのデモアプリがラベルのデータを読んでRetrievalを通して実験を行って記録に残す、という流れになっています。以下のセクションで各フェーズをざっくりと説明していきます。
データセットについて
まずデータセットについて見ていきます。Amazonのリポジトリ https://github.com/amazon-science/esci-data/tree/main/shopping_queries_dataset には以下の3つのファイルがあります。
-
shopping_queries_dataset_products.parquet
(商品情報) -
shopping_queries_dataset_examples.parquet
(ラベル) -
shopping_queries_dataset_sources.csv
(クエリのソース)
商品情報について
各商品は product_id
, product_title
, product_description
, product_brand
, product_color
などの情報を含みます。それぞれどのような値が入っているかを見ていきます。
product_title
"靴", "Z", "8" のような一文字で構成される商品名もあれば "Duduma 偏光 レンズ メンズスポーツサングラス 超軽量 UV400 紫外線をカット / 自転車/釣り/野球/テニス/ゴルフ/レース/ランニング/ドライブ T90 (ブラックマットフレーム/ブラックレンズ)" のように長く、多くのメタ情報を含んでいる商品名もあります。
product_description
文字通り商品の説明文で、<b>
, <br>
, <p>
, <ul>
のようなHTMLが入っていることがあります。そして、56%が空になっています。
商品紹介
1、Dudumaの特別なところのを発見
超軽量TR90フレーム:あなたはサングラスを着けていることをほとんど感じません!
TR90フレームは、最も快適なフィット感と比類を見ない耐久性を提供します。
素晴らしいファッションの限定スポーツサングラスはスキー、ゴルフ、サイクリング、ランニング、釣り、ドライブ、クリケットや全てのアウトドアのスポーツに適しています。
2、特点と利点
カラフルなラインと美的な外観
...
product_bullet_point
箇条書きになっている商品のメタ情報ですが、内容的には description
に入っているようなものと似ていることがあります。
偏光レンズ:99%のUV400保護コーティング、有害な紫外線を100%ブロック、乱反射をカットして心地良い光を取り込むことで目への負担を軽減してくれます。レンズは7枚4層です。1枚目の層は、偏光層です。 2,3枚目の層は、耐久性を増すために、層を結合しています。4,5枚目の層は、紫外線を吸収するUV保護層です。 6,7枚目の層は、飛散防止層です。
真のRVレンズ:我々は使用した材料がすべて最高だと自負しています。当社のレンズは高品質のレンズで製造され、眩しさのない世界をあなたは見ることができます。様々なアウトドア活動で傷つけられないあなたの目を保護して、目の疲労を軽減します。同時に、私たちのメガネは、衝撃に強く傷つきにくいです。
耐久性に優れた柔軟で快適なフレーム:私たちのサングラスフレームは、最高品質のTR90素材で製造されています。そして超軽量、耐衝撃です。弾力性のあるフレームが顔に快適であり、これは屋外の自転車、釣り、野球、テニス、ゴルフ、スキー、ランニング、ドライブや他のさまざまなスポーツ活動に従事しているとき、理想的な選択です。
ファッションの設計:ファッションの設計は主に全体的なラインと多彩なレンズに反映されています。様々なスタイルにあってかけて快適に感じます。ユニセックスの理想的な選択です。
...
product_description
も product_bullet_point
も結構ノイジーに見えますね。
product_brand
会社名や商品名が正規化されずに入っています。たとえば "ソニー(SONY)", "SONY(ソニー)", "プレイステーション", "かっぱえびせん" のようなものがあります。SONYのように出品者側で親切にも同義語に対応してくれていることもありますし、一方で "プレイステーション = PS" となっていないということもあります。
product_color
商品の色がkeywordとして入っていそうな名前をしていますが、こちらも正規化されていない自然言語が入っています。たとえば黒っぽい商品に対して "黒", "黑", "黒色", "漆黒", "クロ", "くろ", "Black", "BLACK", "black", "ブラック", "メタルブラック" と様々なバリエーションがあったり "ブラック/ライトブルー", "赤、青、緑、黄", "色とりどり", "多様な色" のように複数の色が入っていることもあります。
色という概念が主観的ですし、カテゴリーで色の解像度が変わる (ファッションでは色を細かく分けるなど) ことあるので仕方がないと思っていましたが "ホワイト(APP連携・音声でコントロール・遠距離操作可)", "半袖-猫の顔-ホワイト", "お買い得限定品", "歯みがきジェル いちご味", "11.1V 1200mAh 三本型" のように、もはや色ではないものも入っていました。
アノテーションについて
examplesにはクエリと対応する商品とそのが含まれており、ラベルは Exact (1.0), Substitute (0.1), Complement (0.01), Irrelevant (0.0) の4段階のrelevanceで表されています。各ラベルは以下のように定義されています。
-
Exact: 商品がクエリのすべての要件 (商品、材質、サイズなど) を満たしている
- Query: "単4 充電池 8本" => Product: "Amazonベーシック 充電池 単4形8個セット"
-
Substitute: 商品がクエリの一部の要件を満たしている
- Query: "赤星 缶ビール" => Product: "サッポロラガー(赤星) 大瓶 x 6本"
-
Complement: 商品がクエリの要件を満たしていないが、同じシチュエーションで使うことができる
- Query: "充電器 あいふぉん" => Product: "iPhone 充電ケーブル ライトニングケーブル"
-
Irrelevant: 商品がまったく的はずれである
- Query: "風呂 ふた 溝なし" => Product: "重曹の激落ちくん 粉末タイプ 1kg"
- Query: "カップ麺 札幌" => "九州ドライベジ 乾燥野菜 九州産 野菜&わかめ ミックス 100g 1袋 みそ汁の具 ラーメンの具 カップ麵の具 インスタントラーメン スープ 非常食"
これらが人手でアノテーションされています。まずExactのlexically/semanticallyにマッチしている例を見てみましょう。
Exact: クエリと商品がlexicallyにマッチしている例
クエリ内のすべてのtermが商品名に含まれているExactの例を抽出しました。
query | product_title |
---|---|
プランター 木目調 | 鉢カバー 木目調 ウッドカバー 10号鉢用 インテリアカバー ウッドプランター |
人感センサー 電球 | パナソニック LED電球 E26口金 電球60形相当 電球色相当(7.8W) 一般電球・人感センサー LDA8LGKUNS |
バーベキューコンロ | キャプテンスタッグ グリルマスターライト バーベキューコンロ(5段階調節機能付) M-6445 |
スパイダーマン tシャツ | 子供服 キッズ 男の子 スパイダーマン Tシャツ ベビー 赤ちゃん 半袖 肌着 綿100% 変身Tシャツ ボーイズ 子供 幼児服 男児 寝間着 柔らかい 夏 お出かけ 90 100 110 120 130 140 |
これらがExactだと言われても違和感はありませんね。特に何もしなくてもlexaicalベースの検索エンジンでは検索できるのでこれがどうしたということですが、lexicalベースの検索エンジンが返す = relevant ということは常に成り立つわけではないので、これもこれで価値があります。
では、クエリのtermが商品名に見られない場合はどうでしょうか。
Exact: クエリと商品がsemanticallyにマッチしている例
今度はクエリ内の一部のtermが商品名に含まれていないExactの例を抽出しました。
query | product_title |
---|---|
アイロン台 足なし | アダム商会(Adamusyokai) アイロン台 ホワイト 大 |
汗拭きシート | GATSBY(ギャツビー) ギャツビー(GATSBY) ボディペーパー フリーズピーチ 30枚×3パック メンズ 大判 ボディシート |
紅茶いれる道具 | ヨシカワ コーヒープレス & ティーサーバー 750ml ゴールド ブラウニー SJ2226 |
ひよこちゃんのかくれんぼ | うずらちゃんのかくれんぼ (幼児絵本シリーズ) |
アウトドアケトル | trangia(トランギア) ケトル 0.9L TR324 |
クルマ 窓割る | Aerser 緊急脱出用ハンマー 窓ガラスクラッシャー 多機能 シートベルトカッター 日本語説明書付属 12ヵ月保証 カラー:黒色 |
水鉄砲 こども | スプラトゥーン2 スプラスコープ ネオングリーン |
スマホおとさない | カールコードストラップ コイルストラップ 伸びる 太めタイプ ロック機能付き カラビナ リング キーチェーン スマホ 携帯 定期 落下・盗難・紛失防止 太コイル ブラック 2個セット |
鉛筆キャップ可愛い | [鉛筆キャップ]えんぴつカバー5本セット/Everyone’s Happy Bottle JUICE |
これらは人間がアノテーションを行ったので与えられた字面だけでなく、人間が持っている知識を使ってアノテーションが行われています。たとえば "アイロン台 足なし" は [+アイロン台, -足] という変換を暗黙的に行っています。"汗拭きシート" からは "ボディシート" の同義性を見出しています。"ティーサーバー" というのはつまり "紅茶いれる道具" で "ひよこちゃん" は "うずらちゃん" の言い間違えで、"トランギア" は "アウトドア" 用品を作っているブランドで、という高度な推論を行っているように見えます。
Lexicalベースの検索エンジンを使っているシステムでは、このクエリと検索対象の意味的なギャップを埋めるのが一つの大きな課題となっています。
アノテーションのノイズ
ラベルの定義を見て疑問に思った方もいると思いますが、ラベルの境界は主観的で曖昧です。論文ではラベルをランダムにサンプリングしてagreementを検証することでノイズの大きさを見積もっています。その分析でのoverall agreementは91%だったのですが、disagreementの50%はIrrelevantのラベルで見つかったそうです。これはIrrelevantとComplementはかなりconfusingなので納得感があります。一方で以下はIrrelevantとラベル付けされたペアの例ですが、クエリを見る限りは完全に一致しているように見えます。
Irrelevant: クエリと商品がマッチしているように見える例
ラベルがirrelevantかつクエリ内のすべてのtermが商品名に含まれている例を抽出しました。
query | product_title |
---|---|
king camp テント | KingCamp テントマット エアーマット 自動膨張 2人用 枕付き 三段式構造 エアーベッド 耐水加工 防湿 防寒 キャンプ アウトドア テント泊 車中泊用 |
ダイニングテーブル | 萩原 ダイニングテーブル ホワイト 片側ベンチタイプ 4点セット LDS-4934WH |
コットン | クリーン ウォームコットン オードパルファム 60ml |
ギターマガジン | (CD付き) アコースティック・ギター・マガジン (ACOUSTIC GUITAR MAGAZINE) 2020年3月号 Vol.83 |
ipad12.9 | 12.9インチiPad Pro(第4世代)用Smart Keyboard Folio - 日本語 |
いかなる方法でラベルを付けたとしてもノイズをゼロにすることはできないので、ノイズに強いアルゴリズムを作るか、明らかに間違いに見えるラベルをヒューリスティックに削除するようにしてもいいかもしれません。
検索システムを実装する
今までは大規模システムといえばJava一択で、機械学習をプロダクションで使おうと思ったらexternal serviceにするかバッチにするかという感じで難があったのですが、検索システムを賢くしたいという人々の願いがミドルウェアにPythonサポートやcross-languageの機能を追加させて、Pythonを組み込むことが容易になってきました。
このリポジトリではIndexingもRetrievalもPythonで書かれています。以下のような標準的なツールを使っています。
- Pythonのバージョンの切り替え: pyenv
- Apache Beam Python SDKの制限によりPython 3.10で開発しています
- パッケージ管理: poetry
- Lint: black, isort, flake8, mypy
- テスト: pytest, coverage
- タスクランナー: invoke
Document Ingestion Pipeline
検索システムではデータソースからインデックスを復元したり、リアルタイムでインデックスで更新する仕組みが必要で、大量のデータを並列で処理するためにApache BeamやSparkのようなフレームワークが多いです。このプロジェクトではpipelineはApache Beamの Python SDK で実装されていて、全体像は以下のようになっています。
このpipelineはまず商品のデータセットを読み込んでtext processingをしたあとに、重そうな処理を分岐して並列処理したあとにマージすることでcomputing resourcesを有効活用しようとしています (ML inference in Dataflow pipelines | Google Cloud Blog)。
このpipelineはDirectRunnerをサポートしているのでローカル環境でも実行することができますが、もっとadvancedな手法を試すのに巨大なモデルを走らせると思うので Dataflow Prime を使うことも考えています。
Retrieval
いわゆる検索アプリ内ではユーザーが入力したクエリをanalyzeしてsynonymを追加したりvectorにエンコードするなどをしています。Indexingと違ってRetrievalでの処理はlatencyの要件が厳しいので、検証とはいえ現実離れしないようにあまり重い処理はしない前提で作っています。
このプロジェクトでは検索のインタフェースとしてStreamlitを使っています。リポジトリでは src/demo
以下にコードがあります。今年の夏くらいにStreamlitに待望のmultipage appのサポートが入り pages
ディレクトリ以下にファイルを置くだけでsidebarにpageが自動的に追加されるようになって利便性が上がったので、各機能の分析や可視化もStreamlit上で行っています。
以下のように各実験と対応するvariantを追加して、自動的に指標の比較ができるようにしています。
EXPERIMENTS = {
"different_fields": ExperimentalSetup(
index_name="products_jp",
locale="jp",
num_queries=5000,
variants=[
Variant(name="title", fields=["product_title"]), # noqa
Variant(name="title,description", fields=["product_title", "product_description"]), # noqa
Variant(name="title,bullet_point", fields=["product_title", "product_bullet_point"]),
...
タスクと評価について
このリポジトリでは上で説明したデータセットを使って、以下の2つのタスクを作ります。
Task 1: Full-Retrieval
End-to-Endで検索エンジンにクエリを投げて返ってきた商品のラベルを検証します。ラベルはすべてのクエリと商品のペアに付いているわけではなく、一部が事前に観測されていると見做すことができます。このタスク設定での評価はトリッキーで、このincomplete datasetからは真のPrecisionもRecallも計測することはできません。そのためアノテーションを使って以下の2つ方針が考えられます。
- (1) 観測されていない商品をIrrelevantとして扱う
- e.g.
[Unjudged, Exact, Unjudged, Irrelevant]
=>[Irrelevant, Exact, Irrelevant, Irrelevant]
- e.g.
- (2) 観測されていない商品を無視する
- e.g.
[Unjudged, Exact, Unjudged, Irrelevant]
=>[Exact, Irrelevant]
- e.g.
このプロジェクトではそれぞれ (1) NDCG と (2) NDCG' (Sakai and Kando, 2008) を計測できるようにしています。
Task 2: Reranking
このタスクでは、クエリに対して与えられた商品リストをrelevanceの高い順に並び替えます。たとえば [I, I, I, E, I]
が与えられたときに [E, I, I, I, I]
となるようなソートアルゴリズムを考えるものです。実際のデータセットは以下のようになっています。本記事ではfull-retrievalについて見ていくのでここでは詳細は省きます。
ここまでデータがどのようなものかを理解したので、次のセクションでは上記の実装について述べます。
このStreamlitアプリが適当なクエリを各variantごとの設定で投げて、その結果に対してmetricsを計算しています。ちなみに明記されていない場合のcutoffは @100
が適用されています。
検索システムを改善する
ここまでざっくりと Dataset => Pipeline => Search Engine => Retrieval => Evaluation までの流れを実装しました。このセクションではどのように検索システムを継続的に評価、改善するかをいくつか例を挙げながら議論します。
何をインデックスするか
このデータセットでは検索対象の商品は複数のフィールドを持っており、各フィールドが違うアスペクトで情報を与えていると考えられます。商品名がその商品自身を最も示しており、クエリにマッチさせる対象として欠かせないものですが、一方で説明文に類するものは商品名に含まれていない情報の補完をしていると考えることができます。
最初の実験では各フィールドの組み合わせで検索したときに検索指標がどのように変化するかを検証しました。product_title
だけを検索対象にしたときにNDCGが最大になり product_description
や product_bullet_point
を検索対象に含めたときにRecallが最大になるという仮説を立てて実験を行いました。結果は以下のとおりです。
variant | total_hits | zero_hit_rate | recall | ndcg | ndcg' |
---|---|---|---|---|---|
title,brand | 6711 | 0.0794 | 0.464 | 0.673 | 0.928 |
title,color | 6703 | 0.082 | 0.4472 | 0.6704 | 0.9283 |
title | 6694 | 0.0954 | 0.4477 | 0.6693 | 0.9274 |
title,description | 7088 | 0.0934 | 0.4416 | 0.6568 | 0.9253 |
title,bullet_point | 7180 | 0.0934 | 0.4412 | 0.6546 | 0.925 |
title,brand
の組み合わせがNDCGもRecallも一番高くなりました。逆に title,description
と title,bullet_point
はNDCGもRecallも低く、cutoffが入っているということもありますが、そのままだと追加するconsがprosを上回っているように見えます。
あとNDCG'がすごく高いのですが、これはデータセットのラベルの分布が偏っていてexactが多いのでこのようになっている気がします。
このウエストバッグの例だと15個のexactに対してirrelevantが1つしかなく、このようなデータセットだと比較が難しい…どうしたらいいの。
それはさておき、NDCGが下がるからといって説明文を完全に落としてしまうとRecallに問題が出てしまうのではという懸念があります。そこで、商品名が重要だということは分かったので重みを調整してみます。
variant | total_hits | zero_hit_rate | recall | ndcg |
---|---|---|---|---|
title | 6601 | 0.071 | 0.4684 | 0.6775 |
title^5,bullet_point^1 | 7079 | 0.0682 | 0.4703 | 0.6772 |
title^10,bullet_point^1 | 7079 | 0.0682 | 0.4703 | 0.6772 |
title^2,bullet_point^1 | 7079 | 0.0682 | 0.4721 | 0.6764 |
title^1,bullet_point^1 | 7079 | 0.0682 | 0.4632 | 0.6618 |
商品名単体のときが一番NDCGが高いのですが、説明文の影響力を下げながら検索をするとNDCGとRecallのバランスが取れそうだということが分かりました。
さらなる改善として、説明文の中のtermに対して重み付けをしたり (Term Importance Estimation) 重要そうなtermを抽出あるいは生成する (Keyword Extraction, Keyword Generation, Attribute Value Extraction) などのadvanced topicsもあります。
どのように語彙の不一致を軽減するか
上の実験では検索対象になるfieldやtermを選択することの影響を調べました。今度は逆に商品にまったく出現しないtermを追加することの影響、つまり語彙の不一致を軽減する手法を比較していきます。
同義語をindex timeかquery timeに追加するSynonym Expansionは昔から研究されていて様々な方法がありますが、今回は適当な手法をピックしてquery timeに同義語を追加して検索をする (term OR synonym) AND (term OR synonym), ...
ように変更をして実験を行いました。以下がRecallでソートした結果です。
variant | total_hits | zero_hit_rate | recall | ndcg |
---|---|---|---|---|
query expansion + title,brand | 6637 | 0.0458 | 0.4916 | 0.6719 |
query expansion + title,color | 6629 | 0.0434 | 0.4823 | 0.6727 |
query expansion + title | 6620 | 0.0512 | 0.4823 | 0.6719 |
query expansion + title,description | 7017 | 0.049 | 0.4762 | 0.6539 |
query expansion + title,bullet_point | 7112 | 0.0494 | 0.4761 | 0.6546 |
title,brand | 6552 | 0.0608 | 0.4753 | 0.6749 |
title,color | 6545 | 0.0566 | 0.4644 | 0.676 |
title | 6534 | 0.0676 | 0.4643 | 0.6747 |
title,description | 6940 | 0.064 | 0.4587 | 0.6604 |
title,bullet_point | 7035 | 0.064 | 0.4584 | 0.6603 |
すべてのvariantでSynonym Expansionがある方がcutoffが100程度でもRecallが高くなり、その有用性が示せていると思います。
しかしtraditionalなSynonym Expansionはtermベースで行われるため "紅茶いれる道具", "ひよこちゃん", "可愛い" のような意味を理解することが難しいです。そこでDense Retrievalをやろうというのがここ数年の流行りで、このプロジェクトでも検証をしました。時間がかかるのですべての商品をインデックスしていないのですが、結果は以下のとおりです。
variant | total_hits | zero_hit_rate | recall | ndcg |
---|---|---|---|---|
sparse | 819 | 0.1598 | 0.0238 | 0.7314 |
hybrid | 891 | 0 | 0.0258 | 0.7142 |
dense | 100 | 0 | 0.0193 | 0.5747 |
ここに私たちが最初に上の方で見た問題 (すぐ下に再掲) がDense Retrievalによって解決されたのかの分析を載せたかったのですが時間がなかったので省略させてください。
"ティーサーバー" というのはつまり "紅茶いれる道具" で "ひよこちゃん" は "うずらちゃん" の言い間違えで、"トランギア" は "アウトドア" 用品を作っているブランドで、という高度な推論を行っているように見えます。
Sparseだけで検索した方がNDCGが高く、Denseだけでは、常にクエリに対して何かしらの結果を返すことはできますが、NDCGは実用からほど遠いくらい低いです。しかしHybridにすることで少しばかりのNDCGを犠牲にRecallを改善し、犠牲になったNDCGは次のRerankingフェーズで担保するのがいいかもしれない、と次の実験につながっていきます。
おわりに
Apache BeamのPython SDKが充実してきたり、Streamlitがmultipage appサポートをして検証がしやすくなったり、Amazonがデータセットを公開してくれたおかげで遊べるようになったり、今年は検索エンジニアにとって嬉しい一年になったのではないかなと思います。
情報検索は機械学習の発展に伴う技術の進歩が凄まじく、機械学習はすでに検索システムにとって不可欠な技術要素になっていると感じています。それと同時に、検索システムを継続的に改善するための分析や仮説検証の仕組みづくり、そして記事を書くためのタイムマネジメントを行うことの重要さが伝われば幸いです。
Indexingはlatencyの許容度が高いので、Pythonで書いてそのまま機械学習を走らせるでもいいのですが、一方でRetrievalはlatencyは厳しく、JavaとかGoのような言語で書く必要があり、となると機械学習を検索エンジン側に寄せる必要があって、2023年はVespa移行か…という気持ちになっています。
Discussion