Python强化学习作业实战:从Q-learning到DQN的智能决策源码解析

发布时间:2026/9/11 6:39:55
Python强化学习作业实战:从Q-learning到DQN的智能决策源码解析 简介这份基于Python实现的强化学习作业设计源码面向正在学习智能决策技术的高校学生与开发者目标是通过完整项目实践掌握强化学习如何驱动智能体在环境中持续交互、学习最优策略。压缩包共111个文件仅861KB核心内容由32个Python源文件构成涵盖状态值函数更新、策略迭代与动作选择等关键模块24个测试文件用于模块化验证算法正确性与性能24个解决方案文件则提供调优、异常处理等应对思路另有14个布局文件、多个配置及XML文件用于梳理程序结构、管理参数与复现实验环境同时包含Git忽略规则和项目说明等辅助文件。目前已有339人学习下载。整套资源分类清晰、轻量完整既能作为课程设计或毕业设计的参考蓝本也能帮助学习者对照源码理解智能决策从理论到落地的完整链路对于希望深入强化学习源码的读者还可借助项目中的版本信息与文档说明快速上手并二次开发。1. 这个作业题目到底在考察什么智能决策与强化学习的对应关系“基于Python实现的智能决策技术强化学习作业设计源码”如果只看前半段可能会以为是在拼装一个大型系统但拆开看它其实是典型的课程设计型题目场景是“智能决策”方法限定在“强化学习”工程框架指定“Python”交付物要求是“作业设计源码”。也就是说评分人想看到你从问题建模开始一步一步把一个决策问题转成马尔可夫决策过程然后实现可复现的训练与评估流程。很多人拿到题目后第一反应是去GitHub找个现成的CartPole源码跑一下结果因为环境版本、随机种子甚至numpy版本不一致而翻车。这篇文章会沿着一个能直接交的作业项目来展开环境搭建、Q-learning和DQN算法实现、训练曲线、评估与进阶方向。代码基于Gymnasium和PyTorch你用Python 3.10的conda环境可以全部跑通。2. 搭建强化学习作业的Python环境与项目骨架2.1 用 conda 固定 Python 版本和虚拟环境避免作业源码跑不通作业源码跑不通一半以上是环境问题。你在网上随便按一个“python安装教程”装出来的Python可能已经是3.12而老版本的gym、numpy和torch之间并不兼容。最常见的报错是cannot import name saving from numpy这不是你代码写错了是numpy 1.24之后移除了旧接口。我一般会给每个作业创建一个独立conda环境锁到Python 3.10。3.10不是最新但对于Gymnasium、Stable-Baselines3、PyTorch来说都能找到匹配的wheel不用从源码编译。conda create -n rl-hw python3.10 -y conda activate rl-hw pip install gymnasium[classic-control] numpy matplotlib tqdm逻辑说明conda create -n rl-hw是创建名为rl-hw的虚拟环境python3.10锁版本-y跳过确认。后面的pip安装里gymnasium[classic-control]会带回CartPole、MountainCar、Pendulum这些经典控制环境numpy用来处理状态数组matplotlib画奖励曲线tqdm显示训练进度。[classic-control]是Gymnasium的附加依赖组不是装饰性的概念没有它你虽然能import gymnasium但make CartPole会报环境找不到。如果你之后要跑深度网络装PyTorch的推荐命令是根据CUDA版本去官网生成而不是直接pip install torch。我遇到过很多次默认源给了CPU版训练一个DQN要卡到怀疑人生。装完后验证一下环境python -c import gymnasium; import torch; print(gymnasium.__version__, torch.__version__)参数说明这段验证命令对于作业交付很重要把输出贴到README里能让老师快速确定你的运行环境。如果你需要完整复现环境建议在项目根目录手工维护一个requirements.txt不要用pip freeze因为那会把你机器上无关的包也打进去。提示如果pip安装时卡在解析依赖先执行pip install --upgrade pip再重新安装。国内网络环境下可以临时使用镜像源比如加-i https://pypi.tuna.tsinghua.edu.cn/simple。2.2 Gymnasium 环境安装与最小CartPole交互代码环境装好后先跑一个最小的闭环验证OpenAI Gym API是否用对了。CartPole是强化学习作业里最常见的智能决策场景状态是四维连续数小车位置、速度、杆夹角、角速度动作只有0和1奖励每步1目标是让杆子尽量长时间保持竖直。import gymnasium as gym env gym.make(CartPole-v1, render_modergb_array) obs, info env.reset(seed42) for step in range(200): action env.action_space.sample() obs, reward, terminated, truncated, info env.step(action) if terminated or truncated: obs, info env.reset() print(默认到结束的步数:, step 1) env.close()逻辑说明env.reset(seed42)里的seed决定了初始状态和后续随机流方便复现。env.step()在新版Gymnasium里返回5个值其中terminated表示回合因失败结束比如杆子倒下truncated表示因为步数达到上限而结束。如果你还在用旧版写法obs, reward, done, info env.step(action)Gymnasium会直接报ValueError: too many values to unpack。这个报错是环境API版本不匹配的信号不是算法问题。参数说明render_modergb_array返回图像数组但不弹窗适合在服务器上训练后保存成MP4本地调试可以改成human会弹出实时窗口。这里循环只跑200步是预检行为正常训练时用500步上限。2.3 作业目录结构设计算法、环境、配置、结果分离作业源码如果只有一个main.py后面调参和写报告会很痛苦。我习惯让算法、环境、配置和结果各管各的老师也能一眼找到核心实现。下面是一个能直接交上去的目录骨架rl-homework/ ├── config/ │ └── dqn_cartpole.yaml ├── src/ │ ├── algorithms/ │ │ ├── q_learning.py │ │ ├── dqn.py │ │ └── ppo.py │ ├── environments/ │ │ └── grid_world.py │ ├── agent.py │ ├── train.py │ └── evaluate.py ├── results/ │ ├── models/ │ ├── logs/ │ └── plots/ ├── requirements.txt └── README.md目录/文件职责为什么需要config/超参数YAML配置改学习率不用动代码src/algorithms/每个算法一个模块方便横向对比不同算法src/environments/自定义环境Gymnasium自带环境不够用时扩展src/train.py训练入口统一加载环境、算法、配置src/evaluate.py评估入口输出平均回报与成功率results/模型、日志、图表报告截图都从这里取参数说明YAML配置用yaml.safe_load(open(config/dqn_cartpole.yaml))读取即可。把超参数外置不是为了炫技而是因为作业通常需要做对比实验比如把学习率从0.1改成0.01、把折扣因子从0.9改成0.99如果每次改代码再运行最后自己都会忘记哪张图是哪组参数。还有一个小坑项目路径中不要出现中文和空格否则torch.save和TensorBoard的SummaryWriter可能产生编码或路径问题。3. 核心算法实现从表格型Q学习到DQN的决策逻辑3.1 为什么作业里先写Q-learning而不是直接上PPO“智能决策”这个题目没有限定必须用哪种强化学习算法所以算法选型本身就是评分点。我的建议是第一版先用表格型Q-learning因为它逻辑透明、一行行更新公式都能从代码里看到适合写进报告第二版再升级到DQN解决连续状态问题如果还有余力再用Actor-Critic方法处理连续动作空间。这比一上来就跑一个PPO更像“强化学习作业”因为你能解释每一步改了什么、为什么改。Q-learning的核心是把状态动作对映射到一张Q表。对于CartPole这种状态连续的环境Q-learning不能直接用需要把状态离散化。也就是把小车位置、速度、角度、角速度各自切成几个区间把连续四维状态映射成一个离散索引。这样做虽然损失了精度但换来了可调试性训练结束后可以打印Q表检查哪个状态区间被访问得最多。3.2 Q-learning 的完整实现代码离散状态/离散动作下面这个类可以直接用在CartPole上核心更新公式是Q(s,a) - Q(s,a) alpha * (reward gamma * max_a Q(s,a) - Q(s,a))。import numpy as np class QLearningAgent: def __init__(self, env, bins(6, 6, 6, 6), alpha0.1, gamma0.99, epsilon1.0, epsilon_decay0.995, epsilon_min0.01): self.env env self.bins bins self.alpha alpha self.gamma gamma self.epsilon epsilon self.epsilon_decay epsilon_decay self.epsilon_min epsilon_min # 每个状态维度的分箱边界 self.boundaries [ np.linspace(-2.4, 2.4, self.bins[0] 1)[1:-1], np.linspace(-3.0, 3.0, self.bins[1] 1)[1:-1], np.linspace(-0.5, 0.5, self.bins[2] 1)[1:-1], np.linspace(-3.0, 3.0, self.bins[3] 1)[1:-1], ] self.q_table np.zeros(tuple(self.bins) (env.action_space.n,)) def discretize(self, obs): parts [] for value, edge in zip(obs, self.boundaries): parts.append(int(np.digitize(value, edge))) return tuple(parts) def select_action(self, obs): state self.discretize(obs) if np.random.rand() self.epsilon: return self.env.action_space.sample() return int(np.argmax(self.q_table[state])) def update(self, obs, action, reward, next_obs, terminated): s self.discretize(obs) s2 self.discretize(next_obs) target reward if not terminated: target self.gamma * np.max(self.q_table[s2]) self.q_table[s][action] self.alpha * (target - self.q_table[s][action]) def decay_epsilon(self): self.epsilon max(self.epsilon * self.epsilon_decay, self.epsilon_min)参数说明bins(6,6,6,6)表示四个状态维度各分6个箱子分箱越多表示状态分辨率越高但Q表大小是6*6*6*6*2的数组太高会稀疏太低又学不出精细策略。alpha是学习率决定每次更新的步幅gamma是折扣因子0.99表示重视长期收益。epsilon从1.0开始每回合乘以epsilon_decay0.995最低0.01意思是前几十回合大量探索后面逐步转向利用。注意更新时只用terminated来判断是否进入终态而不是用truncated。因为truncated只是步数截断环境本身并没有真正失败如果把截断当终态会让Q值低估后面本来可以获得的回报。这在DQN里尤其明显Q-learning里误差尚可接受但最好从一开始就养成区分习惯。3.3 DQN 的经验回放、目标网络与更新公式Q-learning的Q表在连续状态下是受限的。DQN用神经网络拟合Q函数输入是状态向量输出是每个动作的Q值。要让DQN稳定训练必须配两个机制经验回放和固定目标网络。人的学习经验来自过去大量交互经验回放就是把最近采样的transition存进一个先进先出队列更新时随机抽取一个batch减少相邻样本之间的相关性。import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, obs_dim, act_dim, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, act_dim) ) def forward(self, x): return self.net(x)经验回放缓冲区from collections import deque import random class ReplayBuffer: def __init__(self, capacity10000): self.buffer deque(maxlencapacity) def push(self, s, a, r, s2, terminated): self.buffer.append((s, a, r, s2, terminated)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) s, a, r, s2, d map(np.stack, zip(*batch)) return (torch.FloatTensor(s), torch.LongTensor(a).unsqueeze(1), torch.FloatTensor(r), torch.FloatTensor(s2), torch.FloatTensor(d)) def __len__(self): return len(self.buffer)DQN更新逻辑def dqn_update(q_net, target_net, optimizer, batch, gamma0.99): s, a, r, s2, done batch q_values q_net(s).gather(1, a).squeeze(1) with torch.no_grad(): max_next_q target_net(s2).max(dim1)[0] target_q r gamma * max_next_q * (1 - done) loss nn.functional.mse_loss(q_values, target_q) optimizer.zero_grad() loss.backward() optimizer.step()逻辑说明q_values表示在状态s下实际执行动作a的预测Q值target_q是当前奖励加折扣后的下一状态最优Q值。(1 - done)的作用是当done1时把下一状态部分直接置0表示回合结束。with torch.no_grad()是为了让target网络不计算梯度因为目标值不需要回传到target网络。参数说明经验回放容量capacity在CartPole这种小任务上取10000即可容量太小会让网络忘记早期经验容量太大会让更新时batch里混合太多“旧时代”数据训练变慢。batch_size常用64。目标网络不能每一帧都同步否则网络会追逐自己刚更新完的Q值导致反馈回路发散。一般每1000步把参数从q_net复制到target_net或者用tau0.005做软更新每次向在线网络靠近一点点。这两种方法在作业里都算标准答案但报告里要写清楚你用的是哪一种。3.4 参数说明学习率、折扣因子、探索率衰减深挖参数前先记住一个经验范围。DQN的优化器用Adam时学习率通常取1e-3小一点取3e-4。学习率过大会导致loss震荡过小则训练几千步都看不到奖励上升。折扣因子gamma在CartPole里取0.99或0.995都没问题但如果你的环境每步都获得-1惩罚且你要让agent尽快结束那gamma需要小一些比如0.9。参数典型值调参方向alpha/lr0.1 (Q-learning), 1e-3 (DQN/Adam)loss发散时调小一个数量级gamma0.99奖励稀疏或长周期任务调大epsilon_init1.0不需要太多探索时改成0.5epsilon_decay0.995/回合衰减过快会导致agent永远探索不够epsilon_min0.01最终保留少量随机性方便跳出局部最优batch_size32/64数值不稳时可以加大使用Q-learning时alpha从0.1开始即可。如果发现前100个回合奖励一直上不去可能是分箱太粗把bins改成(8,8,8,8)再看如果训练时间明显变长说明状态空间膨胀了需要配合epsilon衰减。DQN里epsilon衰减通常不按回合而是按训练步数做线性或指数调度。一个常见的做法是10000步内从1.0线性降到0.02之后保持。这样前5000步几乎全是探索后面才开始稳定利用。你可以在训练循环里判断当前global step然后动态计算epsilon而不是写死在agent类里。4. 训练与评估让智能决策源码收敛并输出曲线4.1 训练循环与 rollout 数据流训练循环的实质是“采样-缓存-更新-衰减探索”四个动作不断重复。下面是DQN训练循环的核心框架它已经刻意把truncated和terminated分开处理。agent DQNAgent(env, ...) replay ReplayBuffer(capacity20000) for episode in range(400): obs, _ env.reset(seedepisode) episode_return 0.0 for step in range(500): action agent.select_action(obs) # epsilon-greedy next_obs, reward, terminated, truncated, _ env.step(action) replay.push(obs, action, reward, next_obs, terminated) if len(replay) 64: batch replay.sample(64) agent.update(batch) obs next_obs episode_return reward if terminated or truncated: break agent.decay_epsilon() if episode % 20 0: print(fepisode{episode}, return{episode_return:.0f}, epsilon{agent.epsilon:.3f})逻辑说明replay.push里传入的是terminated而不是truncated因为在计算目标Q值时只有真实终结状态才会让max_next_q失效。truncated只是由于达到步数上限下一状态仍然有效只是出于效率原因停止采样。如果你把done terminated or truncated一起传入训练结果会偏低尤其是最大步数较小时。这是作业里最容易丢分的一个细节。参数说明每个episode重置时使用seedepisode可以保证同一episode在不同运行中产生相同的初始状态便于复盘。400个episode对CartPole足够如果到200个episode平均奖励还在30以下先不要急着加回合数而是检查epsilon是否衰减到了0.01附近以及buffer里是否混入了太多随机数据。len(replay) 64这个条件保证只有缓冲区被填到一定量才开始更新避免刚开始时batch里全是相同样本导致梯度异常。4.2 用 TensorBoard 或 matplotlib 记录奖励曲线训练过程中必须记录奖励曲线这是作业报告里最重要的证据。工程上推荐TensorBoard它记录标量、损失曲线、计算图都非常方便。from torch.utils.tensorboard import SummaryWriter writer SummaryWriter(results/logs/dqn_cartpole) writer.add_scalar(train/return, episode_return, episode) writer.add_scalar(train/epsilon, agent.epsilon, episode) writer.close()启动命令tensorboard --logdir results/logs启动后浏览器访问localhost:6006能看到训练回报随episode的变化曲线。注意SummaryWriter会在指定目录下生成events.out.tfevents文件如果你的机器上TensorBoard没有显示数据检查路径是不是相对路径以及是否在启动命令的目录下运行。如果不依赖TensorBoard用matplotlib画一条经过指数平滑的曲线也可以def smooth(values, weight0.8): last values[0] smoothed [] for v in values: last last * weight (1 - weight) * v smoothed.append(last) return smoothed参数说明weight0.8表示每个点与之前平滑值的混合程度weight越高曲线越平滑但会掩盖真实变化。作业报告里我一般把原始曲线画成浅色线平滑曲线画成深色线两个叠加一眼能看训练噪声和收敛趋势。4.3 Actor-Critic 扩展从 A2C 到 PPO 的作业加分项平稳版本修改。这里有一点扩展内容旨在帮助读者理解更强算法的作用。如果作业要求“智能决策”而不限定算法那么用DQN只能解决离散动作环境一旦换到连续控制问题比如机械臂DQN就不好用了。一个自然的扩展是切换到Actor-Critic家族。A2C同时维护策略网络和价值网络策略网络负责选动作价值网络负责评估当前状态好坏。PPO在A2C基础上加了一个裁剪项防止每次策略更新幅度太大训练更稳。ratio torch.exp(new_log_prob - old_log_prob) clipped_ratio torch.clamp(ratio, 1 - clip, 1 clip) actor_loss -torch.min(ratio * advantage, clipped_ratio * advantage).mean() critic_loss nn.functional.mse_loss(value, returns)逻辑说明ratio是新旧策略的概率比advantage是优势函数表示这个动作比平均水平好多少。clipped_ratio把概率比限制在[1-clip, 1clip]从而不让一次更新改动太大。clip通常取0.2。old_log_prob必须在采样时固定住更新时通过detach()保持不能用当前策略重新计算。和DQN不同PPO不是每一步都更新而是先收集一批完整rollout然后再在这批数据上做多次epoch的梯度上升。如果你只是想快速跑通PPO不必手写用stable-baselines3会省很多事。但作业里手写一份核心更新代码再在报告里和SB3结果做对比这个思路的工程含量会高很多。4.4 常见不收敛问题的排查命令和参数调整不收敛时先不要盲目调超参数而是用固定条件一步步缩小范围。固定随机种子运行一次确认是否稳定复现。命令PYTHONHASHSEED0 python train.py --seed 0。如果同一次代码不同seed结果差异极大说明算法本身不稳定先解决方差问题。打印每个episode的terminated和truncated比例。如果truncated占比过高说明agent已经能撑到步数上限但训练提前停了这时可以把while not done改成允许继续跑或者调整max_steps。观察DQN的Q值。如果Q值在训练后段仍然上下剧烈波动多半是目标网络同步间隔太短。把间隔从1000步调到2000或5000如果曲线变平稳就说明找到了问题。CUDA报错时使用CUDA_LAUNCH_BLOCKING1运行程序报错行会精确到具体语句。大多数情况是维度不对比如q_net(s)得到的shape是[batch, act_dim]而a的shape是[batch, 1]需要先gather再squeeze。如果奖励从0开始很久不涨优先检查epsilon。epsilon衰减过快会把探索窗口压缩得太短agent会在纯随机阶段就把buffer塞满之后再难学到有效策略。把epsilon_decay改到0.999或改成按步数线性衰减。注意生成报告用的模型应该单独保存。建议训练结束时存一个dqn_final.pt同时在收敛最佳时存一个dqn_best.pt。评估时两个模型都跑一遍取结果稳定的那个。5. 作业验收与进阶离线强化学习、IQL 与机械臂决策拓展5.1 用评估脚本计算平均回报和成功率训练结束后不要在训练循环里直接打印一个episode的return就当作结论要单独写一个评估脚本关闭探索。评估命令形如python evaluate.py --checkpoint results/models/dqn_best.pt --episodes 100 --seed 0示例输出Eval return: 482.13 - 35.21, success rate: 0.93这里的评估指标是平均回报、标准差和成功率。CartPole通常把得分450以上视为一次成功成功率0.9以上就可以交付。评估时seed固定否则环境随机性会让同一模型跑出两种结论。5.2 作业报告里写清楚的设计决策报告不要贴大段代码只贴核心公式和曲线图。需要写清楚状态和动作怎么建模、奖励函数怎么设计、为什么用Q-learning而不是策略梯度、以及最终选择的超参数。这实际上就是“智能决策技术强化学习作业设计”的核心要求证明你会把实际问题抽象成强化学习可解的问题。5.3 进阶把Q-learning换成IQL离线强化学习如果想让题目有“智能决策”的深度可以提离线强化学习。IQLImplicit Q-learning的价值函数更新不使用max_a Q(s,a)而是通过expectile回归估计一个保守的价值避免了数据集外的动作被高估。核心代码只有几行diff q_values - v_values weight torch.where(diff 0, tau, 1 - tau) v_loss (weight * diff ** 2).mean()其中tau取0.7到0.9tau越接近1价值估计越保守。这个写法比直接最大化Q值更稳定适合在固定的离线数据集中训练是当前强化学习研究里的一个热门方向。作业里写上这段能明显拉开和普通方案的差距。5.4 用 Gymnasium 和自定栅栏环境模拟机械臂决策如果想把项目做实可以自定义一个二连杆机械臂环境。状态是两个关节角度动作是角度增量奖励使用末端点与目标点的欧氏距离负值。实现时只需继承gymnasium.Env重写reset、step和render。这样训练代码几乎不用动。由于机械臂动作一般是连续空间DQN不适配要用PPO或A2C。评估时把render_modergb_array保存成MP4设置固定seed0后录制的视频可以作为实验证据直接放进作业报告。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询