こんばんは。前回、
を出したところ
など、身辺へのバレが順調に加速しており、日々戦々恐々としている玉置絢(@OKtamajun)です。
◆Qiitaで前回記事をご覧になった皆様へ
Qiitaのノリがよくわからなくて、なんだか古のインターネットのテキストサイトみたいなノリで書いた導入だったにも関わらず、いいねを多くつけてくださり、温かくこの界隈に受け入れてくださった皆様、本当にありがとうございました。
AI実装に日々努力しておられるエンジニアの皆様の気分転換になるジャンキーな記事になれていれば幸いです。
◆リアル知り合いの皆様へ
払戻金はSSRスタミナエアシャカール完凸とSSR十王星南に消えてますので
ご飯は奢れません👍️
今回のテーマ:マルチエージェントをどう動かすか
前回の記事では、なぜ「マルチエージェントLLMオーケストレーション」という手法にしたのかという点からご説明しました。
今回はマルチエージェントにするとして、どのエージェントをどう動かす設計なのかという【骨組み】 をご説明します。
前回に引き続き:作者はAI・競馬、そして今回は統計の分野においても素人です。そもそも例の展示は「勝てる予想AI」を作るコンセプトではなく「キャラ同士の予想会話を楽しむ」ものでした。要するに、話半分でお読みください。
部員たちはどう活動しているか
前回、以下のような概念図をご紹介しました。
この構造を別の角度から見てみましょう。
「リーダーのみちる」と「回答してくれる部員たち」の相互関係にだけフォーカスして図解すると
こんな感じです。
「構造化データ」をなるべく準備する
ところで、部員たちがそれぞれの専門性をもってアドバイスが出来ているのは主に以下の2つの実装が機能しているからです。
-
キャラごとにユニークな情報源
- DBにアクセスして、それぞれの分野の統計データをまとめ考察材料にする
-
キャラごとにユニークなプロンプト
- 統計データに独自の目線で着目し、考察し、みちるにその分野ならではの助言をする
ここでポイントなのは 統計処理などLLMでない仕組みで出来ることは可能な限りデータベース処理で行うということです。LLMに突っ込む手前が大事で、予想に有効なデータをなるべく機械的(プログラム的)な処理で安定して抽出するように努め、LLM処理部分は「データに着眼する」ことにだけ集中するという分担です。上の図の「統計データ」という矢印と、「解説」「考察」という矢印の2段階で問い合わせに答えているところに注目してください。 LLM(部員たち)に直接生データを与えてもいいところを、あえてSQLで一次統計をするということが重要で、LLMは統計した結果データを解説・考察するという役割分担を強く意識しています。
統計と着眼の使い分け
LLMに苦手な数学をさせる意味はない、得意なことだけさせよう
確かに最新のLLMモデルは数学的な計算や統計についても一定のパフォーマンスを示してくれますから、統計処理を行っていないデータの羅列だけを与えて、統計タスク自体までLLMにさせることも可能です。しかし、正確性・スピード・コストの面から考えるとまずは LLMの前に普通の統計を頑張るということが重要です。
例:サンプル数不足問題
出走した全レースのうち「1着か2着に入れた」という結果を残せたレース数の割合を競馬では「連対率」と呼びます。この連対率から有利不利を考えることを展示した「GALLOPIA」では重視しているのですが、この手の確率は サンプル母数が少なすぎると極端な数字になっちゃってアテにならないという問題があります。
例えば…
ある有名な馬AとBがいます。2頭とも今は種牡馬(お父さん)になっており、産駒(子ども)たちも大きくなって競走馬として活躍しています。
ただ、Aの産駒は既に沢山いるのでレースで1,000回走っていますが、Bはまだ種牡馬になった直後で、産駒も多くないので10回しかレース記録がありません。
AとBの産駒の連対率は以下のようになっています。
連対率=(1着回数+2着回数) / 全出走数 |
親 |
産駒が1着 |
産駒が2着 |
それ以外 |
連対率 |
A |
50回 |
200回 |
750回 |
25% |
B |
1回 |
4回 |
5回 |
50% |
このとき「Bの子どもたちはAの子どもたちよりも強い!」と言い切れるでしょうか?
|
このように十分なサンプルがない情報は、合っているかもしれないけどまだ偏ってるだけかもしれないので、信用が置けないわけですが、統計をとる切り口や上記の例のように最近データが採れ始めた情報などの都合で頻繁にサンプル不足が発生するのが競馬のデータにおいては現実です。
このような問題に対処するために展示したシステムでは「ウィルソン信頼区間」と「ベイズ推定」を活用しています。
サンプル数の多少を加味して評価するSQL文(抜粋)
result_with_attention AS (
SELECT
*,
CASE
WHEN total_races > 0 THEN
((win_count + 1.9208) / NULLIF(total_races + 3.8416, 0)
- 1.96 * SQRT((win_count * (total_races - win_count)) / NULLIF(total_races + 3.8416, 0) + 0.9604) / NULLIF(total_races + 3.8416, 0)
)
ELSE 0
END AS wilson_lower_bound,
CASE
WHEN total_races > 0 THEN
(win_count + overall_win_rate * 20) / NULLIF(total_races + 20, 0)
ELSE 0
END AS bayes_estimate,
1 - 1 / NULLIF(1 + EXP(-0.3 * (total_races - 15)), 0) AS data_scarcity_distrust,
CASE
WHEN win_rate_diff >= 0 THEN
LEAST(win_rate_diff / NULLIF(0.025, 0), 1)
ELSE
GREATEST(win_rate_diff / NULLIF(0.04, 0), -1)
END AS normalized_win_rate_diff
FROM result_with_stats
),
result_with_adjusted_attention AS (
SELECT
*,
CASE
WHEN data_scarcity_distrust <= 0.5 THEN
CASE
WHEN normalized_win_rate_diff >= 0 THEN normalized_win_rate_diff * 2
WHEN normalized_win_rate_diff >= -0.625 THEN normalized_win_rate_diff
ELSE normalized_win_rate_diff * 1.5
END
ELSE
normalized_win_rate_diff
END AS adjusted_win_rate_diff
FROM result_with_attention
),
result_with_attention_score AS (
SELECT
*,
(
wilson_lower_bound * 20 +
bayes_estimate * 20 +
adjusted_win_rate_diff * (1 - SQRT(data_scarcity_distrust)) * 40 +
(race_count_zscore / 100) * 20
) * (1 - SQRT(data_scarcity_distrust)) * 50 AS attention_score
FROM result_with_adjusted_attention
),
めちゃいじったので統計的に正しい計算かは知らん😤
このような数式の複雑な処理をポン出しのLLMにさせることはかなり難しいことは、ChatGPTなど大手LLMがPythonコード呼び出し機能を早々に実装したことからも明らかかと思います。ということでプロンプトで「サンプル数が少なすぎる場合は信用するな」とか書くよりもLLM以前のプログラム処理でここの課題を解決したほうが安定する……それがベストプラクティスと思われます。
LLMはそのぶん統計では難しい「着眼する」という作業で活躍します。
競馬予想に限らずですが、意思決定の参考になるデータは一般的に「一長一短」なものばかりになります。100点と0点のデータしかなければ選ぶのは簡単ですが、ある切り口では70点と30点だが、別の切り口では40点と60点みたいなときにどちらを選ぶか? というタスクが多く発生します。このようなときに 「決断して、ある切り口に特別に注目し、その切り口からの分析を強調して見解を述べる」「平凡で選びづらい統計データからアノマリーな箇所を目ざとく発見して、問題提起する」 というタスクはSQLのような機械的プログラム処理では賢く作りづらいですが、よく出来たLLMは人間が見て合理的で共感性の高い判断を、思い切りを持ってこなしてくれます。
構造化データ記述と非構造化データ記述
これらの使い分けを実装するうえでポイントなのは、プロンプトへの与え方です。
- いかなる時でも公平に扱ってほしいエビデンスデータ
- 状況や意思決定の方向次第では無視したり、誇張化・矮小化してもよい経験則
の違いをどうLLMに教えるか? ということです。
今回のシステムでは、
- エビデンスデータはSQLでDBから取得し、構造的に厳格なJSON形式で与える
- 経験則はなるべく文章風・口語調で与える
という使い分けを意識しました。
例:
構造化データとしてLLMに渡すエビデンスデータ
(天皇賞・秋'24 種牡馬傾向分析から抜粋)
[
{
"切り口": "種牡馬",
"名前": "キタサンブラック",
"着度数": "[ 7- 5- 4- 27](全 44走)",
"連対率": "27.3%",
"平均差": "N+4.3%",
"不信度": "0.0%",
"総合注目度": "4772.5",
"Tier": "TierAAA"
},
{
"切り口": "種牡馬",
"名前": "サトノクラウン",
"着度数": "[ 0- 0- 1- 10](全 11走)",
"連対率": "0.0%"
"平均差": "N-23.0%",
"不信度": "76.9%",
"総合注目度": "42.2",
"Tier": "TierD"
},
{
"切り口": "種牡馬",
"名前": "ドゥラメンテ",
"着度数": "[ 11- 7- 4- 65](全 87走)",
"連対率": "20.7%",
"平均差": "N-2.3%",
"不信度": "0.0%",
"総合注目度": "-300.4",
"Tier": "TierNG"
}
]
非構造化データとしてLLMに渡す経験則
(調教LAP プロンプトから抜粋)
-- 1F最速データがちょうどレース本番の前週(7日前~14日前の範囲)である場合は仕上げ十分と判断できる。
- 最終追い切りがレース本番より一週間以内(当週追い切り)である場合は「サラッと息を整える当週追い切り」と判断できる(つまり速い必要はない)。
- 最終追い切りがレース本番より一週間以内(当週追い切り)の成績が良すぎる場合は「調整が間に合っていない」可能性がある。
- 最終追い切りがレース本番より一週間以上前(前週追い切り)である場合は「当週に追い切りができなかった理由がある」可能性がある。
- 逆に1F最速データ(ベスト)が三週間以上前である場合は、本気の強め・一杯調教のポテンシャルがある(レース本番で疲労させないためかなり前に行った)可能性がある。
多くのLLMは構造化データ(JSONやXML)を、構造のあるデータとして理解するだけでなく「エビデンスとして無視してはならない、公正に扱うべきものである」と理解しているかのように無視しない傾向があります。一方、そうではなく口語的に書かれた非構造化データについては、扱いは自分のアドリブ次第と考えているのか、状況に応じて都合よく無視したりしがちです。特にGPT-4oはjson output機能があることでも分かるようにjsonに特段強いため、「JSONで構造的に書くか、口語で書くか」自体が、エビデンスとしての扱い方の指定になっているように感じています。これを利用して、「着眼する対象情報」と「着眼の仕方の参考情報」の違いを伝えるつもりでプロンプトを書いています。
これらのアプローチをふまえて、次回の記事では各キャラクターが行っていることをそれぞれキャラクタープロフィールも添えてご紹介します。この記事の中で続けて書こうと思ったのですが途中まで書いてめちゃくちゃ長くなったので、一旦この記事はここまでということで。
次の記事 → キャラの実装ポイント解説!
細かいポイントを見ていきます!
次の記事
https://qiita.com/oktamajun/items/31d9eb46a4937decb67a