AI の思考過程をめくれる木にする

探索の可視化で難しいのは木を描くことではなく、数値が本物であること、盤を押しのけないこと、押さない人に負担をかけないことでした。

五目並べ と中国将棋 の AI は数層を読みます。「思考ツリー」ボタンを足して、層ごとに何を読み、どの手を選び、どれだけ確信しているかを見られるようにしました。

機能自体は複雑ではありませんが、四つの落とし穴があり、それぞれ書き残す価値があります。

穴一:表示する評価は本物でなければならない

最初の版では、根の候補八つの評価がすべて -4517.7 でした。壊れて見えますが壊れていません。alpha-beta の境界値です。

枝刈りの本質は、ある候補が現状の最善より優れないと分かった時点で探索を打ち切り、真の評価ではなく「境界」を返すことです。よって根のループでは、最初の候補以外はほぼ境界を受け取ります。

修正は、利用者が見る探索では根の候補を全幅で読み直す(-Infinity, Infinity)ことです。根では枝刈りの節約はそもそも小さく、正確さに見合います。

const value =
  atRoot && rootDepth > 1
    ? negamax(ctx, depth - 1, -Infinity, Infinity, !isMax, ai, rootDepth)
    : negamax(ctx, depth - 1, alpha, beta, !isMax, ai, rootDepth);

利用者に見せる数字は本物でなければならない。境界値で組んだ「過程」は、過程が無いより悪い。

穴二:確信度が定数になってはいけない

最初の確信度は、最善と次善の評価差を「活四」の値で正規化したものでした。結果は常に 0.1〜0.5 です。

原因は次元の誤りでした。活四は数十万の桁で、通常の評価差は一万か二万、割ると 0.06 程度になります。確信度は永久に低く抑えられ、定数と同じで、表示しない方がましです。

単位を棒四(二万六千)に変え、深さの項を少し足しました。

const gap = sorted.length > 1 ? Math.abs(top.score - sorted[1].score) : PATTERN.FOUR * 2;
const depthPart = Math.min(1, d / 6);
const gapPart = Math.min(1, gap / PATTERN.FOUR);
const confidence = Math.max(decided, Math.min(1, depthPart * 0.35 + gapPart * 0.65));

これで、どう指しても大差ない序盤の攻防は七割程度、必殺手は満点になります。数字がやっと意味を持ちました。

穴三:パネルが盤を押しのけてはいけない

これは利用者からの指摘でした。講評が盤の上の通常フローにあり、出るたび消えるたびに盤が上下に跳んでいました。盤を見ている最中に跳ねられると非常に煩わしいものです。

二つ同時に直します。

  1. 高さを固定。.notes は height: 5.5rem に固定し、内容がどれだけ長くても伸ばさせません。これは様式の細部ではなく、部品としての契約です。
  2. 頁をめくる方式。講評は「数秒表示して消える」のをやめ、待ち行列に入れて矢印でめくります。「どの手がこの講評を呼んだか」も分かります。

めくり位置の状態機械は純粋なロジック(logic/notes.ts)に切り出しました。間違えやすいのは一件追加したときカーソルがどこへ行くかだからです。

利用者の状態 一件追加後
最新頁にいる 新しい一件へ追従
履歴を遡っている 動かさない
容量で古い件を捨てた 捨てた件数だけ左へ

最初の版は三番目を無条件に適用し、捨てていないのに一つ左へずれました。一件目を見ている最中に新しい一件が来て、表示が二件目に変わったのです。自己試験の該当の断言が、まさにこれを捕まえました。

穴四:必要な人だけが読む

思考ツリーは SVG の配置コードを抱えています。ボタンを押さない人に読ませるべきではありません。defineAsyncComponent で包むと独立したチャンクになります。

const ToolTree = defineAsyncComponent(() => import('@/games/gomoku/ToolTree.vue'));

ここで client:* を付けてはいけません。ハイドレーション指示子はコンパイル時の変換で、動的部品では黙って無効になります。このプロジェクトに既録の落とし穴です。アプリの頁は「静的な殻 + createApp でマウント」なので、非同期部品は実行時に自分を読み込めば済みます。

ビルド成果物で _astro/ToolTree.xxxx.js が独立したファイルになっているのが確認できます。

木は木らしく見えなければならない

木と呼ぶ以上、一覧ではいけません。SVG で「根 → 候補 → 応手」の三層を手書きしました。

  • 節は角の立った枠(全直角の作風に合わせる)、等幅で着手点と評価を表示
  • 線は三次ベジェ、最善の枝は蛍光橙で太く、他は細い墨線
  • 枝刈りされた節は破線の枠で、「ここは読み切っていない」と明示
  • 既定では上位 5 候補と最善枝の応手のみ。「全文岐を展開」で全部描きます。でないと線が団子になります

d3 を入れず手書きにした理由は、構造が三層で固定だからです。描くためだけに描画ライブラリを入れるのは割に合わず、チャンクも一つ増えます。

もう一つ:後から開いたらどうするか

探索の記録は、探索時に集めた場合にしか存在しません。利用者はたいてい AI が指した後に見たくなります。その時点では記録がありません。

そこで、空のパネルで誤魔化さず、現在の局面を純粋な解析として読み直します(盤は変えません)。費用は自分から開いた時に一度だけ、しかもパネルには「現局面で探索をやり直しています…」と出ます。

「データがありません」しか出さないパネルは、存在しない機能と同じです。

← 記事一覧に戻る

コメント

…