☰
Q-learning入门:用网格世界Python实现强化学习核心算法
2026/10/5 9:54:36 网站建设 项目流程

说起强化学习,很多人第一反应是AlphaGo下围棋、是机器人学走路、是自动驾驶,感觉门槛特别高。但真正上手之后你会发现,几乎所有强化学习教程都会把一个算法放在最前面,那就是Q-learning。它虽然年纪不小,却浓缩了强化学习的全部核心要素:智能体、环境、状态、动作、奖励、价值函数、决策策略,一个都不缺。更关键的是,它的Python实现足够简单,简单到用一个几十行的脚本就能跑起来,特别适合作为第一课。

我打算用一个非常小的网格世界例子,把Q-learning从原理到代码完整走一遍。这中间包括Q表是怎么来的、更新公式每一项到底在算什么、训练过程为什么需要贪心策略、以及新手最容易踩的各种坑。如果你刚开始学强化学习,或者想自己写一个能跑的小例子找找感觉,这篇文章可以让你少走很多弯路。代码不需要什么高深技巧,会Python基础语法就能看懂。

1. 从直觉上搞懂Q-learning在干什么

1.1 它解决的是哪一类问题

在动手写代码之前,先得把Q-learning到底是在解决什么问题说清楚。想象你是一只被困在迷宫里的老鼠,目标是找到出口。你每走一步,可以选择往上、下、左、右四个方向。有的路走到一半是死胡同,有的路通向出口。你一开始并不知道哪条路是对的,只能在走的过程中不断试错:走对了就给点“甜头”,撞墙了就记住“这条路以后少走”。

这就是强化学习最基本的场景:一个智能体(老鼠)与环境(迷宫)交互,通过试错来学习一个最优策略。所谓策略,就是“在什么状态下,选择什么动作”的规则。Q-learning所做的事情,就是通过不断试错,把每个状态下每个动作的“好坏程度”估计出来,最后形成一份攻略,照着攻略走就能拿到最大收益。

你可以把Q-learning想象成自己在玩一款没有攻略的老式RPG游戏。刚进一个新地图你肯定一脸懵,每条路都走走看,踩到宝箱记一笔,被怪打死也记一笔。玩多了之后,潜意识里就会形成“这个岔路口往右走有宝箱,往左走是死路”的判断。Q-learning做的事情,就是把这套潜意识里的“账本”变成一张可以计算和更新的表。

1.2 Q表到底是个什么东西

Q表,就是Q-learning用来记录经验的核心数据结构。它的行是状态,列是动作,表格里的每个值Q(s, a)表示“在状态s下执行动作a,之后所能获得累积奖励的期望”。注意“之后”这两个字,它不单指执行完动作立即拿到的奖励,还包括后续所有步骤里可能拿到的奖励,只是越往后要打越大的折扣。

拿上面的迷宫例子来说,假设迷宫有9个格子,每个格子是一个状态,每个状态下有4个动作可选,那Q表就是一个9行4列的矩阵。初始时所有格子全是0,表示“我对这个世界一无所知”。训练过程中,每走一步就更新对应的那一个格子,更新的方向是让Q值越来越接近真实的“好坏程度”。

我见过不少初学者对Q表产生误解,觉得Q(s, a)就是“在状态s下执行动作a立即得到的奖励”。这是不对的。立即奖励只是它的一部分,Q值还包含了对未来收益的估计。打个比方,你上班路上有一站地铁特别挤,但坐到那站能换乘到直达公司的线路,那你不会因为“挤”这个即时体验就放弃它,因为你清楚后续换乘带来的收益更大。Q值就是这种综合判断。

1.3 奖励、折扣因子和“眼前的诱惑”

要让Q-learning工作,环境得给智能体反馈,这个反馈就是奖励(reward)。奖励可以正可以负,正的表示这件事做对了,负的表示这件事做错了。还是拿走迷宫举例,可以这样设奖励:到达终点+10,撞墙-1,普通移动一步-0.1。每步扣0.1是为了鼓励智能体早点到终点,避免它磨磨蹭蹭绕远路。

