☰
从贝尔曼方程到Q-learning:二维格子世界寻路实战
2026/10/14 5:44:55 网站建设 项目流程

这是我这套深度强化学习笔记的第二篇,上篇讲过MDP、策略和贝尔曼方程这些基础概念,这篇直接把它们变成能跑的东西:Q-learning,外加一个二维格子世界的寻路demo。Q-learning大概是强化学习里最经典也最容易让人误解的入门算法,核心更新公式一行就能写完,可要是只背公式不亲手训练,写出来的智能体经常表现得很迷。我当时在这个demo上反复调了一周,踩过奖励设计、epsilon衰减、Q值更新时机这几类坑,这篇笔记就把完整过程记下来。它适合已经有了一点MDP基础、想亲手把第一个强化学习训练循环跑起来的朋友,也适合那种看了不少理论但总感觉隔着一层玻璃的读者,希望通过实际的二维寻路demo,把注意力、理解力和手感都练起来。

1. Q-learning在学什么:从Q值到贝尔曼方程

1.1 为什么学Q值而不是V值

先说一个经常被问到的问题:MDP里状态价值函数 V(s) 已经在衡量某个状态的好坏,为什么做控制决策时,我们却要学 Q 值而不是 V 值?V(s) 描述的是在某个固定策略之下,从状态 s 出发能拿到的期望回报,但它不能直接告诉你下一步该往哪走。就像一个人靠近出口时,V 值会很高,但如果当前格子面前堵了一堵墙,最优策略可能是先往回退两步再绕,这个“往回退”的动作,仅凭 V 值的高低根本看不出来。

Q(s,a) 把动作也装进了价值里,直接回答“在状态 s 下执行动作 a 到底值不值”。决策时把所有动作的 Q 值拿出来比大小,挑最大的那个执行就行,不需要再额外算策略,也不需要转一层函数。这个区别在二维寻路里特别明显:我们最终要的是一个“每个格子往哪走”的策略表,而 Q 表天然就是这个策略表。每一行的 max Q 对应的动作,就是当前格子的最优方向。

更深层的理由是,Q-learning 是异策略算法,它要评估的动作未必是当前正在执行的动作,所以在更新时要用 max Q(s', a'),也就是“下一状态里看起来最好的那个动作值”。如果只学 V 值,更新目标里还得再套一个策略分布,表格法下没必要绕这个弯。

1.2 贝尔曼方程和Q表更新的关系

Q-learning 的核心是一个自我修正的过程。我们不知道真实的 Q 值,但我们知道一个递归关系:当前状态动作的价值,等于立即奖励 r,加上未来按最优方案继续走所能拿到的折后收益。用公式写就是:

Q_target = r + gamma * max Q(s', a')

把这个目标值拿来和自己当前估计的 Q(s,a) 比较,差值就是时序差分误差,用学习率 alpha 去修正一部分,于是每个动作的 Q 值更新会收敛成:

Q[state][action] += alpha * (reward + gamma * max(Q[state_next]) - Q[state][action])

这行更新在训练循环里每走一步都会执行。我当初第一次写这个公式时,总觉得它只是一个普通的“表格填数”过程,直到在渲染窗口里观察到 Q 值逐渐从终点像水波一样扩散回起点,才发现时序差分的信息传播有多直观。终点附近的格子先学到高价值,因为那里有终点正奖励;再往后走,离终点远几格的格子也慢慢有了正的预期。早一步训练时,起点价值还是 0,训练几百回合后,它变成正的,路径也就被“挖”出来了。

这个递归修正方式比蒙特卡洛更高效的关键在于:它每走一步就更新一次,不需要等整个回合结束。也就是说,智能体还在路上随机试探时,已经不断带着新观察修正旧判断了,样本利用率更高,训练也更早能看到反馈。

1.3 表格法的适用边界

二维寻路里表格法天然成立,因为状态和动作都是有限离散的。状态是(row, col),动作只有上下左右四个,一张 25 行乘 4 列的 Q 表就能存下整个策略。这也是做 demo 时最舒服的地方:所有内部数据都能直接打印出来看,适合脑内调试。

可一旦状态量变大,比如地图扩大到 200×200,或者状态变成连续坐标、图像像素,Q 表就完全崩了。你要么无法枚举,要么枚举到内存爆炸。表格法是理解强化学习机制的显微镜,它的意义在于让我们把所有注意力放在核心机制上,而不是与神经网络结构搏斗。理解这一点以后,再看后面的 DQN、策略梯度,思路会顺很多。

2. 二维寻路环境:从网格地图到奖励设计

2.1 用二维数组搭一个迷宫

把环境建简单一点,不用图形界面,也不用任何第三方库,就用一个二维数组来表示地图。我用的是一张 5×5 的网格,0 代表可通行,1 代表障碍物,起点在左上角(0,0),终点在右下角(4,4)。

