简介:这份资源是面向强化学习与逆向强化学习(IRL)方向学习者与研究者的一套示例代码工程,重点解决从专家演示中反推奖励函数这一核心问题的实践落地。作者在实现IRL框架时对BURLAP代码库做了必要修改,因此包内同时附带了BURLAP的快照,便于读者直接复现实验而无需另行配置依赖。压缩包共497个文件,以484个Java源码为主体,辅以少量xml配置、pom构建文件与jar依赖库,整体约2.29MB,结构紧凑、便于导入IDE阅读与二次开发。内容覆盖学徒学习、多智能体性能绘图、网格世界域建模、单阶段标准式博弈以及求解器封装等模块,可帮助读者理解IRL算法如何与经典强化学习环境对接。目前已有1046人学习下载,适合具备一定Java与强化学习基础、希望深入逆向强化学习实现细节的读者参考。
1. 逆强化学习教程的示例代码:从奖励函数黑匣子里把规则挖出来
做机器人控制或者自动驾驶决策的朋友,大概率遇到过这种场景:专家演示轨迹攒了一大堆,模仿学习也跑了,策略在训练集上看着还行,一到新场景就崩。问题往往出在——你只学了「怎么做」,没学「为什么这么做」。逆强化学习(Inverse Reinforcement Learning,IRL)要解决的就是这件事:从专家行为里反推出背后的奖励函数,再用这个奖励函数去训练策略。IRLTutorial 这类教程的示例代码,价值不在于教你调包,而在于把「奖励函数怎么被反推出来」这条链路用最小可运行的代码摊开给你看。它适合已经会写基础强化学习循环、但对奖励设计一直靠拍脑袋的工程师,也适合想把模仿学习从行为克隆升级到奖励建模的人。下面我按自己复现这类教程代码的顺序,把环境、算法、参数和踩过的坑一次讲清楚。
2. 逆强化学习到底在反推什么:从演示轨迹到奖励函数的映射关系
2.1 正向 RL 与逆向 RL 的输入输出对调
先把坐标系摆正。正向强化学习里,环境给你奖励 $r(s,a)$,你的目标是学一个策略 $\pi(a|s)$ 去最大化累积回报。逆强化学习把这个过程反过来:你手里有专家策略产生的轨迹 $\tau = {(s_0,a_0),(s_1,a_1),\dots}$,但不知道奖励函数,目标是求出一个奖励函数 $r^*(s,a)$,使得专家策略在这个奖励下是最优的。
这个「最优」是 IRL 的核心约束,也是它和单纯的行为克隆(Behavior Cloning)最大的区别。行为克隆直接拟合 $s \to a$ 的映射,遇到分布外状态就无从下手;IRL 拟合的是奖励,学到的奖励函数可以迁移到新环境、新动力学里,再用任意 RL 算法去求解。示例代码里通常会把这两条路径都实现一遍做对比,你能直观看到 IRL 在泛化上的优势。
数学上,IRL 要解的是一个欠定问题:满足「专家最优」的奖励函数往往有无穷多个。比如所有奖励都乘以正数、或者加一个常数,最优策略不变。所以任何可用的 IRL 算法都必须引入额外约束来挑出一个合理解,常见的有最大熵、最大间隔、贝叶斯先验这几类。
2.2 最大熵 IRL 为什么成了示例代码的默认选择
翻 IRLTutorial 这类教程的示例代码,你会发现最大熵 IRL(Maximum Entropy IRL)出现频率最高。原因很实际:它把「欠定」这件事用概率框架处理得干净,而且优化目标是个凸问题,好实现、好收敛。
最大熵 IRL 的假设是:专家轨迹的分布服从指数族,概率正比于 $e^{r(\tau)}$。在这个假设下,学习目标变成最大化专家轨迹的似然,等价于让学到的奖励函数下,专家轨迹的期望特征和真实专家轨迹的特征统计量对齐。用一句话概括就是——让模型生成的轨迹在特征层面「看起来像」专家轨迹,但在没被约束的地方保持最大不确定性。
这个性质对示例代码很友好:你只需要定义状态特征 $\phi(s)$,奖励写成 $r(s) = \theta^T \phi(s)$,然后优化参数 $\theta$。整个算法就变成一个「前向跑 RL 求期望特征 → 和专家特征比对 → 更新 $\theta$」的循环。下面这段是这类教程里最典型的核心循环结构:
import numpy as np def maxent_irl(feature_matrix, expert_feature_counts, env, epochs=50, lr=0.01): """ feature_matrix: 每个状态的特征向量 phi(s),形状 [n_states, n_features] expert_feature_counts: 专家轨迹的累积特征计数,形状 [n_features] env: 提供转移概率和初始状态分布的环境对象 """ n_states, n_features = feature_matrix.shape theta = np.zeros(n_features) # 奖励参数,初始为零 for epoch in range(epochs): # 1. 用当前 theta 算奖励 rewards = feature_matrix @ theta # 2. 前向传播求状态访问频率(对应 soft value iteration) # 这里用简化的值迭代近似,实际教程会用 softmax 策略 state_visitation = compute_state_visitation(env, rewards) # 3. 计算模型下的期望特征 model_feature_counts = state_visitation @ feature_matrix # 4. 梯度 = 专家特征 - 模型特征 grad = expert_feature_counts - model_feature_counts # 5. 梯度上升更新 theta theta += lr * grad return theta逻辑说明:第 1 步把奖励参数线性映射到每个状态;第 2 步是整个算法最重的部分,需要根据当前奖励算出策略下的状态访问分布,教程里一般用 soft value iteration 实现,保证策略是随机的而非确定性的;第 3 步把访问频率和特征相乘得到期望特征;第 4 步的梯度方向很直观——专家出现得多而模型出现得少的特征,就提高它的奖励权重。参数方面,lr通常取 0.01 到 0.05,太大梯度会震荡,太小收敛慢;epochs在网格世界这种小环境里 50 到 100 就够,连续控制任务要几百轮。
2.3 特征设计:示例代码里最容易被跳过却最要命的一步
几乎所有 IRL 教程的示例代码都会预先给定特征函数 $\phi(s)$,比如网格世界里用 one-hot 状态编码,或者用「到目标的距离」「是否碰撞」这类手工特征。这一步在教程里一笔带过,但在真实项目里是决定成败的地方。
我一般会遵循两条原则。第一,特征要能覆盖专家行为的关键维度,但不要冗余,冗余特征会让 $\theta$ 不可辨识,训练出来的奖励函数解释性很差。第二,能用环境自带的结构化信息就别硬造,比如机器人任务里关节角度、末端位姿、接触力这些本身就是好特征。示例代码里如果用的是 one-hot 特征,你要清楚那只是为了演示算法,换到连续状态空间必须换成核函数或者神经网络特征提取器,否则状态一多特征维度直接爆炸。
提示:跑示例代码前先确认特征矩阵的维度。one-hot 特征在状态数超过几千时会让
compute_state_visitation里的矩阵运算变得很慢,这是新手最常撞的性能墙。
3. 把 IRLTutorial 示例代码在本地跑通:环境、依赖与最小复现路径
3.1 环境准备与依赖版本控制
这类教程代码通常依赖 numpy、matplotlib,如果带连续控制示例还会用到 gym 或 gymnasium。我的习惯是先建独立虚拟环境,避免和系统里的老版本 numpy 打架。下面是我复现时的标准流程:
# 创建并激活虚拟环境 python -m venv irl_env source irl_env/bin/activate # Windows 用 irl_env\Scripts\activate # 安装核心依赖,版本按教程 requirements 走,没有就锁这几个 pip install numpy==1.24.3 matplotlib==3.7.2 gymnasium==0.29.1 # 进入示例代码目录,先跑最简网格世界例子 cd IRLTutorial/examples python gridworld_maxent_irl.py逻辑说明:先隔离环境是血泪经验,IRL 示例代码里大量用到矩阵运算和随机数,numpy 版本不一致会导致随机种子行为变化,复现结果对不上。参数上,numpy 1.24 之后的随机数生成器 API 有调整,如果教程代码用的是np.random.seed老写法,在新版本里仍然能用但会有警告,不影响结果。gymnasium 是 gym 的维护版,接口基本兼容,如果教程写的是import gym,你装 gymnasium 后把导入改成import gymnasium as gym即可。
3.2 网格世界示例:从专家轨迹生成到奖励热力图
网格世界是 IRL 教程的标配,因为状态空间小、可视化直观。典型流程分四步:定义网格和障碍、用真实奖励跑一个最优策略生成专家轨迹、把专家轨迹喂给 IRL 算法、对比学到的奖励和真实奖励。
import numpy as np # 1. 定义 5x5 网格,终点在 (4,4),中间有障碍 grid_size = 5 goal = (4, 4) obstacles = [(1, 1), (2, 2), (3, 1)] # 2. 真实奖励:终点 +1,障碍 -1,其余 -0.01(鼓励走短路) def true_reward(state): if state == goal: return 1.0 if state in obstacles: return -1.0 return -0.01 # 3. 用值迭代求最优策略,生成专家轨迹 def value_iteration(reward_fn, grid_size, gamma=0.9, theta=1e-6): V = np.zeros((grid_size, grid_size)) while True: delta = 0 for i in range(grid_size): for j in range(grid_size): if (i, j) in obstacles: continue v = V[i, j] # 四个动作的 Q 值 q_values = [] for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]: ni, nj = i+di, j+dj if 0 <= ni < grid_size and 0 <= nj < grid_size: q_values.append(reward_fn((ni,nj)) + gamma * V[ni,nj]) V[i, j] = max(q_values) if q_values else 0 delta = max(delta, abs(v - V[i, j])) if delta < theta: break return V V_expert = value_iteration(true_reward, grid_size) print("专家值函数:") print(np.round(V_expert, 2))逻辑说明:这段代码先构造真实奖励,再用值迭代算出专家值函数,后续按贪心策略采样就能得到专家轨迹。参数上,gamma=0.9是折扣因子,网格世界这种短程任务 0.9 到 0.95 都合理;theta=1e-6是收敛阈值,越小越精确但越慢。障碍物状态在值迭代里直接跳过,因为专家不会进入。跑完你会看到值函数从终点向四周递减,这就是专家行为的「价值地形」。
拿到专家轨迹后,把它转成特征计数,再调用第 2 章那个maxent_irl函数,就能得到学到的 $\theta$。把 $\theta$ 和特征矩阵相乘还原成每个状态的奖励,用 matplotlib 画热力图,和真实奖励并排对比,是教程里最有说服力的一张图。如果学到的奖励在终点和障碍处符号正确、量级接近,说明算法跑通了。
3.3 连续控制示例的适配要点
有些 IRLTutorial 会带连续状态示例,比如倒立摆或者简单的车辆控制。这类代码跑起来比网格世界麻烦,主要卡在状态访问频率的计算上——连续空间没法枚举状态,得用采样近似。常见做法是跑当前策略若干条轨迹,统计落在每个特征上的平均激活值,代替精确的期望特征。
def estimate_feature_expectation(policy, env, feature_fn, n_trajectories=50, horizon=200): """用蒙特卡洛采样估计连续状态下的期望特征""" total_features = np.zeros(feature_fn_dim) for _ in range(n_trajectories): state, _ = env.reset() for _ in range(horizon): action = policy(state) state, _, done, truncated, _ = env.step(action) total_features += feature_fn(state) if done or truncated: break return total_features / n_trajectories逻辑说明:连续场景下用采样均值替代精确期望,n_trajectories和horizon直接决定估计方差。我一般先设 50 条、每条 200 步,看梯度是否稳定,如果梯度噪声大就加到 200 条。这里有个坑:采样用的策略必须是当前奖励下的软最优策略,不能直接用专家策略,否则期望特征永远等于专家特征,梯度恒为零,$\theta$ 根本不更新。教程代码如果没写清楚这一点,你跑出来会发现损失不降,别怀疑人生,先检查策略来源。
4. 参数调不对等于白跑:最大熵 IRL 的四个关键旋钮
4.1 学习率与迭代轮数的配合
最大熵 IRL 的梯度是「专家特征减模型特征」,这个差值在训练初期很大,后期趋近于零。学习率设成固定值会有个问题:初期步子太大容易冲过最优点,后期步子太小收敛慢。我一般用带衰减的学习率,或者干脆用自适应优化器。
# 带指数衰减的学习率 lr_0 = 0.05 decay = 0.95 for epoch in range(epochs): lr = lr_0 * (decay ** epoch) theta += lr * grad参数说明:lr_0取 0.05 适合网格世界这种小特征维度,特征维度上百时降到 0.01。decay取 0.95 意味着每轮衰减 5%,50 轮后学习率降到初值的约 8%,这个节奏在多数教程示例里都稳。迭代轮数不是越多越好,最大熵 IRL 在特征线性可分时会过拟合专家轨迹的噪声,表现为 $\theta$ 范数持续增大但奖励形状不再变化。判断收敛的实用方法是监控梯度范数,连续 10 轮小于 1e-3 就可以停。
4.2 折扣因子对奖励尺度的影响
折扣因子 $\gamma$ 在 IRL 里比在正向 RL 里更敏感,因为它同时影响专家轨迹的生成和模型期望特征的计算。$\gamma$ 太小,专家轨迹只看眼前几步,学到的奖励会偏向局部;$\gamma$ 接近 1,值函数传播范围大,收敛慢但奖励更全局。
我的经验是:网格世界这类回合短的任务,$\gamma$ 取 0.9 到 0.95;连续控制里回合长,取 0.99。关键是专家轨迹生成和 IRL 训练必须用同一个 $\gamma$,教程代码里如果两处不一致,学到的奖励会系统性偏移,这个坑很隐蔽,因为代码不报错,只是结果不对。
4.3 特征归一化与奖励尺度
特征量纲不统一是另一个翻车高发区。比如特征里既有「到目标距离」(0 到 10)又有「是否碰撞」(0 或 1),不归一化的话距离特征会主导梯度,碰撞特征几乎学不到权重。我一般对特征矩阵按列做标准化:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() features_normalized = scaler.fit_transform(feature_matrix)注意归一化要在专家特征计数和模型特征计数上一致处理,否则梯度方向就错了。另外,学到的 $\theta$ 是在归一化特征空间里的,解释的时候要反变换回去才能对应到原始特征的重要性。
4.4 专家轨迹数量与质量
示例代码通常给一条或几条专家轨迹。轨迹太少,特征计数噪声大,学到的奖励不稳定;轨迹太多,如果专家本身不是最优的,IRL 会把专家的次优行为也当成目标去拟合。我一般先用 10 到 20 条专家轨迹,看学到的奖励是否稳定,再决定增减。如果专家轨迹里有明显绕路或者抖动,先做一遍轨迹平滑或者过滤,别直接喂给算法。
注意:专家轨迹的质量比数量重要。一条干净的最优轨迹,胜过一百条带噪声的次优轨迹。这是我在真实项目里用血泪换来的结论。
5. 复现 IRL 示例代码时最容易翻车的五个地方
5.1 现象:损失不下降,$\theta$ 几乎不变
原因:最常见的是策略来源搞错了。连续场景里如果用专家策略去估计期望特征,模型特征恒等于专家特征,梯度为零。另一个可能是特征矩阵和专家特征计数的维度对不上,广播机制悄悄算出了错误结果但不报错。
解决:先打印梯度的范数,如果一直是零或者极小,检查策略是不是当前奖励下的策略。再检查feature_matrix.shape[1]和expert_feature_counts.shape[0]是否相等。这两个地方排查完,九成的不下降问题能解决。
5.2 现象:学到的奖励在终点处是负的
原因:特征设计没有区分终点和普通状态,或者专家轨迹里终点出现次数太少,特征计数被其他状态淹没。还有一种可能是折扣因子在专家轨迹生成和训练时不一致,导致值函数传播方向反了。
解决:确认终点有独立特征,或者在特征里加入「是否终点」的指示位。检查两处 $\gamma$ 是否相同。如果专家轨迹太短,增加轨迹长度或者多采样几条。
5.3 现象:训练后期 $\theta$ 范数爆炸
原因:最大熵 IRL 在专家特征和模型特征完全可分时,似然函数没有上界,$\theta$ 会一直增大。这是算法本身的特性,不是 bug。
解决:加 L2 正则项,或者在梯度更新时做裁剪。教程代码里如果没写正则,自己补一个:
theta += lr * (grad - 0.01 * theta) # 0.01 是正则系数正则系数取 0.001 到 0.01,太大奖励会被压平,太小起不到约束作用。
5.4 现象:可视化热力图和真实奖励形状对不上但数值接近
原因:特征空间到状态空间的映射不是一一对应的,多个状态共享同一组特征时,学到的奖励在这些状态上无法区分。这是特征设计的固有限制,不是算法问题。
解决:增加特征的区分度,比如从 one-hot 换成带位置信息的组合特征。如果状态空间确实需要共享特征,接受这个近似,重点看关键状态(终点、障碍)的奖励符号是否正确。
5.5 现象:换一组随机种子结果差异巨大
原因:专家轨迹采样和模型期望特征估计都带随机性,种子不同导致特征计数波动大。在小状态空间里这个波动尤其明显。
解决:固定所有随机种子,包括 numpy、环境重置、策略采样。如果固定种子后结果仍不稳定,说明专家轨迹数量不够,加到 50 条以上再试。教程代码里如果没设种子,自己在入口处补上np.random.seed(42)和env.reset(seed=42)。
6. 从示例代码到真实任务:奖励函数迁移与验证的一个实用技巧
示例代码跑通只是起点,真正有价值的是把学到的奖励函数用到新任务上。我常用的验证方法是奖励函数交叉验证:在环境 A 上学到奖励 $r_A$,在环境 B 上学到奖励 $r_B$,然后把 $r_A$ 放到环境 B 里跑 RL,看策略表现是否合理。如果 $r_A$ 在 B 里也能引导出接近专家的行为,说明学到的奖励抓住了任务本质而非环境特有条件。
具体操作上,我会留出一部分专家轨迹不参与训练,作为测试集。用学到的奖励训练出新策略后,在测试轨迹的初始状态上跑,比较新策略和专家策略的动作差异。差异小说明奖励迁移性好,差异大就要回头检查特征设计是否过拟合了训练环境。
还有一个实用技巧是奖励塑形对比。把学到的奖励和手工设计的奖励分别用于训练,看哪个收敛快、最终回报高。我做过的一个机械臂抓取任务里,IRL 学到的奖励在训练初期收敛比手工奖励慢,但最终成功率高出约 15 个百分点,因为手工奖励漏掉了「接近物体时的姿态调整」这个隐式目标,而 IRL 从专家演示里把它挖出来了。
最后说个我自己的习惯:每次跑完 IRL,我都会把学到的 $\theta$ 和对应的特征名存成一个字典,下次遇到类似任务先加载出来做初始化,比从零训练快很多。这个习惯帮我省了不少重复调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取