🔍

Auto ML Natural Languageを使ったお手軽Query Categorization

2021/12/20に公開

Algolia Advent Calendar 2021 の~31~21日目の記事です。

メルカリUSの検索チームでバックエンドエンジニアをしている、k-yomoです。

この記事では、趣味で開発している家具検索サービスにAuto ML Natural Languageを使ったQuery Categorizationを試してみた話を紹介していきます。

背景

今年の4月から現職の検索チームに入ったことをきっかけに、9月中旬頃から個人でも週末プロジェクトとしてカグミルという家具・インテリア検索サービスを開発しています。

カグミルは、楽天やYahooショッピング、PayPayモール、Amazon(追加予定)など、ECサイト上で販売されている家具やインテリアを横断で検索することが出来る(ようになる)サービスで、下記のような構成で開発しています。

この家具検索サービスを開発している際に、クエリの意図と違った商品が上位表示されてしまったり、価格の安い順などの並び替えにおいて、関連度が低い商品が上位に表示されてしまう問題を抱えていました。

例えば、「ソファ 革」という検索クエリで価格の安い順にソートした時に、革用のクリームなどが表示されてしまったり等です。

そこで今回はお手軽に文章を任意のラベルにカテゴライズすることが出来る、Auto ML Natural LanguageによるQuery Categorization(今回はクエリの商品カテゴリ分類)を試してみました。

Query Categorization/Classificationについては、hurutoriyaさんのブログのスライド、またQuery Classificationを含むQuery Understandingについてはリクルートのテックブログにある検索体験を向上する Query Understanding とは にとても分かりやすくまとまっているので、ご覧頂ければと思います。

モデルのトレーニング

Auto ML Natural LanguageのPrediction APIを使用し始めるには、データセットを作成し、作成したデータセットを用いてモデルのトレーニングする必要があります。

今回は、検索クエリが複数カテゴリーにまたがる可能性も考慮してマルチラベル分類を選択しました。

そして、データセットには既に楽天・YahooショッピングのAPIから取得済みの商品情報から商品名とその商品のカテゴリーIDをセットにした約50万件のデータを使用しました。

最良の結果を得るには、各ラベルに少なくとも 100 個のアイテムを含める必要があります。

コンソールに上記の注意書きがあるように、ラベル毎に一定のデータ量が必要なため、今回はBigQueryに同期済みの商品情報から、下記のSQLで各カテゴリー最大500件のトレーニング用データを抽出しました。また、今回は前処理などは特に行っていません。

WITH items_with_rn AS (
  SELECT 
    name 
    , category_id
    , ROW_NUMBER() OVER (PARTITION BY category_id) as rn
  FROM `project_id.kagu_miru.items`
)
SELECT name, category_id
FROM items_with_rn
WHERE rn <= 500

インポートしたデータについては、統計情報も確認することができます。トレーニング:検証:テストがおおよそ8:1:1の割合になっていました。(今回のデータではラベルはカテゴリIDになっています)

トレーニング結果

トレーニングは訳8時間ほどで終了しました。下記がトレーニングしたモデルの評価(Confidence threshold: 0.5)になります。

Confidence thresholdとは、予測スコアの閾値になります。閾値が上がると、適合率が上がり再現率が下がります。

Confidence threshold: 0.6で適合率が90%・再現率64.32%と、割と良い精度が出ていることが分かります。但し、この適合率の元となっている今回のテストデータは実際の入力であるクエリではなく、比較的情報量が多い商品名であることには注意が必要です。

なにはともあれ、今回はクエリに対してスコアが0.6以上のカテゴリーを、クエリが意図したカテゴリーとしてフィルターを実装していきたいと思います。

モデルの使用

トレーニング済みのモデルは、GCPのコンソールやJSON API、各種クライアントライブラリから使用することができます。

カグミルのバックエンドはGoで開発しているため、今回はGoのクライアントライブラリを使いました。下記が該当のコードになります。

コード全文はGithubにてご覧ください。

const scoreThreshold = 0.6

