ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

强化学习小车实战:从DQN算法到奖励设计、仿真部署全攻略

强化学习小车实战:从DQN算法到奖励设计、仿真部署全攻略 简介基于Python的强化学习智能体小车项目以Deep Q-learning算法为核心实现无人车在模拟环境中的自主决策面向机器学习与强化学习入门者也可直接作为课程设计的完整参考。压缩包共17个文件整体约10.58MB包含Python源码、注释版地图脚本、Kivy界面配置、预训练模型、训练前后动图与实验截图以及多篇强化学习经典文献如时序差分学习、优先经验回放、基于马尔可夫决策过程的建模等从算法理论到工程实现形成闭环。目前已有145人学习下载。借助带注释的代码和项目说明可逐步理解Q-learning与深度神经网络的结合方式看清奖励设定和状态动作映射的关键细节直接加载预训练模型即可验证小车行为并通过动图对比观察训练前后的性能差异适合需要动手实践强化学习算法或完成无人车AI课程设计的研究者。1. 为什么这个项目值得跑强化学习小车卡住你的不是算法是“奖励”做基于 Python 的强化学习智能体小车绝大多数人不是死在神经网络上而是死在给智能体“发工资”的方式上。训练跑起来了loss 在降但小车要么原地转圈要么一头撞墙要么在仿真里满分、放到实体上直接废掉。这个项目的价值恰恰在于它把强化学习从“调一个 gym 环境里的 CartPole”升级成“让一个智能体在连续状态下做决策”你需要同时处理状态设计、动作空间、reward 塑造、训练稳定性和模型落地缺一环都会翻车。适合的人群也很明确正在做毕设或课程设计的学生、想从监督学习转强化学习的开发者、以及手里有 Arduino / STM32 小车想让它“自己学会跑”的创客。这篇笔记按我自己的落地路径来写从算法选型到仿真迁移把参数和坑位都留在明处。2. 先选对算法再动手为什么入门车控是 DQN 而不是 PPO 或 DDPG2.1 DQN 对小车控制够用而且调试成本低智能体小车的控制任务常见做法是把连续控制离散化成几个动作左转、直行、右转最多加一个刹车。这类离散动作空间的任务DQNDeep Q-Network是性价比最高的起点。PPO 和 DDPG 当然也能做但 PPO 需要调 clip 参数、entropy 系数DDPG 要处理 target policy smoothing 和探索噪声的方差对刚接触强化学习的人来说这些参数会让问题变成玄学。DQN 的调试面收敛很多核心就是 Q 网络、target 网络、replay buffer、epsilon-greedy 探索这四个部件出了问题都能单独定位。我见过的深度强化学习入门项目里用 DQN 变体比如 Double DQN 加 Prioritized Replay跑小车任务是最稳的组合。Double DQN 解决 Q 值过估计Prioritized Replay 让智能体更频繁地学习那些“差点撞墙”的样本这两个改动实现成本不高但对训练的稳定性提升明显。如果你的目标是尽快让小车学会走直线或避障而不是研究算法本身直接在这个组合上动手。注意 如果你的任务是小车在空旷场地自由漫游DQN 足够如果是动态避障且目标点频繁变化建议先跑通 DQN再考虑换 SAC 这类 off-policy 连续控制算法。 /注意2.2 自定义环境还是套用现成框架起步阶段别折腾市面上有现成的强化学习智能体框架比如 Stable-Baselines3、Ray RLlib它们封装好了算法你只需要写环境。但这里有个坑框架版本和 PyTorch 版本之间的兼容问题经常比写算法本身还耗时。我的建议是第一版用手写 DQN网络结构就一个三层 MLP这样每一步你都知道数据是怎么流的。等训练稳定了再迁移到 Stable-Baselines3 做大规模调参也不迟。自定义环境的核心是 reset 和 step下面给一版最小可跑的小车环境骨架import gym from gym import spaces import numpy as np class CarEnv(gym.Env): def __init__(self, max_steps200): super().__init__() # 状态: [距离障碍, 速度, 角度偏差, 距目标距离] self.observation_space spaces.Box(low-np.inf, highnp.inf, shape(4,)) # 动作: 0左转, 1直行, 2右转 self.action_space spaces.Discrete(3) self.max_steps max_steps self.step_count 0 def reset(self): self.step_count 0 # 初始化小车位置、朝向、障碍物位置 self.car_x, self.car_y, self.car_heading 0.0, 0.0, 0.0 self.obstacle_dist 5.0 self.target_dist 10.0 return np.array([self.obstacle_dist, 0.0, 0.0, self.target_dist], dtypenp.float32) def step(self, action): self.step_count 1 # 根据动作更新位置 if action 0: self.car_heading 0.3 elif action 2: self.car_heading - 0.3 self.car_x np.cos(self.car_heading) * 0.2 self.car_y np.sin(self.car_heading) * 0.2 # 计算新状态 self.obstacle_dist max(0.0, self.obstacle_dist - np.cos(self.car_heading) * 0.2) speed 0.2 angle_error np.abs(self.car_heading) self.target_dist np.sqrt((5 - self.car_x)**2 (5 - self.car_y)**2) # 奖励设计 reward -0.1 # 每步小惩罚促使尽快到达 done False if self.obstacle_dist 0.3: reward -10.0 # 撞障碍物 done True elif self.target_dist 0.5: reward 20.0 # 到达目标 done True elif self.step_count self.max_steps: done True obs np.array([self.obstacle_dist, speed, angle_error, self.target_dist], dtypenp.float32) return obs, reward, done, {}这段代码的逻辑说明step 函数里先根据动作更新小车朝向和位置然后重新计算四个状态量再根据“是否撞墙”“是否到达目标”返回 reward 和 done。注意angle_error用的是绝对值因为左偏和右偏对于这个任务来说都是“偏离”对称惩罚更利于收敛。速度在这里假设恒定如果你想加入加速/减速动作需要把动作空间从 3 扩到 5并修改位置更新公式。作为强化学习智能体的核心交互逻辑这个环境就是“智能体—环境闭环”的一半。另一半是 agent接下来把 DQN 的完整训练骨架写出来。2.3 DQN 训练骨架网络、记忆库、主循环一次跑通import torch import torch.nn as nn import torch.optim as optim import random from collections import deque class QNet(nn.Module): def __init__(self, state_dim4, action_dim3): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.net(x) class DQNAgent: def __init__(self, state_dim, action_dim, lr1e-3, gamma0.95, buffer_size10000, batch_size64, target_update100): self.action_dim action_dim self.gamma gamma self.batch_size batch_size self.target_update target_update self.q_net QNet(state_dim, action_dim) self.target_net QNet(state_dim, action_dim) self.target_net.load_state_dict(self.q_net.state_dict()) self.optimizer optim.Adam(self.q_net.parameters(), lrlr) self.buffer deque(maxlenbuffer_size) self.step_count 0 self.epsilon 1.0 self.epsilon_min 0.05 self.epsilon_decay 0.995 def act(self, obs, eval_modeFalse): if not eval_mode and random.random() self.epsilon: return random.randint(0, self.action_dim - 1) with torch.no_grad(): q_values self.q_net(torch.FloatTensor(obs).unsqueeze(0)) return q_values.argmax().item() def remember(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def replay(self): if len(self.buffer) self.batch_size: return batch random.sample(self.buffer, self.batch_size) states, actions, rewards, next_states, dones zip(*batch) 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) q_values self.q_net(states).gather(1, actions) with torch.no_grad(): max_next_q self.target_net(next_states).max(1, keepdimTrue)[0] target_q rewards self.gamma * max_next_q * (1 - dones) loss nn.MSELoss()(q_values, target_q) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # epsilon 衰减 self.epsilon max(self.epsilon_min, self.epsilon * self.epsilon_decay) # 定期同步 target 网络 self.step_count 1 if self.step_count % self.target_update 0: self.target_net.load_state_dict(self.q_net.state_dict())参数说明gamma0.95意味着智能体更看重近期奖励适合小车这种每步都有反馈的任务如果任务改成“走迷宫找终点”需要把 gamma 调到 0.99。epsilon_decay0.995大概在 800 个 episode 后探索率降到 0.05 附近如果你的训练预算只有 200 个 episode可以把衰减改成 0.98。target_update100表示每 100 步从在线网络拷贝一次参数太小会让训练不稳定太大会让目标 Q 值滞后。主循环的训练方式env CarEnv(max_steps200) agent DQNAgent(state_dim4, action_dim3) EPISODES 500 for ep in range(EPISODES): obs env.reset() total_reward 0.0 while True: action agent.act(obs) next_obs, reward, done, _ env.step(action) agent.remember(obs, action, reward, next_obs, done) agent.replay() obs next_obs total_reward reward if done: break if (ep 1) % 20 0: torch.save(agent.q_net.state_dict(), fcar_model_{ep1}.pth) print(fEpisode {ep1}, Total Reward: {total_reward:.2f}, Epsilon: {agent.epsilon:.3f})这里每 20 个 episode 保存一次模型对应到项目标题里的“模型.zip”部分就是你训练结束后会得到的权重文件。保存的粒度不建议太密否则硬盘会被写满但太稀又可能在调参后找不到合适的回滚点。20 是一个比较实用的折中。如果你的环境里有reset返回额外信息注意torch.save前确认网络结构不变否则加载时会报 key 不匹配的错误这一点在后面的避坑章节会展开说。3. 让小车学会“开车”reward 设计决定训练上限3.1 稀疏奖励是新手第一堵墙光给终点奖励智能体学不会很多第一次做强化学习小车的同学reward 只写了两条到达终点给 10撞墙给 -10其余时刻 0。这个设计在 500 步内几乎不可能收敛因为智能体一开始是随机策略它在有限步数里到达终点的概率极低所有尝试得到的 reward 都是 0 或 -10Q 值表格没有正样本可以学。这就是典型的稀疏奖励问题。解决思路是给每一步一个“中间信号”让智能体感受到“我在变好”或“我在变坏”。我一般会在第一版就加入三个分量距离变化、角度偏差、每步时间惩罚。具体公式# 假设上一状态到目标的距离是 prev_dist当前是 cur_dist dist_reward (prev_dist - cur_dist) * 2.0 # 靠近目标为正 angle_penalty -0.5 * abs(angle_error) # 车头偏离目标方向为负 time_penalty -0.05 # 每步微小惩罚 reward dist_reward angle_penalty time_penalty逻辑说明dist_reward用的是“距离差”而不是“当前距离”这是因为距离差能直接反映这一步是否有效而当前距离是绝对量智能体很难从绝对值里学到“我在进步”。系数 2.0 让这个分量在数值上明显压过time_penalty防止智能体发现“原地不动最省事”的偷懒策略。angle_penalty是一个强引导信号小车如果背对目标这个惩罚会持续放大迫使它先调头。参数调整建议如果小车在训练初期总在原地抖动把dist_reward系数从 2.0 提到 3.0如果小车学会了“快速乱撞”来碰运气把time_penalty从 -0.05 调到 -0.15让它为每一步的鲁莽付出代价。3.2 从连续量到波形分析reward 曲线的“假收敛”要盯紧一个很常见的现象训练到 200 个 episode 后总 reward 均值稳定在某个区间不再上升你以为它学到了放出来看却发现小车在原地画圈。原因是智能体发现了“奖励漏洞”——它转圈的时候angle_error一直在变dist_reward偶尔为正整体 reward 能维持在一个“饿不死”的水平但永远到不了终点。这时候的判断依据不是 reward 均值而是“每 episode 的平均步数”。画圈策略的步数分布通常集中在 180~200 步被max_steps截断而真正学会到达终点的策略步数会显著下降到 50~100。我训练时习惯同时打印三个指标total reward、episode steps、碰撞次数。如果 rewards 涨了但 steps 没有下降趋势就是学歪了。另外建议用滑动平均看曲线别盯着单次 episode 的抖动。可以用这个简单的滤波import collections def moving_average(data, window20): q collections.deque(maxlenwindow) avg_list [] for x in data: q.append(x) avg_list.append(sum(q) / len(q)) return avg_list把每轮的训练曲线做平滑后再观察趋势会清晰很多。这个做法同样适用于你保存的模型评估——加载.pth文件后跑 20 个 episode 取平均 reward比单个 episode 的结果更有说服力。3.3 课程学习让智能体从“直道冲刺”开始再上难度如果你的小车任务是“从起点出发绕过障碍到达终点”直接全场景训练同样容易崩。常见做法是课程学习先让小车的起点和终点在一条直线上中间无任何障碍学“走直线”然后加入一个静态障碍最后再加入随机初始位置。我通常是在环境类里加一个difficulty字段reset时根据难度参数决定障碍物位置和起点范围class CurriculumCarEnv(CarEnv): def __init__(self, difficulty0): super().__init__() self.difficulty difficulty def reset(self): obs super().reset() if self.difficulty 0: self.obstacle_dist 999.0 # 无障碍 self.car_x, self.car_y 0.0, 0.0 elif self.difficulty 1: self.obstacle_dist 4.0 # 一个固定障碍 else: self.car_x, self.car_y np.random.uniform(-2, 2, size2) self.obstacle_dist np.random.uniform(2.0, 6.0) return np.array([self.obstacle_dist, 0.0, 0.0, np.sqrt((5 - self.car_x)**2 (5 - self.car_y)**2)], dtypenp.float32)训练时这样接前 50 个 episode 用 difficulty050~150 用 difficulty1150 之后切到 difficulty2。注意切换难度时不要重置 agent 的 epsilon探索率应该按训练进度自然衰减否则智能体又要重新试探一大堆无效动作。4. 智能体小车项目最常见的 5 个坑从黑匣子到玄学调试4.1 训练几十轮 reward 始终为负没有任何上升趋势现象训练了 100 个 episodetotal reward 一直在 -20 到 -10 之间徘徊没有一次到达终点。原因大部分情况是 reward 信号里“正确行为”的正收益被“错误行为”的负收益淹没了。比如time_penalty-0.1每一步都在扣分而dist_reward系数太小智能体每步的行动收益不稳定总体期望为负。另一个常见原因是 replay buffer 里全是早期随机策略产生的“坏样本”正样本被稀释。解决把dist_reward的系数调大比如从 2.0 提到 4.0同时把time_penalty降到一个极小的量比如 -0.01先让智能体能“活下来”并到达终点确认它能稳定到达终点后再逐步加大时间惩罚逼它优化路径。关于缓冲区我一般会在训练前用随机策略灌入 500 条样本再开始正式训练这样 buffer 里有一定数量的“中奖样本”第一次 replay 不至于学不到东西。4.2 reward 曲线涨了但小车在原地转圈或绕远路现象训练中后期reward 均值从 -15 涨到了 30但把模型单独拿出来测试小车的轨迹是一个半径很小的逆时针圆或者绕了超大一圈才到达终点。原因这是典型的 reward hacking。智能体发现“持续转弯”能让angle_error的分量周期性归零同时dist_reward在圆周运动中有部分步长为正整体 reward 能维持为正。但它没有真正学会“朝向目标”。解决给angle_error的权重加大让持续转向的累积惩罚超过偶尔的dist_reward收益。另一个有效手段是限制总步数比如从 200 步降到 100 步让转圈策略根本没有足够步数拿到“平均正收益”。最后检查一下reset时angle_error的初始分布如果初始角度偏差超过 90 度的比例太高小于 200 步的任务里智能体根本没有时间调头这也会诱导它学旋转妥协策略。4.3 仿真里跑满分放到实体小车上一碰就废现象模型在仿真环境里 100% 到达终点换到实际小车上直线都走不稳走两步就偏出赛道。原因sim2real gap。仿真环境的运动模型太理想没有考虑轮胎摩擦、电机响应延迟、传感器噪声等因素。深度学习模型对输入分布特别敏感仿真里的“干净状态”和实际传感器读出的“带噪声状态”差异很大。解决第一是在训练时给状态加噪声模拟真实传感器误差第二是动作执行时加延迟模拟电机和舵机的响应时间第三是使用固定步长仿真比如 10 次仿真步才做一次控制决策模拟实际控制频率低于仿真频率的问题。这个坑在后文第 5 章会继续展开。4.4 加载模型文件报错size mismatch for net.0.weight现象训练完保存了car_model_100.pth第二次运行时加载model.load_state_dict(torch.load(car_model_100.pth))报错size mismatch for net.0.weight: copying a param with shape torch.Size([128, 4]) ...。原因你改了网络结构或者输入维度。最常见的是把状态从 4 维改成了 5 维比如加了“是否接近终点”的标志位但模型文件还是旧结构的权重。PyTorch 的load_state_dict默认要求 key 完全匹配。解决先打印模型结构和权重 shapes再写加载逻辑不要盲加载python -c import torch m torch.load(car_model_100.pth) for k, v in m.items(): print(k, v.shape) 如果是新增了状态维度需要在旧权重上扩展而不是直接加载。我习惯在保存模型时额外存一个state_dim字段进 checkpointtorch.save({ state_dim: 4, action_dim: 3, model: agent.q_net.state_dict(), }, car_model_100.pth)这样加载前可以先校验维度提前发现不匹配而不是等load_state_dict报一个让人懵的错。4.5 训练结果波动大同一套参数跑两次一次成功一次失败现象连续两次训练第一次 300 个 episode 就稳定了第二次 500 个 episode 还没学会总 reward 差了一倍。原因PyTorch 的随机初始化、训练数据的随机采样、epsilon 探索的随机性叠加导致每次训练轨迹完全不同。强化学习对随机种子非常敏感这在业界已经是很出名的“黑匣子”问题不是你的代码写错了。解决在训练入口固定所有随机种子import random, torch, numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)但注意即使固定了随机种子如果你的代码里有并行数据加载DataLoader或者多线程结果仍然可能不一致。一个更务实的方法是每套参数跑 3 次取平均结果用平均值判断参数好坏而不是单次训练结果。5. 从仿真模型到智能体小车实体模型导出、部署与两处关键改造5.1 训练好的 PyTorch 模型怎么部署到小车上训练得到的car_model_100.pth是 PyTorch 权重不能直接在单片机上跑。对于 Arduino 或 STM32 这类资源受限的设备常见做法有两个方向。第一是模型导出为 ONNX再转成 TensorFlow Lite 或者直接用 ONNX Runtime 在树莓派或 Jetson Nano 上推理。第二是只保留网络的前向部分在设备端写一个简单的矩阵乘法和 ReLU 实现。如果你的小车用的是树莓派最省事的方案是直接装 PyTorch CPU 版本加载权重做推理。推理代码很简短import torch import torch.nn as nn class QNet(nn.Module): # 与训练时完全一致的网络结构 pass # 填你的网络定义 model QNet() ckpt torch.load(car_model_100.pth, map_locationcpu) if isinstance(ckpt, dict) and model in ckpt: model.load_state_dict(ckpt[model]) else: model.load_state_dict(ckpt) model.eval() def select_action(obs): with torch.no_grad(): q model(torch.FloatTensor(obs).unsqueeze(0)) return q.argmax().item()参数说明map_locationcpu是必须的因为训练时可能用的是 CUDA部署机器上没有 GPU少了这个参数会直接报错。model.eval()会关闭 dropout 和 batch norm 的训练行为如果你网络里没有这些层写不写都能跑但写上是一个好习惯。如果目标平台是 STM32F103ZET6 这类 MCU没有操作系统跑不了 PyTorch。这时候需要把权重提取成 C 数组在网络训练完成后执行导出import json weights model.state_dict() export_data {k: v.cpu().numpy().tolist() for k, v in weights.items()} with open(car_weights.json, w) as f: json.dump(export_data, f)然后在单片机上实现前向传播核心就是两层矩阵乘法和 ReLU浮点运算建议用float32STM32F103 的主频能跑到 72MHz一个 128×128 的矩阵乘法一次推理大约几毫秒控制频率 20Hz 是没问题的。需要注意的是内存128×128 的权重加上 4×128 的输入层总共约 66KB需要确认单片机 RAM 是否够。如果不够可以把隐藏层从 128 降到 64精度损失通常在可接受范围。5.2 控制频率和推理延迟为什么实车“反应慢半拍”强化学习智能体小车在仿真里用的是“每步决策一次”步进时间是固定的。但实体小车有一个致命差异从传感器读取状态到模型推理出新动作再到执行机构响应整个过程有延迟。如果你的模型推理需要 50ms而小车以 0.5m/s 的速度前进50ms 内已经走了 2.5cm这还没算电机响应时间。我踩过的一个坑是仿真里动作频率设成了 10Hz但推理延迟只有几毫秒实车上传感器采集周期是 30ms电机响应又是 50ms整体延迟了 80ms相当于 4 倍的动作间隔。这个时候智能体做出的每一个决策都是“过时的决策”模型自然崩。解决思路有两种。一是把实车的状态更新频率降下来让每个控制周期 100ms确保“决策时刻”和“状态采集时刻”对齐二是给训练环境的人工延迟加上去我在训练时就是故意在step()里让动作延迟 N 个仿真步再生效模拟实车的执行延迟def step(self, action): # 模拟执行延迟缓存动作延迟 3 步生效 self.pending_actions.append(action) if len(self.pending_actions) self.act_delay: effective_action 1 # 直行 else: effective_action self.pending_actions.pop(0) # 使用 effective_action 更新状态act_delay设 2 到 4对应的就是 20ms 到 40ms 的执行延迟这样训练出来的模型在实车上的鲁棒性会好很多。5.3 别让强化学习包办一切混合控制更实用在实车上做过几次实验后一个非常现实的体会是不要期望强化学习模型直接输出 PWM 波控制电机这个体验很糟模型一旦在一个没见过的情况下失误小车的反应可能是“猛的打死方向盘”。我更推荐的做法是强化学习模型输出“行为意图”左转、直行、右转、减速底层由传统 PID 控制器执行。比如用 PID 控制转向用 DQN 输出的动作来决定目标角度# 伪代码DQN 动作 - PID 目标值 action agent.select_action(obs) # 0左 1直 2右 if action 0: setpoint -20 # 目标角度 -20 度 elif action 1: setpoint 0 else: setpoint 20 pid_output pid_update(measurementcurrent_heading, setpointsetpoint) # pid_output 再映射到电机的 PWM这样做的好处是PID 负责“怎么到”的稳定性强化学习只负责“去哪”的决策系统的可解释性和安全性都提高了。智能体框架再强也需要一个靠谱的执行层这不是技术退步是工程常识。6. 验证智能体真的学会了三种比“看曲线”更靠谱的评估方法6.1 泛化测试换初始位置和障碍布局看模型还认不认路训练时用的 env 是固定的起点和固定的障碍位置模型很可能记住了“路径”而不是学到了“导航”。验证泛化的方法很简单在评估模式下随机生成 50 个不同的起点和障碍位置跑一遍统计成功率。这能直接测出模型是否过拟合。我在测试时会在评估代码里关闭 epsilon 探索test_success 0 for i in range(50): env CarEnv(difficulty2) np.random.seed(i) obs env.reset() # 随机覆盖起点 env.car_x np.random.uniform(-3, 3) env.car_y np.random.uniform(-3, 3) obs[3] np.sqrt((5 - env.car_x)**2 (5 - env.car_y)**2) for _ in range(200): action agent.act(obs, eval_modeTrue) obs, reward, done, _ env.step(action) if done and reward 0: test_success 1 break print(f成功率: {test_success / 50})如果你的成功率低于 80%说明模型过拟合了需要做两件事训练时增加起点随机化以及增加训练数据的多样性。只盯着训练集上的 reward 没有意义智能体是“背题”还是“学会了”泛化测试一测就现原形。6.2 检查 reward hacking给奖励函数“打补丁”的常见姿势有些智能体的行为“作弊”了但成功了。比如dist_reward按距离减少给正分但小车不是走向目标而是高速冲向障碍物旁边再绕回来利用“接近目标”的瞬间拿分。这类行为单看测试成功率可能还是很高但路径质量明显不对。我会在测试时额外记录平均步数和平均路径长度如果步数远大于最优路径所需步数我就可以判断存在“低效策略”。这时候要回到第 3 章的 reward 设计增加对“步数”的惩罚压缩智能体绕路的空间。要留意这个环节是强化学习项目里最需要耐心的地方需要反复调参测试不是一次能写对的。6.3 用“checkpoint 集成”救回一个不稳定的模型最后一个我常用的验尸技巧如果最终模型表现不稳定不要只盯着最后一次的存档。训练过程中保存的car_model_20.pth、car_model_40.pth这些检查点可以在评估时拼接起来投票。强化学习里一个很实用的小技巧是分别加载若干个 checkpoint每个模型输出一个动作小车的最终动作取多数投票import torch import collections models [] for ckpt in [car_model_60.pth, car_model_80.pth, car_model_100.pth]: m QNet() m.load_state_dict(torch.load(ckpt)) m.eval() models.append(m) def ensemble_action(obs): votes [] for m in models: with torch.no_grad(): q m(torch.FloatTensor(obs).unsqueeze(0)) votes.append(q.argmax().item()) return collections.Counter(votes).most_common(1)[0][0]这个方法能抹平单个 checkpoint 的“偏执”成功率往往比最后一个模型高出 10~15 个百分点。代价是推理时间增加几倍。实车一般选两到三个 checkpoint 做投票树莓派的算力还能承受如果跑更重的网络就只做单模型部署。我自己的习惯是训练时每 20 个 episode 保存一次训练结束后先做一轮批量评估挑出评估指标最好的 3 个 checkpoint再在实车上逐个测试。这么做虽然麻烦一点但能帮你建立一套完整的“训练—评估—部署”闭环。希望这些细节对你有用祝你的智能体小车早日学会跑直线。本文还有配套的精品资源点击获取
返回列表