最初的启发式

围棋启发式一开始就在中央落子,然后像盘蛇一样只在中央下棋。它常常在应对AI着法的过程中,引出当时AI的异常反应而获胜。与其说是智能,不如说更接近一团棋子。

以R5为对手进行修正

于是我们开始以最近训练的模型Mixed R5为对手来修正启发式。启发式执黑,R5执白,下到第80手后进行数子。一天之内修正了五次。

  • 战术:能提的子就提,被叫吃的子要救活,避免自己走进叫吃的着法。
  • 布局:在前70手内,先下角上的三、四线,避免一、二线和无理由地在中央分散落子。中央盘蛇的下法就是在这一步消失的。
  • 势力:优先下与己方棋子松散相连的着法,比如单关跳、小飞、拆二。
  • 要点:如果有能救活的被叫吃的棋子,就先于其他着法救活它。
  • 在与R5的对局中确认的失误:对走进虎口的着法、无效的征子、填自己的目以及附近的双叫吃进行扣分。

修正之后,启发式很快就步入正轨,现在看起来启发式甚至比R5下得更好。由于有80手的限制,白棋虽然靠贴目赢了,但几乎是势均力敌。最后一个版本在22局中输了21局,但R5领先差距的中位数是5目,小于6.5目的贴目。22局中有12局在不算贴目的情况下是启发式领先。实际上如果没有贴目,R5就输了。

80手限制对局中R5领先目数的中位数。从最初的9.5目减少到要点v6的5目,降到了贴目6.5目以下
80手限制对局中R5领先目数的中位数。从最初的9.5目减少到要点v6的5目,降到了贴目6.5目以下

增加手数限制并让双方互相学习

于是我们把手数限制提高到150手,让双方连续对战100局。在留下记录的78局中,启发式以47胜31负领先。

接着,我们把对局内容直接反映到启发式和R5中。只从一方下出的着法中挑选提子的着法、救活被叫吃棋子的着法以及真正能吃掉的征子,作为加分传给对方。PASS、自杀、虎口和无效的征子不传递,获胜一方的着法则加倍累积。这样让双方互相学习之后,以150手下了189局,启发式是125胜64负。

启发式(黑)胜率。80手5%,150手60%,互相学习150手66%,200手86%(7局,进行中)
启发式(黑)胜率。80手5%,150手60%,互相学习150手66%,200手86%(7局,进行中)

现在正在把手数限制提高到200手,进行40局对弈。结果还不得而知,但启发式和R5都开始出现令人惊讶的变化。在目前已经结束的7局中,启发式赢了6局。

启发式成了老师

统计双方互相传递的加分,从启发式传给R5的要多得多。R5主要从启发式那里学到了提子和征子,而启发式主要从R5那里学到了救活被叫吃棋子的着法。

双方互相传递的加分。启发式 → R5 5,957(提子3,158 · 征子2,771),R5 → 启发式 3,429(救叫吃2,102)
双方互相传递的加分。启发式 → R5 5,957(提子3,158 · 征子2,771),R5 → 启发式 3,429(救叫吃2,102)

这相当于以启发式为老师来教R5。从某种意义上说,用规则编写的启发式正在下出比经过训练的人工智能更好的着法。

所有对局都是在手数限制处停止后只计算目数和提子的参考数子。没有判定死子的死活。

这一步的实际代码

这是把一方下出的好着法累积到对方表中的部分。黑棋(启发式)的着法传给R5,白棋(R5)的着法传给启发式,获胜一方的着法按两倍计算。

vlm_server/cross_lessons.pypython · 12 行
def absorb_move(book: dict, actor: str, tags: Iterable[str], winner: Optional[str]) -> None:
    """actor 가 둔 좋은 수를 상대 테이블에 넣는다. actor 는 black|white."""
    tags = [t for t in tags if t in ("capture", "atari_save", "working_chase")]
    if not tags:
        return
    if actor == "white":
        target = "to_heuristic"
        weight = 2 if winner == "white" else 1
    else:
        target = "to_r5"
        weight = 2 if winner == "black" else 1
    _add_tags(book, target, tags, weight)

这是把在R5对局中确认的失误加入启发式分数的部分。虎口和无效的征子大幅扣分,真正能吃掉的征子则按学到的程度给予加分。

vlm_server/cross_lessons.pypython · 25 行
def r5_live_delta(board, r: int, c: int, who: int) -> int:
    cells = _cells(board)
    score = 0
    hogu = hogu_verdict(cells, r, c, who)
    if hogu == "hogu_entry":
        score -= 9000
    elif hogu == "hogu_fill":
        score -= 2000
    if (r, c) in _futile_points(cells, who):
        score -= 14000

    if _puts_opponent_in_atari(cells, r, c, who):
        verdict = chase_verdict(cells, r, c, who)
        if verdict is not None:
            if verdict.kind == "working_chase":
                score += 2500 + verdict.group_size * 80
                score += int(_bonus(load_book(), "to_heuristic", ("working_chase",), 35.0, 1400.0))
            elif verdict.kind == "broken_chase":
                score -= 2000

    if _own_territory_fill(cells, r, c, who):
        score -= 3500
    if _nearby_double_atari(cells, r, c, who):
        score -= 6000
    return score

使用的模型:Qwen3-VL-8B-Instruct + LoRA适配器(lora_mix_qwen_r5)。完整源代码和模型说明可以在“8. 完整源代码与使用的模型”一文中查看。