💥

Vertex AI で記事のレコメンドを実現しようとしたら、穴に落ちまくった話。

2024/12/18に公開

はじめに

何をしたいの?


ohtasoji -「Vertex AI Agent Builderって何に使えるの?」より引用 (画像リンク)

「ある記事を読んだ人が、次にどの記事を読む傾向があるのか」などの閲覧傾向を反映した記事レコメンド機能を記事ベージに実装し、サイト全体の閲覧回数を向上させることを目的とする。
その方法として、Google Analyticsで収集した行動データを、BigQueryを経由して Vertex AI で学習させる。学習結果は、サイトから API で呼び出すことによって取得できるようにする。そして、次に読むべき記事を提案させる動的なページを作成する。

私は、そんなことを自らの手で実現できるという事実に高揚感を覚えていた。ちょうど一か月前のことであった…

要するに、とあるメディアサイトを対象に、Vertex AI を用いておすすめ記事のレコメンド機能を実装しました!

What is Vertex AI?

主な構成

ほぼ 公式ドキュメント を引用。 ???「 Google のちからってすげー!」

Vertex AI Agent Builder

  • Vertex AI Agent … LLM(大規模言語モデル)上に構築された自然言語理解プラットフォーム。チャットbotや音声レスポンスなどに使える。
  • Vertex AI Search … LLMを活用したフルマネージドプラットフォーム。
    • 検索 … 独自データに基づいたGoogle品質の検索アプリを作れる。
    • レコメンデーション … 閲覧コンテンツに類似したコンテンツを提案してくれるアプリを作れる。
      • 汎用(プレビュー)
      • 小売 … ECサイトなど商品を扱うサイトに対応。
      • メディア … 動画、音声、ニュースなどのコンテンツを扱うサイトに対応。← 今回使うのはこれ!

注意事項(必ずお読みください)

本編

穴1. 公式ドキュメントしか頼れない

経験豊富なエンジニアの方から「当たり前だよなぁ?」って怒られそうだが、ひよっこからすると公式ドキュメントは「正確だが難解。説明を噛み砕いた記事とかあれば超助かるんですけど。」って感じの存在だ。公式だけで挑むのは、まるでレベル上げを一切せずにジムに挑むような気分だ。(それでもエンジニアとして仕事をしていくなら公式で殴れなきゃダメだとも考えている。)

とりあえず、公式ドキュメントを見てみよう。
そして気づくだろう。(いや、もう気づかない。)

ALL ENGLISH という事実

日本語のドキュメントでも難しいなと思いながら読んでいるのに、すべて英語だって…
DeepLに導いてもらいながら必死で読み進めた。

難しい…と半ベソをかいていたが、そんな私にGoogleは2つの贈り物をくれた。

Google「まずはチュートリアルをやってみなさいよ!」

https://cloud.google.com/generative-ai-app-builder/docs/try-media-recommendations?hl=ja
公式ドキュメントには、映画の評価データなどを使ってレコメンデーション機能を試せるチュートリアルが掲載されている。SQLの知識がある程度必要だが、大枠を掴むことはできる。
触った感想としては、内容がガチであるため、「Hello World!を出したいなー」的な軽いテンションでやると火傷する恐れがあるかも。でもやる価値は絶対にある!

Google「ほら、チェックリスト渡すからこの通りに進めてみなさい!」

https://cloud.google.com/generative-ai-app-builder/docs/media-recommendations-checklist?hl=ja
最強アイテム。ひよっこはまずこれを読んだ方がいい!
これのおかげで、情報の大海原で遭難せずに作業を進めることができる。それでも穴に落ちちゃうんだけどね。

穴2.そもそもサンプルでエラー発生!

チェックリストを確認すると、レコメンデーションアプリの実装には、構造化メディアデータユーザイベントの2種類を用意する必要があるそうだ。
そこで、コードサンプルフィールドの仕様 を確認しながら構造化メディアデータを作成してみる。
ただ、Vertex AIへダイレクトに渡せないので、Cloud Storageを経由する必要がある。

渡した結果、次のエラーが発生。

invalid JSON in   google.cloud.discoveryengine.v1main.Document, near 1:1 (offset 0):  unexpected character:  '"'; expected '{'

これがコードサンプルをそのまま突っ込んだ時にも発生したから困った。ひよっこな私は、「どっかでダブルクォーテーションが勝手に付いた?」とか邪推していた。(制御文字や改行コードなどを真剣に調べていた気がする。)
言うまでもないが、正解はJSONの形式が違うのだ。別ページの記述ではあるが、構造化データを Cloud Storage に取り込む時は、各ドキュメントを1行で表現する必要があったのだ。なお、複数データあるからといって配列形式にまとめたり、文末にカンマを入れたりしてはいけない!

{"hotel_id": 10001, "title": "Hotel 1", "location": {"address": "1600 Amphitheatre Parkway, Mountain View, CA 94043"}, "available_date": "2024-02-10", "non_smoking": true, "rating": 3.7, "room_types": ["Deluxe", "Single", "Suite"]}
{"hotel_id": 10002, "title": "Hotel 2", "location": {"address": "Manhattan, New York, NY 10001"}, "available_date": "2023-07-10", "non_smoking": false, "rating": 5.0, "room_types": ["Deluxe", "Double", "Suite"]}
{"hotel_id": 10003, "title": "Hotel 3", "location": {"address": "Moffett Park, Sunnyvale, CA 94089"}, "available_date": "2023-06-24", "non_smoking": true, "rating": 2.5, "room_types": ["Double", "Penthouse", "Suite"]}

