LLMを活用した大規模商品カテゴリ分類への取り組み

こんにちは、メルカリの生成AIチームで ML Engineer をしている ML_Bear です。

以前の記事[1]では商品レコメンド改善のお話をさせていただきましたが、今回は、大規模言語モデル (LLM) やその周辺技術を活用して30億を超える商品のカテゴリ分類を行なった事例を紹介します。

ChatGPTの登場によりLLMブームに火がついたということもあり、LLMは会話を通じて利用するものだと認識されている方が多いと思いますが、LLMが有する高い思考能力はさまざまなタスクを解決するためのツールとしても非常に有用です。他方、その処理速度の遅さや費用は大規模なプロジェクトでの活用にあたっての障壁となり得ます。

本記事では、こうしたLLMの課題を克服するためにさまざまな工夫を施し、LLM及びその周辺技術のポテンシャルを最大限に引き出して大規模商品データのカテゴリ分類問題を解決した取り組みについて説明します。

課題

まずは今回のプロジェクトの背景と技術的な課題を簡単に説明します。

メルカリは2024年にカテゴリリニューアルを行い、階層構造を見直すとともに商品カテゴリの数を大幅に増やしました。しかしカテゴリ数やその階層構造がかわるということは、それに紐づく商品のデータも変更する必要があります。

通常であれば商品のカテゴリ分類は機械学習モデルやルールベースモデルを利用します。しかし今回のケースでは過去の商品に対する「新しいカテゴリ階層での正解カテゴリ」がわからないため、機械学習を使用した分類器を作成することができませんでした。また、カテゴリ数が非常に多いため、ルールベースモデルの構築も困難でした。そこで、この課題に対してLLMを活用できないかというアイディアが出てきました。

解決策: LLMとkNNによる2ステージ構成の予測アルゴリズム

結論としては以下のような2ステージ構成のアルゴリズムを組むことで今回の課題に対応しました。

  1. ChatGPT 3.5 turbo (OpenAI API[2])で過去商品の一部の正解カテゴリを予測する
  2. 1.を学習データとして過去商品のカテゴリ予測モデルを作成

全てをChatGPTで予測できれば楽だったのですが、メルカリの過去商品は30億商品を超えるため[3]、全てをChatGPTで予測するのは処理時間的にもAPIコスト的にも不可能でした。そのため、紆余曲折を経てこのような2ステージのモデル構成としました。(すべての商品をChatGPT 3.5 turboで分類するとコスト見積もりは約100万ドル、処理時間見積もりは1.9年という非現実的な数字でした)

以下にモデルの内容を簡単に説明します。詳細については「工夫した点」で述べるため、一旦はシンプルな解説に留めます。

1. ChatGPT 3.5 turbo (OpenAI API)で過去商品の一部の正解カテゴリを予測する

まず、過去に出品された商品を数百万点サンプリングし、ChatGPT 3.5 turboにその商品の「新しいカテゴリ構成での正しいカテゴリ」を予測させました。 具体的には、各商品の商品名や商品説明文、元のカテゴリ名をもとに新しいカテゴリの候補を10個程度作成し、その候補の中から正解を答えさせました。

2. 1.を学習データとして過去商品のカテゴリ予測モデルを作成

次に、1. で作ったデータセットを正解データとして、シンプルな kNN モデル[4] を作成しました。

具体的には、まず、1.で正解カテゴリを予測した商品のEmbeddingと正解カテゴリをベクトルデータベースに保存しておきます。その後、予測したい商品のEmbeddingを元に、ベクトルデータベースから類似商品をX個抽出し、そのX個の商品の最頻カテゴリを正解カテゴリとしました。

Embeddingは各商品の商品名、商品説明文、メタデータ、元のカテゴリ名などを連結した文字列をもとに計算しました。より複雑な機械学習モデルも検討しましたが、シンプルなモデルで及第点の性能が出たためシンプルなモデルを採用しました。

工夫した点

さて、ここからは今回のプロジェクトで工夫した点をご紹介します。以下のような点を工夫したので、ひとつづつ説明します。

  • OSSのEmbeddingモデルの活用
  • Sentence Transformers ライブラリによるMulti-GPUの活用
  • Voyager Vector DBによるCPU上での高速な近傍検索
  • max_tokensとCoTの活用によるLLM予測の高速化
  • Numba・cuDFの活用

