7.1 老手艺:单智能体强化学习


7.1 老手艺:单智能体强化学习

摘要:强化学习让镇民不靠程序员手写规则、靠试错与奖励自己学会策略。本节讲 MDP 五元组、贝尔曼方程的直觉、Q 学习的时间差分更新、epsilon 贪婪的探索利用平衡,并用纯 Python 实现表格型 Q 学习,让镇民在网格小镇里自学出绕泥潭的最短路。

学骑车的人没有一个人是看着说明书学会的——摔几次、找手感、慢慢就会了。这种"从后果里学"的功夫就是强化学习,也是学堂的启蒙课。上一章会馆证明了均衡可以被"筛选"出来,这一章我们要让镇民自己"试"出来。本节只教一个人的功夫:环境固定、奖励给定、一个学习者反复试错。别嫌它老——下一节的多智能体大教室,用的全是这里的零件。

MDP:把"试错学习"写成数学

马尔可夫决策过程(MDP)用五件套给学习问题建模:状态集(镇民能处在哪些局面)、动作集(每个局面能做什么)、转移函数(做了动作后落到哪个新状态的概率分布——马尔可夫性指它只看当前状态,不看历史)、奖励函数(每个转移给的即时分值)、折扣因子(未来的分值在今天打几折)。策略是从状态到动作的映射;学习的目标是找一条策略,让折扣累计奖励的期望最大。

为什么非要折扣?两个理由:数学上它保证无限长 horizon 的总和不发散;工程上它表达"明天的快乐不如今天"的偏好。折扣越接近一,镇民越有远见;接近零,就成了只看眼前甜头的短视鬼。

值函数与贝尔曼的递归直觉

学 MDP 绕不开贝尔曼方程,它的直觉其实就一句话:一个局面的价值 = 立刻拿到的奖励 + 下一局面的(打折)价值。把"走到终点的价值"拆成"这一步的甜头加剩下路程的价值",大问题就递归成小问题。据此定义两张表:状态价值(这个局面本身值多少)与动作价值 Q(在这个局面做这个动作值多少)。Q 表一旦准确,策略就是白送的:每个局面挑 Q 最大的动作,一步到位。

Q 学习:一边走一边修表

问题是 Q 表没人给你,得自己试出来。Q 学习的更新式是全章的发动机:

新Q = 旧Q + 学习率 ×(即时奖励 + 折扣 × 下一状态最大Q − 旧Q)

括号里那串叫时间差分误差——"实际尝到的甜头"与"原来预期"的差距。误差为正,把这格 Q 往上调;为负,往下调;学习率控制每次修正多少(全信新经验会抖,全不信新经验学不动)。Q 学习是离策略的:哪怕行动时故意乱试(为了探索),更新时仍按"下一状态最优"的口径修表——探索的学费不白交。

探索与利用是绕不开的两难:总挑当前 Q 最大的动作(利用),可能永远发现不了隔壁更好的路;总乱走(探索),又把学到的本事浪费掉。最常用的折中是 epsilon 贪婪:以小概率随机乱走,其余时间贪心;训练早期 epsilon 调高多探索,后期退火调低多用功。

代码实验:网格小镇的 Q 学习

环境是四乘四网格:左上角是家,右下角是面包坊,中间撒两格泥潭(踩中扣分),每走一步扣一点点分(隐性地逼镇民抄短路)。奖励设计是强化学习的"课程设计"—— reward shaping 的好坏直接决定学出来的是良民还是怪物。

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 = {"上": (0, -1), "下": (0, 1), "左": (-1, 0), "右": (1, 0)}[action] nx, ny = max(0, min(self.SIZE-1, x+dx)), max(0, min(self.SIZE-1, y+dy)) self.pos = (nx, ny) if self.pos == self.BAKERY: return self.pos, 10.0, True # 到站,大奖励 reward = -1.0 if self.pos in self.MUD else -0.1 # 泥潭重罚 return self.pos, reward, False ACTIONS = ["上", "下", "左", "右"] def q_learning(env, episodes=400, alpha=0.3, gamma=0.9, eps_start=1.0, eps_end=0.05, seed=42): random.seed(seed) Q = {} # (状态, 动作) -> 值 def q(s, a): return Q.get((s, a), 0.0) eps = eps_start history = [] for ep in range(episodes): s, done, steps = env.reset(), False, 0 while not done and steps < 100: if random.random() < eps: # 探索 a = random.choice(ACTIONS) else: # 利用 a = max(ACTIONS, key=lambda x: q(s, x)) s2, r, done = env.step(a) best_next = max(q(s2, x) for x in ACTIONS) Q[(s, a)] = q(s, a) + alpha * (r + gamma * best_next - q(s, a)) s, steps = s2, steps + 1 history.append(steps) eps = max(eps_end, eps * 0.99) # 退火 return Q, history env = GridTown() Q, history = q_learning(env) print("前 20 回合平均步数:", round(sum(history[:20])/20, 1)) print("末 20 回合平均步数:", round(sum(history[-20:])/20, 1)) def greedy_path(Q, env): s, done, path = env.reset(), False, [env.HOME] for _ in range(30): a = max(ACTIONS, key=lambda x: Q.get((s, x), 0.0)) s, _, done = env.step(a) path.append(s) if done: break return path print("学到的路线:", greedy_path(Q, env))

典型的学习曲线是这么走的:开局平均几十步乱撞(探索期把 Q 表踩热),中期步数快速下压,末段稳定在短路附近——四乘四的网格从家到面包坊最短六步,学会的路线应当避开两格泥潭。把泥潭奖励改成零试试:镇民会学会"绕不绕无所谓",路线立刻松垮——你奖励什么,它就学什么,字面意义上的一字不差

def sweep_epsilon(eps_list, seed=7): """探索率起点的敏感性:每组跑一遍看收敛速度。""" for e0 in eps_list: env2 = GridTown() _, h = q_learning(env2, episodes=200, eps_start=e0, eps_end=0.05, seed=seed) early = sum(h[:30]) / 30 late = sum(h[-30:]) / 30 print(f"eps起点{e0:.1f}: 前30回合均值 {early:5.1f} -> " f"末30回合均值 {late:5.1f}") sweep_epsilon([0.3, 0.6, 1.0])

扫描结果通常显示:探索起点太低收敛快但容易卡次优路(泥潭绕行没被发现),起点高学得慢但更稳——探索利用的平衡没有免费午餐,靠退火曲线调。

⚠️ 常见坑一:奖励设计的"字面主义"。你想让它绕泥潭,它学会在原地打转刷安全分(每步负分太小时);你想让它快点到,它学会一头扎进泥潭抄近道(泥潭惩罚不够重时)。改行为先改奖励,改奖励后重跑全部实验。坑二:Q 表初始化全零时,"从未试过的动作"与"试过很烂的动作"同为最差,探索不足会让镇民死守第一眼看中的路——乐观初始化(把没试过的记高分)是便宜的解药。

💡 关键直觉:Q 学习修的是一张表,但"表"只是形式的躯壳。状态太多存不下表时,把 Q 换成神经网络来逼近(深度 Q 网络),公式一字不改——这就是从表格学习到深度强化学习的全部门槛。本章刻意用表格,是为了让概念裸奔给你看。

沙盘推演小结

开学第一周,扫地镇民在网格小镇里摔出了自己的地图:泥潭记大过、面包坊记大功、每一步悄悄记账。期末考试它走的路线,连老住户都挑不出毛病。但教室里很快坐进了第二个、第三个学习者——一旦大家同时学习,脚下的大地就开始移动。下一节进大教室:非平稳的世界里,怎么学。


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