2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI判定を複雑にしたら逆に精度が落ちたので、JevのJudgeをシンプルに作り直した話

2
Posted at

はじめに

現在、Sideways という水平思考ゲームを開発しています。

いわゆる「ウミガメのスープ」に近いゲームで、プレイヤーは謎めいた状況に対して質問しながら真相を探します。

例えば、

is it light?

と質問すると、

Yes

と返ってくる。

あるいは、

does the wake up mechanism make sound?

なら、

No

と答える。

一見すると、AIとしてはかなり簡単な処理に見えます。

しかし、実際に作ってみると、この「Yes / Noを自然に返す」ことが思った以上に難しい問題でした。

特に興味深かったのが、

意味判定を細かく設計すればするほど、実際のJevの回答が不自然になった

ことです。

最終的には逆方向に進み、

AIに判断させる内容を減らす

ことでかなり改善しました。

今回は、具体的に何を変更し、実際の回答がどう変わったのかを書きます。


基本設計:AIは意味を判断し、コードが結果を決める

Sidewaysでは、AIにゲーム全体を制御させていません。

基本構造は、

プレイヤーの自然言語
        ↓
AIによる確率的な意味判定
        ↓
型付き出力
        ↓
TypeScriptによる決定論的処理
        ↓
ゲーム上の結果

です。

考え方としては、

AI = semantic judgment

Code = deterministic policy

User = consequential intent

です。

例えばQuestionなら最終的にユーザーへ返すのは、

YES
NO
PARTLY
IRRELEVANT
UNCLEAR

です。

Theoryなら、

SOLVED
PARTIAL
NOT_SOLVED

です。

AIが勝手に画面を決めたり、自由文で答えたりするわけではありません。

この分離自体は現在も良い設計だと思っています。

問題は、

AIにどれだけ細かい意味情報を判定させるべきか?

でした。


Version 2ではかなり細かく意味を分解した

Version 2のQuestion Judgeでは、だいたい次のような情報をJevから受け取っていました。

{
  wellFormed,
  relevant,
  evidence: {
    entailed,
    contradicted,
    undetermined
  },
  mixedClaims: {
    supported,
    contradicted
  }
}

それぞれ0〜1の確率です。

その後TypeScript側で、

if (wellFormed < 0.8) {
  return "REPHRASE";
}

if (relevant <= 0.2) {
  return "IRRELEVANT";
}

if (relevant < 0.8) {
  return "NOT_ENOUGH_INFORMATION";
}

if (
  mixedClaims.supported >= 0.8 &&
  mixedClaims.contradicted >= 0.8
) {
  return "PARTLY";
}

if (evidence.entailed >= 0.8) {
  return "YES";
}

if (evidence.contradicted >= 0.8) {
  return "NO";
}

のような処理をしていました。

Theoryを評価するSolution Judgeはさらに細かく、

{
  coreMechanism,
  causalRelation,
  supportingInsight,
  contextualError,
  competingMechanism
}

という5軸です。


なぜここまで細かくしたのか

理由は、次のような文章を区別したかったからです。

There is a light.

これは、

光があることには気づいた

程度なので、

PARTIAL

にしたい。

一方、

The light wakes the guest.

なら、

光が客を起こす

という因果関係まで理解しています。

これは、

SOLVED

にしたい。

さらに、

There is a light, but vibration wakes the guest.

なら、light という正しい単語が入っていても、

客を起こしたのは振動

という間違った原因を主張しています。

これは、

NOT_SOLVED

にしたい。

そのため、

core mechanism
causal relation
supporting insight
contextual error
competing mechanism

を分けて評価する設計にしました。

TypeScriptとして見ると、かなり綺麗です。


Offlineテストはかなり良かった

Version 2では、

880 unit/API tests
82 desktop/mobile E2E tests
Leak scan
Lint
Typecheck
Build

などがすべて通りました。

Calibration corpusも作りました。

Generalization用のengineering holdoutも作りました。

Hidden Solutionはserver-only。

通常のQuestionはJev呼び出し1回。

Theoryも1回。

セキュリティやソフトウェア構造としてはかなり良い状態でした。

ところが、実際のJevでテストプレイすると問題が出ました。


実際にプレイするとかなり不自然だった

ある問題では、

音が鳴らず、何も触れていないのに
眠っている客が起きる

という状況を扱っています。

真相には「光」が関係しています。

そこで、

is it light?

と質問しました。

Version 2:

Not enough information

次に、

is it light that wakes the guest?

Version 2:

Not enough information

さらにこれをTheoryとして送ると、

Not quite yet.

でした。

人間のゲームマスターなら、かなり簡単に意味を理解できる文章です。