1. OSSのEmbeddingモデルの活用

第2ステージのモデル (kNN) では商品のEmbeddingの計算が必要でした。自前でニューラルネットワークを組むことも可能でしたが、OpenAI Embeddings API (text-embedding-ada-002) [5]で十分な精度が出ることが確認できたので、当初はこのAPIを利用する方針としていました。

しかし、試算してみたところ、すべての商品にOpenAI Embeddings APIを利用するのは処理時間的にもコスト的にも少し厳しいということがすぐにわかりました。

そんな中、MTEB[6]やJapaneseEmbeddingEval[7]を眺めていると英語以外の言語でもOpenAI Embeddings APIに匹敵するOSSのモデルが多数あることに気づきました。自分たちで評価用データセットを作って試してみたところ、OpenAI Embeddings API同等の精度が出たためOSSのモデルを利用することにしました。

私たちがこのプロジェクトを行なっていた2023年10月時点のデータでは、以下のモデルが高い精度を示しており、最終的には計算コストと精度のバランスを鑑み intfloat/multilingual-e5-base を利用しました。(MTEBのランキングは常時入れ替わっているため、2024年4月現在はもっと強いモデルがあると思います)

  • intfloat/multilingual-e5-large [8]
  • intfloat/multilingual-e5-base [9]
  • intfloat/multilingual-e5-small [10]
  • cl-nagoya/sup-simcse-ja-large [11]

このように、OSSでも非常に高性能なEmbeddingモデルが存在しているため、Embeddingを利用するプロジェクトを行う場合は、シンプルな問題を作成して、OSSでも十分な性能を持つモデルがあるかどうかを確認してみることをお勧めします。

2. Sentence Transformers ライブラリによるMulti-GPUの活用

OSSモデルを利用することで OpenAI Embeddings APIに比べて飛躍的に処理速度が上がったものの、数十億商品を処理するにはもう少し改善が必要でした。

A100などの強力なGPUを利用できれば話が早かったのですが、世界的なGPU枯渇の影響を受けてか、プロジェクト実施時の2023年11-12月時点では強いGPUを掴むことはなかなか困難でした。(現在もあまり状況は変わっていないかと思います)

そのため、V100やL4などのGPUを複数台並列で利用して対応することにしました。幸いなことに、Sentence-Transformers[12]ライブラリを利用すると以下のようなシンプルなコードで複数台のGPUを簡単に並列化できたため、非常に助かりました。

from sentence_transformers import SentenceTransformer

def embed_multi_process(sentences):
    if 'intfloat' in self.model_name:
        sentences = ["query: " + b for b in sentences]
    model = SentenceTransformer(model_name)
    pool = model.start_multi_process_pool()
    embeddings = model.encode_multi_process(sentences, pool)
    model.stop_multi_process_pool(pool)

強力なGPUを大量に使えれば理想的ですが、それが難しい状況でも工夫次第で処理を高速化することができます。Sentence-Transformersのようなライブラリを活用して、限られたリソースを最大限に活用することが重要だと感じました。

3. Voyager Vector DBによるCPU上での高速な近傍検索

kNNを利用する際にはベクトルデータベースが必要でした。サンプリングしたとはいえ数百万商品の学習データになったため、GPUのメモリに載らない状況でした。A100 80GBなどの大きなメモリを持つGPUを使えば載ったかもしれませんが、前述の通り強力なGPUは確保が困難だったので試すことすらできませんでした。

そんな折、Spotify社製のVoyager[13]がCPUでも高速に動作すると聞いたので試してみたところ、実用に足る速度を簡単に実現できました。Embedding計算に比べると近傍探索の時間はそれほど影響が大きくなかったため厳密に他のプロダクトと比較していませんが、十分な速度を出すことができていました。

Voyagerにはメタデータ管理機能がなかったので自分たちでクライアントを書く必要がありましたが、それでも全体的には良い選択だったと思っています。

4. max_tokensとCoTの活用によるLLM予測の高速化

今回のプロジェクトでは ChatGPT 4 はコスト面から利用できず、ChatGPT 3.5 turboを使わざるを得ませんでした。ChatGPT 3.5 turboはコストの割に賢いとは思いますが、精度には少し不安がありました。そのため、Chain of Thoughts[14]を利用して説明を生成させることで精度向上を図りました。

