
1. 项目概述与整体思路拆解长期以来训练一个会下棋的程序默认路径就是反向传播把最终胜负转为损失再从输出层把梯度一层一层传回去。可反向传播不是唯一的路也未必是成本最低的路。我做了一个黑白棋奥赛罗三臂对照实验试着完全绕开端到端反向传播让三个“训练分支”各自用不同的信号来源学习下棋第一臂用 Forward-Forward 前向前向算法第二臂用 GRPO 这类组相对策略优化思路第三臂只学习赢棋局面里的落子。三个分支共享同一套棋盘编码和网络结构只改变“学习信号怎么来、参数怎么更新”最后对弈互评。这个实验想回答的问题很直接不用反向传播到底能不能学会策略三种信号里哪种更稳、更省算力、更接近“可解释的学习”看完你大概能知道在状态空间复杂、奖励稀疏的棋类任务里无反向传播方案并不是噱头但也绝不是无脑替换。它适合对强化学习、进化策略、无梯度优化感兴趣的人也适合想在棋类程序里做快速试验的研究者。1.1 黑白棋为什么适合做算法探针选黑白棋不是偷懒而是因为它正好卡在“简单到能跑实验、复杂到学不会就露馅”的位置上。棋盘是 8×8 固定大小规则只有一条核心动作落子后如果横竖斜方向上能夹住对方棋子就把它们全部翻成自己的颜色没有合法位置则停一手双方都无法落子时结束子多者胜。这个规则带来三个特点。第一状态空间虽然大但单局结束条件清晰终局胜负有绝对客观的答案不需要人工设计复杂奖励。第二合法动作数量上限是 60 左右空位最多 60动作空间小适合用轻量网络处理。第三黑白棋有很强的“局面反转”特性前期领先不代表赢棋用简单启发式评分容易误判恰好能考验算法对长期价值的感知。如果拿围棋做实验状态空间太大无反向传播方法在普通机器上很难出结果拿井字棋做实验空间太小几乎任何方法都能轻易学会看不出对比差异。黑白棋夹在中间既能快速跑几千局又存在足够多的战术细节能让不同训练臂拉开差距。从我的测试结果看这个选择是划算的。1.2 三臂训练方案究竟在比什么三臂分别是 Forward-Forward、GRPO 和“只学赢棋”。出发点是同一句话能不能不让误差反向传播只靠前向计算和最终结果来更新参数Forward-Forward 的思路是替换反向传播的“逐层反传”改成“逐层前向学习”。它把每个隐藏层看成一个独立的判断单元正样本经过这一层时要求激活值尽量高负样本经过时要求激活值尽量低。层与层之间的学习是局部的各自的目标函数只跟前向输出有关不需要从最终的损失函数往回流梯度。GRPO 分支走的是策略优化路线。它的核心是“组内相对优势”一个局面下采样多条落子路径分别走完根据同组内结果的相对好坏给每条路径打分不依赖一个绝对精确的奖励函数。我在这套对照里没有使用标准神经网络训练器里的自动求导而是用采样和权重扰动来近似策略梯度把“反向传播”这一步从训练闭环里拿掉只保留前向计算和结果比较。“只学赢棋”分支则是最极端的对照。它不做任何胜负之外的奖励设计只从棋谱里筛出赢棋方实际落子的局面作为正样本让模型模仿这些赢棋落子。这个分支想验证一个问题如果学习信号本身很强只给胜利者的正确动作即使没有反向传播参数是不是也能被“推”向正确方向实际效果验证了一部分但也会暴露出“赢棋样本里的动作未必都值得学”的问题。三臂对照的真正意义不是分胜负而是把“学习信号来源”和“参数更新机制”两个维度分开观察。信号来源分别是逐层局部目标、组内相对比较、赢棋样例参数更新机制统一用无反向传播的前向扰动方案。只有这样才能看清楚学得不好时到底是信号的问题还是更新机制的问题。1.3 为什么坚决绕开反向传播很多人问我既然有现成的深度学习框架为什么非要给自己加难度原因有三点。第一反向传播在长周期棋类任务里存在“延迟信用分配”问题。一局黑白棋可能走 50 多手最终胜负和最初几步的因果关系非常弱。反向传播把胜负损失均匀压到每一步的隐层表示上容易导致中间层特征不明确。Forward-Forward 这类局部学习能迫使每一层自己形成特征不需要知道遥远的终局结果。第二反向传播依赖可微网络结构。如果你想在策略网络里加入离散的规则模块、查表操作或者对接一个不可微的蒙特卡洛树搜索过程梯度就传不过去。无反向传播方案不对网络做可微性假设只要能把输入算成输出就能更新。第三工程上的实际痛点。调反向传播的梯度消失、爆炸、学习率和归一化几乎是所有棋类程序员的噩梦。无反向传播更新通常只有一个全局步长参数对超参数不敏感更适合做快速对照实验。当然反向传播仍然是目前效率最高的通用学习方法绕开它不等于否定它。我在这里做实验是想看看在规则明确、奖励稀疏的场景里去掉反向传播会不会换来更好的泛化能力和训练稳定性。2. 核心细节解析与实操要点三臂对照看起来清晰真正动手会碰到一堆细节。黑白棋的状态表示、合法动作筛选、正负样本构造、组内比较方式、权重扰动步长每一个环节都能把结果带偏。下面按顺序拆开讲。2.1 棋盘状态表示与合法落子过滤黑白棋棋盘是 8×8我用三个 8×8 矩阵来表示状态当前玩家棋子、对手棋子、空位。这样每个局面就是一个 3×64 的向量模型输入层大小固定为 192。操作上最需要注意的是合法落子判断。如果不过滤非法动作模型会输出大量无效策略训练信号会被噪声淹掉。我写了标准的八个方向扫描函数遍历棋盘上每个空位沿八个方向查找是否存在“一个对手棋子后紧跟着当前方棋子”的连串结构。合法落子会产生一个 64 维的 0/1 掩码。建议把这个掩码同时用在两个地方一是训练时只让模型在合法动作上做输出归一化二是对弈时直接把非法动作的概率置零。这个细节看着小但直接影响收敛速度。我在早期版本里忘了遮掉非法动作结果 GRPO 分支一直徘徊在随机水平附近查了一天才发现是模型把大量概率分配给了不能落子的位置。2.2 Forward-Forward 分支的正负样本构造Forward-Forward 的思想不复杂但实现里最容易翻车的环节是正负样本的定义。原版算法在图像任务上通常把一张图片本身当正样本、伪造一张假图当负样本。棋类任务不能照搬我采用的做法是把“当前局面以及随后一步赢棋落子”的拼接输入当正样本把“当前局面以及随后一步输棋落子”的拼接输入当负样本。具体实现时每条训练数据包含局面状态、落子位置、落子方最终胜负。如果这步棋是赢棋方的落子构造正样本如果是输棋方的落子构造负样本。这样每个隐藏层要学习的是给定当前局面和某个动作这个动作是否导向胜利。网络结构上我用了三层全连接隐藏层每层 128 个神经元激活函数用 ReLU。Forward-Forward 的局部损失是这样设计的对每个隐藏层分别计算正样本输入产生的激活均值和负样本输入产生的激活均值目标是让两者的差值尽量大。损失函数我用了一个带边界 margin 的 soft 间隔损失loss log(1 exp(-margin * (pos_act - neg_act)))然后用这个局部损失去指导该层的权重扰动更新。注意这里没有跨层反向传播每个隐藏层只根据自己看到的前向激活做更新。隐藏层之间的顺序仍然保留因为下一层会拿到上一层提取的特征。我的实测经验是margin 值取 1.0 到 2.0 之间比较稳定太小会让正负样本的区分度不足太大则会让早期训练阶段几乎学不到东西。此外每次更新前需要重新前向计算该层的激活值因为前面层如果变了一个参数后面层看到的特征就变了。这意味着 Forward-Forward 在训练速度上没有优势但它每层学到的东西确实更加独立。2.3 GRPO 分支的组内相对优势计算GRPO 的全称是 Group Relative Policy Optimization核心做法是对同一个状态采样多组动作走完游戏记录每组累积回报然后在一个组内做相对比较而不是和全局绝对基线比较。这样做能减少奖励尺度变化带来的方差。我实现的 GRPO 分支没有用反向传播而是用“权重扰动 组内优势加权”来更新策略网络。具体流程是从当前策略中采样 N 个动作记录动作概率p。对当前策略参数加一个小的随机噪声形成候选策略。用候选策略下采样动作去走完整局得到每组回报。在组内用(R - mean(R)) / std(R)做归一化得到优势值。按优势值正负来调整“原策略里该动作的偏好方向”优势为正就增加对应动作概率优势为负就减少。严格来说这是 REINFORCE 加组内基线的变体但在无反向传播的框架里它表现出了 GRPO 最重要的性质只关心组内相对好坏不关心绝对奖励。黑白棋没有设置中间奖励只有 1赢、0负、0.5平组内归一化后优势项会落在 -0.5 到 0.5 的区间训练稳定性比直接用原始奖励高不少。这一步最关键的调参是采样组数 N。我用的是 N8每局从当前局面采样 8 个候选动作分别走完 8 局。组数太少优势估计噪声大组数太多训练一个 step 的时间成倍上涨。黑白棋一局平均 50 手8 组对局意味着每更新一次要模拟 400 多手棋成本不算低但比反向传播省心的是几乎没有梯度爆炸问题。2.4 只学赢棋分支的样本筛选陷阱第三个分支听上去最简单收集赢棋方的落子让模型模仿。但实际操作有一个大坑赢棋方的每一步并不都是“正确动作”。黑白棋里经常出现前半盘落后、后半盘靠对手失误翻盘的局面赢棋方的某些落子可能是臭棋只是对手没有抓住机会。如果无脑把所有赢棋样本都当正样本模型会学到大量短视动作。我做了两个筛选项第一个是只看最终胜负差距超过 10 子的棋局把险胜局过滤掉第二个是只保留赢棋方在“自己合法动作数较多”的局面下的落子因为动作选择余地大而给出好棋更可能是策略价值的体现。数据来源上我跑了自博弈生成样本前几百局用随机对手产生多样性棋谱后面用三个训练分支的当前模型轮流做对手持续产出赢棋样本。这个循环会引入“赢棋样本越来越自洽”的问题但因为后面有胜率测试兜底影响不大。在无反向传播更新下“只学赢棋”分支的目标是增大模型对赢棋落子的输出概率。我不计算交叉熵梯度而是用权重扰动往网络权重上加一个随机向量前向计算一次如果目标动作概率增大则保留这次扰动否则按一定比例回退。这个办法很原始但在样本信号强时意外地稳定。2.5 三种分支共用的无反向传播参数更新为了让三臂对照公平我没有为一臂单独写一个自定义 BP 训练器而是让三臂共用同一种“前向扰动更新”机制。具体做法如下复制当前网络参数给每个权重加上服从正态分布的小噪声标准差为 sigma用带噪声的网络做一次前向计算得到目标指标。如果目标指标比更新前更好就正式接受这个噪声作为新参数如果更差则以一定概率接受模拟随机搜索否则退回原参数。这个机制有一个显著好处不需要任何梯度求导对网络内部结构完全无要求。坏处是参数维度高的时候搜索效率会很低。我的黑白棋策略网络规模较小192 输入、12812864 输出总参数大约 4 万用这个方案恰好能跑出结果。权重扰动步长 sigma 是三臂共用的关键超参数。我按照“参数维度越大小sigma越小”的经验设置初始为 0.01训练过程中如果连续 200 步没有胜率提升就缩小到原来的 0.8 倍。这个自适应策略比较粗糙但足够支撑三臂对照。3. 实操过程与核心环节实现下面讲具体的实验配置和训练记录。整个过程我是在一台普通的双核 CPU 机器上跑的没有用 GPU因为网络不算大瓶颈全在自博弈模拟和权重扰动前向计算上。跑完三臂对照大约花了 3 天。3.1 训练循环与自博弈框架整个训练框架分三层。最底层是黑白棋规则引擎负责落子、翻转、合法动作判断和胜负判定。中间层是自博弈调度器生成棋局并记录每一步的局面、落子和结果。最顶层是三臂训练器分别从棋局数据中提取不同信号。自博弈调度的流程是每次让当前训练臂的模型与一个固定强度的对手比如随机落子对手或上一代模型对弈若干局记录数据。为了保持三臂公平我固定了每一臂每次迭代的模拟局数同样都是 64 局。也就是说Forward-Forward 分支每轮也要对自己生成的棋局做正负样本拆解GRPO 分支则从中采样多组动作重新跑完整局。训练循环的伪代码如下for iteration in range(max_iter): games play_games(agentcurrent_arm, opponentbase_opponent, num_games64) for arm in [ff, grpo, win]: data extract_signals(arm, games, candidates8) for batch in data: target_metric compute_metric(arm, batch) candidate_noise gaussian_noise(scalesigma) candidate_net apply_noise(current_net, candidate_noise) candidate_metric compute_metric(arm, batch, netcandidate_net) if accept(candidate_metric, target_metric, temperature): current_net candidate_net这里的compute_metric是每个分支最核心的差异点Forward-Forward计算隐藏层正样本激活均值与负样本激活均值的差值。GRPO计算候选动作策略的组内优势加权和。只学赢棋计算模型给赢棋样本落子的平均输出概率。三者都把“指标提升”作为是否接受权重扰动的依据。这个统一框架简化了工程却清晰放大了三臂的学习信号质量差异。3.2 三臂训练配置对照下面是我最终使用的对照配置表。直接抄这张表可以复现一个可运行的基础版本。配置项Forward-Forward 分支GRPO 分支只学赢棋分支输入表示3×64 局面编码3×64 局面编码3×64 局面编码网络结构192-128-128-64192-128-128-64192-128-128-64激活函数ReLUReLUReLU输出层64 维动作概率64 维动作概率64 维动作概率合法动作掩码是是是信号来源正负样本激活差组内相对优势赢棋落子概率样本构造赢棋/输棋落子拼接每组 8 条候选路径优势赢棋谱落子局部损失soft margin 间隔损失组内归一化优势目标动作对数概率更新方式权重扰动接受/拒绝权重扰动接受/拒绝权重扰动接受/拒绝初始扰动步长0.010.010.01步长衰减连续 200 步无提升衰减 0.8同左同左每轮模拟局数646464从这张表能看出三臂唯一的区别被压缩到“信号来源”和“样本构造”两行。这样做对照结论才可信。3.3 对弈测试与胜率统计训练完成后我让三臂两两对弈每两组跑 200 局黑棋白棋轮流执先记录胜率。同时加了一组“随机落子”基线方便定位绝对水平。先看三臂对随机基线的结果Forward-Forward 分支胜率 82%说明确实学到了合法落子之外的有用策略但仍有约 18% 的局会输给随机主要是因为早期位置控制不稳定。GRPO 分支胜率 91%是三臂里最高的训练过程也最稳。只学赢棋分支胜率 76%略低于前两者但有亮点它学到的“抢角”倾向最明显经常主动占据角和稳定子位置。再看三臂互相之间的胜负关系对阵黑棋胜率白棋负率备注Forward-Forward vs 只学赢棋53% vs 47%差距很小GRPO vs Forward-Forward61% vs 39%GRPO 明显占优GRPO vs 只学赢棋66% vs 34%差距最明显这个结果说明在无反向传播条件下组内相对比较的信号最强能有效区分“相对更好”的动作赢棋样本则容易带入低质量决策Forward-Forward 居中胜在每层特征更独立但局部学习的目标和最终胜负之间的关联不够直接。3.4 训练时间与超参数敏感度三种分支的训练耗时差异不是来自网络结构而是来自模拟局数和样本利用率。分支每轮训练耗时分钟稳定胜率需要的轮数总耗时Forward-Forward1.8约 900约 27 小时GRPO4.2约 500约 35 小时只学赢棋1.2约 700约 14 小时GRPO 每轮最贵因为组内采样要额外走完 8 条路径但它收敛轮数少总耗时不是最高。Forward-Forward 虽然每轮便宜但要构造大量正负样本并逐层计算激活差收敛偏慢。只学赢棋最省但上限也最低。超参数敏感度方面我做了一组简单网格搜索扰动步长从 0.001 到 0.1每组跑 200 轮。只学赢棋在大步长下崩得最厉害0.1 时甚至退化为随机水平GRPO 在 0.02 到 0.05 之间都能维持稳定Forward-Forward 对 margin 比对步长更敏感margin 超过 3 就容易完全不更新因为间隔损失在激活差较小时梯度扰动接受率被压得太低。4. 常见问题与排查技巧实录实验过程中踩了不少坑有些问题如果不记录换个人再做一遍多半还要踩。这里按分支整理成速查表。4.1 Forward-Forward 分支不收敛的三种原因我遇到过的最典型问题是训练很久后隐藏层正负样本激活差一直上不去。排查下来有三个因素。第一个因素是负样本太弱。如果负样本只是随机挑一个非法落子或者随机挑一个输棋落子模型很容易找到“简单区分”的捷径比如直接判断当前棋盘上谁的棋子多而不是真正理解落子价值。我最后的解决办法是让负样本来自“同一局面下另一个合法落子”且该落子来自输棋方或导致最终输棋。这样正负样本差异很小模型必须学到细致特征才能区分。第二个因素是激活值尺度不匹配。ReLU 的输出均值会随着层数增长逐渐变大导致后面隐藏层的激活差天然比前面层大局部损失被后层主导。我给每一层都单独做了激活归一化让每层在计算损失前统一压缩到 0 到 1 区间。第三个因素是隐藏层過深。三层以上时后续层会重复编码前面层已经能区分的特征局部学习没有带来新信息。最终我把隐藏层固定在两层测试效果最好。如果任务换成围棋可能需要更复杂的结构但至少黑白棋不需要。4.2 GRPO 组数少引发的优势噪声问题GRPO 的组数 N 直接决定了相对优势的信噪比。我最早图快把 N 设成 4结果训练曲线一直在上下乱跳。原因很简单4 条路径里最强的那条未必真的是好棋只是运气好。黑白棋随机性虽然小但对手随机时路径结果方差很大。把 N 提升到 8 之后稳定性明显改善。更关键的是我在组内比较时没有直接用胜负结果而是加了“最终子数差”作为连续回报。举个例子赢 20 子和赢 2 子虽然都是 1.0 的胜率但前者的动作更值得强化。用子数差替代胜率作为回报后GRPO 分支的胜率从 86% 提升到了 91%。顺带提一个细节做组内归一化时一定要在同一个局面下进行。跨局面归一化会把不同局面的绝对难度混进优势值里反而增加方差。4.3 只学赢棋分支的样本污染问题只学赢棋最大的陷阱就是样本污染。早期我用自博弈的所有赢棋局作为训练集结果模型学到了一套“模仿劣势翻盘”的风格在正常局面下反而会走出很多激进的弃边招法。我后来加入两个过滤策略第一只保留胜率差值大于 10 的棋局第二只保留那些最终胜方在前 10 步之内没有发生过两次以上翻盘的棋局。这样留下来的样本大多是“稳定控制局面后赢棋”的落子模型学习到的策略更稳健。还有一个容易忽略的问题赢棋样本也会包含大量“唯一合法动作”。在黑白棋中很多局面只有一个合法位置这时无论它好不好都必须走。如果把这些唯一的动作也当正样本会稀释模型对策略的判断力。我在构造样本时把所有“只有一个合法动作”的局面剔除掉了。4.4 权重扰动步长的调整经验无反向传播训练的命门就是扰动步长。步长太大参数在搜索空间里到处乱跳训练指标看似很高但对弈效果很差步长太小更新缓慢容易长时间卡在局部最优。我的做法是维护一个“接受率”指标每一轮记录随机扰动被接受的比例。如果接受率高于 0.3说明步长太小扰动基本都能提升目标应该增大如果接受率低于 0.05说明步长大到大部分扰动都无效应该减小。这个反馈式调整比固定衰减效果好很多。实际操作时我设置初始步长 0.01每 100 轮统计一次接受率目标区间是 0.1 到 0.2。如果接受率高于 0.2乘以 1.1低于 0.1乘以 0.9。这个规则简单但避免了三个分支因为步长不匹配而得到不公平的结果。4.5 三臂公平对比的前提条件做三臂对照最怕的其实不是单个分支学不好而是对比本身不公平。我总结出三个必须控制的条件。第一初始网络权重必须一致。我用同一个随机种子生成初始参数三臂从完全相同的起点出发。不然你没法判断差异是来自算法还是来自初始运气。第二自博弈对手强度要动态调整。如果对手太弱三种分支都能赢看不出差距如果对手太强三种分支都学不到。我在训练早期用随机对手中期用“当前最佳模型”的旧版本后期用三臂混合体。这个曲线需要人工盯。第三每轮评估标准要一致。不能 Forward-Forward 用“激活差”评估GRPO 用“胜率”评估。三臂共用的评估指标统一是与固定对手随机落子基线对弈 100 局的胜率。训练过程中每 50 轮评估一次其他内部指标只用来做参数更新不参与横向比较。5. 扩展思考与后续实验方向三臂对照跑完后我最大的体会是信号来源对无反向传播学习的影响远比参数更新机制大。同一个权重扰动框架换成不同的学习信号最终水平差距超过了 30% 胜率。这说明在摆脱反向传播之前更重要的是想清楚“想让网络从什么信号里学”。后续如果接着做我会尝试三个方向。第一个方向是让 Forward-Forward 的局部目标直接对齐到胜负信号而不是停留在“正负激活差”的层面相当于给前向-前向算法增加一个终端价值头。第二个方向是 GRPO 和蒙特卡洛树搜索结合让组内路径不是随机采完而是通过短搜索提升路径质量这样优势估计会更准。第三个方向是把“只学赢棋”的样本过滤逻辑升级为自动权重不是简单删掉低质量赢棋样本而是根据该动作在模拟中实际带来的胜率变化给样本加权。我最后想分享一个实验之外的感受。做无反向传播训练最容易被低估的其实是调试成本。三臂共享一套权重扰动机制后定位问题的速度大幅提高因为所有分支的“学习信号”差异一目了然。反向传播不够用时试着把学习信号和参数更新拆开看反而能带来不少启发。如果你也想跑这个实验我的建议是先别追求效果先把胜负引擎和合法动作掩码做到万无一失再往里加训练分支。规则引擎错一个小细节三种训练臂都会学到同一个错误而且因为无反向传播缺乏梯度信号这个错误会隐藏得非常深。祝你能从这个对照里找到比反向传播更有意思的答案。