
简介本资源是面向机器学习初学者与强化学习实践者的PyTorch实战教学包聚焦标准RL算法原理与代码实现帮助读者从零理解DQN、DDPG等主流方法在经典控制环境CartPole-v0、Pendulum-v0中的建模逻辑与训练流程。压缩包共9个文件含4个核心Python源码含训练主逻辑与网络定义、2个预编译pyc文件便于快速验证、2个配套讲解MP4视频涵盖环境配置、关键参数调优与结果可视化以及1份结构清晰的README.md说明文档整体仅2.6MB轻量易下载、即开即学。目前已有158人学习下载内容兼顾理论严谨性与工程可复现性提供完整可运行代码、典型超参设置、训练曲线分析思路及常见收敛问题提示特别适合高校学生课程实践、自学进阶或竞赛算法基线搭建。1. 这不是“学完就能上手”的强化学习课它是一套用 PyTorch 把 DDPG、DQN 跑通在 Pendulum-v0 上的最小可验证闭环你搜“动手学强化学习 pytorch”点开一堆教程最后卡在env.reset()报错、agent.step()返回 NaN、或者训练 1000 轮 reward 还是 -12.5 —— 不是因为你没看懂贝尔曼方程而是因为没人告诉你Pendulum-v0 的 reward 是负值、action space 是 [-2, 2] 连续区间、而 PyTorch 默认 float32 在 critic 网络里梯度爆炸有多快。这个[动手学强化学习]系列基于pytorch.zip不是理论讲义它是一组经过实机验证的.py文件集合从requirements.txt里精确锁定torch1.13.1cu117不是最新版到ddpg_agent.py中 actor 网络最后一层用tanh*2.0显式缩放输出再到train_pendulum.py里replay_buffer的batch_size128和gamma0.99组合——所有参数都踩过坑、调过三轮以上。它面向的是已经写过 MNIST 分类、能跑通torch.nn.Linear但第一次碰torch.distributions.Normal的工程师目标很窄让你在本地 WSL 或 Windows Anaconda 环境下30 分钟内复现 DDPG 在 Pendulum-v0 上从 -12.0 到 -150 的 reward 攀升曲线并能改出自己的 DQN 版本。别被“强化学习”四个字吓住——这里没有黑匣子只有torch.no_grad()块里每一行 tensor 操作的来龙去脉。2. 从零搭起训练骨架PyTorch 环境、Gym 环境与 Pendulum-v0 的硬性适配2.1 PyTorch 安装必须匹配 CUDA 架构不是“装最新版就对了”很多翻车始于pip install torch后torch.cuda.is_available()返回False。这不是显卡问题而是版本错配。根据热词中高频出现的7900xtx pytorch wsl和pytorch安装教程gpu当前主流 WSL2 NVIDIA GPU 场景下必须用torch1.13.1cu117对应 CUDA 11.7而非2.x系列。原因有二一是gym0.21.0Pendulum-v0 所需版本与 PyTorch 2.x 的torch.compile存在兼容冲突二是torch.distributions在 1.13.1 中对连续动作空间的log_prob计算更稳定。执行以下命令注意--index-url不可省略# 先确认 CUDA 版本nvidia-smi 查右上角 CUDA Version: 11.7 pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117提示若用conda务必用conda install pytorch1.13.1 torchvision0.14.1 cpuonly -c pytorchCPU 模式或conda install pytorch1.13.1 torchvision0.14.1 cudatoolkit11.7 -c pytorchGPU 模式。anaconda配置pytorch环境的关键在于cudatoolkit版本与torch的cuXXX后缀严格一致否则cuda.device_count()会返回 0。2.2 Gym 版本锁死在 0.21.0Pendulum-v0 的 reward 结构依赖此版本Pendulum-v0 在gym0.26.0中已被移入gymnasium且 reward 计算逻辑变更新版本用np.cos(theta)替代np.cos(theta) - 1。而本系列代码所有 reward 曲线、target network 更新阈值均基于gym0.21.0设计。执行pip install gym0.21.0验证是否正确加载import gym env gym.make(Pendulum-v0) print(env.action_space) # Box(-2.0, 2.0, (1,), float32) print(env.observation_space) # Box(-inf, inf, (3,), float32) print(env.reward_range) # (-16.27360439453125, 0.0) —— 注意最大 reward 是 0不是正数参数说明action_space是连续一维 Box范围 [-2, 2]DDPG 的 actor 输出必须经tanh * 2.0映射至此区间observation_space为 3 维[cos(θ), sin(θ), θ̇]不能直接输入全连接网络需先做归一化代码中state (state - mean) / stdreward_range为负值区间意味着训练目标是让 reward越来越负即绝对值越大越好这是初学者最易误解的点。2.3 Pendulum-v0 的物理本质为什么 reward 是负的如何设计 reward shapingPendulum-v0 的 reward 公式为r -(θ² 0.1×θ̇² 0.001×a²)其中 θ 是角度弧度θ̇ 是角速度a 是 torque力矩。负号意味着系统越接近 uprightθ0且越静止θ̇0reward 越接近 0即“损失”越小。因此-150 的 reward 比 -12.0 更好。这直接影响训练目标不是“最大化 reward”而是“最小化 |reward|”。在train_pendulum.py中你将看到# reward shaping原始 reward 太稀疏加一个 dense reward reward - (theta**2 0.1 * theta_dot**2 0.001 * action**2) # 但代码中实际使用 reward reward - 0.01 * (theta_dot**2) # 额外惩罚角速度加速收敛这个0.01是血泪经验调出来的——太大导致 agent 过度保守不敢动太小则收敛慢。所有 reward shaping 必须在env.step()后立即修改不能在 replay buffer 存储前漏掉。3. DDPG 核心组件拆解Actor-Critic 网络、Target Network 与 Ornstein-Uhlenbeck 噪声3.1 Actor 网络输出必须严格约束在 [-2, 2]tanh 是唯一安全选择Pendulum-v0 的 action space 是连续 BoxDDPG 的 actor 必须输出合法 torque。常见错误是用sigmoid或linear层后 clip这会导致梯度不平滑、训练震荡。正确做法是最后一层用tanh再乘以env.action_space.high[0]即 2.0class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action): super().__init__() self.l1 nn.Linear(state_dim, 256) self.l2 nn.Linear(256, 256) self.l3 nn.Linear(256, action_dim) self.max_action max_action # 2.0 def forward(self, state): a F.relu(self.l1(state)) a F.relu(self.l2(a)) a torch.tanh(self.l3(a)) * self.max_action # 关键tanh → [-1,1] → [-2,2] return a参数说明max_action2.0是硬编码不可用env.action_space.high动态获取避免 runtime errortorch.tanh的导数在[-1,1]内非零保证反向传播* self.max_action必须在tanh之后否则tanh会压缩回 [-1,1]。3.2 Critic 网络双 Q 网络防 overestimation输入拼接 stateaction 是关键Critic 评估(s,a)对的价值输入是 state 和 action 的拼接向量。必须用两个独立 critic 网络Q1, Q2取 min 作为 loss 目标否则 Q 值会系统性高估class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() # Q1 self.l1 nn.Linear(state_dim action_dim, 256) self.l2 nn.Linear(256, 256) self.l3 nn.Linear(256, 1) # Q2结构相同参数独立 self.l4 nn.Linear(state_dim action_dim, 256) self.l5 nn.Linear(256, 256) self.l6 nn.Linear(256, 1) def forward(self, state, action): sa torch.cat([state, action], 1) # 拼接是核心不能分开输入 q1 F.relu(self.l1(sa)) q1 F.relu(self.l2(q1)) q1 self.l3(q1) q2 F.relu(self.l4(sa)) q2 F.relu(self.l5(q2)) q2 self.l6(q2) return q1, q2逻辑说明torch.cat([state, action], 1)将 state3维和 action1维拼成 4 维向量这是 critic 输入的唯一合法形式双网络结构要求forward()同时返回q1,q2后续torch.min(q1, q2)取较小值用于 TD error 计算抑制 overestimation。3.3 Target Network 更新软更新polyak比硬更新更稳tau0.005 是经验值Target network 用于计算 TD target必须比 online network 更新慢。硬更新target.load_state_dict(online.state_dict())会导致训练抖动软更新τ * online (1-τ) * target是工业级标准def update_target_networks(self, tau0.005): # 更新 actor target for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()): target_param.data.copy_(tau * param.data (1 - tau) * target_param.data) # 更新 critic targetQ1 Q2 for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()): target_param.data.copy_(tau * param.data (1 - tau) * target_param.data)参数说明tau0.005意味着每次更新 target 的 0.5%相当于每 200 步更新一次 full copy过大如 0.1导致 target 追不上 onlineTD error 偏大过小如 0.0001则 target 滞后太多学习缓慢。该值在ddpg_agent.py中已固化勿随意修改。4. DDPG 训练循环与 Replay Buffer 实现batch size、gamma 与 OU 噪声的协同4.1 Replay Buffer固定容量 FIFO uniform sampling不支持 prioritized本系列采用最简 replay buffer无 prioritized experience replayPER因 Pendulum-v0 任务简单PER 反而增加复杂度。关键实现class ReplayBuffer: def __init__(self, max_size1000000): self.storage [] self.max_size max_size self.ptr 0 def add(self, state, action, next_state, reward, done): data (state, action, next_state, reward, done) if len(self.storage) self.max_size: self.storage.append(data) else: self.storage[self.ptr] data self.ptr (self.ptr 1) % self.max_size def sample(self, batch_size): ind np.random.randint(0, len(self.storage), sizebatch_size) states, actions, next_states, rewards, dones [], [], [], [], [] for i in ind: s, a, s_, r, d self.storage[i] states.append(np.array(s, copyFalse)) actions.append(np.array(a, copyFalse)) next_states.append(np.array(s_, copyFalse)) rewards.append(np.array(r, copyFalse)) dones.append(np.array(d, copyFalse)) return ( torch.FloatTensor(np.stack(states)), torch.FloatTensor(np.stack(actions)), torch.FloatTensor(np.stack(next_states)), torch.FloatTensor(np.stack(rewards)).unsqueeze(1), torch.BoolTensor(np.stack(dones)).unsqueeze(1) )逻辑说明add()用ptr实现 FIFO确保 buffer 总是包含最新 transitionsample()返回torch.FloatTensor且rewards和dones增加维度unsqueeze(1)这是为了匹配 critic 的Q(s,a)输出形状[batch,1]np.stack()将 list of array 转为 2D array再转 tensor避免torch.stack()对 numpy array 的隐式转换错误。4.2 Training StepCritic 先更新Actor 后更新OU 噪声只加在 actor 输出上DDPG 的更新顺序不可颠倒先用 critic 评估当前策略再用 policy gradient 更新 actor。且噪声只加在 actor 的 action 输出上不加在 critic 输入def train(self, replay_buffer, batch_size128, discount0.99, tau0.005): # 1. Sample batch state, action, next_state, reward, done replay_buffer.sample(batch_size) # 2. Critic update计算 target Q with torch.no_grad(): next_action self.actor_target(next_state) # target actor next_q1, next_q2 self.critic_target(next_state, next_action) # target critic next_q torch.min(next_q1, next_q2) # double Q target_q reward (1 - done.float()) * discount * next_q # 3. Critic loss current_q1, current_q2 self.critic(state, action) critic_loss F.mse_loss(current_q1, target_q) F.mse_loss(current_q2, target_q) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() # 4. Actor update只用 Q1避免梯度耦合 actor_loss -self.critic.q1(state, self.actor(state)).mean() # 注意q1 是 critic 的第一个输出 self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # 5. Update target networks self.update_target_networks(tau)参数说明batch_size128是平衡内存与梯度稳定性的经验值小于 64 易震荡大于 256 显存溢出discount0.99匹配 Pendulum 的动力学时间尺度单步影响衰减慢actor_loss中self.critic.q1(...)调用的是 critic 的第一个 head不能用torch.min否则梯度无法回传到 actor。4.3 Ornstein-Uhlenbeck 噪声不是高斯噪声是带记忆的探索Pendulum 需要持续 torque高斯噪声np.random.normal会导致 action 随机跳变破坏物理连续性。OU 噪声公式dx θ*(μ−x)dt σ*dW代码实现class OUNoise: def __init__(self, action_dim, mu0, theta0.15, sigma0.2): self.action_dim action_dim self.mu mu * np.ones(action_dim) self.theta theta self.sigma sigma self.reset() def reset(self): self.state self.mu # 初始化为均值 def sample(self): x self.state dx self.theta * (self.mu - x) self.sigma * np.random.randn(len(x)) self.state x dx return self.state逻辑说明theta0.15控制回归速度越大越快回到 μsigma0.2控制噪声强度reset()在每个 episode 开头调用确保 noise 序列连续sample()返回np.array需在agent.select_action()中转为torch.tensor并乘以max_action。5. 避坑指南DDPG 在 Pendulum-v0 上的 4 个致命陷阱与修复方案5.1 现象训练初期 reward 突然崩到 -500loss 爆炸到 inf原因critic 网络输出q1,q2未做 clip当next_q计算中next_action超出 [-2,2] 时next_q值极大TD error 爆炸。解决在train()中添加next_q torch.clamp(next_q, -300, 0)因 Pendulum 最大 reward 为 0最小理论值约 -300由物理方程推导超出此范围必为异常。5.2 现象actor loss 为 nantorch.isnan(actor_loss)返回 True原因self.critic.q1(state, self.actor(state))中self.actor(state)输出含 nan根源是 actor 网络tanh前某层Linear权重初始化不当如nn.init.uniform_范围过大。解决统一用nn.init.xavier_uniform_(layer.weight)初始化所有 Linear 层并在Actor.__init__()末尾添加for m in self.modules(): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) nn.init.constant_(m.bias, 0.0)5.3 现象reward 曲线长期卡在 -12.0不下降不改善原因replay buffer 初始阶段数据质量差随机探索产生的(s,a,r,s)无效直接用这些数据训练 critic 导致 Q 值偏差。解决在train()前添加 warmup 机制——前 1000 步只存 buffer不训练total_steps 0 while total_steps max_steps: if total_steps 1000: # 只收集数据 pass else: agent.train(replay_buffer) # 正式训练 total_steps 15.4 现象WSL 下训练速度极慢GPU 利用率 10%原因PyTorch 默认num_workers0数据加载瓶颈在 CPU且replay_buffer.sample()返回 numpy arraytensor 转换在 CPU 上完成。解决在ReplayBuffer.sample()末尾添加.to(device)return ( torch.FloatTensor(np.stack(states)).to(self.device), torch.FloatTensor(np.stack(actions)).to(self.device), torch.FloatTensor(np.stack(next_states)).to(self.device), torch.FloatTensor(np.stack(rewards)).unsqueeze(1).to(self.device), torch.BoolTensor(np.stack(dones)).unsqueeze(1).to(self.device) )并在train_pendulum.py初始化时指定device torch.device(cuda if torch.cuda.is_available() else cpu)。6. 从 DDPG 到 DQN仅改 3 个文件把连续控制迁移到离散动作空间Pendulum-v0 是连续控制任务但dqn算法和dqn路径规划热词表明用户需要离散动作版本。本系列提供dqn_pendulum.py其核心改造仅三处无需重写整个框架6.1 修改环境用gym.make(CartPole-v1)不用离散化 Pendulum直接换环境会丢失物理一致性。正确做法是对 Pendulum-v0 的 action space 离散化将 [-2,2] 划分为 5 个离散 torque[-2.0, -1.0, 0.0, 1.0, 2.0]。在env_wrapper.py中class DiscretePendulum(gym.Wrapper): def __init__(self, env): super().__init__(env) self.action_map [-2.0, -1.0, 0.0, 1.0, 2.0] self.action_space gym.spaces.Discrete(len(self.action_map)) def step(self, action): cont_action self.action_map[action] return self.env.step(cont_action)逻辑说明DiscretePendulum继承原 envstep()将离散 index 映射回连续 torqueaction_space变为Discrete(5)与 DQN 兼容reward 不变仍为负值。6.2 修改 AgentDQN 的网络结构与 loss 计算DQN agent 与 DDPG 的主要差异在dqn_agent.pyclass DQNAgent: def __init__(self, state_dim, action_dim, lr1e-3): self.q_net QNetwork(state_dim, action_dim).to(device) # 单网络输出 [batch, 5] self.target_net QNetwork(state_dim, action_dim).to(device) self.optimizer optim.Adam(self.q_net.parameters(), lrlr) self.action_dim action_dim def select_action(self, state, epsilon0.1): if np.random.random() epsilon: return np.random.randint(self.action_dim) # 随机选离散 action state torch.FloatTensor(state).unsqueeze(0).to(device) q_values self.q_net(state) return q_values.argmax().item() # argmax 得到离散 index def train(self, replay_buffer, batch_size128, gamma0.99): state, action, next_state, reward, done replay_buffer.sample(batch_size) # Q-learning lossQ(s,a) ← r γ * max_a Q(s,a) q_values self.q_net(state).gather(1, action.long()) # gather 取出对应 action 的 Q 值 with torch.no_grad(): next_q_values self.target_net(next_state).max(1, keepdimTrue)[0] target_q reward (1 - done.float()) * gamma * next_q_values loss F.smooth_l1_loss(q_values, target_q) # Huber loss 更鲁棒 self.optimizer.zero_grad() loss.backward() self.optimizer.step()参数说明QNetwork是三层 FC输出维度action_dim5gather(1, action.long())是 DQN 的关键操作从q_values[batch,5]中按action[batch,1]索引取出对应列smooth_l1_loss替代mse_loss对 outlier 更鲁棒target_net更新方式同 DDPG 的 soft update。6.3 修改训练脚本同步更新 target network 的频率DQN 的 target network 更新频率远低于 DDPG后者每 step 更新前者每 C steps 更新。在train_dqn.py中update_target_every 50 # 每 50 步更新一次 target if total_steps % update_target_every 0: agent.target_net.load_state_dict(agent.q_net.state_dict())表格对比 DDPG 与 DQN 关键参数 | 组件 | DDPG连续 | DQN离散 | |--------------|---------------------------|---------------------------| | Action Space |Box(-2,2,(1,))|Discrete(5)| | Network | Actor输出 action Critic评估 | QNetwork输出 Q 值向量 | | Loss | Critic MSE Actor Policy Gradient | Q-learning Huber Loss | | Target Update| Soft update (τ0.005) | Hard update every 50 steps| | Exploration | OU Noise | ε-greedy (ε0.1) |我坚持在每个新项目启动前先跑通train_pendulum.py的 baseline再改 DQN。不是因为懒而是发现 80% 的 DQN 翻车其实源于 Pendulum 环境本身 reward 设计没吃透——比如忘了doneflag 在 Pendulum 中永远为 False它不终止所以1-done.float()永远是 1gamma的作用被放大。这种细节文档不会写只有亲手把 reward 打印出来一行行看才信。希望帮到你。本文还有配套的精品资源点击获取