☰
无反向传播训练揭秘:黑白棋三臂对照实验对比Forward-Forward与GRPO
2026/10/20 7:09:02 网站建设 项目流程

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 分支没有用反向传播,而是用“权重扰动 + 组内优势加权”来更新策略网络。具体流程是:

  1. 从当前策略中采样 N 个动作,记录动作概率p。
  2. 对当前策略参数加一个小的随机噪声,形成候选策略。
  3. 用候选策略下采样动作去走完整局,得到每组回报。
  4. 在组内用(R - mean(R)) / std(R)做归一化,得到优势值。
  5. 按优势值正负来调整“原策略里该动作的偏好方向”:优势为正就增加对应动作概率,优势为负就减少。

严格来说,这是 REINFORCE 加组内基线的变体,但在无反向传播的框架里,它表现出了 GRPO 最重要的性质:只关心组内相对好坏,不关心绝对奖励。黑白棋没有设置中间奖励,只有 1(赢)、0(负)、0.5(平),组内归一化后优势项会落在 -0.5 到 0.5 的区间,训练稳定性比直接用原始奖励高不少。

这一步最关键的调参是采样组数 N。我用的是 N=8,每局从当前局面采样 8 个候选动作,分别走完 8 局。组数太少,优势估计噪声大;组数太多,训练一个 step 的时间成倍上涨。黑白棋一局平均 50 手,8 组对局意味着每更新一次要模拟 400 多手棋,成本不算低,但比反向传播省心的是几乎没有梯度爆炸问题。

2.4 只学赢棋分支的样本筛选陷阱

第三个分支听上去最简单:收集赢棋方的落子,让模型模仿。但实际操作有一个大坑:赢棋方的每一步并不都是“正确动作”。黑白棋里经常出现前半盘落后、后半盘靠对手失误翻盘的局面,赢棋方的某些落子可能是臭棋,只是对手没有抓住机会。

如果无脑把所有赢棋样本都当正样本,模型会学到大量短视动作。我做了两个筛选项:第一个是只看最终胜负差距超过 10 子的棋局,把险胜局过滤掉;第二个是只保留赢棋方在“自己合法动作数较多”的局面下的落子,因为动作选择余地大而给出好棋,更可能是策略价值的体现。

数据来源上,我跑了自博弈生成样本,前几百局用随机对手产生多样性棋谱,后面用三个训练分支的当前模型轮流做对手,持续产出赢棋样本。这个循环会引入“赢棋样本越来越自洽”的问题,但因为后面有胜率测试兜底,影响不大。

在无反向传播更新下,“只学赢棋”分支的目标是增大模型对赢棋落子的输出概率。我不计算交叉熵梯度,而是用权重扰动:往网络权重上加一个随机向量,前向计算一次,如果目标动作概率增大,则保留这次扰动;否则按一定比例回退。这个办法很原始,但在样本信号强时意外地稳定。

2.5 三种分支共用的无反向传播参数更新

为了让三臂对照公平,我没有为一臂单独写一个自定义 BP 训练器,而是让三臂共用同一种“前向扰动更新”机制。具体做法如下:

复制当前网络参数,给每个权重加上服从正态分布的小噪声(标准差为 sigma),用带噪声的网络做一次前向计算,得到目标指标。如果目标指标比更新前更好,就正式接受这个噪声作为新参数;如果更差,则以一定概率接受(模拟随机搜索),否则退回原参数。

这个机制有一个显著好处:不需要任何梯度求导,对网络内部结构完全无要求。坏处是参数维度高的时候搜索效率会很低。我的黑白棋策略网络规模较小(192 输入、128+128+64 输出),总参数大约 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(agent=current_arm, opponent=base_opponent, num_games=64) for arm in ["ff", "grpo", "win"]: data = extract_signals(arm, games, candidates=8) for batch in data: target_metric = compute_metric(arm, batch) candidate_noise = gaussian_noise(scale=sigma) candidate_net = apply_noise(current_net, candidate_noise) candidate_metric = compute_metric(arm, batch, net=candidate_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 和蒙特卡洛树搜索结合,让组内路径不是随机采完,而是通过短搜索提升路径质量,这样优势估计会更准。第三个方向是把“只学赢棋”的样本过滤逻辑升级为自动权重:不是简单删掉低质量赢棋样本,而是根据该动作在模拟中实际带来的胜率变化给样本加权。

我最后想分享一个实验之外的感受。做无反向传播训练,最容易被低估的其实是调试成本。三臂共享一套权重扰动机制后,定位问题的速度大幅提高,因为所有分支的“学习信号”差异一目了然。反向传播不够用时,试着把学习信号和参数更新拆开看,反而能带来不少启发。

如果你也想跑这个实验,我的建议是先别追求效果,先把胜负引擎和合法动作掩码做到万无一失,再往里加训练分支。规则引擎错一个小细节,三种训练臂都会学到同一个错误,而且因为无反向传播缺乏梯度信号,这个错误会隐藏得非常深。祝你能从这个对照里找到比反向传播更有意思的答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询