7.3 学徒观察:模仿学习与逆强化学习


7.3 学徒观察:模仿学习与逆强化学习

摘要:没有奖励函数时,向老师学。行为克隆把示范当监督数据学"这个局面该做什么",败于分布漂移;逆强化学习反过来从示范行为推老师的奖励函数——让专家看起来最优的那个奖励;对抗式模仿把两者拧成生成对抗训练。本节用代码复现克隆的漂移病与奖励反推的最小案例。

「Apprenticeship Learning」,学徒问题——机器学习文献里这个词指的是:老工匠不会写教案(奖励函数),但他干活的样子就在那里,学徒盯着看能不能偷师。学堂的选修课讲这条路。前两节的强化学习都要一个明确的奖励函数,可奖励恰恰是最贵的东西:泥潭扣几分、面包坊加几分还能拍脑袋,"把这批零件摆得像老师傅那样整齐"的奖励怎么写?写不出来的时候,示范本身就是最丰富的教材。

行为克隆:监督学习的直球

最朴素的偷师是行为克隆(Behavior Cloning):把老师的轨迹拆成(局面,动作)样本对,当成监督学习数据,学一个"局面到动作"的映射。简单、直接、不需要环境模型;自动驾驶的早期系统就是这么从人类驾驶数据里学的。

它的死穴叫分布漂移:老师的数据只覆盖老师走过的路,学徒一旦犯一个小错(或遇到点噪声)滑进老师从未到过的局面,克隆模型在这些陌生局面上胡乱出招,错误越滚越大——小错误复利成大灾难,这是克隆与强化学习的本质差距:克隆学的是"在老师的路上怎么做",不是"在任何路上该怎么做"。

纠偏的代表作是数据集聚合(DAgger 思路):让学徒自己上路跑,把跑出来的新局面拿去问老师"换你会怎么做",把答案聚合进数据集再训——用老师的标签覆盖学徒的漂移轨迹,分布漂移被持续抹平。

逆强化学习:从"怎么做"反推"图什么"

逆强化学习(IRL)换个姿势偷师:不学动作,学意图。假设老师是某个未知奖励函数下的最优决策者,IRL 要找的就是那个奖励——形式上,找一个奖励函数,使得老师的轨迹在它之下比其他任何轨迹都得分更高。找到奖励后再用上一节的标准强化学习求解,得到的策略自然像老师,且泛化到老师没走过的路(因为规划器会自己做主)。

IRL 有个先天毛病叫不适定性:能解释老师行为的奖励函数有无穷多个("老师绕开泥潭"既符合"泥潭扣分",也符合"干燥路面加分"——甚至"宇宙的一切都扣分只有这条路线加分")。解药有二:加正则(在所有能解释的奖励里挑最简洁或最随机的,最大熵 IRL 一脉)与缩小假设类(只考虑少数几个特征加权的线性奖励,学徒问题一脉——先提取老师轨迹的特征均值,再找一组权重使任意策略达到同样特征均值时必须拿高奖励)。

对抗式模仿(GAIL 一脉)是第三条路:一个学徒生成器努力学老师,一个判别器努力分辨"这轨迹是老师的还是学徒的"——对抗训练到判别器分不清时,学徒的行为分布就逼近了老师。它与生成对抗网络(GAN)同构,把"模仿"变成了"欺骗测谎仪"。

代码实验一:克隆的漂移病

在上一节的网格小镇里造一位老师(用学好的 Q 策略当老师),采集他的轨迹做行为克隆;再在执行时注入一点噪声,看克隆策略与原版强化学习策略的表现差距。

import random class GridTown: SIZE = 4 HOME, BAKERY = (0, 0), (3, 3) MUD = {(1, 2), (2, 1)} def reset(self): self.pos = self.HOME return self.pos def step(self, action): x, y = self.pos dx, dy = {"U": (0, -1), "D": (0, 1), "L": (-1, 0), "R": (1, 0)}[action] self.pos = (max(0, min(3, x+dx)), max(0, min(3, y+dy))) if self.pos == self.BAKERY: return self.pos, 10.0, True return self.pos, (-1.0 if self.pos in self.MUD else -0.1), False ACTIONS = ["U", "D", "L", "R"] def q_learning(env, episodes=300, seed=1): random.seed(seed) Q = {} q = lambda s, a: Q.get((s, a), 0.0) eps = 1.0 for _ in range(episodes): s, done, steps = env.reset(), False, 0 while not done and steps < 80: a = random.choice(ACTIONS) if random.random() < eps \ else max(ACTIONS, key=lambda x: q(s, x)) s2, r, done = env.step(a) Q[(s, a)] = q(s, a) + 0.3 * (r + 0.9 * max(q(s2, x) for x in ACTIONS) - q(s, a)) s = s2; steps += 1 eps = max(0.05, eps * 0.99) return Q def collect_demos(Q, env, n=30, noise=0.0, seed=2): """老师跑 n 条轨迹;noise 让老师偶尔手抖,制造更真实的数据。""" random.seed(seed) data = [] for _ in range(n): s, done, steps = env.reset(), False, 0 while not done and steps < 40: a = random.choice(ACTIONS) if random.random() < noise \ else max(ACTIONS, key=lambda x: Q.get((s, x), 0.0)) data.append((s, a)) s, _, done = env.step(a) steps += 1 return data def behavior_cloning(data): """多数表决的克隆模型:每个局面取示范中最常见的动作。""" votes = {} for s, a in data: votes.setdefault(s, {}) votes[s][a] = votes[s].get(a, 0) + 1 return {s: max(cnt, key=cnt.get) for s, cnt in votes.items()} env = GridTown() Q = q_learning(env) # 造老师 data = collect_demos(Q, env, n=30, noise=0.0) # 完美老师 bc = behavior_cloning(data) def rollout(policy, env, slip=0.0, seed=5): """执行策略;slip 为执行时的手滑率。返回是否到达与步数。""" random.seed(seed) s, done, steps = env.reset(), False, 0 while not done and steps < 60: a = policy.get(s) if a is None: # 陌生局面:克隆没见过 a = random.choice(ACTIONS) if random.random() < slip: a = random.choice(ACTIONS) s, _, done = env.step(a) steps += 1 return done, steps def avg_perf(policy, env, slip, seeds=range(20)): """跨二十个随机种子统计到站率与平均步数。""" runs = [rollout(policy, env, slip=slip, seed=s) for s in seeds] return (sum(r[0] for r in runs), sum(r[1] for r in runs) / len(runs)) for slip in (0.0, 0.1, 0.3): ok, avg = avg_perf(bc, env, slip) print(f"手滑率 {slip}: 克隆到站 {ok}/20,平均 {avg:.1f} 步") ok, avg = avg_perf(lambda s: max(ACTIONS, key=lambda x: Q.get((s, x), 0.0)), env, 0.1) print(f"对照 手滑率 0.1: 老师全图Q 到站 {ok}/20,平均 {avg:.1f} 步")