GRID = [ [0, 0, 0, 0, 0], [0, 1, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], ]

画地图时有几个小经验。第一,障碍物不要设计成完全包围死路的迷宫,先保证从起点到终点存在一条可达路径,否则智能体练到天荒地老也找不到终点。我做第一版时随便画了几堵墙,结果把路堵死了,训练曲线一片混乱,还以为是参数问题。第二,尽量让障碍物分布有区分度,不要全是直线墙,这样智能体需要学会“绕”,而不是靠一条直线走到终点,demo 的可观察性会好很多。

你也可以在环境代码里写一个简单的可达性检测,用宽搜遍历一遍,如果终点从起点不可达就直接报错,避免把无效地图喂给训练循环。

2.2 状态空间和动作空间的定义细节

状态很好定义,一个格子就是一个状态,但我们在代码里不会用二维坐标直接索引 Q 表,最好把 (row, col) 转换成一维下标,一张 5×5 的地图就是 25 个状态:

def state_id(row, col): return row * COLS + col

动作空间是四个方向:上、右、下、左,在代码里用一个方向增量列表表示,索引 0~3 对应四个动作。这样 Q 表是一张(ROWS*COLS) × 4的二维表,第 state 行、第 action 列,就是 Q(s,a)。

这里有一个很容易忽视的细节:当智能体向上移动时,如果上面是边界或者障碍物,它应该留在原地,而不是穿墙。实现时需要在 step 函数里做一次坐标合法性检查。合法的移动才返回新坐标,不合法的移动返回原坐标,同时给一个额外负奖励。智能体通过这个信号学会“撞墙不好”,慢慢学会避开边界和障碍物。

2.3 奖励设计:整个demo最容易翻车的地方

奖励函数决定了智能体最终长成什么德行。我这一版用的奖励方案是这样的:

  • 到达终点:+10
  • 撞到障碍物:-5,并且留在原地
  • 越过边界:-0.5,并且留在原地
  • 每走一步:-0.1

这个设计有三个层次的目的。终点 +10 是强正信号,让智能体知道目标在哪。撞墙给重罚,让智能体在障碍边缘学会绕行。步数惩罚是让智能体追求尽量短的路线,没有这个惩罚,它完全可以在地图里兜一圈再走到终点,路径看着就像喝醉了。

有一类经典错误我栽过跟头:为了加快训练,在终点附近的几个格子额外给正奖励,结果智能体学到在那些格子附近来回横跳,用最低代价反复吃奖励,看起来训练曲线一路向上,实际上它已经学会钻漏洞了。所以奖励设计的核心原则是:只在真正重要的节点给大额奖励,中间状态尽量不要给奖励,要让智能体自己学会“靠近终点”是通过价值传播体现的。

2.4 回合终止条件与最大步数

一个回合从起点出发,直到发生两件事之一:到达终点,或者步数超过上限。为什么必须设最大步数?因为在一个回合里,如果不设上限,智能体可能在一个局部反复转圈直到永远,经验一直没法推进,整个训练效率会非常低。

最大步数我一般取地图格子数的 10 倍左右。5×5 的地图设 50 步,小地图上足够走出各种绕行路径,又不至于让无意义的荡秋千拖太长。如果训练十几轮步数就下降得很快,说明策略在快速变好;如果步数迟迟降不下来,先检查奖励设计和 epsilon 衰减,再检查是不是地图本身存在死循环陷阱。

3. 训练循环:超参数、代码实现与运行观察

3.1 三个超参数:alpha、gamma、epsilon

Q-learning 的更新公式里只有三个关键参数,但它们的作用各不相同。学习率 alpha 决定每次修正旧估计的幅度,取值通常在 0.1 到 0.2 之间。alpha 太大,Q 值会在真值附近来回震荡不收敛;alpha 太小,信息传播得太慢,训练轮数会成倍增加。我习惯先用 0.1,训练很快找到目标后再调大看是否还能稳住。

折扣因子 gamma 控制智能体对远期收益的看重程度。gamma 越接近 1,智能体越有远见;gamma 太小,智能体眼里只有眼前几步的奖励,在寻路任务中就会显得短视,明明绕两步就能避开障碍,它偏要一头撞墙。我一开始用 0.9,后来调到 0.95,两者最直观的区别就是路径起始段是否会出现“贴着墙绕远路”的倾向。

epsilon 是探索概率,它负责在决策时决定是随机乱走还是选当前最优动作。这三个参数互相作用,单独调一个往往看不出效果,最好把 alpha、gamma 先定下来,再单独调 epsilon 的初始值和衰减曲线。

3.2 epsilon的衰减策略

