はじめに
こんにちは!レバレジーズのテクノロジー戦略室でデータエンジニアをしている鈴木です。 普段、データエンジニアリング業務に勤しんでいる私ですが、先日、同じ部署のR&Dチームから、フリーテキストを構造化したいという相談を受け、Airflow, BigQueryML, Dataformを使ったアーキテクチャでいい感じのものができたので、紹介したいと思います。
概要
今回のお題をもう少し具体化すると、 フリーテキストに含まれているスキルを正規化したいというものでした。
例えば、以下のような案件情報があったとします。
データがこのままだと、検索や機械学習など二次利用するには、ちょっと使いにくいですよね??
テキスト丸々エンベディングすれば、コサイン類似度で比較できそうですが、精度高くやるのは、ちょっと大変、、
であれば、以下のように、スキルだけ抜き出してarray方とかで保持してた方が扱いやすいです。
また、上記のように正しく表記されたスキルを抽出するのは、形態素解析とかでもできそうですが、フリーテキストの実態はそんな優しいものではなく、
例えば、AWSというスキル1つとっても、AWS、Amazon Web Service、aws、AmazonWebService、Awsなど、表記揺れが起きてしまいます。
今回の課題は、これらの課題を含んだ20万件のフリーテキストから、スキルを正確に抽出して、データモデリングに組み込むことです。
課題のポイント
本件の課題のポイントは以下の3つになります!
- コスト
- これは単純な話ですが、大量のデータをLLMのAPIに投げることになるので、これを毎回やっていると継続的に大きなコストがかかってしまいます。
- 冪等性の担保
- データエンジニアリングの中で重要な要素である冪等性(何回やっても同じ結果になる性質)ですが、LLMをモデリングに組み込むと、その不安定さにより、毎回微妙に違う回答が返ってきてしまいます。これでは、正確なデータマートは作成できません。
- 429エラー
- Vertex AIなど、LLM系のAPIは、リクエストを連続で大量に投げてしまうと、too mucn requestでこけてしまいます。
- ハルシネーションのリスク
- LLMなので当然ハルシネーションが起こりえます。特にプロンプトが長い場合は、正確な回答を返してくれるか、怪しくなってきます。
アーキテクチャ
これらの課題を解決するために、以下のアーキテクチャを採用しました。
- Airflow
- パイプライン全体のオーケストレーション
- Dataform
- プロンプトテーブルの作成、データレイク~データマートまでのデータモデリング
- BigQuery ML Stored Procedure
- ストアドプロシージャを実行して、大量データのLLM処理
開発のポイント
上記アーキテクチャの肝は、BigQuery MLのStored Procedure機能を活用したことです。
https://docs.cloud.google.com/bigquery/docs/iterate-generate-text-calls?hl=ja
この機能は、以下のようなプロンプトテーブルを作成することで、テーブルに格納されたプロンプトをBigQueryMLで効率よく処理することができるというものです。
プロンプトテーブル
| ID | prompt |
|---|---|
| 1 | 日本の首都は?? |
| 2 | 世界で2番目に高い山教えて |
テーブルを準備したら、以下のようなクエリで、ストアドプロシージャを呼び出します!
SQL
CALL `bqutil.procedure.bqml_generate_text`(
"bigquery-public-data.bbc_news.fulltext", -- source table
"PROJECT_ID.sample.news_generated_text", -- destination table
"PROJECT_ID.sample.generate_text", -- model
"body", -- content column
["filename"], -- key columns
'{}' -- optional arguments
);
すると、以下のように、プロンプトテーブルに対応した、結果テーブルが作成されます。(実際は、Json形式で返ってきますが、わかりやすいように簡略化してます)
結果テーブル
| ID | result |
|---|---|
| 1 | 東京 |
| 2 | K2 |
この機能の優れているポイントは、プロンプトをID管理しているため、プロンプトテーブルと結果テーブルがキャッシュの役割を果たしていることです!!!
これにより、1度LLMによる処理を行ったものは、次回以降は処理されず、冪等性を担保することができるようになります。
また、Vertex AIにリクエストを投げる回数を劇的に減らすことができるので、大幅なコストカットにも繋がります!
上記のおかげで、コストと冪等性の問題は解決することができましたが、429エラーとハルシネーションの問題が残っています。
429エラーの解決には、ストアドプロシージャとAirflowが役立ちます。
ストアドプロシージャの呼び出し時に、バッチサイズ(何レコードずつ処理するか)を設定することができます。
SQL
CALL `bqutil.procedure.bqml_generate_text`(
"your-project.your-dataset.prompt_table",
"your-project.your-dataset.result_table",
"your-project.your-dataset.gemini_flash_lite_model",
"prompt",
["id"],
'{"ml_options": "STRUCT(0.1 as temperature)", "projection_columns": ["raw_skil"], "batch_size": 2000, "termination_time_secs": 10800}'
);
ただ、上記でバッチサイズをいい感じに設定しても、429エラーが発生してしまっていたので、AirflowのDAG上で、指数バックオフのリトライを設定することで、安定化させることができました。
指数バックオフについて補足しておくと、失敗するごとに、リトライ間隔を伸ばしていく、リトライ方法になります。
最後にハルシネーションの問題についてですが、こちらは、スキルのマスターテーブルを作成し、そのリストをプロンプトに組み込むことで、かなり正確性を上げることができました。ただ、プロンプトのチューニングについては、まだまだ改善の余地があるので、今後プロンプトやモデルのバージョン管理とともに、注力して行きたいところではあります!
まとめ
- LLMをデータモデリングに組み込む際に、ただ、BigQueryMLを繰り返し呼び出すだけだと、冪等性やコスト、429エラーなどでうまくいかないことが多い。
- BigQueryMLのストアドプロシージャや、リトライなどの工夫をすることで、上記の問題を解決することができる。
おまけ
上記のアーキテクチャに辿り着く前、というか、ストアドプロシージャを知る前、BigQueryMLをDataformのSQLの中に組み込んでいたのですが、なかなかうまくいかず、キャッシュテーブルの仕組みをスクラッチで構築しようとしました。ただ、途中で「これ絶対世の中にあるくないか?」と思い、調べたところ、Googleが開発してくれており、ありがたさを感じましたw