
强化学习Reinforcement Learning大概是2024年最让人“又爱又恨”的方向。爱的是它的想象力——游戏AI能打出超越人类极限的操作、机器人在仿真环境里自己学会行走、推荐系统可以根据实时反馈持续优化策略恨的是它的学习曲线实在陡峭数学推导一层套一层代码实现看着简单跑起来全是问题很多人在“看过无数教程”之后还是写不出一个真正能学习的demo。这篇文章就是这个系列的第一篇不搞虚的目标就一个把强化学习从“听着很高深”拉回到“自己能亲手跑起来”。如果你是Python基础还不错的开发者或者学过一点机器学习但对强化学习始终停留在“听说过Q-learning”这个层面那这篇很适合你。我会从2024年的视角重新梳理强化学习最核心的理论骨架——马尔可夫决策过程、回报、价值函数、Bellman方程然后带大家用几十行代码从零手写一个能收敛的Q-learning算法去解决一个网格世界寻宝问题。先把地基打牢后面再接触DQN、策略梯度那些大家伙才不会一头雾水。1. 内容怎么组织这个系列到底要做什么1.1 为什么偏偏从网格世界切入很多人学强化学习喜欢一上来就挑战CartPole、Atari游戏甚至机器人控制结果被环境接口、奖励设计、训练稳定性这些杂七杂八的问题淹没了最后连核心算法都没看清楚。我强烈建议反过来先用一个你能完全理解的环境把核心机制吃透再去碰复杂系统。网格世界Gridworld就是这个“能吃透的环境”。它只有十几个状态、四个动作Q表展开来看就是一个小矩阵每一步训练发生了什么完全可视化。训练到一半你就能直观地看到哪些状态的值在涨、哪些动作被偏好这种“可解释、可调试”的特性让它成为学习RL时的最佳第一站没有之一。1.2 理论讲到什么程度才算“够用”这个系列不追求把所有数学公式都推导一遍也坚决避免把算法讲成只剩代码注释。我的原则是八个字够用且讲明。所有理论讲解都围绕一个问题你写的每一行代码到底在解决什么比如Bellman方程我会告诉你它的直观含义、为什么Q-learning的更新公式长成那个样子、如果你把符号搞反了会发生什么。数学在这里是工具而不是门槛理解它背后的直觉比死记公式有用得多。1.3 学习这个系列需要哪些基础说实在的门槛比你想象中低。需要你会Python基础语法能看懂numpy的基本操作理解简单的概率概念比如期望值是什么。如果你接触过一点机器学习的损失函数、梯度下降之类的概念会有帮助但不是必须。线性代数和微积分在入门阶段基本用不上用到的时候我会现场解释。所以不要被“强化学习”这四个字吓退它本质上就是一套教智能体“通过试错学会做决策”的框架跟人学游泳、学骑车的逻辑是一样的。2. 核心理论拆解从MDP到Bellman方程2.1 马尔可夫决策过程强化学习的骨架强化学习的几乎所有问题都能抽象成马尔可夫决策过程MDP。MDP由五样东西组成状态集合S、动作集合A、状态转移概率P、奖励函数R、折扣因子γ。是不是看着很抽象我们用网格世界来对照一下就清楚了。假设有一个4×4的网格左上角是起点右下角是终点宝藏。状态集合S就是我们可能站在的所有格子位置动作集合A就是“上、下、左、右”四个方向。当你执行某个动作后会从当前格子转移到下一个格子这个转移规则就是状态转移概率P——在这个确定性环境里P就是“往右走一定会到右边那个格子”。每到达一个位置环境会给你一个即时奖励R这里是到达终点给1其它情况给0。为什么要这个抽象框架因为它把所有决策问题都统一成了一个描述模型不管你是做游戏AI、机器人控制还是推荐系统都能映射成这几个要素。理解了MDP你就抓住了强化学习的“语法”。2.2 折扣因子γ为什么未来的奖励要打折现在说MDP里最需要一个“直觉解释”的参数——折扣因子γ取值在0到1之间。为什么要给未来的奖励打折两个原因。第一未来是不确定的你今天承诺给我100块和一年后承诺给我100块价值显然不一样。第二从数学上考虑智能体在一个无限长的任务里如果所有奖励都不打折总回报可能无穷大那比较策略好坏就无从谈起了。γ的取值直接影响智能体的“眼光”γ接近1意味着智能体很有远见愿意为长期收益放弃眼前利益γ接近0则意味着智能体只看重眼前的即时奖励是个“短视鬼”。实际操作中0.9到0.99是最常见的区间具体取多少要看任务的时间跨度。网格世界这种短任务0.95是一个稳妥的起点。2.3 价值函数V与动作价值Q拿什么衡量“好”有了奖励我们还需要一个衡量“一个状态值多少”的指标这就是价值函数。状态价值函数V(s)表示从状态s出发按照某个策略行动未来能获得的期望总回报。注意这里强调的是“期望”也就是把所有随机性都考虑进去的平均水平。但只看状态还不够很多时候我们想知道“在这个状态下我该选择哪个动作”。这时就需要另一个函数动作价值函数Q(s, a)表示在状态s下执行动作a然后继续按照某个策略走下去能获得的期望总回报。这两个值的关系可以这样理解V是站在路口看整个前景Q是走到其中一条路上去感受前途。强化学习的终极目标就是找到最优策略——让每个状态下都能选出Q值最高的动作。记住这句话后边的算法本质上都在做这一件事。2.4 Bellman方程全篇的含金量所在Bellman方程是整个强化学习最重要的公式它用一个简洁的递归关系揭示了价值函数的本质。话不多说V(s)的Bellman方程长这样V(s) E[R γ * V(s)]用大白话翻译就是当前状态的价值等于立即能拿到的奖励R加上未来状态价值的折扣期望γV(s)。这个递归结构非常优雅——它把“一个长期问题”拆成了“现在的问题 下一步的问题”而下一步的问题和自我定义形式相同。Q(s, a)的Bellman方程也同理多考虑一个动作的影响。Q-learning的更新公式正是从这个方程里“抽”出来的Q(s, a) ← Q(s, a) α * [r γ * max_a Q(s, a) - Q(s, a)]这个公式后面会手写实现现在先说直觉中括号里那一坨是“当前估计”和“真实目标”之间的差距——r γ乘以下一状态最优Q值是用新信息估计出来的目标值减去当前Q值就是误差项。α学习率控制我们迈多大步子去修正这个误差。整个Q-learning就是不断沿着误差方向修正Q表的过程。3. 探索与利用强化学习独有的“两难”3.1 如果只选最优你永远学不会最优强化学习和监督学习最大的不同在于它没有现成的“正确答案”只能靠试错得到反馈。而试错就引出了一个绕不开的矛盾探索exploration与利用exploitation的取舍。什么叫“利用”就是你根据当前学到的经验选那个Q值最高的动作确保当下收益最大化。什么叫“探索”就是你明明知道某个动作当前看起来不是最优还是故意去试试别的因为经验还不够可能低估了一个真正的好动作。这里有个很经典的陷阱如果一开始就“贪心”地只选择Q值最大的动作你会发现Q表永远不会收敛到最优。因为某些动作因为没有被探索Q值一直停留在初始值导致它们永远不会被选中形成恶性循环。这就像一个人只吃自己吃过的菜永远发现不了更好吃的菜。3.2 ε-greedy策略与ε衰减怎么配最经典的解决方案是ε-greedy以ε的概率随机选择一个动作探索以1-ε的概率选择当前Q值最大的动作利用。参数ε怎么定我的习惯是分阶段来。训练初期ε可以设到0.3甚至0.5让智能体大胆跑来跑去积累经验随着训练推进逐渐衰减到0.01、0.05让智能体越来越“自信”更多地利用已经学到的经验。衰减方式也讲究线性衰减和指数衰减都可以。指数衰减可以用一个分段函数实现前几百个episode保持一个较高的探索概率之后再快速衰减。不要一上来就用很小的ε那等于没探索Q表根本长不出好形状来。3.3 比ε-greedy更“聪明”的几种探索方式ε-greedy简单好用但确实有点“笨”——它是盲目的随机探索不考虑哪些动作可能更有信息量。这里顺带提一下更智能的方案后面系列会详讲。一种是置信上界UCB方法它基于一个直觉“被尝试次数越少的动作不确定性越大越值得优先探索”。另一种是汤普森采样Thompson Sampling它是从一个概率分布里采样动作自动平衡探索与利用。这些方法在复杂环境里往往更高效但入门阶段完全不需要ε-greedy足够让你把所有核心概念跑通。4. 2024年的Python环境到底怎么搭4.1 工具链与库的选择说明2024年做强化学习Python生态已经相当成熟。如果只是在本地学习和调参不需要上GPU。核心依赖其实就两个numpy负责矩阵运算matplotlib负责可视化。想用标准环境库的话现在社区最主流的是Gymnasium——它是当年OpenAI Gym项目被接手后持续维护的版本接口更稳定文档也更清晰。用哪个Python版本我建议3.10或3.11。3.12也不是不行但某些老库的兼容性偶尔会出问题新手没必要在这种地方消耗时间。环境管理强烈建议用虚拟环境别一股脑装进全局环境里后面项目多起来会非常难受。4.2 最小安装与快速验证创建一个虚拟环境然后安装依赖命令大概长这样python -m venv rl_env source rl_env/bin/activate # Windows下用 rl_env\Scripts\activate pip install numpy matplotlib gymnasium装完之后可以用一段非常短的代码测试整条链路是否正常。以Gymnasium自带的经典控制环境为例import gymnasium as gym env gym.make(CartPole-v1, render_modehuman) obs, info env.reset() for _ in range(50): action env.action_space.sample() # 随机动作 obs, reward, terminated, truncated, info env.step(action) if terminated or truncated: obs, info env.reset() env.close()能弹出环境窗口并正常渲染说明你的环境没问题了。注意新版接口返回的是五元组比老版本多了一个truncated标志用来区分“任务终止”和“超时截断”这个细节挺重要老教程不更新会在这里踩坑。4.3 写在环境搭建之后的小建议不少人会花大量时间折腾复杂的深度学习框架和GPU环境结果卡在安装环节就劝退了。我的建议是入门阶段越朴素越好。一个小网格世界numpy就完全能搞定根本不需要引入PyTorch。等后面真到了该上神经网络的时候再顺势加框架那一刻的安装配置压力也小得多。5. 实操从零手写Q-learning求解网格世界接下来进入重头戏——我们用纯numpy实现一个完整的Q-learning算法解决一个4×4网格寻宝问题。整个代码量不到100行但麻雀虽小五脏俱全所有强化学习核心要素都在里面。5.1 第一步定义GridWorld环境虽然Gymnasium里有现成的网格世界环境FrozenLake但我更推荐教学场景下自己写一个环境类。只有亲手写过环境你才会真正理解“智能体-环境交互”这个抽象概念到底指什么。import numpy as np class GridWorld: def __init__(self, size4): self.size size self.start (0, 0) self.goal (size - 1, size - 1) # 动作0上, 1下, 2左, 3右 self.actions [0, 1, 2, 3] def reset(self): self.state self.start return self.state def step(self, action): i, j self.state if action 0: i max(0, i - 1) elif action 1: i min(self.size - 1, i 1) elif action 2: j max(0, j - 1) elif action 3: j min(self.size - 1, j 1) self.state (i, j) if self.state self.goal: reward 1.0 done True else: reward 0.0 done False return self.state, reward, done, {}解释几个设计细节状态用元组(i, j)表示代码直观。碰到边界时max和min操作让智能体停在原地而不是“出界”。奖励设计得很稀疏只有到达终点才给1其它位置都是0。稀疏奖励任务更难学但对理解Q-learning特别有效。5.2 第二步Q表的初始化与更新逻辑Q表的形状应该是(状态数, 动作数)这里状态数就是16动作数是4。初始值我建议设成全0。有人说初始值也可以设成偏大的数能鼓励探索那是另一个技巧入门阶段全0即可。class QLearningAgent: def __init__(self, env, alpha0.1, gamma0.95, epsilon0.3): self.env env self.alpha alpha # 学习率 self.gamma gamma # 折扣因子 self.epsilon epsilon # 探索率 self.q_table np.zeros((env.size * env.size, len(env.actions))) def state_index(self, state): return state[0] * self.env.size state[1] def choose_action(self, state): idx self.state_index(state) if np.random.random() self.epsilon: return np.random.choice(self.env.actions) return int(np.argmax(self.q_table[idx])) def update(self, state, action, reward, next_state, done): idx self.state_index(state) next_idx self.state_index(next_state) if done: target reward else: target reward self.gamma * np.max(self.q_table[next_idx]) self.q_table[idx, action] self.alpha * ( target - self.q_table[idx, action] )这一段是整个算法的心脏。重点看update函数里的target计算如果到达终点done为True目标值就是实际奖励不再加未来价值。否则目标值 即时奖励 γ乘以下一状态Q值中的最大值。这就是Bellman方程的直接落码。5.3 第三步训练循环与ε衰减训练循环就是不断重复“选动作→执行→看反馈→更新Q表”这个过程。为了更贴近实际使用我加了一个简单的ε衰减逻辑前80%的episode用较高的探索率之后逐渐降下来。def train(env, agent, episodes800): rewards_history [] for episode in range(episodes): state env.reset() done False total_reward 0 # 简单分段衰减前600轮探索率保持0.3之后逐步衰减到0.01 if episode 600: agent.epsilon max(0.01, agent.epsilon * 0.99) while not done: action agent.choose_action(state) next_state, reward, done, _ env.step(action) agent.update(state, action, reward, next_state, done) state next_state total_reward reward rewards_history.append(total_reward) return rewards_history env GridWorld(size4) agent QLearningAgent(env, alpha0.1, gamma0.95, epsilon0.3) history train(env, agent)注意我把衰减条件设成episode 600这是一种实践上常见的做法先保证充分的探索再切换到更“保守”的策略去巩固已经学到的知识。你也可以用线性衰减效果差不多。5.4 第四步看结果、验证策略训练结束后打印Q表看看它到底学到了什么print(np.argmax(agent.q_table, axis1).reshape(4, 4))这个输出是4×4的“最优策略表”每个格子上写着数字0-3对应“上下左右”。如果训练成功你会在表里看到一条从起点蜿蜒到终点的方向链。另一个更直观的验证方式是把策略走一遍def show_policy(agent): state env.reset() path [state] done False while not done: action agent.choose_action(state) # 这里epsilon已经很小基本是纯利用 state, reward, done, _ env.step(action) path.append(state) print( - .join(map(str, path))) show_policy(agent)如果路径是类似(0,0) - (0,1) - (0,2) - (1,2) - (2,2) - (3,2) - (3,3)这样的序列恭喜你的Q-learning算法已经完全跑通了。6. 调试经验与常见问题排查训练强化学习模型代码能运行只是第一步训练不收敛才是常态。这里把我踩过的一些坑和大家分享。6.1 Q值为什么会“越学越乱”Q值振荡是新手最常碰到的现象。表现为明明训练了很久Q表还是不稳定或者某次训练效果好、某次效果很烂。常见原因之一是学习率α设太大。Q-learning每步用的是增量更新α太大意味着每次修正步子迈得过分Q值会在目标附近来回震荡。解决办法是把α从0.1降到0.05甚至0.01试试。另一个原因是奖励设计不够合理比如奖励非常稀疏且随机性大导致误差信号不稳定。我调试时有个习惯把每一轮episode的总奖励画成曲线。如果曲线在大幅抖动先降α如果曲线整体有上升趋势但噪声很大这是正常的继续训练就好。6.2 学习率α和折扣因子γ到底怎么配参数配平是RL实践里的核心手感。我常用的起点组合是α0.1, γ0.95然后在两个方向上试探。如果你发现学习速度太慢几百轮还没摸到终点先检查是不是α太小。但也要警惕α太大虽然学得快但后期精度差、容易震荡。γ的影响更微妙。γ设得太大比如0.99价值会传播得很远需要更多轮次才能收敛γ设得太小比如0.5智能体会变得很短视只追求眼前奖励。针对网格世界这种短任务0.9到0.95是最舒服的区间。如果是长线任务、奖励密度高可以适当调高γ。6.3 训练不收敛时的检查清单我把常见的坑整理成一个速查表症状可能原因排查手段一直得不到正奖励探索率太低把ε提到0.3以上再看Q值整体偏大且发散学习率过大降低α到0.01路径只往边界撞Q表初始化问题检查维度是否填反训练后期还在乱走ε没有衰减检查衰减逻辑是否生效每次结果差异巨大随机种子没设训练前设置固定随机种子奖励一直为0终点判定没触发在step里手动打印状态6.4 一个容易被忽略的“索引”坑代码里state_index函数看着简单但特别容易写错。我用的是i * size j这是行优先的编码方式。如果你另一处用了j * size iQ表行维度不变但状态索引全乱了训练出来的策略会完全错误。这种错误很难通过报错发现因为程序能正常运行只是结果不对。我的经验是写完索引函数后立刻做一个小测试打印几个关键状态的索引比如左上是0、右下是15确保映射关系符合预期。7. 后续进阶路线与个人体会7.1 这个系列接下来要讲什么这一篇搭好了“强化学习的地基”——MDP、价值函数、Bellman方程、Q-learning。下一篇会往前走一步对比Q-learning和SARSA的异同让你理解在线策略on-policy和离线策略off-policy到底差在哪。再往后就会引入神经网络讲DQN为什么能处理高维状态、如何解决经验回放和目标网络等等。路会越走越宽但核心始终是这一篇里那些基本概念。7.2 新手学RL的三点个人心得第一一定不要跳过“自己写环境”这一步。很多教程直接用现成环境库虽然方便但容易让人失去对“环境接口”的敏感度。亲手实现一个环境你会深刻理解reset、step、奖励信号这些概念是怎么协同工作的。第二调参一定要有耐心。强化学习不是训练一次就完事的参数不同、随机种子不同结果可能天差地别。我建议每次只改一个参数用固定的随机种子把结果记录下来再对比这样你才能逐步建立起“这个参数变化会带来什么效果”的直觉。第三遇到不收敛先别急着怀疑算法实现先回头审视环境是否正常。很多时候问题不在Q-learning本身而是奖励设计出了bug——比如终点永远到达不了或者状态转移逻辑写错了。先做单元测试再调算法。说句实在话第一次看到Q-learning在自己的代码里成功收敛路径沿着网格世界一路走到终点那一刻那种“它真的在学东西”的兴奋感是其它编程任务很少能给的。希望这篇文章能让你也体会到这种感觉。下一篇我们继续往下走。