さらに、

does the wake up mechanism make sound?

は、

Please rephrase

でした。

スペルを少し間違えて、

does the wake up mechines make sound?

としても、

Please rephrase

です。

英語として完璧ではありません。

しかし、

wake up mechines

が、

wake-up mechanism

のような意味だということは、人間ならかなり容易に分かります。

そこで、

Jevの能力というより、こちらのJudge設計が厳しすぎるのでは?

と考え始めました。


原因候補1:0.8という閾値が高すぎた可能性

これは今でもかなりあり得ると思っています。

例えばJev内部では、

wellFormed = 0.74
relevant = 0.92
contradicted = 0.89

くらいまで意味を理解していた可能性があります。

しかしコード側では、

if (wellFormed < 0.8) {
  return "REPHRASE";
}

です。

そのため、

wellFormed = 0.74

の時点で、

Please rephrase

になってしまいます。

その後の、

relevant = 0.92
contradicted = 0.89

は使われません。

本来0〜1の連続値を使っていたのに、

0.7999 → NG
0.8000 → OK

という「崖」をアプリ側で作っていたことになります。

閾値を0.6程度まで下げれば改善した可能性はあります。

ただし、Version 2には0.8の条件が複数存在しました。

そのため問題は単純に、

0.8が高かった

だけではないと考えています。


原因候補2:複数の不確実な判断をANDでつないでいた

Version 2は実質的に、

意味が十分分かる
AND
関連性が十分高い
AND
正しい可能性が十分高い
AND
...

という巨大な条件式になっていました。

それぞれの確率がそこそこ正しくても、

どれか1つがthresholdを下回る

と最終結果が変わります。

つまり、

1つの不確実なAI判断

を、

複数の不確実なAI判断 + hard threshold

に分解した結果、逆に壊れやすくなっていた可能性があります。


原因候補3:人間が一度に理解するものを分割しすぎた

例えば、

does the wake up mechines make sound?

を人間が読んだ場合、

"mechines" は mechanism の誤字だろう
        ↓
wake-up mechanismのことだろう
        ↓
音が出るか聞いている
        ↓
No

くらいを一体として理解します。

一方Version 2では、

wellFormedは何点?
relevantは何点?
truthは何点?

と分割しました。

TypeScriptの型としては綺麗です。

しかし、自然言語の意味理解としては分けすぎだった可能性があります。


原因候補4:「何を解こうとしているか」が明示されていなかった

例えば、

is it light?

という文章だけなら、

itとは何か?

は確かに曖昧です。

しかし水平思考ゲームでは、プレイヤーは今、

なぜ客が起きたのか?

を調べています。

人間のゲームマスターなら、

is it light?

を、

客を起こした原因は光?

と自然に補完できます。

そこでVersion 3では、

mysteryFocus

という情報を追加しました。

例えば、

What causes the sleeping guest to wake
when the alarm makes no sound
and nothing touches the guest?

です。

これは答えではありません。

単に、

今プレイヤーが何を解こうとしているか

をserver-sideで明示しています。


Version 3では逆にJudgeをシンプルにした

Version 2をさらに細かく調整するのではなく、構造そのものを簡単にしました。

Question Judgeでは、

type QuestionSemanticClass =
  | "SUPPORTED"
  | "CONTRADICTED"
  | "MIXED"
  | "UNKNOWN"
  | "IRRELEVANT"
  | "AMBIGUOUS";

という1つのChoice distributionだけをJevに返してもらいます。

その後TypeScriptで、

SUPPORTED
→ YES

CONTRADICTED
→ NO

MIXED
→ PARTLY

UNKNOWN
→ NOT ENOUGH INFORMATION

IRRELEVANT
→ NOT RELEVANT

AMBIGUOUS
→ PLEASE REPHRASE

と変換します。

Version 2にあった、

wellFormed >= 0.8
relevant >= 0.8
entailed >= 0.8

のような判定chainはactive pathからなくしました。


Solution Judgeも1 Choiceにした

Theoryについても同じです。

type SolutionSemanticClass =
  | "CORE_CAUSAL_EXPLANATION"
  | "CORE_WITH_CONTEXT_ERROR"
  | "SUPPORTING_INSIGHT_ONLY"
  | "COMPETING_WRONG_MECHANISM"
  | "NO_MEANINGFUL_INSIGHT";

Jevはこの中のどれに近いかというprobability distributionだけを返します。

TypeScriptでは、

CORE_CAUSAL_EXPLANATION
→ SOLVED

CORE_WITH_CONTEXT_ERROR
→ PARTIAL

SUPPORTING_INSIGHT_ONLY
→ PARTIAL