Vertex AI ガイド - 取り込み用のデータの準備より引用
慣れないうちは強弱を付けて読めないので、私は盛大にスルーしていた。
そして、ドキュメントにも載っているが、JSON Lines でデータ形式は絶対に確認しよう!
https://jsonlines.org/validator/

穴3.フォーマットに従ったのにERROR!!

穴2をやっと抜け出し、今度こそは!と意気込んでデータを渡した結果…

invalid JSON in google.cloud.discoveryengine.v1main.Document, near 1:13 (offset 12): no such field: 'id'

またエラーかよ…。え、idフィールドが見つからない?
確かに、サンプルに倣って構造化メディアデータを作成するとidフィールドが存在しないことに気づく。関連情報を辿ってidフィールドを入れてみるがうまく行かない。
頭を抱えたままドキュメント内を徘徊していたら正解が現れた。

{
   "id": "sample-01",
   "schemaId": "default_schema",
   "jsonData": "{\"title\":\"Test document title\",\"categories\":[\"sports > clip\",\"sports > highlight\"],\"uri\":\"http://www.example.com\",\"media_type\":\"sports-game\",\"available_time\":\"2022-08-26T23:00:17Z\"}"
}

Vertex AI ガイド - メディア データストアを作成するより引用
なるほど。サンプルデータはjsonDataフィールドにまとめ、id と schemaId を入れなきゃいけなかったんだな。でも、せめてこの事実をチェックリスト2.で教えてほしかった。(涙)

Google 「最初にチェックリストを通読せずに作業を進めた君が悪いっ!」

なお、この事実は次の3.に記載されている。

穴4.GA4のイベントはそのままでは使えません。

「諦めずにここまで進められた君はもう大丈夫!あとは適当なイベントを投入して一日待てばレコメンドできるよ!」なんて甘い言葉を掛けてもらえると思ったら大間違い。正直、ここからの作業の方がよっぽどきつい

先述した通りに作業を進めれば、おそらく構造化メディアデータをデータストアへ取り込めたはず。もし、取り込めない(統合できない)方は次の項目を確認してほしい。

  • 各ドキュメントはすべて1行で表現されている
  • フィールド値(キー)はすべて仕様に従っている(自分で定義していない)

イベントの収集に関しては様々な方法があるが、今回は、Google Analytics(GA4)をサイトと紐づける方法を採用した。
ここで注意しておきたいのが、GA4のデータを直接 Vertex AI側へ移すことはできず、一度 BigQueryを経由する必要がある。その方法については、「GA4 BigQuery 連携」と検索すればたくさんヒットする。
連携が完了すると、BigQueryに連携日以降のイベントデータが反映される。

ただ、「BigQueryへエクスポートしたデータをそのままデータストアへ投入すればOKじゃね?」なんて楽観的に考えていたら撃沈した。公式ドキュメントを確認してみよう。
要するに、イベントデータの構造が指定スキーマと違うからダメということである。なので、指定スキーマを満たすようにイベントデータを再構成しなければならない(もちろんクエリで)!

こんな風になればOK(view-itemに準拠)

クエリ作成時のポイント(コード例付き)

  • フィールド名は AS を使って置き換える
    user_pseudo_id AS userPseudoId,
    
  • フィールドの一部は REQUIREDモード(変更できないため、テーブルを新規作成する)
    eventType STRING NOT NULL,
    userPseudoId STRING NOT NULL,
    eventTime STRING NOT NULL,
    
  • RECORDはSTRUCT<>、REPEATEDは**ARRAY<>**で指定する
    documents ARRAY<STRUCT<
      id STRING
    >>,
    
  • event_timestamp はRFC3339形式に変換する
    FORMAT_TIMESTAMP("%Y-%m-%dT%H:%M:%SZ", TIMESTAMP_MICROS(event_timestamp)) AS eventTime    -- event_timestampはUNIX形式, マイクロ秒
    
  • event_params.valueの取得にはネストを解除する必要がある
    FROM 
      UNNEST(event_params) AS event_params
    
  • keyに対応したvalueを抽出するにはWHEN句を使う
  • event_nameは指定されたイベント名に書き換える
    WHEN MAX(CASE WHEN event_params.key = 'page_location' THEN event_params.value.string_value ELSE NULL END) = 'https://basuke-yaritai.com/' THEN 'view-home-page'
    
  • document.id は構造化メディアデータのidと対応させる

よく考えたら、チュートリアルでもクエリで操作する箇所があったので、この作業はおそらく回避不能。正直、同じ Googleのサービスなのだからデータ形式の互換性があってもいいのにな~と思ってしまうが、現実はそんなに優しくはないようだ

おまけ まだまだ穴に落ち続ける…

ここまで進められれば、レコメンド機能で要求されるデータはすべて満たすことになる。あとは、Vertex AI側で学習してもらって、その結果をプレビューで確認って感じに行けたらどんなに嬉しかったことでしょう。穴はまだまだ存在する。
穴の一例

  • データ要件・品質はすべて満たしているのに、学習が一向に進まないだって? - どうして仕事してくれないの(泣)

本記事がウケれば続編を書くつもり。

おわりに

Vertex AI は使いこなせると本当に便利な機能です。機械学習の知識に自信がない人でも簡単に取り扱うことができます。ただ、私のように穴に落ちてしまう人(主にひよっこ)も一定数いるかと思ったので本記事を執筆しました。(関連記事がほぼ無いので)
本記事を機に、多くの方が Vertex AI の世界に飛び込み、この界隈がもっと賑やかになればいいなと思っております。
なお、「AI活用を考えてはいるけど、自前で準備するのは難しいな…」と感じている方は、ぜひ弊社へご相談ください!経験豊富なエンジニアが懇切丁寧に対応致します。
https://www.mambacom.com/

株式会社Mambacom

Discussion