今の五目並べAIの構成

五目並べAIは三つの部分でできています。

  • Qt アプリ: C++ で作った五目並べのエンジンとローカルのヒューリスティックAI。人対AI、人対VLM、AI対VLM の対局ができます。
  • VLM サーバー: Qwen3-VL が盤の画像と文字の盤を見て、次の手の座標を答えます。
  • Web対局: [omook.mariago.kr](https://omook.mariago.kr/play) で人が黒を持ち、相手はローカルAIと VLM から選びます。

人と打った成績

最後まで打った対局だけを数えると、人が43局中39局勝ちました。Webでは24局中22局、Qt アプリで VLM を相手にしたときは19局中17局を人が勝ちました。Web対局のうち2局は、黒が五つの石を並べる間にAIが一度も止めず、9手で終わりました。

人との対局。Web 人22勝 AI 2勝、Qt アプリ 人17勝 VLM 2勝、合計 人39勝 AI 4勝
人との対局。Web 人22勝 AI 2勝、Qt アプリ 人17勝 VLM 2勝、合計 人39勝 AI 4勝

AI同士の学習対局

一週間、Qt アプリでローカルAI(黒)と VLM(白)を自動で打ち続けさせました。終わった487局のうち白が457局勝ちました。黒が勝ったのは29局、引き分けは361手まで続いた1局です。

AI対VLM 学習対局の日別勝敗。白(VLM)457勝、黒(ローカルAI)29勝、引き分け1
AI対VLM 学習対局の日別勝敗。白(VLM)457勝、黒(ローカルAI)29勝、引き分け1

VLM は座標から間違える

VLM が選んだ25,413手のうち7,089手(28%)は、すでに石がある場所のように打てない位置でした。このときサーバーはもう一度尋ね、それでも間違えたら最も近い空きマスに移して打ちます。コードのコメントに書いたとおり、この補正は座標を救う仕組みにすぎず、良い手を保証するものではありません。

VLM 座標補正の割合。日別 22〜33%、全体 28%
VLM 座標補正の割合。日別 22〜33%、全体 28%

自動学習も壊れていた

学習対局が終わるたびに、勝った側の手で LoRA を学習し直すようにしていました。データは12,080件まで集まりましたが、最後の学習記録を見ると損失が248,414から0に落ち、勾配は NaN です。学習がうまくいかなかったということです。今のサーバーは LoRA なしで、基本モデルの Qwen3-VL-2B で動いています。

残っている学習記録は3回です。すべて自動対局の5日目に動き、1回あたり25ステップの間に例を200件しか使いませんでした。3回とも10ステップで損失が500を超え、20ステップで0になりました。勾配は6回の記録がすべて NaN です。train_vlm.py は損失が100を超えると発散と見なしますが、3回ともその線を超えました。

集めたデータも偏っています。12,080件のうち10,513件(87%)が白の手です。学習対局で白が94%勝ったためで、この白の手は VLM が打ったものなのに、ローカルAIが打ったものとして記録されていました。ローカルAIが黒で勝った対局の手は1,291件、人が勝った対局の手は276件しかありません。

LoRA 自動学習3回の損失と、学習データ12,080件の構成
LoRA 自動学習3回の損失と、学習データ12,080件の構成

まとめると、今の五目並べAIは人にほとんど負け、VLM はまだ何も学んでいない状態です。次の記事から、ルール、弱点、直す計画を順にまとめます。

この段階の実際のコード

VLM が打てないマスを選んだとき、近くの空きマスに移す部分です。

vlm_pva_server.pypython · 25行
if (
    not illegal_adj_fallback
    and board is not None
    and not _is_legal_vlm_move(board, r, c, size)
):
    if args.strict:
        raise RuntimeError(f"모델이 부적절 칸을 반환: ({r},{c})")
    why = _move_rejection_reason(board, r, c, size)
    if _is_opening_board(board, size):
        near = _opening_center(size)
    else:
        near = _nearest_empty(board, r, c, size=size)
    if near is None:
        raise RuntimeError("빈 칸 없음 (보드 가득 참)")
    logging.warning(
        "모델 좌표 (%d,%d) 부적절 — 사유: %s → 인접·근접 빈 칸 (%d,%d) 로 보정 "
        "(보정은 좌표 복구용 휴리스틱이라 좋은 수를 보장하지 않음)",
        r,
        c,
        why,
        *near,
    )
    r, c = near
    corrected = True
    correct_reason = why

次の記事は「2. 連珠ルールが白を無敵にした」です。