尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

强化学习实战:从CartPole到游戏AI训练的完整避坑指南

强化学习实战:从CartPole到游戏AI训练的完整避坑指南 简介基于Python强化学习与深度强化学习的游戏AI训练源码包专门面向大学本硕阶段开展人工智能相关课程设计与毕业设计的学生也可用于游戏AI方向的入门实践。整个压缩包共49个文件除Python脚本与pyc编译产物外还包括PDF论文、PNG训练曲线与示意图、Markdown说明及TXT实验日志等大小仅2.4MB目录按代码、参考论文、迷宫Plus Q-learning、报告等模块划分能帮助快速定位训练脚本、模型文件和结果记录。内容既覆盖经典表格型Q-learning的迷宫Plus实现也提供Pong游戏的深度强化学习Demo内含dqn.py训练脚本、pong_load_model.py模型加载运行代码及requirements依赖清单同时附有《Playing Atari with DRL》等经典论文和项目报告细致展示从传统强化学习到深度网络训练游戏Agent的完整链路从环境配置、依赖安装到游戏环境启动均有说明便于复现实验。已有267人学习下载资料完整且体量紧凑适合作为毕业设计选题或课程项目复现的便捷参考资料。1. 游戏AI训练源码包第一步先分清“能跑”和“能复现”是两件事“基于python强化学习与深度强化学习的游戏AI训练源码项目说明论文报告.zip”这类压缩包很多人拿到第一步就是解压、装依赖、直接跑train.py结果卡在环境报错上半天没了。真正判断一份强化学习源码值不值得投入不是看能不能import成功而是看三样东西项目说明里有没有写清楚环境版本训练脚本里有没有固定随机种子报告里有没有给出评估协议。这三样齐全才能从“跑通”走到“复现结果、改参数、出新实验”。这套方案适合三类人拿游戏AI当课程设计或毕业设计题目的学生、想系统入门强化学习但缺一个完整参照的开发者、以及需要一套baseline来快速验证新算法的研究者。下面按我自己的落地顺序展开先跑最小闭环再选算法然后调参最后把结果写成报告。2. 跑通最小闭环用 gymnasium 和 CartPole 验证强化学习环境拿到源码包不要直接冲进你的目标游戏先花半小时跑一个最小的 CartPole。原因很简单强化学习代码里环境与算法之间的耦合度比想象中低绝大多数报错出在环境接口差异和依赖版本上。CartPole 是文档最全、单回合最短的标准环境用来验证“你的 Python 深度学习环境是不是真的能训练强化学习智能体”再合适不过。2.1 为什么从 CartPole 起步一秒一个回合问题暴露得足够快CartPole 的任务是让小车上的杆子尽量不倒下状态是 4 个连续数值小车位置、速度、杆角度、角速度动作是两个离散值左推、右推每撑过一步给 1 奖励单回合最多 500 步。这个设计让它有三个对新手极其友好的特点状态维度低MLP 就够用不需要一开始碰 CNN动作空间小epsilon-greedy 探索的行为很容易观察回合短几秒就出一个结果调参试错成本很低。我自己在实际项目里的习惯是不管最终目标是什么先用 CartPole 把 DQN 或 PPO 跑通再替换成目标环境。很多“源码包跑不起来”的问题根子不是算法写错了而是环境版本不对或者是 Python 版本太新导致依赖编译失败。Python 3.8 到 3.10 之间的兼容性比较好3.11 以上有些强化学习依赖会出现奇怪的编译报错这算是血泪经验。另外项目说明里如果标了 gym 环境的版本尽量按它的来不要自己升到最新版否则大概率翻车。2.2 一个可运行的最小 DQN源码结构与参数注释下面这段是我在多个项目里反复用的最小 DQN 结构把经验回放、双网络、epsilon 衰减全部压缩在一个文件里适合先跑通再拆解。import gymnasium as gym import numpy as np import torch import torch.nn as nn from collections import deque class DQN(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, act_dim) ) def forward(self, x): return self.net(x) class Agent: def __init__(self, obs_dim, act_dim, gamma0.99, lr1e-3, buffer_size10000, batch_size64, target_update100, epsilon_start1.0, epsilon_end0.02): self.q DQN(obs_dim, act_dim) self.target DQN(obs_dim, act_dim) self.target.load_state_dict(self.q.state_dict()) self.opt torch.optim.Adam(self.q.parameters(), lrlr) self.buffer deque(maxlenbuffer_size) self.gamma gamma self.batch_size batch_size self.target_update target_update self.epsilon epsilon_start self.epsilon_min epsilon_end self.epsilon_decay_len 15000 self.steps 0 def act(self, obs, greedyFalse): # greedyTrue 表示评估模式直接选最大 Q 值动作 if not greedy and np.random.rand() self.epsilon: return np.random.randint(2) with torch.no_grad(): q self.q(torch.FloatTensor(obs)) return q.argmax().item() def store(self, s, a, r, ns, done): self.buffer.append((s, a, r, ns, done)) def update(self): if len(self.buffer) self.batch_size: return 0.0 batch np.random.choice(len(self.buffer), self.batch_size, replaceFalse) batch [self.buffer[i] for i in batch] s torch.FloatTensor([b[0] for b in batch]) a torch.LongTensor([b[1] for b in batch]).unsqueeze(1) r torch.FloatTensor([b[2] for b in batch]) ns torch.FloatTensor([b[3] for b in batch]) done torch.FloatTensor([b[4] for b in batch]) q self.q(s).gather(1, a).squeeze(1) with torch.no_grad(): target r self.gamma * self.target(ns).max(1)[0] * (1 - done) loss nn.MSELoss()(q, target) self.opt.zero_grad() loss.backward() self.opt.step() self.steps 1 if self.steps % self.target_update 0: self.target.load_state_dict(self.q.state_dict()) self.epsilon max( self.epsilon_min, self.epsilon - (1.0 - self.epsilon_min) / self.epsilon_decay_len ) return loss.item() env gym.make(CartPole-v1) agent Agent(env.observation_space.shape[0], env.action_space.n) returns [] for step in range(30000): obs, _ env.reset() episode_return 0.0 for _ in range(500): act agent.act(obs) n_obs, reward, terminated, truncated, info env.step(act) done terminated or truncated agent.store(obs, act, reward, n_obs, done) loss agent.update() obs n_obs episode_return reward if done: break returns.append(episode_return) if (step 1) % 20 0: print(fepisode {step1}, avg_return{np.mean(returns[-20:]):.1f}, floss{loss:.3f}, eps{agent.epsilon:.2f})这套代码的逻辑是Agent 维护一个当前 Q 网络和一个目标 Q 网络每步更新当前网络每 target_update 步把参数同步给目标网络。经验回放 buffer 用 deque 实现满了自动丢最旧的数据容量对 CartPole 来说 10000 足够。训练时用 epsilon 从 1.0 线性衰减到 0.02前 15000 步里完成大部分探索后面基本转成纯利用。几个参数要特别说明gamma0.99 表示更看重长期收益CartPole 这种每步都有奖励的任务调成 0.95 也能收敛但速度会慢一点batch_size64 在 CartPole 上是经验值太小梯度噪声大会导致曲线更抖太大则单步更新慢target_update 设 100 表示每 100 步同步一次目标网络太频繁会失去目标网络的意义太慢则目标滞后明显。loss 和 eps 每 20 个 episode 打印一次如果 avg_return 能稳定到 400 以上说明 DQN 实现本身没问题。2.3 从 CartPole 平移到目标游戏reset/step/reward 三件事CartPole 跑通后源码包里的目标游戏通常也是 gymnasium 环境只需要换掉 env gym.make(你的游戏ID)Agent 代码基本不用动。关键是要理解所有 gymnasium 环境的接口都围绕三件事reset 返回初始状态step 接收动作并返回 (next_state, reward, terminated, truncated, info)reward 由环境自己计算。当你想用自己的游戏训练 AI 时先定义一个继承 gymnasium.Env 的类把这三件事实现出来class MyGame(gymnasium.Env): def __init__(self): super().__init__() # 动作空间离散动作个数或者 Box(low, high, shape) 表示连续动作 self.action_space gymnasium.spaces.Discrete(4) # 观察空间状态是向量还是图像决定了算法用 MLP 还是 CNN self.observation_space gymnasium.spaces.Box( low0, high255, shape(84, 84, 3), dtypenp.uint8 ) def reset(self, seedNone, optionsNone): super().reset(seedseed) obs self._get_obs() return obs, {} def step(self, action): self._apply_action(action) obs self._get_obs() reward self._compute_reward() terminated self._check_game_over() return obs, reward, terminated, False, {} def _get_obs(self): # 把游戏画面或状态转成 numpy 数组 pass观察空间定义是选算法的分水岭如果 observation_space 是 Box 且 shape 是 (高度, 宽度, 通道)那就得用 CNN后面算法选型也要往 DQN 家族走如果是低维向量MLP 就够。动作空间是 Discrete 还是 Box 决定了能不能用 Q 学习类方法连续动作基本要直接上 PPO 或 SAC。这个三件套接口同样适用于机械臂强化学习实战这类非游戏场景所以把 CartPole 吃透等于给后面所有项目打底。3. 强化学习与深度强化学习的算法选型状态空间和动作类型决定用哪一套源码包里往往同时出现 Q-Learning、DQN、PPO 多个文件很多新手默认“复杂游戏用深度强化学习简单游戏用普通强化学习”这个标准是错的。正确的选型标准只有两个状态怎么表示、动作是离散还是连续。搞清楚这两件事再看每个回合能交互多少次基本就能锁定算法范围。3.1 状态价值函数与动作价值函数选算法前先分清两个接口强化学习里有两个接口概念绕不开状态价值函数 V(s) 回答“当前局面本身有多好”动作价值函数 Q(s,a) 回答“在当前局面做出某个动作有多好”。选算法之前先问自己一个问题状态空间能不能用一个有限大小的表格装下比如井字棋9 个格子每个有三种状态总共只有 3^9 种组合Q 表完全装得下用 Q-Learning 就够了不需要深度学习这是“普通强化学习”的经典场景。一旦状态变成连续值比如 CartPole 的 4 维浮点数Q 表直接失效因为状态组合是无穷多个这时候就要用神经网络来逼近 Q 函数这就是 DQN 的出发点。所以普通强化学习与深度强化学习的分界线不是“游戏复不复杂”而是“状态能不能离散化”。状态组合爆炸或者本来就是连续值直接上深度强化学习状态枚举得过来Q 表更快更可解释。很多源码包的仓库里会同时给 Q-Learning 和 DQN 两份代码用这个标准去对照项目说明里描述的环境就能判断该用哪份。3.2 离散动作与像素输入DQN 家族三个改造点怎么选游戏画面是像素输入时观察空间从低维向量变成 (84, 84, 4) 这样的张量网络结构要从 MLP 换成 CNN算法上通常用 DQN 的三个改造点来提升效果。第一个是 Double DQN把“选动作”和“评估动作”拆成两个网络解决 Q 值过估计问题第二个是 Dueling DQN把 Q 网络拆成 V(s) 和优势函数 A(s,a) 两个支路在动作不影响局面优劣的任务里训练效率更高第三个是 Prioritized Experience ReplayPER采样时优先抽那些 TD 误差大的经验让模型把注意力放在“还没学会”的状态上。这三个改造点是叠加关系工程上常见的做法是先跑通朴素 DQN 作为 baseline再逐个加 Double、Dueling、PER每加一个就记录一次曲线。源码包里如果游戏是打飞机、贪吃蛇、赛车这类视觉游戏训练入口通常长这样dqn.py、dqn_double.py、dqn_dueling.py 三个文件对比实验就靠它们之间的参数差异说明问题。这里有个容易踩的坑像素输入一定要做预处理灰度化、缩放到 84×84、连续 4 帧堆叠这决定了 CNN 能不能学到运动信息。3.3 连续动作与样本效率PPO、SAC 和离线强化学习的取舍动作是连续值时比如赛车游戏的方向盘角度、机械臂的关节力矩Q 学习类方法直接失效因为 argmax 无法在连续动作空间里穷举。这时行业默认方案是 PPO通过重要性采样比率和 clip 机制限制单次更新步幅实现简单、收敛过程相对平稳成为大多数项目的首选。SAC 则在 PPO 基础上引入熵正则化鼓励策略保持随机性样本效率更高适合“能交互的回合数有限”的任务代价是实现复杂度高一些超参数也更敏感。额外提一类场景当你手里只有历史对局日志、没法在线探索时PPO 和 DQN 都用不了这时要用离线强化学习比如 IQL它从固定数据集中学习策略不依赖实时交互适合做日志数据复盘。多智能体场景则是另一个分支如果目标游戏里有多个角色协同先把每个角色拆成独立 agent 分别训练跑通后再考虑 MADDPG 这类协作式方法一上来就上多智能体会让问题复杂度翻倍。3.4 一张算法对比表加源码包入口划分拿到代码先找 train 和 eval为了快速对照我把常见算法的选型要点整理成一张表源码包里的项目说明如果写得规范一般也会带类似的表算法状态输入动作类型适合场景一句话选型理由Q-Learning离散/表格状态离散井字棋、迷宫状态可枚举Q 表最直接DQN低维向量或像素离散CartPole、Atari、打飞机状态连续化后用网络逼近 Q 函数Double DQN像素/向量离散需要抑制过估计的场景解耦动作选择与评估Dueling DQN像素/向量离散动作对结果影响小的任务拆分 V 与 A提升训练效率PPO向量/像素皆可离散连续大多数游戏与机器人控制默认首选收敛平稳且易实现SAC向量连续样本量有限的控制任务熵正则化样本效率更高IQL固定数据集离散/连续只有日志数据、不能在线探索离线学习不依赖实时交互拿到一份游戏 AI 源码包不要急着看模型定义先找 train 入口和 eval 入口。规范的项目会把训练和评估拆成两个脚本train 负责采样、更新、保存权重eval 负责加载权重、固定种子跑若干局、输出平均奖励。如果源码里只有 Jupyter Notebook 或者一个 main.py 全搞定复现成本就会高不少需要你自己拆。4. 训练调参实战六个必调参数和一条可复现的调参路径调参经常被说成玄学但实际调参是有信号可循的训练曲线不涨、震荡、跳水分别对应不同的参数问题。调参的关键不是“知道很多参数”而是“每次只动一个参数并且把每次改动记录下来”。下面六个参数是我每次拿到新环境都会先检查一遍的。4.1 六个必调参数从学习率到目标网络更新频率逐一拆解参数常见取值范围调大之后调小之后失败时的现象learning_rate1e-4 到 3e-3学得快但容易震荡学得慢但更平缓loss 不降反升或直接 NaNgamma0.95 到 0.99更看重长期收益更看重即时收益任务需要远期规划时奖励停滞epsilon含衰减速度1.0 衰减至 0.01-0.05探索充分但收敛慢过早转纯利用曲线早期涨后期不涨buffer_size1万到100万样本多样但占用内存样本旧导致重复学习训练曲线周期性回退batch_size32 到 256梯度平稳但单步慢噪声大、波动明显同一环境两次训练结果差异大target_update100 到 1000 步目标更新频繁、接近在线学习目标滞后明显训练初期 Q 值发散learning_rate 是最容易出问题的一个强化学习和监督学习不一样损失函数本身在变调得太大 loss 会直接爆炸调得太小又学不动。我一般先用 3e-4 起步这个值在 Adam 优化器下对大多数任务都够用不收敛再往 1e-4 或 1e-3 方向试。gamma 的调整逻辑是“任务需要的视野长度”像 CartPole 这种每步都很关键的任务 0.95 就能跑像赛车这类需要提前规划的任务就往 0.99 以上调。epsilon 衰减速度要按总训练步数来算常见做法是让衰减区间占到总步数的 60% 到 70%确保模型在中期还有一定探索能力。buffer_size 和 batch_size 是一对回放缓冲区太小会让模型反复学习近期的旧经验训练曲线容易出现周期性回退batch_size 太小则梯度噪声大同一份代码跑两次结果差异明显。target_update 配合双网络使用我习惯设成 100 到 500 步之间CartPole 这种简单环境 100 步就够复杂环境适当加大到 1000 步。4.2 训练曲线三种“不涨”现象按顺序查 reward、epsilon 和学习率第一种现象是全程 reward 都停留在个位数从头到尾没有上升趋势。先打印状态里每个维度的均值和方差如果各维度数值范围差异过大比如一个特征在 0 到 1 之间、另一个在 -100 到 100 之间网络很容易被大数值维度带偏需要做归一化。其次检查 reward 尺度如果奖励从 -1000 到 1 分布TD 误差会被极端值主导考虑把 reward 裁剪到 [-1, 1] 区间。第二种现象是曲线涨到一半突然掉下来之后反复震荡。这通常是 epsilon 衰减过快模型过早进入纯利用阶段后面一旦遇到没见过的状态就不知道该怎么做。解决方式是拉长衰减区间或者把 epsilon 下限从 0.02 提到 0.05保留一点随机性防止“把路走死”。第三种现象是训练曲线一直在涨但每次跑到同一个节点就跳水这是 buffer 溢出后旧策略数据被丢出的信号调大 buffer_size 或者把 batch_size 调小让采样更均匀。4.3 一份可直接抄的 PPO 参数模板PPO 的超参数比 DQN 多但大部分项目只需要调前五个。下面这份是我常用的模板直接从训练脚本里摘出来照着注释理解每个参数的含义config { lr: 3e-4, gamma: 0.99, gae_lambda: 0.95, clip_epsilon: 0.2, entropy_coef: 0.01, value_coef: 0.5, epochs: 4, batch_size: 2048, minibatch_size: 64, update_times: 10, } for epoch in range(config[epochs]): for mb in minibatches: # p_old 在更新前固定避免同一个 batch 内重要性权重反复变化 ratio torch.exp(policy(mb_states).log_prob(mb_actions) - p_old) surr1 ratio * mb_advantages surr2 torch.clamp(ratio, 1 - clip_epsilon, 1 clip_epsilon) * mb_advantages policy_loss -torch.min(surr1, surr2).mean() # value_loss 让价值网络逼近 GAE 算出的回报 value_loss nn.MSELoss()(value(mb_states), mb_returns) entropy_loss -policy(mb_states).entropy().mean() total_loss (policy_loss config[value_coef] * value_loss config[entropy_coef] * entropy_loss) total_loss.backward() optimizer.step()clip_epsilon 是 PPO 的灵魂参数0.2 是论文推荐值也是大多数项目不动它也能跑通的原因。gae_lambda 控制优势估计的偏差与方差平衡0.95 是通用值任务需要更长视野时调到 0.99。entropy_coef 是探索项设太大会让策略一直随机瞎逛设太小则容易过早收敛到局部最优0.01 是保守起点发现探索不够再往 0.05 方向调。epochs 表示每个 batch 的数据复用几次理论上可以大于 1 提升样本利用率但设太大会导致过度拟合当前 batch。5. 避坑排查训练翻车到复现失败的五个问题定位训练强化学习智能体的过程里大部分时间不是在写算法而是在排错。下面这五条是我在各种游戏 AI 训练项目里反复遇到的真实问题每一条都按“现象 → 原因 → 解决”的顺序写清楚希望能帮你少走弯路。5.1 训练很久 reward 一直在低位不动先查 reward 尺度再查 epsilon现象训练了两万步打印出来的平均奖励始终在 10 附近横盘偶尔跳一下又掉回来。原因有两类一是 reward 尺度不平衡游戏中“存活奖励 1”但“死亡惩罚 -100”TD 误差被极端死亡惩罚主导网络只顾着降低惩罚忽略了正常存活时的正向信号二是 epsilon 在训练前 10% 就衰减到接近 0模型几乎不做探索永远在重复几个早期碰巧选过的动作。解决方式是先做 reward scaling把奖励裁剪到 [-1, 1] 区间再看 epsilon 衰减曲线确保它到总训练步数的 60% 时才接近下限。如果两条都正常最后一步检查状态归一化是否做对。5.2 训练曲线正常、评估时却大面积崩盘探索噪声和评估模式是两回事现象训练时日志显示平均奖励已经到 470看起来很完美但一运行 eval 脚本平均奖励只有 100 多。原因是训练过程里动作选择带了探索噪声epsilon 即使到 0.02 也还是会随机选动作而评估脚本里如果沿用同一套 act 逻辑就会把探索噪声带进评估。另外评估环境的随机种子没有固定每次跑出来的结果差异巨大也会看起来像崩盘。解决方式是把评估写成独立函数动作选择强制 greedyTrue并且每个评估回合固定一个起始种子跑 10 局求平均和标准差再跟训练曲线的最后一段对比。5.3 loss 不降反升并出现 NaN学习率过大或 reward 未缩放现象训练日志里的 loss 从一开始的 0.01 涨到几百最后出现 NaN进程直接报错。原因一般是学习率过大导致参数更新步子踩空或者 reward 绝对值过大让梯度爆炸网络权重在几步之内变成无穷大。解决方式是先检查 reward 分布把奖励缩放到 [-1, 1] 或除以常数值然后降低学习率到 1e-4同时给优化器加一个梯度裁剪常见的做法是 max_grad_norm1.0。经验回放 buffer 在这里相当于后悔药训练出 NaN 后不要清掉 buffer把最近的几条 transition 打印出来看是哪一个状态或奖励引起的爆炸往往能直接定位问题。5.4 换台机器结果对不上随机种子只锁了 Python 没有锁 CUDNN现象同一份代码、同一个随机种子在自己电脑上训练结果是一条平滑上升的曲线换到另一台机器上就变成剧烈震荡甚至完全收敛不到同一水平。原因是只调用了 random.seed 和 np.random.seedPyTorch 的 GPU 计算还有一套非确定性后端cuDNN 的自动调优会引入随机性。解决方式是在训练脚本开头补上完整的种子固定逻辑import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.deterministicTrue 让卷积选择确定算法benchmarkFalse 关闭自动搜索这样训练过程在 CPU 和 GPU 上的可复现性会好很多。注意即便如此多线程数据加载的顺序仍可能造成微小差异如果对复现要求极高就把 DataLoader 的 num_workers 固定且设置相同的 shuffle 种子。5.5 gymnasium 接口差异导致报错reset 和 step 的返回值随版本变化现象源码包里用的还是 gym系统里装的是 gymnasium运行 env.reset() 时报错 AttributeError: TimeLimit object has no attribute seed 或者 reset 返回的元组数量不对。原因是 gym 0.21 到 gymnasium 0.26 之间做了一次较大的接口调整旧版 reset 返回单值 observation新版返回 (observation, info) 二元组旧版 step 返回 4 个值新版返回 5 个值多了一个 truncated。解决方式是在代码里统一按新版接口写obs, info env.reset()step 解包为 obs, reward, terminated, truncated, info终止条件用 terminated or truncated 而不是单看 done。如果项目说明里标注了 gym 依赖最省事的做法是用 requirements 文件里的版本创建独立虚拟环境不要让它在全局环境里和环境冲突。6. 论文与报告用一份固定种子的评估协议收尾训练结束后最难的不是写算法而是让实验结果站得住脚。论文和报告里最常被导师或评审质疑的就是“你的数字能不能复现”。解决方式是把评估过程固定成脚本核心是固定种子、固定局数、开 greedy 决策这样任何人都能重新跑出一致的结果平均值和标准差也才具有可比性。6.1 评估脚本先固化固定种子、固定局数、greedy 决策def evaluate(agent, env, episodes10, seed42): rewards [] for i in range(episodes): obs, _ env.reset(seedseed i) # 每个评估回合固定一个种子 episode_reward 0.0 done False while not done: action agent.act(obs, greedyTrue) # 评估必须开 greedy obs, reward, terminated, truncated, _ env.step(action) done terminated or truncated episode_reward reward rewards.append(episode_reward) return np.mean(rewards), np.std(rewards)评估协议里最容易被忽略的是 greedy 参数训练时 actor 输出的策略本身是概率分布评估时如果还按概率采样方差会非常大必须取最高概率动作。种子加 i 是为了让 10 局评估覆盖不同的起始状态又能保证每次重复完全一致。训练脚本保存权重时建议同时把 config 字典存成 json这样报告里的每个数字都能追溯回当时的超参数。6.2 主结果表与消融论文放什么、报告放什么论文正文放一张主结果表横向是算法纵向是平均奖励、标准差、成功率和训练步数。报告则多放一张消融表列出基线和每个改造点逐步叠加后的效果让读者看清每个改动带来的增量。参考格式如下数字部分需要替换成你自己的实验结果实验平均奖励标准差成功率训练步数DQN baseline251.478.258%100kDQN PER312.655.374%100kPPO468.220.194%100k写作顺序按“先 baseline再改进再消融”来项目说明里把环境版本、Python 版本、依赖列表写在最前面报告里把训练曲线截图、评估日志、超参数表按训练顺序附上。我自己每次拿到一个强化学习源码包第一件事不是读算法而是先把评估脚本跑通再动代码养成这个习惯之后报告阶段的返工少了很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表