乗り遅れた話
TypeSafe AI の Jev が盛り上がっていた頃、僕はルービックキューブにかまけていた(cubepic)。サーバリソースを食う方のソルバーは、課金して EC2 を回す以上、有料に見合う性能を出さないといけない。ランダムなスクランブルに対して決定論的に、5x5x5 で平均 66 BTM、Megaminx で平均 77 BTM を返せる、ちゃんとしたものにたどり着くまであれこれ試行錯誤していたら、ばっちり乗りそこねた。Jev は瞬間最大風速でとんでもない人気を集めて満員御礼になり、僕は悲しいウェイトリストゆきになった。
ツールの部品に、ぜひ欲しかった
もともと、LLM 支援開発をしながら、LLM が働く割合を下げたいと思っていた。電力の限界という話はよく目にするし、そもそも汎用的なものがあらゆることをする、というのが非効率なのは、人間だって同じだ。人間は、電卓もコピーも写真も使うのだ。類推すれば LLM の働き方だって、そうなるべきで、しかも彼らはプログラミングが得意だと来たら、確立した作業はツールに落とすのが適切だろう。
そんな考えから僕のリポジトリ群には、LLM が使うための Rust / Python 製の開発支援 CLI ツールがたくさん並んでいる。ローカル LLM を呼ぶもの、埋め込みモデルを使うもの、NLI モデルを使うもの、ドメイン特化の機械学習モデルを呼ぶもの、自然言語解析ライブラリを呼ぶもの、ブラウザやほかのアプリケーションを操作して動くもの、そして決定的なロジックだけで動くもの。AI との距離感はツールによってさまざまだ。
正直、LLM 支援開発で開発リポジトリの様相はものすごく変わった。それでも、こういうツールがピタゴラスイッチのようにつながるスタイルは、一周回ってすごく居心地の良い状態になったと思う。
その部品として、「文章を読んで、選択肢のどれに当たるかを確率で返してくれる」要素技術はまさに欲しかったピースだった。ストラクチャード出力の極致。値が帰ってくるなら、振り分けコードが、そのまま書ける。
手元のローカル LLM ベースで、同じ考え方のものを作ってみた
とはいえ、あの人気ではいつ使えるようになるか分からない。そこで、手元のローカル LLM で、同じ考え方の判定ツールを作ってみた。安くて速い、という事実そのものが、内部の仕組みをそこそこ露わにしている。もうみんなが知っていると思うので、ここでは改めて書かない。
ただ、評価値がピーキーすぎる
作ってみて、まあこんなものか、という感じ。ローカル LLM と埋め込みの間を埋めてくれる、塩梅のいいツール基盤になりそうだ。ただ、気になることがあった。評価値がピーキーすぎる。確率が 0 の近く、1 の近くに寄りすぎるのだ。
僕の理解では、理由はこうだ。指示に従うよう調整された LLM モデルは、トークンの分布が鋭くなりやすい。しかも「この画像はジグソーパズルに向いているか?」などと絶対評価を問われると、モデルは言い切ろうとしてしまう。その結果、「どのくらい良いか」の情報が潰れて、候補どうしの差が見えなくなる。
相対評価にしたら、指標が蘇った
そこで、相対評価を実装した。結局、絶対評価というものは本質的に不安定で、だからこそ製造ラインには「限度見本」がある。人間でも、単独で点数をつけるより、見本と並べて比べるほうがぶれない。
最初のユースケースは、バズりはじめた話題を RSS リーダーから拾ってくる自分用ツールだったので、色々試してみた。もちろん共起語を用いたクラスタリングとか既知かつよくある手法は併用している。ただ、最後にランキングで表示したい、そうすればじっくり読まなくて済む、というのが目指すところだった。なんとなく、類似画像から質の良いものを選び出すときに、比較を繰り返すのが効いたことを思い出した。比較は、使えるかもしれない。1 件ずつ確率を聞く方式では差が出なかった指標が、2 件のペアを比べる方式にしたら、はっきり差がつくようになった。これこれ。
位置バイアスを打ち消す
ただし、2 つを並べて聞くと、別の癖が出てくる。わりかし、先に見せた方を選ぶのだ。実際、ある画像のペアで「より落ち着いた印象なのは?」と聞いたら、A を先に見せると A を 0.9996、B を先に見せると B を 0.9998 で選んだ。どちらも自信満々で、どちらも「先に見せた方」を選んでいる。1 回だけ聞いたのでは、本当の勝ちと、この癖とを区別できない。
そこで、1 つのペアを順番を入れ替えて 2 回聞くことにした。
- 確率は、同じ候補どうしで 2 回分を平均する(位置の癖が打ち消し合う)
- 2 回とも同じ側を選んだときだけ勝敗をつけ、食い違ったら「判定不能(undecided)」にする
上の例なら、平均はほぼ 0.5 になって、判定は undecided。「これは順番で答えが決まっていただけだ」と正直に言ってくれる。
公開版では、この決め方を用途に合わせて変えられるようにした。必ずどちらかに決めたいとき(候補の自動選択など)は force_decision で食い違いを平均で決め、僅差を「差なし」として扱いたいとき(人の確認に回すなど)は margin で 0.5 ± margin に入った結果を undecided にできる。同着のない判定が必要なユースケースは少なくない。
多数の候補を順位付けする(Bradley–Terry モデル)
2 つを比べられるようになったら、次はたくさんの候補に順位をつける段階に進める。多数の候補に平等な回数の試合をさせて、ランキングを出すようにした。慧眼な方はお気づきだろう。Bradley–Terry モデルだ。将棋などでもレーティングの考え方を横展開したランキングの試みが知られるようになって久しいので、有名と言っていいだろう。
考え方
候補 i にはそれぞれ見えない「強さ」s_i があって、i が j に勝つ確率は、強さの差だけで決まると考える。
P(i が j に勝つ) = 1 / (1 + e^-(s_i - s_j))
強さが同じなら勝率 50%、差が開くほど 100% に近づく。やることは、実際の試合結果が一番起こりやすくなるように、全員の強さを一斉に決めることだ。これを最尤推定という。
普通は試合結果を「勝ち / 負け」で数えるが、今回の場合、比較の結果が確率で返ってくる。だから「0.8 勝 0.2 敗」のように確率のまま集計する。僅差の勝ちと圧勝を区別できるので、少ない試合数でも順位が安定する。全敗した候補の強さがマイナス無限大に飛んでいかないよう、弱い事前分布も入れている。
最後に、強さをチェスでおなじみの Elo レーティングの尺度(平均 1500)に直して返す。200 点差なら、上の方が勝つ確率がおよそ 76% という読み方になる。
試合の組み方
総当たりは、候補が増えると試合数が n(n−1)/2 で爆発する。そこで、候補を円環に並べてから組むことにした。
- 2〜5 件: 総当たり
- 6 件以上: 各候補が「円環の両隣」と「円環でなるべく遠い相手」とだけ戦う。n 件で 2n 試合、全員がちょうど 4 試合
両隣との試合で全体がつながり、遠い相手との試合が円環を横切るので、「隣どうしの比較を数珠つなぎにしただけ」のような尺度にならない。全員の試合数が同じなので、たまたま多く戦った候補が有利になることもない。並び順は候補の内容から決まるので、同じ候補なら何度やっても同じ組み合わせになる(seed で変えることもできる)。エッジケースをきっちり評価しているわけではないので、異常値を出すような集合を与えることはできるだろうが、そこそこランダム化された「健康な」データならこれで行けそうだ。
そして、画像に対応した
そんなわけで、元サービスではおそらくあまり有効な値が返しづらいと判断して見送ったのではないかと想像している画像に関しても、相対評価を使えば結構実用的になる。画像 + 文章で、ユースケースは一気に広がる。「この写真は商品説明と合っているか」「どちらのサムネイルがクリックされそうか」「この 10 枚を、食欲をそそる順に並べて」。これだけ流行っていて、しかも僕のように乗り遅れた人がいるなら、使ってもらえるんじゃないかと思って、急いで API として公開するためのガワを被せた。よかったら使ってみてくれると嬉しい。
使い方
ダッシュボードで API クライアントを発行したら、あとはこんな感じで呼べる(詳しくは API リファレンス。curl / Python / TypeScript の例がある)。
判定(/v1/judge)
curl -sS -X POST https://gyojy.hakadoru.ai/api/v1/judge \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"state": "今日は半袖でも心地良い気温でした。",
"questions": {
"temperature": {
"type": "choice",
"instructions": "気温として最も適切なのは?",
"criteria": {"18度": null, "22度": null, "26度": "暑い日"}
}
}
}'
選択肢ごとの確率(probabilities)と、確信度(confidence)が返ってくる。画像は images に data: URL で最大 4 枚まで渡せて、はい / いいえ(noul)や段階評価(score)の問いも使える。
2 つの比較(/v1/compare)
画像 2 枚でも、文章 2 つでも比べられる。
{
"texts": {
"A": "ご注文いただいた商品は、明日の午前中にお届けします。",
"B": "ご注文の品は明日午前着の予定です。お届け時間の変更はマイページから承ります。"
},
"criterion": "判断基準: お客様が次にとれる行動が分かる案内文が勝ち"
}
順位付け(/v1/rank)
画像なら 10 枚まで、文章なら 30 件まで。
{
"criterion": "判断基準: 読んだ人が食べたくなる商品説明が勝ち",
"items": [
{"id": "a", "text": "季節の果実を使った、甘さ控えめのタルトです。"},
{"id": "b", "text": "濃厚なチーズと、ほろ苦いキャラメルの二層仕立て。"},
{"id": "c", "text": "朝摘みの苺をたっぷりのせた、春だけのショートケーキ。"},
{"id": "d", "text": "定番のガトーショコラ。"}
]
}
順位・レーティング・勝率と、各試合の結果が返ってくる。
TypeSafe AI の SDK で使う
TypeSafe AI, Inc. の公式 SDK(Python の typesafe-sdk、JavaScript の @typesafe-ai/sdk)は、接続先と API キーを差し替えるだけで、そのまま GyoJy に向けて使える。Jev には乗り遅れて試せなかったが、ウェイトリストで待っている間に使い慣らしておきたい人のための機能と思ってほしい。ついでに自明に近いのだけれども、この「行司ぃ」はTypeSafe AI, Inc.とは無関係で、提携・承認・後援の関係にはないので念の為。
export TYPESAFE_BASE_URL=https://gyojy.hakadoru.ai/api
export TYPESAFE_API_KEY=gyojy_cs_xxxxxxxx # ダッシュボードで発行した Secret
名前のこと
https://gyojy.hakadoru.ai/ なのだ。狂ったネーミングは性分なのでご勘弁。最初は、相対評価が特長だからそれを前面に出そうと思って「軍配」にしようとしたら、同じ名前で有名な先行サービスがあった(そういえばなんかの話題でちらっと見た覚えがある)。そこで、ちょっと考えて「行司ぃ」にした。落ち着いて考えてみれば、勝ち負けを裁くのは軍配ではなく行司なのだ。
3つの秘訣、というのは釣りタイトルだけど、一応3つ以上は素敵情報が含まれると思うので、ご勘弁を。