皆さまもご存知かと思いますが、ChatGPTに説明を行わせるとずっと喋り続けることもあり、処理時間が問題となりました。そこで、max_tokensパラメータを利用して長い話を途中で打ち切ることで処理時間の短縮に努めました。

回答を打ち切ると(Function Callingの) JSONが壊れるので、LangChain[15]のllm.stream()を利用したり、もしくは自分でJSONを復元してパースする必要があり少し手間がかかります。厳密な比較はしていないものの、この手法によって処理時間短縮と精度向上の良いバランスを取れたと感じています。

以下がLangChainのllm.stream()を利用した場合のサンプルコードです。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

from typing import Optional
from langchain_core.pydantic_v1 import BaseModel, Field

class ItemCategory(BaseModel):
    item_category_id: int = Field(None, description="商品説明から予測したカテゴリID")
    reason: Optional[str] = Field(None, description="このカテゴリIDを選択した理由を詳しく説明してください")

system_prompt = """
与えられる商品情報を元に、商品のカテゴリを予測してください。
商品のカテゴリは候補から選んでください。選んだ理由も説明してください。
"""
item_info = " (商品データと新カテゴリ候補などを入れる) "

llm = ChatOpenAI(
    model_name="gpt-3.5-turbo",
    max_tokens=25,
)
structured_llm = llm.with_structured_output(ItemCategory)
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{item_info}"),
    ]
)
chain = prompt | structured_llm

# streamingの最後の要素だけ取り出す
# - 通常、max_tokensで回答を打ち切るとjsonが壊れてパースの処理が必要
# - langchainのstreamで実行すると常にjsonを完成させてくれるため、
#   max_tokensで回答を打ち切ってもjsonをパースする必要がない
for res in chain.stream({"item_info": item_info}):
    pass

print(res.json(ensure_ascii=False))  # res: ItemCategory
# {"item_category_id": 1, "reason": "商品名に「ぬいぐるみ」が含まれ"}

5. Numba・cuDFの活用

数十億商品を処理する際は些細な処理でも処理速度が気になるため、可能な限りすべての処理をcuDF[16]およびNumba[17]で高速化しました。

正直なところ Numba を書くのは苦手だったのですが、Pythonの素のコードをChatGPT 4に見せると書き直してくれるため、ほとんど自分で書く必要がなくコーディング工数を大幅に削減することができました。

まとめ

ChatGPTは会話形式で利用されることが多く注目を集めていますが、その高い思考能力を活用することで、これまで面倒だったり不可能であったタスクを簡単に解決できるようになります。私たちのプロジェクトでは、膨大な商品データを新しいカテゴリに短期間で分類し直すという面倒な課題を、ChatGPTを活用することで解決することができました。

また、OSSのEmbeddingモデルやマルチGPUの活用、高速な近傍検索が可能なベクトルデータベースの採用、ChatGPTでの予測の高速化、Numbaを用いた処理の高速化など、様々な工夫を行うことで、限られた時間とリソースの中でも最大限の成果を出すことができました。

今回の事例が、ChatGPTをはじめとする大規模言語モデルの可能性の一端を示し、皆様のプロジェクトの参考になれば幸いです。ぜひ、様々な場面でLLMを活用し、これまでは難しかった課題にチャレンジしてみてください。

Refs

  1. 協調フィルタリングとベクトル検索エンジンを利用した商品推薦精度改善の試み
  2. OpenAI API
  3. フリマアプリ「メルカリ」累計出品数が30億品を突破
  4. k-nearest neighbors algorithm
  5. OpenAI Embeddings API
  6. Massive Text Embedding Benchmark (MTEB) Leaderboard
  7. JapaneseEmbeddingEval
  8. intfloat/multilingual-e5-large
  9. intfloat/multilingual-e5-base
  10. intfloat/multilingual-e5-small
  11. cl-nagoya/sup-simcse-ja-large
  12. Sentence-Transformers
  13. Voyager
  14. Chain-of-Thought Prompting Elicits Reasoning in Large Language Models (Wei et al. 2022)
  15. LangChain
  16. rapidsai/cudf
  17. Numba: A High Performance Python Compiler
  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加