COMPETING_WRONG_MECHANISM
→ NOT_SOLVED

NO_MEANINGFUL_INSIGHT
→ NOT_SOLVED

と処理します。

つまり、

意味上の違い

は残しました。

しかし、

5つの意味軸を別々に採点する

ことをやめました。


Rubric自体も短くした

Version 3では、

minor spelling mistake
non-native English
missing article
telegraphic wording
ordinary shorthand
natural pronoun resolution

は許容するようにしています。

ただし、それぞれを独立して採点することはしません。

最終的に聞くのは、

このプレイヤーの文章は意味的にどのカテゴリなのか?

です。


実際の結果はかなり改善した

同じ問題で再度テストしました。

Version 3:

is it light?
→ Yes

次:

is brightness involved?
→ Yes

さらに、

something bright wake him?
→ Yes

です。

この英文はかなり崩れています。

それでも意味は正しく理解されました。


以前失敗していた、

does the wake up mechines make sound?

も、

No

になりました。

さらに、

does it buzz?
→ No
is there vibration?
→ No

という短い表現も機能しました。

これは mysteryFocus があることで、短い質問をゲーム文脈から解釈しやすくなった可能性があります。


PARTLYも意図した使い方になった

次のように2つの内容を一度に入力しました。

something bright wake him?
does the wake up mechines make sound?

結果:

Partly

UIでは、

Some of that is right, but another part isn't.

となりました。

これは良い結果です。

PARTLY は、

50%くらい正しそう

という意味ではありません。

正しい主張
+
間違った主張

が実際に同居している場合だけ使いたかったからです。


短いTheoryも正解になった

Question:

does light wake him?

結果:

Yes

同じ文章をTheoryとして送ると、

You got it.

になりました。

さらに、

the room gets bright and that wakes him

も、

Question → Yes
Theory   → You got it.

です。

Version 2では同程度の表現でも、

Not quite yet.

になっていました。

この差はかなり大きいです。


正しい単語だけではSOLVEDにならない

シンプル化したことで、何でも正解になるのは困ります。

例えば、

lamp is there but vibration wakes him

では、

Question
→ No

Theory:

Not quite yet.

でした。

lamp という関連単語が入っていても、

客を起こした原因はvibration

と主張しているので正解にはなりません。

これは重要です。


別の問題でも試した

Case 1は「空のframeの中で何かが変化して見える」という問題です。

例えば、

is the frame big

結果:

Not relevant
is it an animal

結果:

No
is it a TV

結果:

No
is it a machine

結果:

No

そして、

it changes its color with sun light

結果:

Yes

同じ文章をTheoryにすると、

You got it.

になりました。

文法的には完璧ではありません。

しかし正しい仕組みは理解されています。

これなら水平思考ゲームとしてかなり自然です。


もちろんまだ判定揺れはある

完璧になったわけではありません。

例えば同じセッションで、

is it the sunrise and sunset
→ Yes

となった一方、

is it sunrise and sunset
→ Not enough information

となることもありました。

これはまだsemantic boundaryに多少の揺れがあることを示しています。

ただし、Version 2の問題とは性質が違います。

Version 2では、

意味が明らかに分かる文章
→ Please rephrase

が頻繁に起きていました。

Version 3では、

かなり境界的な文章
→ YesだったりUnknownだったりする

という程度まで改善しています。

PoCとしては後者の方がかなり良い失敗の仕方だと考えています。


では、具体的に何が効いたのか?

ここは正直に書く必要があります。

Version 2 → Version 3では同時に複数の変更をしています。

複数Noulを1 Choiceへ変更

0.8 / 0.2 threshold chainを削除

mysteryFocusを追加

rubricを短くした

意味回復を個別scoreではなく
最終classificationの中で行うようにした

そのため、

0.8を消したから改善した

とはまだ断定できません。

もしかすると、

0.8 → 0.6

に変えるだけでもVersion 2はかなり改善した可能性があります。

逆に、

mysteryFocus

が一番効いた可能性もあります。

あるいは、

複数の独立判断をやめた

ことが重要だった可能性もあります。

現在分かっているのは、

これらをまとめて単純化したVersion 3の方が、実プレイでは明らかに自然だった

ということです。


Jevは「AI導入」というより、意味を理解できるIF文に近いと感じた

開発を続けていて、Jevに対する感覚も少し変わりました。

一般的な生成AIのように、

AIに仕事を任せる

というより、

自然言語を理解できるIF文を作る

感覚に近いです。

普通のコードなら、

if (text.includes("light")) {
  return "YES";
}

かもしれません。

しかしこれでは、

brightness wakes him
something bright wake him
the room gets bright and that wakes him

