资讯详情 强化学习作业实战:Python+DQN在CartPole上的智能决策
📅 2026/10/8 7:37:32
简介基于Python实现的智能决策技术强化学习作业设计源码是一份面向高校相关课程学习者的完整项目资料。资源围绕强化学习中的智能决策问题包含状态值函数更新、策略迭代与动作选择等核心算法实现并提供测试与解决方案文件适合正在完成同类作业或希望系统掌握强化学习实践的读者。压缩包共111个文件以32个Python源文件、24个测试文件、24个solution解决方案文件、14个lay布局文件为主另含config配置、xml数据文件等包体约861KB结构清晰便于按模块学习。已有339人学习下载。通过这份源码可获取一套可运行的强化学习作业代码框架、模块化测试思路与问题处理策略配套布局及配置文档则帮助理解工程组织方式有助于从算法原理过渡到完整项目实现。1. 当“智能决策”落到 Python 代码里这份强化学习作业到底在做什么大学里第一次交强化学习作业十个人里有八个不是卡在公式推导上而是卡在让训练稳定跑完不翻车。我今天想聊的这份“智能决策技术强化学习作业设计源码”说白了就是一套用 Python 把强化学习闭环跑通的课程项目给定一个决策环境智能体通过试错学到策略最后把源码、训练曲线和设计文档一起交上去。它适合正在做课程作业、毕设预研或者刚入门深度强化学习算法的人。你可能以为难点在算法的数学推导实际做下来真正的坑都在环境接口、训练稳定性、超参数调优这些“黑匣子”里。这篇笔记就按我做这类作业的顺序来写先把环境搭起来再让一个最基础的 DQN 算法在一个卡通小车上真正学会平衡最后告诉你哪些地方值得画进作业报告、哪些地方会把你绊一跤。2. 从环境到策略智能决策作业的强化学习框架怎么搭2.1 为什么作业选 Python Gymnasium而不是手推公式强化学习作业最常见的形态是让你实现一个能在“标准环境”里跑出结果的算法。十年前大家喜欢自己写格子世界、写迷宫环境逻辑都堆在一个文件里现在主流做法是直接用 GymnasiumOpenAI Gym 的维护分支它把“环境”和“智能体”的职责拆得很干净。你只需要调用env.reset()拿初始状态env.step(action)让环境往前走一步环境就会返回新的状态、奖励和“是否结束”的标记。这一层抽象的价值在于作业里算法代码和环境代码可以分开提交老师只需要跑你的智能体文件就行。我自己做作业时一般不会一上来就手推贝尔曼方程然后直接写公式而是先跑通最小闭环再回头补推导。原因很简单手推公式只能帮你算清楚“理想情况下的更新方向”但强化学习真正难的是训练过程中的不稳定比如奖励一直上不去、loss 突然变成 NaN。这些只有跟真实环境交互之后才会暴露。所以框架选型上我建议直接接受 Gymnasium 的标准接口把精力留给算法实现。顺便提一句安装 Python 环境时很多人会踩到 Gymnasium 和旧版 OpenAI Gym 的混用问题。如果你照着网上老教程的代码写的是env.reset()返回两个值在新版 gymnasium 里也是两个值但语义从obs变成了(obs, info)而旧版 gym 0.21 的reset()只返回obs。最好的做法是新建一个 conda 环境统一安装gymnasium和pytorch不要让系统里的全局 Python 背锅。2.2 最小可跑的决策环境用 CartPole 把“状态-动作-奖励”闭环跑通CartPole 是强化学习作业里最常见的入门环境没有之一。它模拟的是小车顶上竖着一根杆子智能体只能向左或向右推小车目的让杆子尽量保持竖直。环境给你的状态是 4 个数小车位置、速度、杆子角度、角速度动作空间是 2 个离散动作左/右。这个环境的好处是单步推理极快、奖励定义清晰每坚持一帧就给 1 分倒下就结束。你的智能决策系统要学的其实就是一套“看状态选动作”的映射。先写一个最不智能的随机策略验证环境本身能跑通import gymnasium as gym env gym.make(CartPole-v1, render_modehuman) obs, info env.reset() total_reward 0 for step in range(200): action env.action_space.sample() # 随机选动作 obs, reward, terminated, truncated, info env.step(action) total_reward reward if terminated or truncated: print(f第 {step} 步倒下累计奖励 {total_reward}) obs, info env.reset() total_reward 0 env.close()逻辑说明env.action_space.sample()是从动作空间里均匀随机采样代码里只用来验证环境能不能正常步进。env.step()返回 5 个值新版 gymnasium 把这个接口统一了。其中terminated表示环境因为“任务失败/成功”而结束比如杆子倒了truncated表示因为时间步数上限或外部条件截断比如达到了单局最大步数。新手经常把这两个混在一起条件写成if terminated:结果 CartPole 在 500 步被截断时不会触发重置训练循环就卡死了。2.3 定义策略网络与价值网络PyTorch 模型的三层结构作业里真正体现“智能决策”的地方是用神经网络来做决策。最常见的做法是用一个 Q 网络输入是状态4 个浮点数输出是各个动作的 Q 值2 个浮点数表示“在这个状态下选左/右的长期回报期望”。网络本身不需要多深三层全连接足够因为 CartPole 的状态维度只有 4。import torch.nn as nn class QNetwork(nn.Module): def __init__(self, state_dim4, action_dim2): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, action_dim) ) def forward(self, x): return self.net(x)参数说明中间层 64 是一个保守选择太少了学不好太多了在 CPU 上训练反而慢且容易过拟合。forward里的输入x形状是(batch_size, state_dim)输出形状是(batch_size, action_dim)。注意这里没有在最后一层加激活函数因为 Q 值可以是任意实数加了 sigmoid 或 tanh 反而把输出范围限制住影响收敛。为什么说“策略藏在网络里”因为对于离散动作空间策略就是从q_values里取最大值的那个动作也就是argmax。作业报告里如果老师让你画“决策流程图”通常也是画这个状态向量输入网络 → 前向传播 → 得到各动作 Q 值 → 选最大值动作 → 执行。这也是后面 DQN 训练的最小单元。3. 把“决策”变成“学习”DQN 算法落地与训练脚本3.1 DQN 的核心公式与代码对应关系作业里要求实现的强化学习算法最常见的是 DQNDeep Q-Network。它解决的问题是当状态空间不是离散几个格子而是像 CartPole 这样连续 4 维输入时不能再用表格式 Q-learning 存一张大表得用神经网络来逼近 Q 函数。这里有一个很多人觉得玄学的地方DQN 并没有直接“学”一个策略网络它学的是“价值网络”策略只是从价值网络里argmax出来的副产品。DQN 的更新目标来自贝尔曼方程当前状态动作的 Q 值等于即时奖励加上下一步最优动作的 Q 值乘以折扣因子。写成损失函数就是import torch.nn.functional as F # q_net 是当前要训练的网络target_net 是固定的目标网络 # 从经验回放里采样的一个 batchobs, actions, rewards, next_obs, dones current_q q_net(obs).gather(1, actions) with torch.no_grad(): next_q target_net(next_obs).max(dim1, keepdimTrue).values target_q rewards gamma * next_q * (1 - dones) loss F.mse_loss(current_q, target_q)逻辑说明gather(1, actions)是从 Q 网络输出的所有动作 Q 值里把当前 batch 里实际执行的那个动作的 Q 值挑出来。target_q的计算使用了target_net而不是q_net并且包在torch.no_grad()里防止梯度从这个分支反向传播这是稳定训练的关键。dones是一个 0/1 张量1 表示该条经验已经结束结束状态下没有“下一步”所以要把未来部分置为 0。参数说明gamma是折扣因子作业里一般取 0.99表示远期奖励的衰减程度。如果你把gamma设成 1.0智能体就会把遥远的、不确定的奖励看得和眼前一样重CartPole 这种没有稀疏奖励的环境容易震荡。3.2 经验回放与目标网络两个稳定训练的部件DQN 比普通 Q-learning 多出来的两个关键设计作业报告里必须写清楚。第一个是经验回放把智能体每步的(状态, 动作, 奖励, 下一状态, 是否结束)五元组存进一个队列训练时从中随机抽一批打破样本之间的时间相关性。第二个是目标网络让 TD 目标的计算网络参数在一段时间内保持冻结避免“目标跟着网络一起变”导致训练发散。经验回放队列可以直接用collections.deque实现不用自己写链表from collections import deque import random class ReplayBuffer: def __init__(self, capacity10000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, float(done))) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) return states, actions, rewards, next_states, dones def __len__(self): return len(self.buffer)deque(maxlencapacity)的好处是容量满了会自动丢弃最旧的数据非常适合回放池。float(done)把布尔值转成 0/1方便后面与gamma相乘。采样时用random.sample做不放回抽样这里不需要太复杂但如果作业环境是连续动作空间后面你可能要换成优先经验回放那就得自己维护权重先不用管。目标网络的维护也很简单每隔 N 步把q_net的权重复制给target_net。注意不是每步都复制否则目标网络就成了当前网络的克隆稳定训练的意义就没了。常见做法是每 100 到 200 步同步一次。3.3 训练循环怎么写采样、更新、日志的完整代码跑通 CartPole 训练的主循环是这份作业源码的核心。它把环境交互、经验存储、网络更新、日志记录全部串起来。我一般会把训练循环写成一个函数方便反复调整参数后重跑。import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import deque def train_dqn(env_nameCartPole-v1, episodes1000, lr1e-3, gamma0.99, batch_size64, target_update_steps100, epsilon_start1.0, epsilon_end0.01, epsilon_decay0.995): env gym.make(env_name) 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 optim.Adam(q_net.parameters(), lrlr) buffer ReplayBuffer(capacity20000) epsilon epsilon_start step_count 0 log [] for episode in range(episodes): state, _ env.reset() episode_reward 0 while True: # epsilon 贪心随机探索与利用结合 if np.random.rand() epsilon: action env.action_space.sample() else: with torch.no_grad(): q_values q_net(torch.FloatTensor(state).unsqueeze(0)) action q_values.argmax(dim1).item() next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated buffer.push(state, action, reward, next_state, done) episode_reward reward state next_state step_count 1 # 每步都尝试更新但只有 buffer 里有足够样本才开始 if len(buffer) batch_size: states, actions, rewards, next_states, dones buffer.sample(batch_size) states torch.FloatTensor(states) actions torch.LongTensor(actions).unsqueeze(1) rewards torch.FloatTensor(rewards).unsqueeze(1) next_states torch.FloatTensor(next_states) dones torch.FloatTensor(dones).unsqueeze(1) current_q q_net(states).gather(1, actions) with torch.no_grad(): next_q target_net(next_states).max(dim1, keepdimTrue).values target_q rewards gamma * next_q * (1 - dones) loss nn.functional.mse_loss(current_q, target_q) optimizer.zero_grad() loss.backward() optimizer.step() if step_count % target_update_steps 0: target_net.load_state_dict(q_net.state_dict()) if done: break epsilon max(epsilon_end, epsilon * epsilon_decay) log.append(episode_reward) if (episode 1) % 50 0: print(fEpisode {episode1}, reward {episode_reward:.0f}, epsilon {epsilon:.3f}) return log rewards train_dqn() print(训练完成最近 10 局平均奖励:, np.mean(rewards[-10:]))逻辑说明主循环里最关键的是 epsilon 贪心策略——初始阶段完全随机探索随着训练推进逐步收敛到利用最优动作。epsilon_decay每局结束后乘一次所以epsilon是按“局”衰减的不是按“步”衰减。你会发现这个脚本在 CartPole 上通常 200 局左右就能跑到单局 200 分以上也就是杆子一直不倒。target_update_steps用全局步数作为计数避免在局内同步造成目标网络变化太频繁。参数说明lr1e-3是我调过的稳定点。如果调大到一个数量级loss 曲线会剧烈抖动调小到 1e-5你可能会以为算法没效果。batch_size64对 CartPole 来说足够如果你的环境状态维度很大建议跟着增大到 128 或 256。epsilon_decay0.995对应大约 900 局后 epsilon 从 1.0 降到 0.01 附近刚好能覆盖整个训练过程。这个脚本里我没加梯度裁剪因为 CartPole 的 Q 值范围不大后面换复杂环境时再补。4. 作业里的“设计文档”部分如何用图表和指标证明你的策略学会了4.1 奖励曲线与滑动平均判断收敛的指标作业报告里老师最看重的不是你会不会背 DQN 论文而是你能不能拿出训练曲线说明“策略真的在不断变好”。原始逐局奖励曲线非常毛糙因为 epsilon 贪心带来的随机性会让单局分数忽高忽低。这时候就要做滑动平均把最近 N 局奖励取平均曲线立刻变得平滑趋势一目了然。def moving_average(data, window20): return np.convolve(data, np.ones(window) / window, modevalid)用np.convolve实现滑动平均是最省事的写法window20表示看最近 20 局的平均。很多作业报告直接贴原始曲线然后说“震荡中上升”这句话会暴露你根本没统计过。正确做法是同时画两条线浅色的原始奖励曲线和深色的滑动平均曲线并在图例里标注窗口大小。4.2 超参数表学习率、折扣因子、epsilon 怎么设作业报告里如果只放一张训练曲线图评委大概率会追问一句“你这些超参数怎么定的”。所以我会在报告里放一个超参数表把每个参数的角色和最终取值说清楚。默认可以这样列参数取值作用影响lr0.001Adam 优化器学习率太大会震荡太小收敛慢gamma0.99折扣因子越大越重视长期回报epsilon_start1.0初始探索概率1.0 完全随机探索epsilon_end0.01最小探索概率保证后期仍有少量探索epsilon_decay0.995每局探索衰减速率衰减越快利用越早batch_size64每次更新采样的经验条数影响梯度稳定性target_update_steps100目标网络同步间隔间隔太短易发散buffer_capacity20000回放池容量太旧的经验不一定有益这张表既是给你自己调参用的也是给老师看的“设计依据”。注意不要只写“默认值”最好在表后面加一句我对比了lr0.001和lr0.01前者训练曲线更平滑所以选了前者。这种“对比测试”的表述在作业评分里特别加分。4.3 把训练过程可视化用 matplotlib 画置信区间曲线单次训练跑出来的曲线有运气成分我今天跑出 200 分你换个随机种子可能就卡在 100 分。所以在作业里我会用多个随机种子比如 5 个各训练一遍把每一局的奖励按“种子”对齐然后统计均值和标准差画一个带置信区间的曲线。这一步被问到的概率极高术语叫“多次实验的置信区间曲线”。import matplotlib.pyplot as plt # runs: shape (num_seeds, num_episodes) 的奖励矩阵 def plot_confidence_curve(runs, window20, labelDQN): means [] stds [] for ep in range(runs.shape[1]): ep_rewards runs[:, ep] means.append(ep_rewards.mean()) stds.append(ep_rewards.std()) smooth_means moving_average(means, window) smooth_stds moving_average(stds, window) x np.arange(len(smooth_means)) plt.plot(x, smooth_means, labellabel) plt.fill_between(x, smooth_means - smooth_stds, smooth_means smooth_stds, alpha0.3, labelf{label} ±1std) plt.xlabel(Episode) plt.ylabel(Average Reward) plt.legend()代码说明fill_between的上界和下界分别是均值减/加标准差alpha0.3让阴影区域半透明这样能看清曲线主体。为什么要画 std 而不是标准误因为作业里展示“多次实验的稳定性”比“均值的置信度”更直观评审也想看你算法在不同初始条件下的方差。如果阴影带特别宽就说明你对超参数太敏感需要回头找更强的稳定措施比如梯度裁剪或延长目标网络同步间隔。5. 避坑与常见问题DQN 训练翻车时的 5 个排查点5.1 现象奖励一直不涨训练曲线像心电图原因epsilon 没有按预期衰减。常见情况是epsilon的更新写在了step里而不是episode里导致每个 step 都乘decay几百步后探索率就降到 0.01智能体变成纯利用早期学到的劣质策略再也没法探索新动作。还有一种情况是epsilon_decay设得太小比如 0.9探索率下降过快训练变成“盲目利用”。解决把 epsilon 衰减放在每个 episode 结束后执行并打印出来确认它真的在下降。我自己的血泪经验是至少每 50 局打印一次epsilon如果训练 200 局后它已经小于 0.1说明衰减速度合理如果还是 0.9说明衰减逻辑没生效。5.2 现象loss 一开始很小训练过程中突然变成 NaN 或暴涨原因梯度爆炸通常是因为目标 Q 值的计算方式有误或者学习率太大。CartPole 的奖励范围很小一般不应该是数值问题但如果你把gamma设成 0.999并且环境单局很长Q 值积分会越来越大梯度也跟着大。解决立刻在optimizer.step()前加nn.utils.clip_grad_norm_(q_net.parameters(), max_norm10)这一步能兜住大多数梯度爆炸。同时检查target_q里有没有把dones漏乘(1 - dones)如果漏了结束状态的 Q 值会被错误地叠加下一步奖励训练中期就会出现奇异的 spike。5.3 现象训练曲线上升得很漂亮但测试时智能体表现很随机原因你在测试时忘了关闭探索。q_net(states).argmax是在训练循环内部用的如果测试代码也用了 epsilon 贪心比如if random epsilon: sample()那必然会有随机动作。另一个原因是你测试的环境渲染模式render_modehuman导致每一步都在等待窗口刷新但不影响策略本身。解决写一个独立的测试函数推理时强制with torch.no_grad(): action q_net(state).argmax().item()完全去掉随机采样。测试时不更新网络、不存回放、不衰减 epsilon。如果测试平均奖励大于训练曲线的滑动平均那就说明你训练时确实被探索拖累了这是正常的。5.4 现象gym.make报错 “Version v1 not found” 或者reset()报错原因环境版本与 gymnasium 库版本不匹配。CartPole-v1 是从 0.26 版 gym 开始被保留的但如果你用的是旧版gym0.21reset()只返回一个值如果你用的是gymnasium但把render_mode传给了旧版gym.make也会报错。还有一个常见问题你的环境里安装了gym和gymnasium两个包import 的时候冲突。解决把所有依赖固定在一个文件里先用 conda 新建环境然后执行pip install gymnasium0.29.1 torch2.1.2这类明确版本号。不要用pip install gym因为那可能装回旧版 OpenAI Gym。如果你的作业要求用旧版gym那env.reset()返回的是obs而不是(obs, info)代码要按旧 API 写。这里没有万能解药按你的实际安装版本选择。5.5 现象拿到老师的源码包发现缺requirements.txt、跑起来报 ModuleNotFoundError原因这是作业源码最常见的“坑”——作者在自己机器上装了很多包但打包时没固化依赖。你拿到手直接运行缺少torch、gymnasium、numpy、matplotlib就会立刻停下来。解决拿别人的源码永远先问三件事Python 版本、依赖清单、目标环境。如果源码里没有requirements.txt自己创建一个里面至少写torch、gymnasium、numpy、matplotlib。安装不要污染全局环境conda create -n rl python3.10然后conda activate rl再pip install -r requirements.txt这一整套流程本身就是作业源码复现能力的一部分。6. 从作业到真实项目把 DQN 换成 PPO 的迁移路线与验证技巧作业交完后如果你打算把这个项目往更深的方向延伸最常见的路径是把 DQN 换成 PPO。DQN 只能做离散动作空间而真实场景里机械臂、游戏、自动驾驶很多都是连续控制这时候策略梯度家族更实用。做迁移不是从零写而是先理解两者在接口层面的差异DQN 输出的是每个离散动作的 Q 值然后用argmax选动作PPO 直接输出动作分布比如高斯分布的均值和方差再根据优势函数更新策略。你不需要自己实现全套 PPO但可以用 stable-baselines3 的现成实现让你自己的环境能跑起来再用前面说的置信区间曲线方法对比两者。我的习惯是先保留 DQN 的train_dqn作为 baseline然后写一个调用 PPO 的脚本让它们在同一个环境、同一个随机种子下训练同样多的步数最后把两条滑动平均曲线画在同一张图里。验证技巧有三条第一每个算法至少跑 5 个随机种子否则分不出性能差异是算法还是运气第二测试时不带探索用deterministicTrue或者直接取策略均值第三比较“达到目标分数的最少训练步数”而不是只比较最终分数因为很多时候 PPO 的最终分数更高但 DQN 更快达到及格线。我自己当年做这个对比时发现 DQN 在 CartPole 上其实比 PPO 更早达到 200 分但 PPO 的阴影带更窄、方差更小写进报告里就有说头了。说到底强化学习作业源码的价值不在代码量而在你能不能把一个算法从“能跑”变成“可控”。保存一份带固定随机种子、固定依赖的版本再留一份带对比实验的分析脚本这份作业才算真的有复现价值。希望这些踩过的坑能帮到你以后不再被“环境报错”和“loss 起飞”逼到删代码重来。提示训练 DQN 前先备份一个“最小可运行版本”每次改超参数都另存一份。强化学习训练失败的常客就是“改着改着跑不起来却不知道是哪一次改动造成的”。有后悔药才能大胆实验。本文还有配套的精品资源点击获取