接下来引入一个重要的参数:折扣因子γ(gamma),取值在0到1之间。它的作用是对未来奖励打折。γ小的时候,智能体更看重眼前利益;γ大的时候,智能体更愿意为了长远目标放弃眼前的即时奖励。γ=0意味着完全只看下一步,γ=0.9意味着未来10步的奖励都已经很重要了,γ趋近1则意味着几乎不对未来做折扣。

为什么非要折扣不可?一个现实原因是环境的不确定性,你越往后估计越不可靠,与其相信未来不如优先相信眼前。另一个原因是数学上的收敛性,无限时间步的累积回报如果不打折会趋于无穷大,表格里的值根本没法稳定下来。实际做项目的时候,γ通常取0.9到0.99之间。如果任务本身步数很短,比如几步以内就能结束,那γ可以设小一些,甚至可以取0。

Q-learning的核心更新公式长这样:

Q(s, a) ← Q(s, a) + α [r + γ * max_{a'} Q(s', a') - Q(s, a)]

这个式子看起来唬人,其实拆开就三个部分:当前估计Q(s, a),实际观察到的新信息r + γ * max Q(s', a'),以及学习率α。其中r是执行动作后获得的立即奖励,s'是下一步进入的状态,max_{a'} Q(s', a')表示在下一个状态里选择价值最高的那个动作对应的Q值。整个括号里的东西叫时序差分误差(TD error),它衡量的是“我们原本以为这个动作有多好”和“实际走一步之后发现这个动作有多好”之间的差距。Q-learning就是不停地用这个差距来修正Q表中的值,一次修正一小步。

2. 设计一个适合入门的小例子

2.1 为什么选网格世界而不是CartPole

Q-learning的经典实验环境其实有好几个,比如OpenAI Gym里的CartPole(小车平衡杆)、FrozenLake(冰冻湖面滑动)、Cliff Walking(悬崖行走)等。但我个人强烈建议初学者自己写一个网格世界,而不是直接调Gym库。原因很简单:自己写环境能强迫你去理解状态转移、奖励、终止条件这些底层概念。用Gym的话,你调一下step方法就完事了,环境内部发生了什么你是不清楚的,出了问题也不知道从哪排查。

网格世界就是一张方格地图,智能体从起点出发,目标是到达终点,中间可以设置一些障碍物来增加难度。它的状态表示起来特别直观,就是格子的坐标;动作也好理解,就是上下左右四个方向。整个环境的逻辑不过二三十行代码,一个下午就能写完并且弄懂。

FrozenLake那种环境虽然也很经典,但它多了“滑动”设定,走了不一定到得了想去的格子,这对刚接触强化学习的人来说容易产生混淆。网格世界的状态转移是确定性的,说你往右走就是往右走,这样你在验证算法正确性的时候脑子不用多转一道弯。先把确定性环境搞明白,再去碰随机环境,学习曲线会平滑很多。

2.2 游戏规则与奖励设置

我设计的这个网格世界长这样:5行5列一共25个格子,起点在左上角(0,0),终点在右下角(4,4),中间摆了3个障碍物,分别在(1,1)、(2,3)、(3,1)。智能体每一步可以选择往上、下、左、右四个方向移动,不能斜着走。移动到边界上的时候,如果继续往外走,会留在原地并受到-1的惩罚;踩到障碍物也一样,原地不动且罚1分;到达终点奖励+10,整个回合结束;其他正常移动的格子每走一步扣0.1。

这里每一步扣0.1是很有讲究的。如果不扣,智能体走一条绕远的路和走一条近路,拿到的总奖励是一样的,反正最后都拿到10分,那它就没有动力去找最短路径。加了每步惩罚之后,路径越长总奖励越低,智能体就会在训练中逐渐倾向于选择步数更少的路线。这就是奖励设计的微妙之处,它不直接规定“你必须走最短”,而是通过奖励数字的变化引导智能体自己总结出这个结论。

障碍物放在这三个位置也考虑过。我把它们放在(1,1)、(2,3)、(3,1),目的是逼着智能体绕路,而不是一条直线从左上角冲到右下角。走到(1,1)附近会发现此路不通,必须绕开;走到(2,3)附近也得绕;走到(3,1)又得绕。这样训练出来的路径会明显呈现出“遇障绕行”的特征,视觉效果也能说明算法确实学到东西了。你可以随意改这些障碍物坐标,代码里都有自适应逻辑。

2.3 直观理解最优路径是怎么形成的

训练起步阶段,Q表全是0,智能体基本是在瞎走。它靠ε-greedy策略控制探索,有一定概率随机选动作,这个概率一开始很高。走了几步之后,它可能偶然走了一条能到终点的路径,终点+10的奖励就会顺着更新公式一层层往前面回溯,这条路前面的状态Q值也会跟着被抬高。

下一次再走到那个岔路口,智能体查Q表发现某个方向的值更高,就更倾向往那边走。随着训练回合越来越多,这个“高值路径”会被反复强化,而其他乱走的路径因为拿到的奖励低、惩罚多,Q值慢慢被压下去。最终整张Q表会稳定在一个状态:每个状态都有一个明显高于其他选项的动作。你把Q表导出来看,从起点一路取最大值对应的动作,就能走出一条从起点到终点的路径,这条路就是智能体学到的策略。

这个过程特别像小孩子学走路:一开始东倒西歪,走一步摔一跤,但每次走到爸妈那边都会被接住、被夸一句,于是“走向爸妈”这个动作的价值被不断强化,慢慢就走得越来越直。

3. Python代码一步一步实现

3.1 环境准备

写代码之前先把工具准备好。这个例子只需要Python 3.7以上的环境,外加numpy和matplotlib两个库。numpy用来做Q表的矩阵运算,matplotlib用来画训练曲线和可视化路径。如果你只是看逻辑不想画图,那连matplotlib都可以不装。

安装命令很简单:

pip install numpy matplotlib

注意如果你的机器上同时装了多个Python版本,要确认pip是对应你当前使用的那一个。装完之后写一句python -c "import numpy; print(numpy.__version__)"验证一下,能看到版本号就说明环境没问题。跑代码的时候建议在项目的venv虚拟环境里跑,避免污染系统Python环境,这算是我踩过比较多的坑,后面在常见问题里细说。

3.2 环境类:定义状态、动作和奖励

整个代码我拆成两部分:第一部分是环境类GridWorld,第二部分是Q-learning的训练逻辑。先看环境这一段。

import numpy as np import matplotlib.pyplot as plt class GridWorld: def __init__(self, size=5, obstacles=None): self.size = size self.obstacles = obstacles if obstacles else [(1, 1), (2, 3), (3, 1)] self.start = (0, 0) self.goal = (4, 4) self.state = self.start def is_valid(self, pos): r, c = pos if r < 0 or r >= self.size or c < 0 or c >= self.size: return False if pos in self.obstacles: return False return True def reset(self): self.state = self.start return self.state def step(self, action): moves = {0: (-1, 0), 1: (0, 1), 2: (1, 0), 3: (0, -1)} r, c = self.state dr, dc = moves[action] nr, nc = r + dr, c + dc next_pos = (nr, nc) if not self.is_valid(next_pos): return self.state, -1.0, False self.state = next_pos if self.state == self.goal: return self.state, 10.0, True return self.state, -0.1, False

这里动作编号我规定得很明确:0上、1右、2下、3左。step方法返回三个值:下一步状态、奖励、是否结束。撞墙或者其他无效移动返回当前状态而不是目标位置,奖励给-1,这样智能体就会慢慢学会哪些方向不能走。到达终点返回+10并且标记done为True,这一局就算完了。

有个小设计值得说一下:普通移动的奖励是-0.1而不是-1。如果你设成-1,那么每走一步都要付出不小的代价,智能体可能会倾向于原地撞墙来结束回合,虽然它不能结束,但训练过程会变得很慢。设成-0.1之后,走一步的代价和撞墙的惩罚拉开了一个量级,智能体能在“尽量少走”和“避开障碍”之间找到更好的平衡。

3.3 训练核心:Q表、策略和更新公式

环境写好了,接着就是Q-learning的训练流程。这里我先把里面会用到的两个函数写出来:一个是动作选择,一个是Q值更新。

def choose_action(state, Q, epsilon): r, c = state if np.random.rand() < epsilon: return np.random.randint(4) return int(np.argmax(Q[r, c])) def q_learning_update(state, action, reward, next_state, Q, alpha, gamma, done): r, c = state nr, nc = next_state if done: target = reward else: target = reward + gamma * np.max(Q[nr, nc]) Q[r, c, action] += alpha * (target - Q[r, c, action])

choose_action实现的是ε-greedy策略:以epsilon的概率随机挑一个动作,这个概率下智能体会去探索未知区域;剩下的概率选择当前Q表中价值最大的动作,也就是利用已有经验做出最优选择。这个策略是整个算法能成功的发动机,没有它,算法就变成纯贪心,永远学不会新东西。

q_learning_update是公式的代码化。注意我在done为True的时候直接令target=reward,没有再往后面加未来回报。这是因为智能体已经到达终点,游戏结束,后面不存在任何状态了。如果你在终止状态下还去取next_state的max Q值,会因为状态不存在而索引报错,这也是很常见的一个坑。另外Q表我初始化成zeros,也就是所有状态动作的初始价值都是0。对这个小环境来说这是没问题的,但对一些奖励比较稀疏的复杂任务,初始化为0可能导致智能体完全不想探索,这时候可以用乐观初始化的技巧,比如把Q表初始化为一个比较大的正数,诱导智能体先到处走走。

接下来是训练主循环。训练回合数我设成500轮,每轮从起点开始一直走到终点或者超过最大步数为止,每轮结束记录一下总奖励。epsilon从1.0开始慢慢衰减,最终降到0.05。

env = GridWorld() Q = np.zeros((env.size, env.size, 4)) episodes = 500 alpha = 0.3 gamma = 0.9 epsilon_start = 1.0 epsilon_end = 0.05 epsilon_decay = (epsilon_start - epsilon_end) / (episodes * 0.7) episode_rewards = [] for episode in range(episodes): state = env.reset() epsilon = max(epsilon_end, epsilon_start - epsilon_decay * episode) total_reward = 0 done = False while not done: action = choose_action(state, Q, epsilon) next_state, reward, done = env.step(action) q_learning_update(state, action, reward, next_state, Q, alpha, gamma, done) state = next_state total_reward += reward episode_rewards.append(total_reward)

我看了下这个代码,有一个可以优化的地方:正常情况下一个episode内部还有个可能超过最大步数的保护逻辑,防止agent因为某些bug永远陷入死循环。可以在while not done里面加一个steps计数器,超过比如100步就强制break。网格世界比较小,不加也不至于出事,但如果之后你把网格改大了,建议加上这个保护。

训练结束后,我们来把学到的策略可视化出来。先写一个函数根据Q表生成从起点到终点的路径:

def extract_policy_path(Q, env, max_steps=50): path = [env.start] state = env.start moves = {0: (-1, 0), 1: (0, 1), 2: (1, 0), 3: (0, -1)} arrows = {0: "↑", 1: "→", 2: "↓", 3: "←"} for _ in range(max_steps): if state == env.goal: break r, c = state action = int(np.argmax(Q[r, c])) dr, dc = moves[action] nr, nc = r + dr, c + dc if env.is_valid((nr, nc)): state = (nr, nc) path.append(state) else: break return path

这个函数其实就是在训练结束后,用纯贪心的方式沿着Q表指出的最优动作一路往前走。如果你训练得够好,它应该能顺利到达终点。

3.4 加上可视化看策略收敛

光看数字比较枯燥,我习惯把三个东西画出来看一眼:到达终点的路径长什么样、每回合总奖励的变化曲线、以及Q值热力图。这三张图能让你一眼看出训练效果。

plt.figure(figsize=(15, 4)) plt.subplot(1, 3, 1) grid = np.zeros((env.size, env.size)) for (r, c) in env.obstacles: grid[r, c] = -1 grid[env.goal] = 1 plt.imshow(grid, cmap='coolwarm', vmin=-1, vmax=1) for i, p in enumerate(path): if i > 0 and i < len(path) - 1: plt.text(p[1], p[0], "o", ha='center', va='center', fontsize=14) plt.subplot(1, 3, 2) plt.plot(episode_rewards) plt.title('Episode Rewards') plt.xlabel('Episode') plt.ylabel('Total Reward') plt.subplot(1, 3, 3) q_max = np.max(Q, axis=2) q_max[env.goal] = 10 plt.imshow(q_max, cmap='viridis') plt.colorbar() plt.title('Max Q Value per State') plt.tight_layout() plt.show()

热力图就能很直观地看出Q值分布。正常情况下,终点附近的Q值最高,然后向四周逐渐衰减,起点附近的Q值相对比较低。这是因为从起点走到终点还有很长一段路,累积折扣回报自然不如终点附近高。你如果有兴趣,还可以把Q表直接打印出来,看看每个状态下的四个动作值分别是多少,那份数据能告诉你很多细节。比如某些格子Q表里上下两个方向都很高,说明这两个方向都能走通,只是最优方向稍微高一点。

我还想问一个问题:那这个任务能不能收敛?能。500轮在这个简单网格世界里已经绰绰有余了,通常100轮左右策略就已经比较稳定了。不同随机种子可能略有差异,但最终学到的路径是一致的。我建议你把episodes改成1000再跑一次,观察训练曲线是不是更平滑。如果改成5000轮会明显看到曲线在大约200轮之后几乎是一条水平线,说明算法已经完全收敛了。

4. 跑起来之后:常见问题与调参心得

4.1 Q值为什么一直震荡不收敛

这个是我在带新手的时候被问得最多的一个问题。明明代码照着写了,也跑了挺多轮,但Q值就是不消停,忽高忽低,奖励曲线抖得跟心电图似的。这里有几个非常典型的原因。

第一个原因是学习率α太大。α相当于你每次更新时对新信息采取多大步长,如果α是1.0,那就等于每次完全相信新信息,旧经验一笔勾销,这样很容易震荡。一般α取0.1到0.5之间比较好。我这套代码里取0.3,实测效果稳定。你也可以试试α从0.9一路降到0.1,观察Q值曲线平滑度的变化,感受会非常直观。

第二个原因是epsilon衰减太慢。训练后期epsilon还很高,等于智能体时不时还要随机乱走,这当然会导致Q值抖动。解决方法是让epsilon快速降到较低水平。我代码里用的是线性衰减,在训练总回合数的70%时已经降到了最终值。你也可以用指数衰减,或者干脆在训练过程中做分段衰减:前100轮epsilon=1.0,中间300轮线性衰减到0.1,最后100轮保持0.05。这个策略在很多任务里都能用。

第三个原因是你环境里的奖励设得不够有区分度。如果走每一条路的奖励都差不多,Q表就不容易拉开差距,表现出来就是怎么训练都不稳定。这时候你应该回头检查一下奖励设计,看不同行为方向是否真的对应了明显不同的奖励信号。

4.2 ε-greedy的ε到底怎么调

ε是强化学习里最典型的“探索-利用困境”的体现。它控制的是“随机乱走”和“按经验走”的比例。初学者最容易犯的错是把ε固定在一个值上从头用到尾。如果ε一直很大,智能体永远都在乱逛,学不到稳定策略;如果一开始就很小,智能体又缺少探索,可能永远发现不了终点在哪儿,Q表里全是0。

比较合理的做法是训练初期让ε接近1,让智能体充分探索地图,然后随着训练推进逐渐减小ε,让智能体从“探索者”慢慢变成“老司机”。我见过的最朴素的衰减方式就是:ε = max(最小ε, 初始ε - 衰减率 × 当前回合数)。你也可以用指数衰减:ε = 0.99 ^ episode,这样下降速度是先快后慢。

具体衰减速率取决于你的任务复杂度。任务越复杂,需要探索的空间越大,ε在较高水平维持的时间就越长。如果你发现智能体最终找到了终点,但路径绕来绕去不是最优的,很可能是ε衰减太快,后期没有机会去尝试更短的路径。反过来如果发现训练曲线一直很乱,那就是ε衰减太慢。

4.3 学习率α和折扣因子γ的搭配经验

α和γ这两个超参数在Q-learning里是非常关键的。我之前说过,α控制更新幅度,γ控制对未来回报的重视程度,它们其实是相互影响的。如果你γ取得很大(接近1),那么未来很长一段时间的回报都会被纳入考量,Q值的传播范围很广,这时候α就得稍微小一点,不然Q值会因为传播太猛而震荡。如果γ取得比较小(比如0.5),那智能体基本只关心近期的回报,Q值更容易稳定,α可以稍微大一点加倍学习速度。

有一个常见的误区是认为γ必须取到0.99以上才算合理。这个真不一定。对于网格世界这种回合较短的任务,γ取0.9就已经很够用了。如果任务里每回合最多只有十几步,那γ取0.95和0.99的区别其实很小。你用下面的公式估算一下就能明白:γ^10在γ=0.9时是0.35,在γ=0.99时是0.9,差别确实存在,但如果任务本身只有几步,乘上很小的回报之后差异就微乎其微了。

我个人的习惯是先把γ固定在0.9,调α和ε;等策略基本收敛了再微调γ。顺序很重要,不要一上来四个参数一起调,不然你根本不知道是哪个参数起了作用。

4.4 状态和动作表示这类新手最容易踩的坑

说一个我教过很多学生踩过的坑。他们用numpy数组来存Q表,但维度和状态访问方式搞混了。我说的这个坑,是写Q[state]的时候,state是一个元组,比如(2, 3),但数组维度是(size, size, 4),如果你写成Q[state]就等于是Q[(2, 3)],在numpy里这会被解释成Q[2, 3],得到的是一个长度为4的向量。这其实没问题,但如果你写成Q[state[0], state[1]]也可以。麻烦的是搞混了维度,有的人会把Q表开成(size*size, 4),然后直接用Q[state]访问,就报索引错误了。

另一个很容易忽略的地方是step方法中撞墙的返回。很多人的第一版代码里,撞墙之后状态一直变化,导致Q值更新时用错了next_state。撞墙的本质是“动作无效,状态不转移”,所以返回值里的next_state必须是当前状态,而不是你想去的那个位置。如果搞错了,智能体相当于可以瞬移到墙外面,整个更新逻辑就全乱了。写完之后记得用几个手工测试用例验证一下:比如在起点往上走,看返回的状态是不是起点。

再有一个容易出问题的点是动作方向编号与坐标变换的一致性。如果你把0定义成“上”,那么moves字典里就必须写0: (-1, 0),因为行号减1代表往上。这个坐标方向搞反了,智能体还是能收敛,但学出来的路径会和你预想的不一样,排查起来特别费劲。我给自己的建议是,写环境的时候把方向映射写成全局常量放最前面,后面所有地方都用这个常量。

4.5 环境依赖和包管理的问题

虽然这不是Q-learning本身的问题,但每次带项目都会遇到环境相关的报错。最常见的是pip install numpy装好了,但代码里import numpy仍然失败,一般都以为是装错了框架或者没安装成功,其实多半是当前python解释器的路径和pip对应的解释器不一样。你可以在终端里先跑which python和which pip,看看两者指向的是不是同一个路径。如果不是,要么用python -m pip install,要么切换到正确的虚拟环境里运行。

另外建议把项目放在一个独立的目录,用python -m venv .venv创建虚拟环境,然后source .venv/bin/activate激活。这样不仅不会弄乱系统的Python环境,跑数据分析、深度学习那些依赖冲突严重的项目时也能隔离得更好。

5. 从这个例子出发还能往哪走

5.1 表格型Q-learning的边界在哪里

你如果把这个网格世界跑熟练了,会慢慢体会到Q-learning的一个天然瓶颈:Q表是拿“状态×动作”当索引的二维表格,它要求状态必须是离散且有限的。一旦状态是连续的,比如小车的位置是多少厘米、速度是多少米每秒,或者状态空间特别大,比如高清图像里的每个像素组合,那张表就彻底没戏了。围棋的状态数是10的170次方数量级,拿表格存,多少个内存都不够用。

表格型Q-learning只能解决小规模离散状态问题,这是它最核心的边界。突破这个边界的方法是让Q值不再查表得到,而是用一个函数去近似:输入状态和动作,输出一个Q值。这个函数可以用神经网络来拟合,这就是Deep Q-Network(DQN),也就是把Q-learning从“查表版”升级成“深度学习版”的做法。你在很多游戏AI的文章里看到“AI自己学会了打游戏”,底层原理多半就是这个。

不过我要提醒一句:不要急着跳到DQN。DQN涉及经验回放、目标网络、双Q网络等一堆工程技巧,没有扎实的表格型Q-learning基础直接上手,很容易被各种细节淹没。先把这个小例子搞透,理解清楚Q值如何通过时序差分更新、ε-greedy如何控制探索,后面学DQN才不会被搞得晕头转向。

5.2 把它扩展成更有意思的项目

这个网格世界框架潜力不低,你可以在它上面做很多扩展。比如把5×5改成8×8,多放几个障碍物,你会发现Q表规模从25×4变成了64×4,训练需要的回合数明显增加,这就能直观体会状态空间增大带来的学习难度提升。再比如把障碍物改成随机生成,每次reset的时候重新摆障碍物,这样智能体要学的不是某一条路径,而是一套面对不同地形都能尽快到达终点的策略。

还有一个方向是改奖励设计。你可以把奖励改成到达终点+100、撞墙-10、每步-1,看看策略会不会有变化;也可以设一个“毒区”,踩到它每回合结束扣5分,但不会立刻结束本回合,这样智能体需要在“绕远路避开毒区”和“抄近路吃惩罚”之间做权衡,学出来的策略更有意思。

如果还想更进一步,可以把环境换成OpenAI Gym里的FrozenLake。这个环境是滑动的,动作和实际移动方向不完全一致,但它同样是离散状态离散动作,Q-learning可以直接套用。跑通之后你对随机环境下Q-learning的鲁棒性就会有更感性的认识。再往后,你可以去翻一翻SARSA算法,它表面上和Q-learning就差一行代码,但一个on-policy、一个off-policy,学出来的策略风格差别很大。对比着看这两个算法在Cliff Walking这种环境里的表现,是加深理解最好的方法之一。

6. 写在最后的一些体会

如果你完整跟着走了一遍,现在应该已经有了一个能跑、能看、能调参的Q-learning实现。我个人学这个算法的过程里,最大的收获其实不是背熟公式,而是亲手把环境写出来之后,对“奖励设计”这件事产生了敬畏心。环境里一个奖励数字的改动,可以让智能体学出完全不同的行为,这种因果关系的直观感受是看书看不出来的。建议你拿到这段代码之后,别只是跑通就完事,大胆改一改:把每步惩罚从-0.1改成-1,把γ从0.9改成0.5,把ε衰减速度调快一倍,每一个改动都跑一遍,然后去看看Q表长什么样、路径有什么变化、训练曲线是否平滑。这么做几次之后,你对Q-learning的直觉会比读十篇教程都管用。

顺带分享一个我调试时的小技巧:每一轮训练结束,都打印一下当前轮的总奖励和是否到达终点。如果前几十轮一直到不了终点,说明探索还不够,把ε衰减调慢一些;如果到了终点但后面还会偶尔走错,说明ε降到太低或者学习率太大。这样用指标反推问题,比盯着Q表猜来猜去高效得多。希望这个例子能成为你强化学习路上的第一个踏实台阶。

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

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

立即咨询