探索和利用必须平衡。刚开始智能体对世界一无所知,Q 表全是 0,要让它多探索,乱走,把地图各个角落都摸一遍。过了一段时间,它开始知道哪些地方有价值,就应减少随机乱走,多按照策略行动。

我最初把 epsilon 设成 1.0,然后每个回合乘以 0.9 衰减,结果 200 轮之后 epsilon 已经小到 0,完全是贪心策略,后面所有训练都练不到新的状态,策略卡在一个很差的水平上。后来我把衰减因子设成 0.99,同时设一个下限 0.05,确保训练后期还有少量探索,结果路径明显更合理。经验是:衰减不要太快,下限不要设成 0,尤其在表格法里,永远留一点探索余量,可以减少陷入局部最优的概率。

3.3 一个完整的训练主循环

下面这个实现是我实践下来最容易读懂的版本,不依赖任何图形库,直接用纯 Python 和列表实现,你复制到本地就能跑:

import random ROWS = len(GRID) COLS = len(GRID[0]) START = (0, 0) GOAL = (4, 4) ACTIONS = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 上、右、下、左 ALPHA = 0.1 GAMMA = 0.95 EPSILON = 1.0 EPSILON_MIN = 0.05 EPSILON_DECAY = 0.99 EPISODES = 500 MAX_STEPS = 50 Q = [[0.0] * len(ACTIONS) for _ in range(ROWS * COLS)] def state_id(row, col): return row * COLS + col def choose_action(state, epsilon): if random.random() < epsilon: return random.randrange(len(ACTIONS)) max_q = max(Q[state]) best_actions = [a for a, q in enumerate(Q[state]) if q == max_q] return random.choice(best_actions) def step(row, col, action): dr, dc = ACTIONS[action] nr, nc = row + dr, col + dc if not (0 <= nr < ROWS and 0 <= nc < COLS): return row, col, -0.5, False if GRID[nr][nc] == 1: return row, col, -5.0, False if (nr, nc) == GOAL: return nr, nc, 10.0, True return nr, nc, -0.1, False step_records = [] for episode in range(EPISODES): row, col = START done = False steps = 0 total_reward = 0 while not done and steps < MAX_STEPS: state = state_id(row, col) action = choose_action(state, EPSILON) nrow, ncol, reward, done = step(row, col, action) next_state = state_id(nrow, ncol) max_future_q = max(Q[next_state]) current_q = Q[state][action] Q[state][action] += ALPHA * (reward + GAMMA * max_future_q - current_q) row, col = nrow, ncol total_reward += reward steps += 1 EPSILON = max(EPSILON_MIN, EPSILON * EPSILON_DECAY) step_records.append(steps)

学习循环里有一个细节:如果当前这一步动作是撞墙,奖励是 -5,next_state 等于原状态,max_future_q 取的就是当前状态的下一状态最大值,等价于原地打转。撞墙惩罚一定得足够重,否则智能体会练成“贴墙滑行”,在边界上来回蹭着走,就是不走正路。

另一个细节是 choose_action 里做了并列最大值的处理。直接用max(range(4), key=lambda a: Q[state][a])遇到两个 Q 值相等时会固定返回第一个动作,造成隐藏的偏向性,训练出来的路径就可能出现歪歪扭扭的偏好。把它改成把并列最大的动作都挑出来再随机选,行为更干净。

3.4 训练完怎么检查策略

训练结束后,从起点开始按照贪心动作往下走,看看整个轨迹是否合理,是最直接的检验方式。我习惯把每个格子的最佳动作打印成一张箭头地图,一眼就能看清策略长什么样:

arrow = ['↑', '→', '↓', '←'] for r in range(ROWS): line = '' for c in range(COLS): if (r, c) == GOAL: line += 'G' elif GRID[r][c] == 1: line += '█' else: best_action = max(range(4), key=lambda a: Q[state_id(r, c)][a]) line += arrow[best_action] print(line)

第一次训练时我打印这张图,发现靠起点的几个格子箭头指向乱七八糟,远处接近终点的格子已经形成了正确的指向,这就是价值传播还没到位的表现。再跑两百回合,整张图的箭头基本都指向合理路线了,这个过程比任何指标都直观。

4. 调试细节与常见问题排查

4.1 奖励曲线与步数曲线,一定要一起看

很多人训练完喜欢画每回合的总奖励曲线,但 Q-learning 的奖励曲线其实很能骗人。初期探索比例高,随机动作多,每回合总奖励取决于运气,波动极大;后期哪怕策略已经很稳定,总奖励也会因为终点的正奖励和步数惩罚之间的平衡关系,而不一定会单调上升。