などに弱い。

Jevを使うと、

この文章の意味は
SUPPORTEDなのか?

という自然言語上の条件を評価できます。

つまり、

if (semantic condition estimated by AI) {
  doSomething();
}

です。

個人的には、

Probabilistic semantic IF statement

あるいは、

AI-powered switch/case

と考えるとかなり分かりやすくなりました。


Version 2は巨大なIF文になっていた

そう考えると、Version 2の問題も分かりやすくなります。

最初は、

意味的に正しい?
→ YES

程度だったものが、

IF wellFormed >= 0.8
AND relevant >= 0.8
AND entailed >= 0.8
AND ...

という巨大な条件式になっていました。

Version 3では、

switch (semanticClass) {
  case "SUPPORTED":
    return "YES";

  case "CONTRADICTED":
    return "NO";

  case "MIXED":
    return "PARTLY";

  ...
}

くらいまで戻しました。

この方が今回の用途には合っていそうです。


JevのコストはAPI料金だけではない

今回もう1つ感じたのが、

calibrationには人間の労働コストがある

ことです。

実際、このゲームでは、

semantic categoryを考える
実際に遊ぶ
短文を試す
typoを試す
false positiveを見る
false negativeを見る
Theoryを試す
別のPuzzleでも確認する
rubricを調整する
再度Previewで確認する

という作業が必要になりました。

これは立派な開発コストです。

Jevによって、

LLMの自由文をJSONに変換する

ような処理は減ります。

一方で、

decision design
calibration
threshold設計
test-play
behavior evaluation

の重要性が高まります。

そのため、

Jevは普通のLLMより労働コストが高い

とまではまだ言えません。

比較実験をしていないからです。

ただ、

Jevでは開発工数の一部が、output parsingからdecision designとcalibrationへ移動する

という感覚はかなりあります。


849テストが通っても、実プレイテストは必要だった

現在Version 3では、

849 unit/API tests
82 desktop/mobile E2E tests
Leak scan
Lint
Typecheck
Build

が通っています。

しかし、それでも実Jevを触らなければ今回の改善は確認できませんでした。

Offlineテストで確認できるのは、

semantic class
↓
TypeScript mapping
↓
API
↓
UI

の正しさです。

一方、本当に知りたいのは、

未知の人間の文章
↓
Jev
↓
適切なsemantic class

です。

この2つは別物です。

今後は、

Layer 1
Software correctness

Layer 2
Model behavior

として分けて考える必要があると思っています。


ここからは、あえて直しすぎない

まだ改善できそうなポイントはあります。

例えば、

sunrise and sunset

の揺れも直そうと思えば直せるかもしれません。

しかし、ここでまた、

特定フレーズをrubricに追加
categoryを追加
thresholdを追加
exceptionを追加

し始めると、Version 2に戻ってしまいます。

そのため現在は、

Version 3をPoCのFreeze候補にする

方向です。

完璧なJudgeを作ることより、

自然な短文を理解する

typoがあっても意味を理解する

正しい因果ならSOLVEDになる

間違った因果ならSOLVEDにならない

Hidden Truthを漏らさない

ゲームとして楽しい

ことを優先します。


それでもダメならYES / OTHERまで落とす

さらにシンプルなfallbackも考えています。

Questionを、

YES
OTHER

だけにする方法です。

OTHERには、

NO
UNKNOWN
IRRELEVANT
AMBIGUOUS

などを全部まとめます。

情報量は大きく減ります。

しかし、もしその方がゲームとして安定するなら採用する価値があります。

このプロジェクトでは、

AIからできるだけ多くの情報を取り出す

こと自体は目的ではありません。

必要なのは、

プロダクトに必要な最小限の判断を、安定して返してもらうこと

です。


まとめ

今回一番大きかった学びは、

AIの意味判定を細かく設計すればするほど良くなるとは限らない

ということでした。

Version 2は高度でした。

semantic signalsが多い
probabilityが多い
thresholdが多い
grading axisが多い

状態です。

しかし実際のプレイでは不自然でした。

Version 3では逆に、

1 semantic Choice
        ↓
1 probability distribution
        ↓
TypeScript switch

まで簡単にしました。

すると、

is it light?
→ Yes
something bright wake him?
→ Yes
does the wake up mechines make sound?
→ No
does light wake him?
→ You got it.

のように、かなり自然になりました。

今回の経験から、AIプロダクトでは、

Understand more.

より、

Decide less.

の方が良い場合があると感じています。

そしてJevのようなdecision modelを使う場合には、

「何をAIに判断させないか」を設計することも、重要なAI設計なのかもしれません。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?