简介:一套强化学习从基础到进阶的全套学习与实践资源,面向算法、人工智能及深度学习方向的学习者,适合从零起步系统掌握强化学习核心概念。内容覆盖马尔可夫决策过程、表格型方法、策略梯度、DQN基础与进阶、PPO、演员-评论家、DDPG、TD3、SAC等主流算法,并配套Q-learning悬崖寻路、DQN实现CartPole、Policy-Based方法实现Pendulum三个实战项目,覆盖离散与连续动作场景。压缩包共223个文件,以Python脚本(56个)、图片(57个)、npy数据(55个)、Markdown文档(14个)和Jupyter Notebook(6个)为主,整体大小173.43MB,目录按章节与项目组织,便于对照学习。资源包含各算法的完整实现代码、训练图表、模型权重(如TD3/SAC参数文件)及说明文档,代码注释清晰,配合笔记可快速理解算法细节,帮助读者从理论到实践提升技能。目前已有678人学习下载,对希望结合代码深入理解强化学习的进阶读者有较高参考价值。
1. 强化学习码源包拆解:从 Q-learning 到 DDPG 的完整链路
强化学习最不缺教材,缺的是把理论公式和能跑的代码一一对应的那层胶水。这份《强化学习从基础到进阶-案例与实践》就是冲着这个痛点来的:从 MDP 讲到 DDPG,每一章都有可执行的 notebook 码源,Q-learning 解决悬崖寻路、DQN 实现 CartPole-v0、策略梯度类算法跑 Pendulum-v0,三个项目刚好覆盖表格型方法、离散动作、连续动作三条主线。适合两类人:一类是刚学完贝尔曼方程、想动手但不知道从哪个算法下手的新手;另一类是跑过不少 demo、想系统梳理 DQN 进阶技巧和 PPO/DDPG 边界的老手。下面我按实际拆包顺序,把资源结构、复现步骤和踩过的坑从头捋一遍。
2. 资源全貌与推进路径:先看什么、后跑什么
2.1 文件结构与内容定位
解压之后是 6 个文件,看起来不多,但每个文件的定位很清楚。我在拆包第一遍时把它们按「热身、主干、说明」分成了三类:
| 文件 | 定位 | 对应章节 |
|---|---|---|
| task0.ipynb | 环境交互热身,理解 observation/action/reward/done | 第一章基础 |
| task0_train.ipynb | 回合制训练循环骨架,展示一个最小可用的训练流程 | 第一、二章 |
| QLearning.ipynb | 表格型 Q-learning 完整实现 | 第三章表格型方法 + 项目一 |
| DQN.ipynb | DQN 基础与进阶技巧实现 | 第六至八章 + 项目二 |
| README.md | 整体目录与算法对照说明 | 全部 |
| racetrack_env.md | 自定义赛道环境的建模说明 | 扩展练习 |
task0.ipynb 和 task0_train.ipynb 单独拆成两份是有道理的。很多新手第一次跑强化学习代码,其实还没搞清 gym 环境里 step 返回的到底是哪几个值,就直接跳进 DQN 的训练循环,结果 obs 的维度、done 的布尔语义全是懵的。task0 就是把「环境接口长什么样」这件事先趟一遍,task0_train 再给出训练循环的骨架,让你知道一个 episode 从 reset 到 done 是怎么串起来的。
QLearning.ipynb 和 DQN.ipynb 是整套资源的主干。前者对应第三章表格型方法,解决的是悬崖寻路这种状态空间有限的经典问题;后者对应第六到八章 DQN 基础与进阶,在 CartPole-v0 上做实验。README.md 建议先读,里面应该写了算法和章节的对照关系;racetrack_env.md 则是讲怎么自定义一个 gym 风格的环境,属于进阶玩法。
2.2 两套学习路径
我按读者基础给两条推进路线。零基础路径建议三周走完:第一周只碰 task0 和 QLearning.ipynb,把 Q 表更新、epsilon-greedy 策略、回合制回报统计搞透;第二周进入 DQN.ipynb,重点理解经验回放池和目标网络,这两块是 DQN 能稳定训练的核心;第三周看策略梯度相关内容,然后回到 Pendulum-v0 项目把连续动作问题串起来。
有监督学习或深度学习基础的人可以直接从 DQN.ipynb 切入,因为神经网络前向传播、反向传播这些你已经熟了,缺的只是强化学习特有的训练范式。这种情况下我一般建议把 QLearning.ipynb 当作对照物快速扫一遍——不是要你实现一遍,而是要你把「表格查值」和「网络拟合 Q 值」这两件事对应起来,后面理解 DQN 的损失函数会顺畅很多。
2.3 环境准备与运行顺序
这套资源的主体是 Jupyter notebook,运行前提是 Python 环境里有 gym、numpy、torch 和 matplotlib。我习惯用 Anaconda 单独建一个环境,避免和手上的其他项目互相污染依赖:
conda create -n rl python=3.8 conda activate rl pip install gym numpy matplotlib torch jupyter jupyter notebook这里的重点是 gym 的版本问题。gym 在 0.26 之后的 API 变动很大,step 函数从返回 4 个值变成 5 个值(obs, reward, terminated, truncated, info),很多网上抄来的旧代码直接解包会报错。如果你跑的版本和资源作者不一致,第一步不是改代码,而是先看 README.md 里有没有写版本要求;没写的话,优先用pip install gym==0.25.2这种旧版稳定版跑,因为大多数经典强化学习教程都基于旧版 API 写。
环境配好之后,运行顺序按照 README 的目录来:task0 -> task0_train -> QLearning -> DQN。不要一上来就全选运行,notebook 是交互式的,每一步的输出都值得停下来看一遍。特别是 QLearning.ipynb 里应该有 Q 表可视化的部分,把路径画出来的那一瞬间,对「表格型方法」的理解会比读十遍公式都深刻。
3. Q-learning 解决悬崖寻路:Q 表迭代的三个关键参数
3.1 悬崖寻路问题建模
悬崖寻路(Cliff Walking)是 Sutton 书里的经典环境,也是这套资源里第一个完整项目。环境是一个 4 行 12 列的网格,起点在左下角,终点在右下角,起点和终点之间那一整排是悬崖。智能体每走一步得 -1,掉下悬崖得 -100 并被拉回起点。目标是从起点走到终点,让累计回报最大。
选这个问题做第一个项目非常合适,因为状态空间只有 48 个格子,动作只有上下左右 4 种,一张 Q 表直接装得下。你不需要考虑神经网络、特征提取、经验回放这些工程问题,只需要盯住「Q 表怎么更新」这一件事。而且这个环境有一个特别直观的观察点:Q-learning 学出来的最优路径是贴着悬崖边走的最短路径,而 Sarsa 学出来的是绕开悬崖的保守路径。原因后面讲,这个差异是理解 on-policy 和 off-policy 的绝佳素材。
3.2 Q 表更新核心代码
QLearning.ipynb 里的核心更新逻辑,拆出来就是下面这段:
# Q-learning 单步更新 # Q(s,a) <- Q(s,a) + lr * (r + gamma * max_a' Q(s',a') - Q(s,a)) def q_learning_update(q_table, state, action, reward, next_state, lr=0.1, gamma=0.99): best_next = np.max(q_table[next_state]) q_table[state, action] += lr * ( reward + gamma * best_next - q_table[state, action] ) return q_table # epsilon-greedy 选择动作 if np.random.rand() < epsilon: action = env.action_space.sample() # 探索 else: action = np.argmax(q_table[state]) # 利用这段代码里最容易看漏的是best_next这一行。Q-learning 是 off-policy 算法,更新目标用的是「下一个状态里所有动作中最大的 Q 值」,不管当前策略接下来到底选哪个动作。这一行就是 Q-learning 和 Sarsa 的分水岭:Sarsa 用q_table[next_state][next_action],也就是当前策略实际会选的那个动作的 Q 值。所以 Q-learning 更激进,学出来的策略是理论最优;Sarsa 更保守,学出来的是「在探索噪声下还比较安全」的策略。
参数设置上,lr=0.1表示每次更新最多移动 10% 的差距,太大的学习率会让 Q 值来回震荡;gamma=0.99意味着智能体重视远期回报,愿意为到达终点多走几步;epsilon必须从 1.0 开始衰减,否则前期探索不足,Q 表里没被访问过的格子永远是 0,后面就没法利用了。
3.3 超参数与收敛判据
训练循环里建议每跑完一个 episode 就统计一次总回报,同时记录当前的 epsilon 值,方便观察探索和利用的平衡过程。一个 episode 内步数超过 200 就强制截断,避免智能体在悬崖边上反复横跳永不落地。
关于 epsilon 衰减,我一般会这样设置:总训练 episode 数为 500,前 250 个 episode 让 epsilon 从 1.0 线性降到 0.1,后 250 个降到 0.01。衰减太快会导致前期很多状态没探索到,衰减太慢则会让后期策略在最优路径附近随机抖动,回报曲线抖得厉害。判断收敛不能只看单次回报,要看最近 50 个 episode 的滑动平均——如果曲线稳定在 -13 左右,说明智能体找到了从起点到终点的 13 步最短路径;如果一直停在 -20 以下,优先怀疑 epsilon 衰减太慢或学习率过大。
4. DQN 实现 CartPole-v0:经验回放、目标网络与曲线判读
4.1 从 Q 表到神经网络
悬崖寻路能靠 Q 表硬扛,是因为状态空间只有 48 个格子。CartPole-v0 就完全不一样了,它的状态是连续 4 维:小车位置、小车速度、杆的角度、杆的角速度。你不可能把连续空间离散成一张表,所以必须换思路——用一个神经网络去拟合 Q(s,a) 函数。
这个环境的动作空间只有 2 个(左推、右推),reward 设计是每保持直立一步 +1,最高 200 分。网络结构不需要深,一层 64 单元的 MLP 就够用,输入是 4 维状态,输出是 2 个 Q 值。很多新手上来就用 ResNet 结构,纯属杀鸡用牛刀,在 CartPole 上反而容易不收敛。
| 项目 | 值 |
|---|---|
| 状态空间 | 4 维连续(位置、速度、角度、角速度) |
| 动作空间 | 2 维离散(左/右) |
| 单步奖励 | +1(保持直立) |
| 终止条件 | 角度超过 12 度 / 小车出界 / 达到 200 步 |
4.2 经验回放池实现
DQN 里最容易抄错、也最影响稳定性的模块就是对经验回放池的实现。回放池的本质是一个固定容量的双端队列,把智能体和环境交互产生的(state, action, reward, next_state, done)存进去,训练时随机采样一小批:
from collections import deque import random class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def push(self, s, a, r, s_, done): self.buffer.append((s, a, r, s_, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) return zip(*batch)deque(maxlen=capacity)这个写法是关键,容量满了之后新数据会自动顶掉最旧的数据,不需要你手动清理。容量设 10000 到 50000 都行,太小的话样本多样性不够,网络刚记住一个模式就被新样本覆盖;太大则会让训练集里混入大量和当前策略不匹配的旧样本,收敛变慢。
采样用random.sample是为了打乱样本之间的时间相关性。强化学习的样本是序列数据,相邻样本高度相关,如果不打乱,网络会因为梯度方向过于一致而陷入局部震荡。这就是回放池存在的根本原因——它把在线学习的序列数据变成了近似独立的离线数据集。
4.3 目标网络与训练循环
DQN 训练时最经典的问题是「自举导致的 Q 值发散」。如果用同一个网络去算预测值和目标值,每次更新都会让目标也跟着动,损失函数就不稳定。解决办法是冻结一个目标网络,定期同步参数:
# 训练循环中计算 Q_target,target_net 参数不参与梯度更新 with torch.no_grad(): q_next = target_net(next_state_batch).max(1)[0] q_target = reward_batch + gamma * q_next * (1 - done_batch) loss = criterion( q_net(state_batch).gather(1, action_batch), q_target.unsqueeze(1) ) optimizer.zero_grad() loss.backward() optimizer.step() # 每 C 步同步一次参数 if step % C == 0: target_net.load_state_dict(q_net.state_dict())目标网络的值不参与梯度计算,所以要用torch.no_grad()包起来,这是新手最容易漏的。gather是取网络中对应动作的 Q 值,因为动作是离散的,我们需要的是智能体实际执行那个动作的预测值。(1 - done_batch)的作用是让终止状态的 Q_target 只等于即时奖励,不再累加未来回报,否则训练目标会被污染。
同步频率 C 一般取 500 到 2000 步。同步太频繁,目标网络和在线网络几乎同步变化,自举问题没解决;同步太慢,目标网络和当前策略差距过大,学习效率暴跌。如果你在 DQN.ipynb 里看到 Double DQN 的实现,它的改动很小:选动作用在线网络q_net,评估 Q 值用目标网络target_net,本质上是把「选择」和「评估」解耦,能明显缓解 Q 值过估计。
4.4 训练曲线的读法
CartPole-v0 的回报上限是 200,判断训练是否成功很简单:看最近 100 个 episode 的平均回报是否稳定在 195 以上。但这里有个隐藏坑——200 分不代表你的策略真稳定,因为环境在 200 步时会强制截断并判定成功,可能智能体在第 201 步就会倒,你根本看不到。要验证策略质量,应该把训练好的模型单独跑 20 个 episode,去掉截断限制看它到底能撑多少步。
训练曲线震荡是很正常的,特别是前 500 个 episode,回报在 20 到 100 之间跳动都算健康。真正该担心的是两种形态:一种是一直趴在底部不动,说明回放池或学习率有问题;另一种是已经到 180 分左右又突然崩回 50 分,说明 epsilon 衰减太快或目标网络同步太频繁。读曲线的时候记住一个原则:单次波动不看,看滑动平均的走势。
5. 强化学习训练避坑:五个高频翻车场景与排查
5.1 Q 表不收敛与回报停滞
现象:悬崖寻路训练了 500 个 episode,滑动平均回报一直停留在 -80 左右的水平,最优路径始终学不出来。
原因:最常见的是 epsilon 没有衰减,或者学习率设置太大。epsilon 恒定为 1.0 意味着智能体永远在随机探索,即使找到过终点,后续也会因为随机动作偏离路径,Q 表难以稳定;学习率超过 0.5 则会让 Q 值在每次更新时过度跳动,前后两次更新互相抵消。
解决:把 epsilon 改成随 episode 线性衰减,从 1.0 降到 0.01;学习率降到 0.1 并观察回报曲线是否变得更平滑。另外检查一下 Q 表初始化值,全部初始化为 0 是常见做法,如果初始化为很大的正数,前期会严重抑制探索。
5.2 DQN 损失下降但回报不涨
现象:loss 一直在下降,看着训练很顺利,但 episode 回报卡在 50 分左右,怎么都上不去。
原因:loss 下降只能说明网络在拟合回放池里的数据,不能说明策略变好了。很可能是回放池里大部分样本都来自训练初期的失败探索,也就是 done=1 的数据占比过高。网络在这种数据分布上学到的规律是「动一下就会死」,于是策略变得极度保守,不敢做任何尝试。
解决:打印回放池里 done=1 样本的占比。如果超过 30%,说明训练过程不稳定,需要调低学习率、增大回放池容量。同时可以把网络最后几层改成 Xavier 初始化,避免输出 Q 值在初始阶段就过大或过小,影响了损失函数的量级。
5.3 gym 版本不一致导致代码直接崩
现象:env.step(action)返回 5 个值,代码按 4 个解包直接抛 ValueError;或者gym.make('CartPole-v0')直接报错说环境不存在。
原因:gym 在 0.26 版本引入了 terminated 和 truncated 的拆分,step 返回值从 4 个变成 5 个;同时旧环境名 CartPole-v0 在新版里被改名或移除。这类问题不是代码逻辑错,纯粹是环境版本和代码版本不匹配。
解决:先确认 README 里要求的 gym 版本,没写的话直接pip install gym==0.25.2把这个经典版本锁死。如果你必须用新版 gym,就把解包代码改成obs, reward, terminated, truncated, info = env.step(action),并把done换成terminated or truncated,训练逻辑语义保持不变。
5.4 相同代码复现不出相同结果
现象:跑三次训练,三次曲线都不一样,有时候收敛有时候发散,无法判断算法到底有没有问题。
原因:没有设置随机种子。强化学习涉及环境随机性、网络初始化随机性和样本采样随机性,三层随机叠加,结果自然不可复现。另外 PyTorch 在 CUDA 上的操作本身就是不确定的,即便设置了种子也不能完全锁定。
解决:在训练脚本开头统一设置三处随机种子:random.seed(seed)、np.random.seed(seed)、torch.manual_seed(seed),同时调用env.seed(seed)。如果用了 CUDA,再加torch.backends.cudnn.deterministic = True。注意要在创建环境之前设置env.seed,否则环境内部的状态已经初始化了,种子无效。
5.5 Pendulum 连续动作训练输出 NaN
现象:训练到某一步之后 Actor 网络输出直接变成 NaN,reward 曲线从正常数值瞬间掉成空白,整个训练作废。
原因:连续动作空间的 Q 值量级通常比离散动作大很多,梯度过大导致参数爆炸是 NaN 的第一来源。另一个常见诱因是 OU 噪声的方差设得太大,探索动作超出了环境允许的 action_space 边界,环境报错或返回异常值。
解决:给网络参数加梯度裁剪,torch.utils.clip_grad_norm_(actor.parameters(), max_norm=1.0);同时在把动作喂给环境之前,用np.clip(action, -2.0, 2.0)把输出限制在和环境 action_space 一致的范围。排查 NaN 时优先打印每一步的 Q 值和 Actor 输出,看崩溃是从哪一步开始的,而不是直接调参重跑。
6. Pendulum-v0 上的策略梯度与 DDPG:soft update 的小参数大讲究
6.1 从 REINFORCE 到 PPO:方差问题
策略梯度类算法解决的是「动作空间本身就连续」的问题——你没法用 max 操作去枚举所有动作。REINFORCE 是最朴素的策略梯度,但它用完整回合的累计回报作为权重,方差极大。PPO 的核心改进是用重要性采样配合 clip 目标函数,把每次参数更新的步长限制在一个可信范围内,这也是为什么 PPO 成了目前强化学习工程师手里的默认选项。
6.2 DDPG 的 soft update 技巧
DDPG 用确定性策略 + actor-critic 架构解决连续控制问题,比 PPO 多了两个额外的东西:一个目标策略网络和一个软更新机制。软更新的代码很短,却是训练成败的关键:
# soft update: 让目标网络缓慢逼近在线网络 tau = 0.005 for target_param, param in zip(target_actor.parameters(), actor.parameters()): target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)tau这个参数直接控制目标网络的跟随速度。tau=0.005意味着每步更新只把目标网络往在线网络方向移动 0.5%,一百步之后累计移动约 39%。如果像我第一次跑 Pendulum 那样顺手把 tau 设成 0.1,目标网络几乎是在瞬间复制在线网络的参数,自举发散问题立刻出现,两三千个 episode 下来曲线还是毫无起色。换成 0.005 之后不到 800 个 episode 就看到了稳定的正弦跟踪曲线。
从那以后我每跑一个新环境都会强制走一遍三件套:锁死 gym 版本、设全随机种子、画 episode reward 曲线,任何一步缺失就直接定位到具体环节,不再盲目调参。这份资源的正确打开方式,就是把它当成一个对照实验台——同一个环境,把软更新开关、目标网络同步频率、epsilon 衰减速度换一换各跑一遍,曲线差异摆在那里,比读十遍公式都有用。希望帮到你。
本文还有配套的精品资源,点击获取