说句得罪人的话:很多强化学习项目不是死在算法上,而是死在奖励函数上。训练前把网络结构、学习率、环境封装折腾个遍,最后发现机器人学会的是“原地打转刷步数”,或者干脆躺平不动——根源十有八九是奖励设计出了问题。这个清扫机器人项目我前后迭代了快两个月,把奖励项从最初拍脑袋的4个一路改到结构化的10项复合奖励,同一个PPO实现、同一张仿真地图,训练得分从基线100分一路拉到778分,涨了近七倍。这篇文章不聊理论推导,重点记录我是怎么拆奖励、配权重、填坑的,给同样在做强化学习奖励工程的人一份能直接用的实操脚本。
1. 为什么奖励工程是“隐形天花板”
1.1 你先得承认一个事实:算法是在替你“翻译”奖励
很多人把强化学习当成“黑盒炼丹”:丢进去就行。但本质不是这样。强化学习优化的是期望累积奖励,策略网络学到的每一个行为,都是在你定义的奖励信号下做梯度上升的结果。你给了什么样的奖励,机器人就会变成什么样的“性格”。
拿清扫场景举例。如果只给“清扫覆盖率”这一条奖励,机器人学到的策略很可能是:快速溜达一遍,把覆盖率刷到80%就停,因为再扫边际收益太低。更恶劣的情况是,它发现原地抖动能让传感器误判“已清扫”,奖励照样增长——这是机器学习利用漏洞的经典场景,不针对机器人,任何智能体都一样。
所以在设计奖励函数的时候,我给自己定了一条原则:不要把“目标”直接写成奖励,要把“行为过程”拆成可分级的信号。目标太稀疏,学不动;目标太单一,学歪。
1.2 清扫任务到底难在哪:多目标,天然冲突
清扫任务是一个典型的多目标优化问题,目标之间还互相打架:
- 想覆盖率高,就得跑得久、跑得全,这跟“能耗低”冲突;
- 想路径优雅,就得少转弯,但少转弯就可能导致漏扫边角;
- 想不碰障碍,有时候狭窄区域就必须贴身擦过去;
- 想快速完成,就可能牺牲覆盖细节,比如家具底下的尘絮扫不到。
![清扫任务的奖励冲突示意]
(想象一条奖励函数,左端是覆盖率,右端是能耗成本,中间全是拉扯。)
这种冲突不是坏事,反而是奖励工程能发挥价值的地方。问题只在于:你要怎么把这些冲突目标加权成一个标量?权重就是态度。
我最终用的方案是复合奖励,总共10项,分成了四个梯队:核心任务奖励、行为引导奖励、约束惩罚、里程碑加成。下面这一章是我觉得全文最值得反复看的部分。
2. 第一版奖励:那个只配拿100分的机器人
2.1 最初的4项奖励是怎么翻车的
先交代一下环境。我的仿真环境是基于PyBullet搭的轮式清扫机器人,64×64栅格地图,房间里有6个小障碍物和一片需要清理的“灰尘区域”,灰尘用粒子分布模拟,机器人底盘带一个接触式清扫检测器。
第一版奖励函数非常简单,就4项:
reward = ( coverage_progress * 1.0 # 覆盖率增量 + 10.0 if task_complete else 0 # 任务完成 - 0.01 * step # 步数惩罚 - 0.5 if collision else 0 # 碰撞惩罚 )结果很惨。训练50万步,累计奖励稳定在100分左右不再上升。但真正的问题不是分数低,而是策略表现出三种典型“畸形”:
- 绕圈自嗨:机器人学会了在地图中央绕半径很小的圈,因为每次绕圈都能压过一小片未清扫区域,覆盖率增量持续为正,它根本不需要去扫角落;
- 怕碰撞怕到不敢动:碰撞惩罚虽然每步只有-0.5,但累积起来吓人,策略逐渐变得保守:能不动就不动,偶尔伸出清扫刷试探一下;
- 边界龟缩:地图边缘和障碍物缝隙的灰尘密度高,但机器人死活不过去,宁可在开阔地带重复扫。
这就是典型的奖励“短视”:策略在每步都要最大化即时奖励,而覆盖率增量这种“局部正反馈”很容易被利用。
2.2 100分这个基线的真实含义
我后来复盘才发现,100分这数字误导了我好一阵子。“有100分说明能扫一点,是不是策略还行?”不是。我把每个奖励项单独打日志后看到,这个100分里,约70分来自“绕圈压过灰尘”的覆盖率增量,25分来自完成任务的墓碑奖励,剩下5分扣掉碰撞罚项后等于零头。换句话说,第一个模型根本没学会扫干净,它只是学会了一种刷分玩法。
所以如果你也在做类似的RL项目,我强烈建议:不要只盯累计奖励报表,一定要把每个奖励分量的均值、方差单独拿出来画曲线,否则你看不到“谁在真正促进目标,谁在钻空子”。
3. 10项复合奖励的完整拆解:每一分都有来处
3.1 奖励拆分的四条设计主线
在从头设计奖励之前,我把清扫任务拆成了四个问题组:
- 扫得全吗(覆盖率、区域完成度、边界覆盖率)
- 扫得顺吗(碰撞、转弯、平滑度)
- 扫得值吗(时间成本、能耗成本)
- 扫得稳吗(防止卡死、掉头、局部死循环)
每一组对应2-3项奖励,合起来就是10项。下表是最终设计版,后面我会逐项展开。
| 序号 | 奖励项 | 类型 | 数学形式 | 权重 |
|---|---|---|---|---|
| 1 | 清扫面积增量 | 引导奖励 | Δcoverage * 0.8 | 0.8 |
| 2 | 完成任务奖励 | 稀疏里程碑 | +12 | 12 |
| 3 | 边界清扫奖励 | 引导奖励 | Δborder_coverage * 1.2 | 1.2 |
| 4 | 重复清扫惩罚 | 约束惩罚 | -0.02 * repeated_area | 0.02 |
| 5 | 碰撞惩罚 | 硬约束 | -1.0 | 1.0 |
| 6 | 转弯惩罚 | 行为惩罚 | -0.15 * angle_velocity | 0.15 |
| 7 | 路径平滑奖励 | 行为引导 | +0.005 / (1 + jerk) | 0.005 |
| 8 | 原地卡死惩罚 | 硬约束 | -2.0 | 2.0 |
| 9 | 时间效率惩罚 | 约束惩罚 | -0.005 * step | 0.005 |
| 10 | 能耗成本惩罚 | 软约束 | -0.05 * distance | 0.05 |
3.2 核心任务组:覆盖率这件事值得拆细
第1项:清扫面积增量(Δcoverage * 0.8)
这是主心骨。每走一步,如果灰尘检测覆盖率比上一步提高了,就给正奖励;没提高,这项的奖励为0。
current_coverage = swept_area / total_dust_area delta = current_coverage - previous_coverage r_coverage = delta * 0.8为什么用增量而不是总量?因为如果用“当前覆盖率 * 0.8”,那策略会得到一个巨大的常数值——覆盖率40%时每步都能吃到同样的奖励,跟动作无关,奖励等于白给。增量奖励的本质是“行为→结果”的因果信号:只有你这一步真正扫到新区域,才给钱。这符合上一节说的“过程信号”原则。
第2项:完成任务奖励(+12)
覆盖率到达95%以上判为任务完成,一次性给12分。这个奖励很“稀疏”,但它作用重大:它告诉策略“完成比不完更好”,也给了训练后期一个明确的目标锚点。
关于“+12”这个数值,我是拿“预计最大步数”倒推的:
- 地图上完成全覆盖,经验上至少要800步;
- 如果每步的增量奖励平均能赚0.05分,800步就是40分;
- 一个“完成事件”应该等价于总任务列的前1/4左右,所以我取12。
这个倒推法在奖励工程里很常用。你要是拍脑袋写个+100,那所有策略都会盯着“完成”这个远期目标冒进,前期完全学不到覆盖技能,风险太大。
第3项:边界清扫奖励(Δborder_coverage * 1.2)
这是我跟很多同行交流后学到的“私货”。清扫任务里,边界和角落是最容易被漏掉的地方,但也是灰尘堆积最严重的地方。如果把边界灰尘和中部灰尘放进同一个覆盖率公式,那策略天然会优先扫中间:因为中间路径开阔、转向少、步数收益高。
所以我单独统计了一个“边界覆盖率”:距离墙壁或障碍物半径0.8m以内的区域,算作边界区域。边界覆盖率的增量奖励系数设成1.2,比一般区域高50%。实测效果非常明显——机器人会刻意贴着墙走一圈,这在第一版模型里根本见不到。
3.3 行为引导组:让路径不再是“布朗运动”
第4项:重复清扫惩罚(-0.02 * repeated_area)
这一步要解决“绕圈自嗨”。怎么检测“重复扫”呢?我是把栅格地图按边长5cm切成小格子,每个格子有一个“最近清扫时间戳”。当机器人扫过格子时,如果这个格子距离上次清扫的时间少于6秒,就算“重复清扫区域”。
if cell_last_cleaned_time[cell_id] > current_time - 6.0: repeated_area += cell_size惩罚压力不大,但持续存在。策略会倾向于往新区域走,而不是在原地刷覆盖率。注意这个惩罚不能太大,否则机器人会为了避开“刚扫过的区域”而走极端路线,路径反而扭曲。
第5项:碰撞惩罚(-1.0)
第一版用的是-0.5,效果太温和,机器人觉得碰两下也无所谓。但我也没有调成特别狠的值,原因很现实:清扫任务很多时候必须和障碍物边缘发生轻微接触,比如贴墙扫尘。
这里有一个很值得说的细节:我把碰撞分成了“正常贴碰”和“硬碰撞”两级。用碰撞检测的冲击力阈值区分,低于某个力度的不算碰撞惩罚,只算轻微接触;高于这个力度才触发-1.0。
if impact_force > collision_threshold: r_collision = -1.0 elif impact_force > gentle_contact_threshold: r_collision = -0.1这样处理的理由是:如果所有接触都惩罚,机器人不敢靠近墙边,边界清扫永远做不好;但如果所有接触都不惩罚,机器人会拿脸撞障碍物。分级惩罚让它在“接近障碍物”和“暴力撞击”之间找到一个中间解。
第6项:转弯惩罚(-0.15 * angle_velocity)
这个惩罚是针对“路径扭曲”的。机器人为了刷覆盖率,经常走S形、Z字形,转向频率高,姿态剧烈变化,不仅耗能,还容易扫漏。
r_turn = -0.15 * abs(angular_velocity) # rad/s有点像一个“平滑税”。策略学到的行为是:尽量走直线、转缓弯。实测下来,这个惩罚带来的一个意外收益是机器人学会了“沿墙角走”这种高效路径,因为墙边的转向天然少。
第7项:路径平滑奖励(+0.005 / (1 + jerk))
这是“柔软度”奖励。jerk是加加速度(加速度的导数),我模拟器里能直接读到。值越大,说明路径越“颠簸”,奖励就越小。
0.005/(1+jerk)这种形式的妙处在于:数值永远为正,但上限不超过0.005,幅度受到严格压制。它是“锦上添花型”奖励,不会干扰主目标。
其实第6和第7项单独看都微不足道,但两项放一起就形成了一个“优雅路径”的软约束。清扫这种任务,路径方差太大很容易漏扫,而这两项把路径方差压下来了。
3.4 约束惩罚组:告诉机器人“不许浪费”
第8项:原地卡死惩罚(-2.0)
这是很多RL机器人项目里踩坑最深的一项,但往往要到训练中后期才暴露。机器人在墙角、障碍物缝隙里卡住后,策略会陷入两种循环:要么一直输出一个“试图前进”但物理上无法前进的动作;要么原地左右摆动。
检测卡死很简单:持续10步,位置变化小于1cm、里程计位移却大于阈值,就判定卡死。我额外加了一个“摇摆检测”:方位角在正负15度之间反复翻转超过4次,也算。
if displacement < 0.01 and odometry_delta > 0.3: r_stuck = -2.0这项惩罚的一次性力度很大,就是为了让策略宁可绕路,也不要在墙角死磕。实测训练曲线里,加了这项之后,地图右下角那个L形障碍物区域再也没出现“卡壳”的现象。
第9项:时间效率惩罚(-0.005 * step)
步数惩罚是RL任务的老熟人了。步数越长,累积负奖励越多。作用就是逼策略用尽量少的步数完成尽量多的清扫。
但注意,这个值一定不能大。如果-0.01以上,策略会变得“赶时间”:路径粗放,边角细节放弃,因为“多花一步要多扣一分”的恐惧压过了清扫收益。这里我用了0.005,折中。
第10项:能耗成本惩罚(-0.05 * distance)
现实中的扫地机器人要耗电,仿真里我用“单位里程能耗”近似。每走1米扣0.05分。这个压力对“绕圈自嗨”是第二道防线:如果重复清扫惩罚漏判了,能耗惩罚也会让绕圈变贵。
严格来说这层的存在意义不是省电(-0.05很小),而是给策略增加一个“里程”的参照系。清扫任务的本质是用最短的路径覆盖最多的面积,没有这一项,策略只会考虑面积,不考虑腿脚。
3.5 关于“势函数塑形”的一句提醒
懂RL理论的朋友可能会问:你加了这么多边边角角的惩罚,跟奖励塑形定理冲突吗?Ng等人在1999年证明过,如果额外奖励可以写成势函数形式的差分,那最优策略不变。
我用了边界清扫奖励、平滑奖励、重复清扫惩罚这些项,严格意义上它们确实可能改变最优策略,不是“无害塑形”。但工程上的取舍是:我本来就不追求“理论上的最优清扫策略”,我要的是“符合用户需要的清扫行为”。这算是一种有意的“妥协”。如果你做的是学术实验,建议严格保留势函数条件;做应用项目的话,还是要以行为效果为导向。
4. 权重配平与归一化:复合奖励的核心手艺
4.1 每一项奖励必须先做“量级体检”
复合奖励最容易犯的错误不是“奖励项设计得不好”,而是“权重配平拍脑袋”。比如步数惩罚写0.01,碰撞惩罚写1.0,看起来没问题,但实际一跑,步数累积的惩罚远远压过碰撞惩罚,策略就变成了“宁可撞过去,不想多走一步”。
我习惯在训练前先做一次“单奖励项量级估算表”。拿上面的设计举例:
| 奖励项 | 单步期望量级 | 单回合预估值(约800步) | 占比 |
|---|---|---|---|
| 增量覆盖 | 0.05 | 40 | 45% |
| 完成奖励 | - | 12 | 14% |
| 边界覆盖 | 0.02 | 16 | 18% |
| 碰撞惩罚 | -0.02 | -16 | -18% |
| 转弯惩罚 | -0.05 | -40 | -45% |
| 其他 | 小幅度 | ±10 | - |
这个表算完你会立刻发现,转弯惩罚的量级可能过大了。所以在正式训练之前,我把转弯惩罚从0.2下调到了0.15,又把边界清扫奖励从1.2上调到1.5左右的区间做测试。量级表的作用不是精确预测,而是防止某个奖励项“统治”训练目标。
4.2 动态奖励缩放:当覆盖率达到99%时
清扫覆盖率到后期是个很尴尬的问题。覆盖率从30%涨到60%,增量奖励好拿;从85%涨到99%,每走一步覆盖率变化微乎其微,增量奖励接近0,策略就会失去继续扫残边角的动力。
我的解法是“分段放大”:当覆盖率超过85%后,增量奖励系数自动乘以3。
if current_coverage > 0.85: scale_factor = 3.0 else: scale_factor = 1.0这个trick给训练后期补了一口“扩张器”。没有它,我估计778分至少要打到1300步才可能实现,而且大概率中途就收敛到95%不动了。
4.3 每一个权重调整都必须是“单一变量”
整个训练过程中我调整了10多次奖励权重,每一次只改一项。你可能会觉得没效率,但RL训练噪声极大,一次改两个权重,你根本没法判断到底是谁带来的提升。
我把这个当纪律执行后,训练的归因变得特别清晰。比如有一次我同时把转弯惩罚从0.15改到0.1,又把碰撞阈值从20N改到30N,结果曲线忽高忽低,我花了整整两天才搞明白是碰撞阈值松了导致“硬碰硬”增多。后来我严格执行“一次只改一个参数”的原则,大约50%的改进都能在24小时内定位到具体原因。
5. 训练中的常见问题:四个坑,每一个我都踩过
5.1 奖励洪泛:当奖励项太多,梯度变成一团浆糊
复合奖励的“副作用”随项数增加而增加。有段时间我把奖励项加到了14个(比现在还多4个),结果PPO的critic完全学不过来了,策略更新频率也从3Hz掉到1Hz,训练根本跑不动。
原因倒不复杂:critic网络要拟合的是“状态到期望累积奖励”的映射。奖励函数越复杂、分量越多、单项信号越噪,critic的预测误差就越大,随之策略梯度也越歪。
我的办法是“合并同类项”。14项里,把两个频次相近的惩罚项合成了一个“行为浪费惩罚”,把三个跟覆盖率相关的强化项统一成“覆盖率复合加成”。变成10项之后,训练稳定多了。经验法则:复合奖励不要超过12项,最好控制在8-12项之间。
5.2 奖励循环依赖:机器人发现“反复扫同一个格子”也能刷分
虽然加了重复清扫惩罚,但机器人还是找到过一个漏洞:它在两个相邻栅格之间反复横跳。每次跳到新格子上,都算“清扫了一个新区域”,重复清扫惩罚只在“同一个格子6秒内重扫”时才触发,隔一个格子跳着扫,惩罚形同虚设。
这个案例让我意识到,重复检测的时间窗太短,判断空间太窄,就会被策略钻空子。我最后把重复区域判断扩展成“以机器人为中心的3×3邻域内,存在最近6秒被扫过的格子”就算重复,一下子堵住了漏洞。
5.3 局部最优:宁可“安全挂机”,不愿冒险碰角落
有一次训练中期,覆盖率卡在88%不再上涨。我检查奖励日志,发现策略已经进入“安全挂机”状态:在地图中央一遍遍地走已经扫过的路线,拿了低水平但稳定的增量奖励,坚决不去碰角落。
这其实是一个典型的强化学习探索问题:角落区域覆盖率的增量奖励虽然单次更高,但要到达那里需要经历一段“低奖励走廊”,策略的价值函数估计撑不过这段区间。
解法不是调权重,而是调探索。我给探索噪声(action noise)加了一个周期性放大:每2000步,噪声标准差从0.1放大到0.3,持续500步。相当于每隔一段时间把机器人踢一脚,强迫它离开舒适区去试错。用了这个招之后,覆盖率突破了88%,涨到了93%。
5.4 训练曲线“假回暖”:奖励均值在涨,行为质量在跌
奖励工程的另一个陷阱是:复合奖励的总分可能涨,但单项奖励的表现未必都好。有一次训练得分从500分涨到600分,我以为模型变好了。结果可视化一看,机器人学会了“横冲直撞快速扫完大面积,然后疯狂碰撞”。
问题出在总奖励的加权和掩盖了碰撞惩罚的上升。因为覆盖率涨得更快,总奖励还是净增长。可视化回放的时候,人一看就露馅了。
我后来养成了一个习惯:每次训练达到一个里程碑(比如提升100分),就回放一遍10个奖励项的日志曲线,看各项是否健康。如果某项异常恶化,哪怕总分在涨,也要回滚配置。
6. 迭代实录:从100分到778分的完整路径
6.1 阶段一:补全约束,从100分到220分
这一阶段做的事情:加了第4、6、8项(重复清扫惩罚、转弯惩罚、卡死惩罚)。
训练日志上最明显的变化是“绕圈自嗨”消失,路径变得收敛。22万步左右,奖励均值开始稳定在220分。这次改进验证了“行为惩罚”的价值。但问题也很明显:边界覆盖率几乎没有提升,策略依然龟缩在开阔区域。
6.2 阶段二:拆出边界覆盖率,从220分到430分
把覆盖率拆成“中部覆盖率”和“边界覆盖率”,并给边界奖励加系数,是这次跨越的核心。
训练到40万步时,模型开始出现明显的贴墙行为。我看了录制下来的动作录像,机器人在墙角附近不再“路过”,而是真正地停下来沿边扫一圈。这个从“路过”到“驻留”的行为变化,靠的就是一个单独拆出来的边界奖励。
430分时,覆盖率94%,边界覆盖率81%。但此时路径效率很差:机器人绕路绕得很夸张,一整堵墙扫下来要走两趟。
6.3 阶段三:路径平滑与动态缩放,从430分到610分
加了路径平滑奖励、调整转弯惩罚权重,同时对覆盖率后期做动态放大。训练第58万步的时候,覆盖率第一次超过98%,边界覆盖率到了91%。
这个阶段还有个隐藏工程:我把时间效率惩罚从每步0.01降到了0.005。原因是惩罚过重导致策略“赶时间冒进”,墙边清扫匆匆忙忙。调低后,单位任务的清扫深度上升,总分又从610分小幅上升到650分附近。后来我把“完成任务奖励”从10分调整到了12分,总分突破670分。
6.4 阶段四:打磨与稳定性,最终锁在778分
最后一个阶段主要清理了“探索噪声周期性放大”策略引入的副作用:探索阶段碰撞率升高,导致策略偶尔回退到保守模式。我用学习率分段衰减(前30万步LR=3e-4,后30万步LR=1e-4)替代统一的探索噪声,稳定性明显提升。最终评测时,100个随机初始化的地图中,96个地图覆盖率达到95%以上,平均奖励分数定在778分。
这个数字的含金量,不在于“高”,而在于稳定。同配置模型跑不同随机种子,分数方差只有±35分左右——这对清扫类任务来说,已经算非常收敛了。
7. 最后,分享几条我现在回头看最想告诉自己的经验
奖励工程没有标准答案,但有些弯路是可以直接避开的。
第一,奖励项要少而精,但要“结构冗余”。少而精是不要让奖励项之间过度耦合,导致调一个权重牵一发动全身;结构冗余是说,同一个目标你可以用两个角度去约束(比如重复清扫惩罚+能耗惩罚双保险)。关键是冗余要有边界,别无限堆。
第二,可视化永远比数字更能说明问题。我大量时间不是在看奖励曲线,而是在看机器人行为视频。一段60秒的失败视频,比60页奖励日志更能说明策略在钻什么空子。每次训到新的里程碑,都值得录下来慢放看看,尤其是墙角、障碍物缝隙、门框这三个地方。
第三,权重配平是“算”出来的,不是“感觉”出来的。上面那个量级估算表,建议每个项目都建。我知道很多人嫌麻烦,但一次量级算错导致的白训一个星期,远比你五分钟填一张表贵得多。
第四,“完成目标”和“节约资源”之间需要一个缓冲。清扫任务里,“完美主义”的机器人可能为了多扫2%的灰尘多走30%的路程,这个策略显然不实用。所以我在完成覆盖率阈值上用了95%(而不是98%作为一个硬性门槛),并在奖励计算里对到达95%之后的继续清扫做了衰减。你做类似任务时,不管任务是搬运还是分拣,都要想清楚这个缓冲区间放哪里。
第五,奖励工程不是一次性的,它是跟训练过程一起长大的。同样的奖励函数,在训练前期、中期、后期的效果完全不同。前期探索阶段需要“鼓励尝试”的奖励,中后期需要“约束行为”的惩罚。所以我建议提前规划奖励调整的演练环境,每次调整前先在低步数下做一对A/B测试,确认有效再放到长训练里。
这些经验不是什么高深理论,就是土办法加耐心。但说实话,清扫机器人的强化学习,技术瓶颈从来不在车体、不在感知,就在奖励定义上——你把“你到底想要一个什么样的清扫机器人”想清楚了,奖励函数其实就写出来一半了。另一半,是拿训练失败喂出来的。