news 2026/9/28 16:38:23

强化学习从Q-learning到DDPG:完整代码拆解与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
强化学习从Q-learning到DDPG:完整代码拆解与避坑指南

简介:一套强化学习从基础到进阶的全套学习与实践资源,面向算法、人工智能及深度学习方向的学习者,适合从零起步系统掌握强化学习核心概念。内容覆盖马尔可夫决策过程、表格型方法、策略梯度、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.ipynbDQN 基础与进阶技巧实现第六至八章 + 项目二
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 衰减速度换一换各跑一遍,曲线差异摆在那里,比读十遍公式都有用。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:37:51

基于ADS的40W LDMOS射频功放设计全流程解析

做射频功放这活儿&#xff0c;最磨人的往往不是仿真跑不出来&#xff0c;而是板子回到手上那一刻——明明仿真结果挺漂亮&#xff0c;一上电却没有输出&#xff0c;或者频谱仪上冒出一堆不该有的杂散。我自己刚入行那会儿&#xff0c;在40W LDMOS功率放大器上栽过好几个跟头&am…

作者头像 李华
网站建设 2026/9/28 16:37:43

Agent-native架构实战:如何让Agent成为系统核心引擎

前阵子帮团队把一个跑了快十年的客服工单系统做改造&#xff0c;第一版改法很朴素&#xff1a;在现有代码里加LLM调用&#xff0c;自动生成回复草稿、给客户消息做摘要。跑了两周我就放弃了&#xff0c;因为我意识到真正卡住流程的不是单点AI能力&#xff0c;而是整个系统压根没…

作者头像 李华
网站建设 2026/9/28 16:37:43

AWE2026直击德施曼智能锁:端侧AI与主动安防为何成为展台顶流

AWE2026现场直击&#xff1a;德施曼的展台&#xff0c;凭什么成了智能锁赛道最拥挤的地方逛AWE这么多年&#xff0c;我有个习惯&#xff1a;去展馆不看易拉宝上的参数表&#xff0c;只看展台前的观众密度和停留时长。AI、智能锁这两个词这两年已经被展会玩成烂大街的套路&#…

作者头像 李华
网站建设 2026/9/28 16:37:14

遥感卫星图飞机目标检测:XML标注转YOLO训练实战与避坑指南

简介&#xff1a;这是一套聚焦遥感场景的飞机目标检测数据集&#xff0c;适合目标检测初学者、算法研究者以及遥感图像分析人员使用&#xff0c;帮助解决在复杂卫星背景下识别并定位飞机的任务。包体包含1000张10241024高分辨率彩色卫星图像、1000份配套XML标注文件和1份info.t…

作者头像 李华
网站建设 2026/9/28 16:35:38

JavaWeb小型云盘系统实战:从源码部署到核心代码解析

简介&#xff1a;这份资源是基于JavaWeb的小型云盘系统完整项目&#xff0c;定位为仿照百度网盘核心功能的教学型毕业设计&#xff0c;适合Java初学者、数据库课程设计及求职者作为练习参考。系统前端采用Bootstrap构建界面&#xff0c;后台基于Servlet实现&#xff0c;包含文件…

作者头像 李华
网站建设 2026/9/28 16:35:17

Agent-Native 架构实战:从自主决策到工具编排的工程化落地

"agent-native"这个词最近在圈子里出现的频率明显高了起来。但凭我接触过的不少团队来看&#xff0c;多数人其实把它理解成了“给产品加个聊天框”或者“接个大模型 API”。我做了一年多的 agent 类项目&#xff0c;从最早在传统后端里硬塞 LLM 调用&#xff0c;到后…

作者头像 李华