// CategorizeQuery predicts the query's intended categories and returns the list of ids.
func (q *queryClassifierClient) CategorizeQuery(ctx context.Context, query string) ([]string, error) {
	resp, err := q.predictionClient.Predict(ctx, &automlpb.PredictRequest{
		Name: q.categoryClassificationModelPath,
		Payload: &automlpb.ExamplePayload{
			Payload: &automlpb.ExamplePayload_TextSnippet{
				TextSnippet: &automlpb.TextSnippet{
					Content:  query,
					MimeType: "text/plain",
				}},
		},
	})
	if err != nil {
		return nil, err
	}

	var categoryIDs []string
	for _, payload := range resp.Payload {
		detail, ok := payload.Detail.(*automlpb.AnnotationPayload_Classification)
		if ok && detail.Classification.Score >= scoreThreshold {
			categoryIDs = append(categoryIDs, payload.DisplayName)
		}
	}

	return categoryIDs, nil
}

ちなみに、モデル名は"projects/{GCPプロジェクト名}/locations/us-central1/models/{モデルID}"形式のフルパスで指定しないといけない点に注意が必要です。(エラーがInvalid Argumentのみと厳しく、私はモデルIDだけ指定していて30分ほどハマりました...)

今回はユーザーの検索条件にカテゴリーの指定がなかった場合に、上記の処理から得られたカテゴリーIDによるフィルターを適用してみます。

func (r *queryResolver) Search(ctx context.Context, input gqlmodel.SearchInput) (*gqlmodel.SearchResponse, error) {
	if input.Query != "" && len(input.Filter.CategoryIds) == 0 {
		categoryIDs, err := r.QueryClassifierClient.CategorizeQuery(ctx, input.Query)
		if err != nil {
			logging.Logger(ctx).Error("failed to predict query's category", zap.Error(err))
		}
		input.Filter.CategoryIds = categoryIDs
	}

	resp, err := r.SearchClient.SearchItems(ctx, &input)
	if err != nil {
		return nil, fmt.Errorf("SearchClient.SearchItems: %w", err)
	}
	return mapSearchResponseToGraphqlSearchResponse(resp, r.SearchIDManager.GetSearchID(ctx))
}

結果

まだサービス自体にトラフィックがかなり少ないため、オフライン・オンラインでの評価は出来ていませんが、カテゴリー名のExact matchやその類似語など分かりやすいところは、カテゴリーのフィルターが適切にかかっている事が確認できました。

「ペンダントライト」 (価格昇順)

Query Categorization適用前

テープライトのコネクタやキャンドルライト等、ペンダントライトとは全く関係ないものが上位に表示されてしまっています。

Query Categorization適用後

ペンダントライトが含まれていたり、ペンダントライト用のソケットが含まれていたりと、検索結果が改善されている事が分かります。また、この「ペンダントライト」等、適用されるカテゴリーによっては検索結果が大幅に絞り込まれる事になるので、実用する場合は再現率が下がることによる悪影響にも注意が必要です。

課題

いくつかのクエリで改善が見られた一方で、先ほど例に挙げた「ソファ 革」や、「テーブル ガラス」など、人間が見れば革のソファだな、ガラスのテーブルだなと分かるものは、該当カテゴリーのスコアがまだ閾値には届かず、フィルターが適用されていませんでした。これは、データセットの質や量の問題なのかもしれません。

また、Auto MLでのカテゴリーの推論に平均で1秒ほど掛かってしまっており、モデルのデプロイ先がUSリージョンなことを加味しても、今回のモデルは検索などレイテンシが求められる機能においては実用するのはまだ難しそうです。Prediction APIの割り当てが600req/minなので、トラフィックが多いサービスについては、割り当てについても注意が必要です。

上記のようなレイテンシや再現率の課題はありますが、逆にECなどでの商品登録時のカテゴリ補完や、バッチ処置で推論を行う場合などレイテンシが許容できる用途については有用なのではないかと感じました。

まとめ

今回は、Auto ML Natural Languageを使ったお手軽Query Categorizationを実装してみました。実用するにあたってのレイテンシの課題など見つかったものの、機械学習に明るくない私でもモデルのトレーニングからアプリケーションへの組み込みまで簡単に行え、実際にいくつかのクエリで改善が確認が出来、面白かったです。

また、今回の実装後にAuto MLがVertex AIに統合されていることを知ったため、次はVertex AIで同様のモデルを試してみたいと思います。

Discussion