局面评估与 NNUE
评估回答“这个局面看起来怎样”,搜索回答“双方继续走下去会怎样”。 NNUE 给搜索提供快速判断,不直接输出最佳走法,也不替代将杀、重复和棋规判定。
本文核对 official Pikafish 提交 1c66b9b。下面讲这份代码实际如何使用网络;训练数据来源、规模与训练过程不能仅凭推理代码推断。
从棋盘到分数:各部分负责什么
一次普通的静态评估大致经过以下步骤:
- 把棋子位置和部分棋子间的关系编码成网络特征。
- 复用并更新累加器,再计算后面的网络层,得到原始 NNUE 分数。
evaluate.cpp根据子力、搜索倾向和限招计数等调整它。- 搜索按需要加入评估修正,并比较后续变化,形成搜索结果。
这几种分数不能混用。界面中的分数还经过输出换算,详见引擎运行与分数输出。搜索如何使用评估,见搜索算法。
evaluate.cpp:不只是调用一次网络
当前 Eval::evaluate() 先调用 NNUE,再执行缩放处理。它要求局面未被将军;受将节点由搜索的相应流程处理,不能把普通静态分当成应将结果。
simple_eval() 是一份粗略的子力差,用来与 NNUE 的判断比较,不是网络加载失败时的备用评估器。实际处理主要有四件事:
- 比较子力差与 NNUE 分数是否同向:例如少子却被网络看好的局面,就与单纯多子占优不同。
- 结合搜索传入的
optimism调整结果;它来自根候选评分,不是随机加分,也不是用户的 Elo 等级。 - 根据盘面总子力缩放分数,并随自然限招计数增加,将分数向零收缩。
- 把普通评估限制在将杀分数范围之外,避免把“优势很大”误写成“已经算出杀棋”。
因此,原始 NNUE 分数与引擎实际用于搜索的静态评估可以不同。限招缩放也只是调整估分;是否真正触发和棋或其他规则,仍由规则代码判断。
源码:子力比较与缩放、optimism 的来源。
NNUE 看哪些信息?
这份实现同时使用两类输入:
| 特征 | 可以怎样理解 |
|---|---|
HalfKAv2_hm | 记录“什么棋子在哪个格点”,并结合将帅位置、左右镜像和己方车马炮配置选择特征分组。 |
FullThreats | 在当前盘面上提取经过筛选的棋子间攻击/保护关系,补充“棋子之间怎样互相影响”。 |
前者不是固定的棋子位置分表:位置被编码为网络输入,再由权重共同计算。后者虽然叫 FullThreats,也不等于已经识别所有战术威胁。代码从当前攻击范围与占用格点生成关系,过滤部分组合和重复关系;它没有替双方走完一串变化,也不等于棋规里的完整“捉子”判定。弃子能否成立、进攻是否将杀,仍需搜索。
红黑双方各保留自己的观察视角,送往后层时按“当前行棋方在前、对方在后”排列。网络最终给出一个从当前行棋方看的局面分数。
源码:位置特征与分组、关系特征的筛选与提取、双视角转换。
当前网络结构:共用前段,按子力选择后层
每份 Network 包含一套特征转换器和 16 组后层参数。常规求值根据双方车的数量、马炮合计数量等配置,选择其中一组计算;不是每个局面都运行 16 个完整网络再投票。
特征转换后的后层输入为 1024 维,两级中间仿射层各输出 32 个值,最后形成单一评分。不过,只写成普通的“1024→32→32→1”会漏掉结构:代码还把截断与平方激活的结果拼接,并保留较早层通向输出的连接。
这些中间数值是网络计算的通道,不是“第几步变化”或人工指定的“车分、将帅安全分”。本版本普通求值返回一个分数,不能套用其他版本的双网络、或两个独立评估输出的说明。
增量更新:只补发生变化的部分
相邻局面通常只差一步。如果每一步都把全部输入重新累加,成本会很高。NNUE 的累加器保存前段计算结果:走子时减去旧位置的贡献、加上新位置的贡献;吃子再移除被吃子的贡献,变化的攻击关系也同步更新。
增量复用的是前段结果,不是跳过整个网络。 后层仍要计算。搜索栈记录变化,真正需要评估时才寻找可复用的累加器并补算;撤销走法也会退回对应状态。这种做法与置换表保存搜索结果是两回事。
也不是任何走法都能直接沿用上一份累加器。例如己方将帅移动、吃子使特征子力分组改变、或镜像选择发生变化时,相应视角会要求刷新。刷新仍可借助按将帅位置等组织的缓存,比较棋子差异来更新位置特征,再重新加入当前的关系特征,而非每次都从零做全部工作。
源码:累加器栈与按需更新、特征差分更新、刷新触发条件、刷新缓存。
量化与 SIMD:让每次评估足够便宜
当前推理主要使用整数计算:特征权重用 8 位整数,累加器用 16 位整数,后层乘加结果使用 32 位整数,并配合截断、移位和缩放。这就是这里所说的量化:用规定的整数尺度表示网络数值。
代码还为不同 CPU 提供 SIMD 路径,用一条指令同时处理多个数,例如 x86 的 SSE/AVX 系列与 ARM 的 NEON。部分层会利用输入中大量零值减少工作。它们加速的是同一套评估计算,不是让某个版本额外“懂一种棋理”;实际速度取决于 CPU、编译路径与整个搜索负担。
网络文件:名字一样,不代表兼容
EvalFile 指定网络文件;当前默认名是 pikafish.nnue。加载器会尝试相应路径,读取格式版本、网络结构标识和各层参数,并检查读取是否完整。验证发现要求的网络没有成功加载时,会提示兼容性与路径问题并终止,不会静默改用简单子力评分继续下棋。
所以,更换网络要与引擎架构匹配;仅把文件改成同一个名字,不能把旧架构变成新架构。这里的结构标识用于格式兼容性检查,也不等于对任意文件作了密码学意义上的真伪鉴定。
源码:默认文件名、查找与加载、格式和参数检查、加载验证与错误处理。
看懂 eval 调试输出
引擎的 eval 命令用于查看当前局面的评估细节,不会替你完成一次正常选招搜索。当前输出既有以行棋方为视角的 NNUE 内部单位,也有转换为红方视角的显示分;沿用的 white side 标签在象棋实现里对应红方。
这份调试输出的最终静态分使用零 optimism,也没有还原某个搜索节点的评估修正历史。因此,它不必等于 go 搜索后的分数。被将军时,调试入口会明确表示没有普通的最终静态评估。