我第一次跑时发现奖励曲线一直在下降,差点以为代码写错了,后来把步数曲线也画出来才发现,步数在持续下降,智能体确实在变聪明。因为每多走一步就有 -0.1 的步数惩罚,当它从 40 步优化到 15 步时,每回合的负步数惩罚总量大幅减少,总奖励自然就上去了。所以看曲线时,步数下降才是路径优化最直接的证据,奖励曲线只是辅助判断设置是否合理。

4.2 用热力图观察Q值传播

除了箭头地图,另一个很有用的可视化是热力图。把每个格子的最大 Q 值映射成颜色或数字,打印出来,能看到价值传播的梯度。靠近终点的格子数值最高,向外一圈圈递减,直到起点附近变成很小的正数或负数,这是非常健康的训练信号。

如果发现某个远离终点的区域已经有了异常的高价值,那多半是奖励设计有问题,比如某处有没被发现的反复刷奖励的循环。热力图能帮你快速定位这种“价值异常点”,从而反推到底是哪块地图设计不合理。

4.3 常见问题排查速查表

下面是我在这一次 demo 里反复遇到的现象,整理成一张表,照着排查比自己瞎猜快得多:

现象可能原因解决办法
训练很多回合后仍频繁随机走epsilon 衰减过快,探索窗口太短把衰减因子改大到 0.99,或提高 epsilon 下限
Q 值整体越来越大,不收敛奖励绝对值太大,或 gamma 接近 1 且存在可反复执行的动作降低奖励幅度,检查回合是否总能终止
路径步数先降后升后期探索太少,陷入局部最优提高 epsilon 下限,或增加训练轮数
路径总贴着障碍边缘绕撞墙惩罚不够重,智能体感觉“蹭一下也不亏”把撞墙惩罚从 -1 调到 -5
能走到终点但路径特别怪argmax 并列时固定选第一个动作改成从并列最优动作里随机选一个

这些坑不一定每个你都会遇到,但只要遇到其中之一,先不要急着加大训练轮数,往往都是环境或参数搭配的问题。

4.4 一个隐蔽的坑:小概率随机导致的路径不稳定

还有一个特别容易忽略的问题:即使 Q 表已经收敛,训练出的策略也不一定 100% 稳定。因为 epsilon 下限通常不为 0(比如 0.05),那么每走一步大约有 5% 的概率会突然随机乱走一次。对于一个只测一次或两次的 demo,这可能不算大问题,但如果你在评估路径长度时只跑一个回合,看到的结果可能有很大波动。

正确的评估方式是:让 epsilon=0 跑多个回合,记录路径长度,取中位数。我最后测试时都临时把 EPSILON 设成 0,不让任何随机干扰最终的路径评估,这样看到的数据才反映真实策略水平。

5. 从Q-learning到下一步

5.1 为什么后面一定需要DQN

二维寻路里的一张 Q 表能装下全部策略,但一旦状态空间变大,这条路就走不通。比如把地图扩大成 200×200,Q 表要有四万个状态乘四个动作,虽然内存勉强能放,但每个状态都要访问到足够多次才能学明白,训练量已经没法接受了。更不要说状态本身可能是坐标浮点数,甚至是一张图像,Q 表连枚举都做不到。

那时候的思路是把 Q 函数从“一张大表”换成“一个函数拟合器”,输入状态输出每个动作的评分。神经网络能根据相似状态自动泛化,无需在每个格子都做独立统计。这就是 DQN 的出发点。表格法解决的问题是把机制讲清楚,DQN 解决的问题是在更大尺度上真正落地。

5.2 建议拓展:Sarsa对比与动态地图

如果看完这篇笔记手痒,强烈建议做两个小扩展。第一个是改成 Sarsa 算法,把更新目标从max(Q[ns])换成Q[ns][next_action],也就是用“实际执行的下一个动作”的价值来更新,而不是“下一个状态里最优动作”的价值。在寻路地图上对比一下,就能直观感受到异策略和同策略在训练过程中表现出的差异,尤其是在遇到“悬崖边缘”时,哪条策略更稳健,跑一次就能懂。

第二个是每次重置地图时随机生成障碍物,测试智能体对新地形的适应能力。这个小改动虽然会明显拉长训练时间,但它能让你从“死记硬背路径”的思维定势里走出来,开始考虑泛化和迁移的问题,这些才是强化学习真正的舞台。

这套 demo 做下来,我最大的体会是:Q-learning 的公式很简单,但它背后的异策略更新、探索与利用、奖励传播这些概念,全都要靠亲手改代码、调参数才能真正有体感。不要急着套现成库,哪怕代码写得很朴素,只要是自己一行行敲出来的训练循环,对强化学习的理解就已经比看十遍公式要扎实得多。下一篇笔记我会沿着函数逼近的方向继续展开,把 DQN 的基本流程也补完整,到时候再一起对比看看,表格法和神经网络方法在同一个二维寻路任务里到底差在哪。

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

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

立即咨询