规律在数字里:零手滑时克隆完美复刻老师(与老师同速);手滑加到三成,克隆的平均步数翻倍——滑出老师的路就进陌生局面,只能乱撞到碰回正道;而见过全图的 Q 策略无论怎么滑都能就近修正,步数几乎不受手滑影响。在这张四乘四的小世界里漂移只表现为"变慢",把世界放大、把容错预算收紧(比如限时),同样的机制立刻表现为"到不了站"。克隆的天花板是数据,漂移的地板也是数据

代码实验二:反推奖励的最小案例

老师的示范是"宁可绕两步也不踩泥潭"。候选奖励只有两组特征权重:甲组(泥潭重罚、步数轻罚),乙组(泥潭轻罚、步数重罚)。看哪组能让老师的实际路线得分高于贪图近路的替代路线——这就是逆强化学习的判定内核。

def route_score(steps, mud_hits, w_mud, w_step): """路线得分:泥潭次数与步数各按权重扣分。""" return -w_mud * mud_hits - w_step * steps EXPERT = {"steps": 9, "mud": 0} # 老师的实际路线:绕远九步 全程干净 SHORTCUT = {"steps": 6, "mud": 2} # 备选近路:省三步 踩两块泥地 candidates = { "甲组 泥潭重罚": (4.0, 0.2), "乙组 步数重罚": (0.5, 3.0), } for name, (w_mud, w_step) in candidates.items(): expert = route_score(EXPERT["steps"], EXPERT["mud"], w_mud, w_step) shortcut = route_score(SHORTCUT["steps"], SHORTCUT["mud"], w_mud, w_step) verdict = "能解释老师" if expert > shortcut else "解释不了" print(f"{name}: 老师路线 {expert:+.1f} 近路 {shortcut:+.1f} -> {verdict}")

甲组(泥潭重罚)下老师的绕行大胜近路,能解释;乙组(步数重罚)下近路反超——在乙组的价值观里,老师绕那三步纯属浪费,所以乙组解释不了示范。于是"老师图什么"的反推结论是:他在乎干净鞋胜过在乎省三步,权重该往甲组那边落。当然如前所述,能解释的奖励不止甲组这一组参数(泥潭罚六分也行),不适定性就藏在这份自由度里;工程上靠特征选取与正则化把候选空间约束住。顺带一提,用反推出来的奖励再跑一遍第 7.1 节的 Q 学习,学徒就得到了一条"像老师但属于自己"的路线——这正是学徒问题的完整闭环。

⚠️ 常见坑:把示范当真理。老师也是人,会犯错、会疲惫、会有看不见的私心;克隆与 IRL 都会把老师的坏习惯一起学走,甚至放大(IRL 会把偶尔的失误也解释成"深思熟虑的最优")。示范数据的清洗与多老师融合,是这条路线上被低估的脏活。

💡 关键直觉:模仿学习与大模型时代的"从人类反馈学习"一脉相承——示范、偏好、评分都是"奖励工程写不出来时的奖励替代品"。多智能体场景里它还有个妙用:团队的协调示范(老搭档的配合路线)比任何奖励函数都更能教会新人"怎么跟这支队伍打球"

沙盘推演小结

学期末的成果展上,学徒们的路数五花八门:照猫画虎的克隆派在熟悉的路上有模有样,出了岔子就原形毕露;偷师意图的 IRL 派学会了老师"宁可绕路不踩泥"的价值观,走出了自己的路;对抗模仿派的动作已经骗过了资深判别官。学堂到此结业——镇民们带着各自的学问回到镇上。下一章进沙盘工坊:这些学问要在哪里搭台试炼、怎么量分、如何防身?


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U