1. 从“会走迷宫的老鼠”说起这套强化学习教程到底在讲什么如果你之前翻过强化学习的教材大概率有过这种体验满篇的贝尔曼方程、策略梯度推导符号看得人头大但合上书却连一个能跑起来的例子都写不出来。我当初自学的时候也是这样理论看了一堆真到动手写代码连环境怎么搭、状态怎么表示、奖励怎么设计都一头雾水。这套“强化学习理论与 Python 实现 2024”的第三部分恰恰就是冲着这个痛点来的——它不满足于让你“看懂公式”而是要让你用 Python 把算法一行行敲出来看着智能体从瞎撞到学会任务。强化学习Reinforcement Learning简称 RL说白了就是让一个智能体Agent在环境Environment里不断试错通过奖励信号来调整自己的行为策略。它和 supervised learning 最大的区别在于没有标准答案告诉你“这一步该走哪”只有一个延迟的、稀疏的奖励告诉你“最后结果好不好”。这种“延迟反馈”的特性正是强化学习既迷人又难搞的地方。而 Python 作为当前 AI 领域最主流的语言配合 Gym、Stable-Baselines3、PyTorch 这些库能让你用几十行代码就跑通一个完整的训练流程。这篇文章适合谁看如果你已经了解 Python 基础语法知道什么是 numpy 数组、什么是类与对象同时对强化学习的概念状态、动作、奖励、策略有过初步接触那这篇内容就是为你准备的。我会把第三部分涉及的核心算法、代码实现、调参技巧、踩坑经验全部拆开讲清楚让你看完就能自己复现。哪怕你之前只写过简单的脚本跟着思路走也能上手。2. 整体设计思路为什么这样组织理论与代码2.1 从“表格法”到“函数逼近”的递进逻辑这套教程第三部分最核心的设计思路是沿着“从简单到复杂、从离散到连续”的路径推进。前两部分大概率已经讲完了 MDP马尔可夫决策过程的基础定义、动态规划、蒙特卡洛方法和时序差分TD学习。到了第三部分重点自然过渡到深度强化学习Deep RL——也就是用神经网络来近似价值函数或策略函数。为什么必须走这一步因为表格法Q-table有个致命缺陷它要求状态和动作空间都是离散且有限的。一旦状态变成连续值比如机器人关节角度是浮点数或者状态维度变高比如一张 84x84 的图像有 7056 个像素表格就彻底爆炸了。你不可能为每一个可能的浮点状态都存一个 Q 值。所以必须引入函数逼近用参数化的神经网络来泛化。这个递进逻辑在代码层面体现得很明显前面你可能写的是q_table[state, action]到了第三部分就变成q_network(state)返回所有动作的 Q 值。理解这个转变是跨入深度强化学习门槛的关键。2.2 算法选型的考量DQN、Policy Gradient 还是 Actor-Critic第三部分通常会覆盖几类代表性算法。我推测它的编排会围绕三条主线基于价值的方法以 DQNDeep Q-Network为代表核心是用神经网络拟合 Q 函数再通过 Q 值间接导出策略选 Q 最大的动作。它的优势是样本效率相对高、实现直观但只能处理离散动作空间。基于策略的方法以 REINFORCE、PPO 为代表直接参数化策略 π(a|s)通过梯度上升最大化期望回报。它能天然处理连续动作但方差大、训练不稳定。Actor-Critic 混合方法A2C、A3C、SAC 等用 Critic 估计价值来降低 Policy Gradient 的方差兼顾两者优点。为什么这样安排因为这三类方法在思路上是层层递进的DQN 让你理解“用网络逼近价值”这件事Policy Gradient 让你理解“直接优化策略”这件事Actor-Critic 则把两者缝合起来。学完这一套你再看任何新算法比如 IQL 离线强化学习、基于模型的强化学习都能找到它在这张地图上的位置。2.3 理论与实现的配比为什么不能只讲代码市面上有不少“纯代码”教程直接甩给你一个 DQN 实现就完事。但这套教程坚持理论先行我认为是有道理的。强化学习的坑特别多如果你不理解贝尔曼最优方程、重要性采样、优势函数这些概念遇到训练不收敛时你根本不知道该调哪里。举个例子DQN 里的 target network 和 experience replay 这两个技巧如果你不知道它们是为了解决“目标值不断移动”和“样本相关性太强”这两个问题你就无法理解为什么去掉它们训练就会崩。所以我的建议是理论部分别跳过但也不用死磕推导。先理解每个公式“在解决什么问题”再去看代码怎么落地最后回头补数学细节。这个顺序对工程师更友好。3. 核心细节解析DQN 与 Policy Gradient 的实现要点3.1 DQN 的三大支柱Replay Buffer、Target Network、ε-GreedyDQN 是深度强化学习里最经典的入门算法它的核心思想是用一个神经网络 Q(s,a;θ) 来近似最优动作价值函数。但直接把 Q-learning 和神经网络拼在一起会非常不稳定原因有两个一是样本之间高度相关连续几步的状态几乎一样二是目标值 Q_target 依赖于正在更新的网络本身导致“追着自己的尾巴跑”。解决方案就是三大支柱第一Experience Replay经验回放。把智能体每一步的 (s, a, r, s, done) 存进一个缓冲区训练时从中随机采样一个 batch。这样做打破了时间相关性让样本更接近独立同分布同时提高了样本利用率。缓冲区大小一般设 10000 到 100000太小容易过拟合近期经验太大则训练慢且占用内存。第二Target Network目标网络。复制一份和主网络结构相同的网络专门用来计算 TD 目标每隔 C 步才把主网络的参数同步过来。这样目标值在一段时间内是固定的训练更稳定。C 通常取 100 到 1000。第三ε-Greedy 探索。以概率 ε 随机选动作以 1-ε 选当前 Q 最大的动作。ε 一般从 1.0 线性衰减到 0.01 或 0.05让智能体前期多探索、后期多利用。下面是一个精简的 DQN 核心代码骨架你可以直接参考import torch import torch.nn as nn import numpy as np from collections import deque import random class QNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden128): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim) ) def forward(self, x): return self.net(x) class ReplayBuffer: def __init__(self, capacity50000): self.buffer deque(maxlencapacity) def push(self, s, a, r, s_next, done): self.buffer.append((s, a, r, s_next, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) s, a, r, s_next, done zip(*batch) return (np.array(s), np.array(a), np.array(r, dtypenp.float32), np.array(s_next), np.array(done, dtypenp.float32)) def __len__(self): return len(self.buffer)这段代码里有个细节值得说done用 float 存而不是 bool是为了后面算 TD 目标时直接做乘法(1 - done) * gamma * q_next省去类型转换。这种小技巧在写训练循环时能省不少事。3.2 TD 目标计算一个容易写错的公式DQN 的损失函数是均方误差L E[(r γ · max_a Q_target(s, a) · (1 - done) - Q(s, a))²]这里最容易出错的地方是(1 - done)这一项。当 s 是终止状态时未来回报应该为 0所以要把 γ·max Q_target 这一项乘 0 消掉。我见过不少初学者忘了这一项结果智能体在终止状态附近学出莫名其妙的高价值训练曲线一直抖。对应的 PyTorch 实现def compute_loss(q_net, target_net, batch, gamma0.99): s, a, r, s_next, done batch s torch.FloatTensor(s) a torch.LongTensor(a).unsqueeze(1) r torch.FloatTensor(r).unsqueeze(1) s_next torch.FloatTensor(s_next) done torch.FloatTensor(done).unsqueeze(1) q_values q_net(s).gather(1, a) with torch.no_grad(): q_next target_net(s_next).max(1, keepdimTrue)[0] q_target r gamma * q_next * (1 - done) loss nn.MSELoss()(q_values, q_target) return lossgather(1, a)这个操作是按动作索引取出对应的 Q 值比用 one-hot 做矩阵乘法更高效。torch.no_grad()包住 target 计算避免梯度回传到目标网络。3.3 Policy Gradient 的方差问题与 Baseline 技巧Policy Gradient 直接优化策略 π_θ(a|s)目标是最大化 J(θ) E[Σ γ^t r_t]。它的梯度估计是∇J ≈ E[∇log π_θ(a|s) · G_t]其中 G_t 是从 t 时刻开始的累计回报。这个估计是无偏的但方差极大——因为 G_t 的随机性很强同一状态下不同轨迹的回报可能差很多。方差大的直接后果是梯度噪声大训练慢甚至不收敛。解决办法是引入Baseline把 G_t 换成 G_t - b(s)其中 b(s) 是一个只依赖状态、不依赖动作的基线函数。数学上可以证明这样梯度期望不变因为 E[∇log π · b(s)] 0但方差会显著降低。最常用的 baseline 就是状态价值函数 V(s)于是 G_t - V(s) 就成了优势函数 A(s,a)的估计。这个技巧在 Actor-Critic 里被进一步发扬光大Critic 网络专门学 V(s)Actor 用 A(s,a) 来更新策略。理解这一点你就理解了从 REINFORCE 到 A2C 再到 PPO 的演化脉络。4. 实操过程从零跑通一个 CartPole 训练4.1 环境搭建与依赖安装先把环境弄干净。我强烈建议用 conda 或 venv 建一个独立环境别在系统 Python 里乱装。Python 版本选 3.8 到 3.10 都行太新的版本有些库还没跟上。conda create -n rl python3.9 conda activate rl pip install torch numpy gymnasium matplotlib注意这里装的是gymnasium而不是老的gym。Gym 在 0.26 版本之后 API 有较大变动step()返回五元组(obs, reward, terminated, truncated, info)而不是老的四元组。如果你看的教程还是老 API训练循环里会报错。这个坑我踩过排查了半天才发现是版本问题。验证安装import gymnasium as gym env gym.make(CartPole-v1) obs, info env.reset(seed42) print(obs.shape, env.action_space.n)输出应该是(4,) 2说明环境正常。CartPole 的状态是 4 维小车位置、速度、杆角度、角速度动作是 2 个左推、右推。4.2 训练循环的完整实现下面把 DQN 的训练循环串起来。关键参数我标注了推荐值env gym.make(CartPole-v1) state_dim env.observation_space.shape[0] action_dim env.action_space.n q_net QNetwork(state_dim, action_dim) target_net QNetwork(state_dim, action_dim) target_net.load_state_dict(q_net.state_dict()) optimizer torch.optim.Adam(q_net.parameters(), lr1e-3) buffer ReplayBuffer(capacity50000) gamma 0.99 batch_size 64 epsilon 1.0 epsilon_min 0.05 epsilon_decay 0.995 target_update 200 for episode in range(500): obs, _ env.reset() total_reward 0 for t in range(500): if random.random() epsilon: action env.action_space.sample() else: with torch.no_grad(): q q_net(torch.FloatTensor(obs).unsqueeze(0)) action q.argmax().item() next_obs, reward, terminated, truncated, _ env.step(action) done terminated or truncated buffer.push(obs, action, reward, next_obs, done) obs next_obs total_reward reward if len(buffer) batch_size: batch buffer.sample(batch_size) loss compute_loss(q_net, target_net, batch, gamma) optimizer.zero_grad() loss.backward() optimizer.step() if done: break epsilon max(epsilon_min, epsilon * epsilon_decay) if episode % target_update 0: target_net.load_state_dict(q_net.state_dict()) if episode % 20 0: print(fEpisode {episode}, Reward {total_reward}, Epsilon {epsilon:.3f})跑起来后你应该能看到 reward 从最初的 20 左右逐渐上升到 400 以上。CartPole 的满分是 500能稳定到 450 就算训练成功了。4.3 参数选择的背后逻辑为什么 lr 取 1e-3因为 DQN 的损失是 TD 误差量级不大1e-3 是 Adam 在强化学习里的常用起点。太大1e-2会导致 Q 值震荡太小1e-4收敛慢。为什么 gamma 取 0.99CartPole 每步奖励是 1回合最长 500 步。γ0.99 意味着 100 步之后的奖励衰减到约 0.37200 步后约 0.13。这个折扣率能让智能体既关注近期也不完全忽略远期。如果任务回合更长γ 要更接近 1。为什么 batch_size 取 64太小梯度噪声大太大训练慢且容易过拟合。64 是经验上的平衡点。Replay buffer 容量 50000 大约能存 100 个完整回合的经验足够打散相关性。ε 衰减用乘法0.995而不是线性是因为乘法前期衰减快、后期慢符合“先大胆探索、再精细利用”的直觉。5. 常见问题与排查技巧实录5.1 训练不收敛的五大元凶强化学习最让人抓狂的就是“代码没报错但就是不学”。我把常见原因整理成表现象可能原因排查方法reward 一直不涨学习率过大或过小试 1e-4、1e-3、1e-2 三档reward 剧烈震荡batch_size 太小或未用 target network增大 batch检查 target 更新频率前期涨后期崩过拟合或 ε 衰减太快增大 buffer减慢 ε 衰减Q 值爆炸成 NaN奖励未归一化或 γ 太接近 1对奖励做缩放检查 done 处理学到的策略很怪奖励函数设计有漏洞打印每步 reward检查是否有“钻空子”我印象最深的一次是 Q 值突然变成 NaN查了半天发现是done忘了转 float导致(1 - done)里 bool 和 float 混算出了意外结果。这种细节一定要盯紧。5.2 奖励设计强化学习里最玄学的部分奖励函数设计直接决定智能体学出什么行为。CartPole 的奖励很简单每存活一步 1但真实任务里奖励往往需要精心设计。几个原则稀疏奖励要加 shaping如果只有成功时给 1智能体可能几万步都碰不到一次成功根本学不动。可以加中间奖励引导比如离目标越近奖励越高。避免奖励被“钻空子”我见过一个任务智能体发现原地转圈能刷分结果永远不往前走。设计时要想象“最坏情况下智能体会怎么作弊”。奖励尺度要合理奖励动辄上千会让 Q 值爆炸建议归一化到 [-1, 1] 或 [0, 1] 区间。5.3 调试技巧让训练过程“看得见”别只盯着最后的 reward 数字。我习惯在训练时记录几个指标每回合总奖励、平均 Q 值、loss 曲线、ε 值。用 matplotlib 画出来一眼就能看出问题。import matplotlib.pyplot as plt plt.plot(rewards) plt.xlabel(Episode) plt.ylabel(Total Reward) plt.title(Training Curve) plt.show()如果 loss 一直不降但 reward 在涨别慌这在 DQN 里很正常——因为目标值本身在移动loss 不收敛不代表策略没变好。反过来如果 loss 降得很漂亮但 reward 不涨那多半是奖励设计或环境交互出了问题。提示训练强化学习模型时固定随机种子torch.manual_seed、np.random.seed、env.reset(seed...)能让结果可复现。但要注意即使固定了种子不同硬件上的浮点运算差异仍可能导致细微不同。6. 从入门到进阶后续可以怎么扩展跑通 CartPole 只是起点。想继续深入我建议按这个顺序推进先把 DQN 换成 Double DQN 和 Dueling DQN感受一下改进点带来的提升然后转向连续动作空间用 PPO 或 SAC 跑 LunarLander 或 MuJoCo 任务再往后可以接触离线强化学习比如 IQL研究如何从固定数据集里学策略这在无法在线交互的场景如医疗、自动驾驶里非常关键。另外因果强化学习Causal RL是近年的热门方向核心思路是把因果推断工具嵌入强化学习流程让智能体区分“相关”和“因果”从而在分布偏移时更稳健。如果你对这块感兴趣可以在掌握基础算法后专门找综述来看。我个人在实际操作中的体会是强化学习最难的不是算法本身而是调试。它不像监督学习那样有明确的 loss 下降曲线给你信心很多时候你只能靠 reward 曲线和直觉判断。所以多跑、多记录、多对比比死磕理论更有效。最后分享一个小技巧每次改代码只改一个变量改完记录结果这样你才能知道到底是哪个改动起了作用。一次性改一堆参数最后涨了也不知道为什么涨崩了也不知道为什么崩。