1. 先想清楚一个核心问题:避障控制为什么值得用DQN重做一遍
做移动机器人避障的人,十有八九都有过这种经历:用人工势场法(APF)在空旷场景里调得行云流水,小车丝滑绕障直奔目标,觉得自己已经掌握控制真谛了。然后换一张复杂地图,U型陷阱、密集障碍区、窄通道一上,小车就开始“鬼打墙”——要么在局部极值点原地抽风,要么在窄道里来回摆头,怎么调引力系数和斥力系数都治不好。与此同时,你要是改用A*或者RRT这类全局路径规划,又得预先知道完整地图、实时重规划,动态障碍一出现,延迟和计算量立刻抬上来了。
这个痛点才是整个项目的出发点。传统几何方法不是不好,而是它们强依赖“建模准确”,而真实避障场景恰恰是模糊的、动态的、不可精确建模的。我接触到DQN(Deep Q-Network)时,最大的直觉是:它压根不需要你给出环境模型,它通过智能体与环境交互产生的奖励信号,自己学着做“状态到动作”的映射。也就是说,它不规划一条几何路径,而是学习一个控制策略——这正好补上了传统方法的短板。
但说句实话,如果直接拿原版DQN硬啃避障问题,坑也不少。动作空间离散化设计不合理、奖励函数给不出有效梯度、经验回放里有效样本寥寥无几、探索阶段像无头苍蝇一样乱撞……这些问题我一个没落下全踩了一遍。所以这篇文章想讲的,不只是一个能跑的demo,而是我在Pytorch下完整搭起“DQN → 优先级采样PER → 人工势场APF引导”这一整套避障控制研究流程的真实实操记录,踩过的坑、调过的参、对比过的效果,都会写出来。项目同时保留了Matlab和Python两个版本的环境代码,前者用来做动力学验证和势场参数可视化,后者负责Pytorch网络训练和策略评估,整条链路下来,我觉得对做智能控制、移动机器人、强化学习应用的同学都有参考价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 避障问题的建模与仿真环境搭建:先让算法有地方“翻车”
2.1 状态空间、动作空间和奖励函数的三元组设计
强化学习第一步,也是最容易被忽略的一步,是环境建模。状态空间如果给的信息不够,网络再深也学不出所以然;动作空间太粗糙,策略上限就锁死了;奖励函数设计得不好,智能体学出来的行为一定是歪的。我当时搭的是一个2D平面仿真环境,小车用激光雷达测距感知周围障碍物,任务是从起点走到目标点并且全程不碰撞。
状态向量最终确定为15维:12维是激光测距数据,归一化到0~1之间;1维是当前与目标点的归一化距离;2维是目标方向角的sin和cos编码。没有直接用目标角度数值,是因为角度在±π边界会跳变,直接扔给神经网络会让训练震荡得很厉害,这是很多第一次做的人容易忽略的细节。
动作空间我做了7个离散动作:左转45度、左转30度、左转15度、直行、右转15度、右转30度、右转45度,速度固定。之所以不把速度也做成动作维度,是因为7个转向动作在训练难度上还比较温和,一旦把速度离散加进来,动作空间变成二维组合,样本复杂度会指数上升,前期调试成本会翻好几倍。
奖励函数我采用的是“半稀疏半密集”的设计:
| 事件 | 奖励值 | 设计意图 |
|---|---|---|
| 到达目标点 | +200 | 稀疏主奖励,明确任务终点 |
| 与障碍物碰撞 | -100 | 终止本回合,惩罚危险行为 |
| 距离目标点比上一步减小 | +1.0 | 密集奖励,给学习过程提供持续梯度 |
| 距离目标点比上一步增大 | -0.5 | 抑制绕路和漂移行为 |
| 每一存活步 | -0.05 | 时间成本惩罚,鼓励高效策略 |
这套设计的核心逻辑是:不能完全依赖稀疏奖励,因为在小车乱撞的阶段,它根本到不了目标,整个回合全是0分,网络拿不到任何有效梯度;也不能全给密集奖励,否则小车会发现“我只要反复横跳压缩距离”也能拿分,策略会变得很急功近利。
2.2 Matlab和Python在项目里如何分工协作
标题里提到了Matlab和Python两个版本,这里展开说说它们各自负责什么。我的做法是:先用Matlab快速搭建运动学模型和人工势场法原型,这一步用矩阵运算和绘图极方便,几行代码就能画出势场分布图、合力方向图,调整引力系数ξ和斥力系数η的时候,直接看图就能判断参数合不合理。
等势场算法逻辑验证清楚了,再转到Python端,重新用面向对象方式封装仿真环境接口,状态计算、碰撞检测、激光测距、奖励逻辑全部重写一遍。Python环境负责和Pytorch训练循环对接,Matlab环境则作为独立验证工具留存,后续也可以用Matlab对训练好的策略做离线仿真可视化对比。
这种双语言分工看起来很笨,实际用下来效率很高。因为神经网络训练阶段,调参、改奖励、改网络结构的频率很高,Python环境下改一行就能重训,效率远高于在Matlab里追着改语法;而势场算法的参数探索阶段,Matlab的即时可视化和交互式调试又比Python顺手太多。两边各干各擅长的活儿,整条流水线才顺。
2.3 碰撞检测与激光测距的工程细节
仿真环境的物理细节不需要特别真实,但也不能太顺手。我用栅格地图存储障碍物分布,小车占据栅格与障碍物栅格重叠即判定碰撞。激光测距用射线与栅格求交算法,12条射线均匀分布在车头前方180度范围内,最大探测距离设置为5米,超过就返回5米并归一化为1.0。
这里有一个需要特别警惕的坑:当激光探测距离达到上限时,状态向量里会有一大片1.0,意味着“这个方向很空旷”。如果探索阶段的随机动作占比太低,小车会倾向于一直直行,因为它看不到近处障碍物,等撞上了就晚了。实测下来,探索率初始值设定到1.0,并且在训练前期保持较高的随机动作概率,是缓解这个问题的基本手段。
3. DQN本体:网络结构设计与Pytorch训练循环的工程实现
3.1 四层全连接网络就够用,别一开始就上深度网络
对于这种输入15维向量的决策任务,我选用的网络结构是四层全连接网络:15 → 128 → 256 → 256 → 7。中间激活函数全部用ReLU,输出层不加激活函数,因为Q值本身是回归目标,可以取任意值,加了激活函数反而限制了表达范围。
python复制import torch
import torch.nn as nn
import torch.nn.functional as F
class QNetwork(nn.Module):
def __init__(self, state_dim=15, action_dim=7):
super(QNetwork, self).__init__()
self.fc1 = nn.Linear(state_dim, 128)
self.fc2 = nn.Linear(128, 256)
self.fc3 = nn.Linear(256, 256)
self.fc4 = nn.Linear(256, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.relu(self.fc3(x))
return self.fc4(x)
为什么不加大网络?我在实验里对比过网络容量对训练结果的影响,结论是:在小规模离散动作避障任务中,128-256-256这个体量已经足够表达决策边界,继续加深加宽只会显著拖慢训练速度,而精度和收敛速度提升极其有限。选网络的第一原则是匹配任务复杂度,而不是盲目堆参数。
3.2 经验回放和目标网络:缺哪个都会原地爆炸
原版DQN能够稳定训练,靠的是两个核心机制:经验回放(Experience Replay)和目标网络(Target Network)。
经验回放是解决样本相关性的关键设计。如果每采集一个样本就立刻用它更新网络,相邻时间步的状态高度相关,会导致参数更新方向不断抖动,训练无法收敛。我把样本存进容量为20000的环形缓冲队列,每次从中随机采样128条来计算损失,这样能打破样本间的时间相关性,还能提高样本复用率。
目标网络解决的是“训练目标不断移动”的问题。如果用一个网络既算当前Q值又算TD目标值,那么目标本身会随着参数更新而不断变化,就像一只狗追自己的尾巴。我设置了一个结构完全相同的目标网络,每隔2000步训练步数将当前策略网络参数硬拷贝给它,用它来计算TD目标值,当前网络只负责逼近这个相对稳定的目标。代码实现上再简单不过:
python复制if global_step % target_update_interval == 0:
target_net.load_state_dict(online_net.state_dict())
但就是这一步,我最初忘加的时候,训练loss曲线像心电图一样大起大落,小车永远学不会稳定的转向策略。TD目标一直在跟着Q值参数漂移,网络学来学去都是在追一个移动靶子。
完整的训练循环框架是这样的:
python复制private void train_loop()
用Python描述核心逻辑就是:
python复制online_net.train()
for episode in range(total_episodes):
state = env.reset()
done = False
episode_reward = 0
while not done:
if random.random() < epsilon:
action = random.randrange(action_dim)
else:
state_tensor = torch.FloatTensor(state).unsqueeze(0)
q_values = online_net(state_tensor)
action = torch.argmax(q_values).item()
next_state, reward, done = env.step(action)
replay_buffer.push(state, action, reward, next_state, done)
episode_reward += reward
if replay_buffer.size() >= batch_size:
batch = replay_buffer.sample(batch_size)
loss = compute_dqn_loss(online_net, target_net, batch)
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(online_net.parameters(), 10.0)
optimizer.step()
global_step += 1
if global_step % target_update_interval == 0:
target_net.load_state_dict(online_net.state_dict())
state = next_state
if done:
break
epsilon = max(epsilon_min, epsilon * epsilon_decay)
3.3 Pytorch训练循环里三个容易被忽略的细节
第一个细节是梯度裁剪。我在最初的版本里没有加梯度裁剪,结果就是训练中偶尔某一条样本的TD误差特别大,一次反向传播直接把Q网络参数冲得七零八落,之后几十个回合的表现都会崩坏。加上clip_grad_norm_(parameters, max_norm=10)之后,这种“单样本冲击导致全局技能倒退”的问题几乎绝迹。
第二个细节是优化器选择。我用的是Adam优化器,学习率设为3e-4。Adam对学习率的敏感度比SGD低很多,比较适合强化学习这种非平稳优化场景。但Adam并不万能,后期如果训练曲线陷入震荡,把学习率降到1e-4往往能立竿见影地恢复稳定。
第三个细节是探索率衰减策略。epsilon初始值是1.0,最小0.05,衰减速度不能一视同仁。任务前期探索太少会导致小车永远找不到有效路径,探索太久又会浪费大量算力在无意义的随机动作上。我用了逐步衰减策略:每完成一个回合,epsilon乘以一个略小于1的系数,大致到30000步左右衰减到0.1附近再进入慢速衰减,这个节奏在多种地图下都比较稳定。
4. 优先级采样(PER)改造:从均匀采样到按TD误差采样的完整链路
4.1 均匀回放采样在避障任务里的失效瞬间
标准DQN的经验回放是均匀采样,所有历史样本被抽到的概率完全一样。这在简单的控制任务里够用,但放到避障场景中,问题立刻暴露出来。
小车训练早期的轨迹几乎全是“撞墙-罚分-重置”,这类样本的TD误差非常小——反正动作不好,Q值预测和实际回报差距不大,没有太多可学的。真正有价值的是那些“绕过障碍物之后成功接近目标”“在狭窄通道里选择正确转向成功脱困”的片段,这类样本在缓冲区内占比很低,均匀采样下它们被抽中学习的概率很小,相当于数据库里躺着一堆无效数据等着被随机翻牌。
优先级采样(Prioritized Experience Replay,PER)的解决思路非常直接:谁更值得被学习,谁被采样的概率就更高。我正是因为这个原因,在完成基础DQN之后立刻开始了PER改造。这里的“谁更值得学习”,实际指标用的是TD误差绝对值。TD误差越大,说明当前Q值预测与目标之间的差距越大,网络对这个状态还没学好,就该给它更多训练机会。
用一个直观比方来理解:均匀采样就像老师每次随机点一个学生回答问题,而优先级采样更像老师优先点那些上次作业错得最多的人,前者课堂效率整体偏平,后者则把时间花在最薄弱的地方。
4.2 SumTree数据结构与PER的最小实现方案
实现PER最容易翻车的不是公式推导,而是数据结构。如果每次采样时都对缓冲区里所有样本按优先级排序,代价是O(N log N),训练速度会慢到让人怀疑人生。业界标准方案是用SumTree(求和树)维护样本优先级,插入、更新、采样三个操作都是O(log N)。
SumTree的核心逻辑是:所有叶子节点存储样本优先级,每个父节点存储两个子节点之和,根节点就是整棵树的优先级总和。采样时,随机生成一个0到总优先级之间的数,从根节点开始二分下探,每次根据左右子树的累计和判断往哪边走,直到叶子节点,这个叶子对应的样本就是要抽的数据。最小实现长这样:
python复制class SumTree:
def __init__(self, capacity):
self.capacity = capacity
self.tree = np.zeros(2 * capacity - 1)
self.data = [None] * capacity
self.write_idx = 0
def add(self, priority, data):
idx = self.write_idx + self.capacity - 1
self.data[self.write_idx] = data
self.update(idx, priority)
self.write_idx = (self.write_idx + 1) % self.capacity
def update(self, idx, priority):
change = priority - self.tree[idx]
self.tree[idx] = priority
idx = (idx - 1) // 2
while idx >= 0:
self.tree[idx] += change
idx = (idx - 1) // 2
def get(self, value):
idx = 0
while True:
left = 2 * idx + 1
right = left + 1
if left >= len(self.tree):
break
if value <= self.tree[left]:
idx = left
else:
value -= self.tree[left]
idx = right
return idx, self.data[idx - self.capacity + 1]
这个数据结构看着不复杂,但接进训练循环后有几个关键节点必须想清楚:新样本插入时优先级应该设成一个初始偏大的值,比如该时刻的TD误差加上一个极小量,否则新样本很可能长时间得不到采样,陷入“越不被采样越没优先级”的恶性循环;样本被抽中并训练之后,要根据新的TD误差更新SumTree中对应位置的优先级,未被抽中的样本优先级保持不变。
4.3 优先级采样必须配套的重要性采样权重
PER存在一个理论上的死穴:它改变了样本分布,导致Q值更新不再是独立同分布下的无偏估计。从数学上看,训练周期长了确实会产生偏差,需要通过重要性采样权重(Importance Sampling Weight)来纠正。
每个样本i的权重计算公式为:
w_i = (1/N · 1/P(i))^β
其中N是缓冲区大小,P(i)是样本i的采样概率,β是偏差修正系数。β在训练开始时设到0.4左右,训练过程中逐步线性增加到1.0。初期的β较小,相当于容忍一定偏差换取训练速度;后期β逐步逼近1.0,偏差被完全修正,保证收敛稳定性。
我在初期曾经“偷懒”跳过了这个权重修正,直接把采样的样本喂给网络,当时的观察是:训练前50个回合一切正常,50个回合之后loss曲线开始剧烈震荡,收敛速度直线下降。回退一版加上权重修正后,震荡明显收敛。很多开源实现里样本权重被简单归一化了,但细节在于采样概率P(i)到底怎么算。
标准做法是指数化优先级:P(i) = priority_i^α / Σpriority^α,其中α控制优先级对采样概率的放大程度。α=0时退化为均匀采样,α越小,样本优先级差异对采样概率的影响越温和;我最终固定在0.6,实验对比下来这个值在收敛速度与稳定性之间平衡得最好。
整体的样本权重、优先级与TD误差计算可以这样写入训练循环:
python复制for batch_idx in batch_indices:
sample = replay_buffer.data[batch_idx]
state, action, reward, next_state, done = sample
state = torch.FloatTensor(state).unsqueeze(0)
next_state = torch.FloatTensor(next_state).unsqueeze(0)
with torch.no_grad():
q_next = target_net(next_state).max(dim=1, keepdim=True).values
td_target = reward + gamma * q_next * (1 - done)
q_current = online_net(state).gather(1, torch.LongTensor([[action]]))
td_error = (q_current - td_target).abs().item()
priorities[batch_idx] = td_error + 1e-3
weights[batch_idx] = (buffer_size * probs[batch_idx]) ** (-beta)
4.4 PER改造后的实测收益
PER改造完成之后,我在相同地图、相同随机种子下跑了对照实验,结果比较有代表性:
| 方案 | 收敛轮次 | 最终平均回合奖励 | 到达成功率 |
|---|---|---|---|
| 原版DQN | 约340轮附近曲线才开始抬头 | 约-12 | 62% |
| DQN + PER | 约280轮左右基本稳定 | 约-7 | 74% |
收敛速度提升20%左右,成功率提升12个百分点,训练过程的loss震荡幅度明显变小。最直观的感受是:早期有效样本的学习次数变多之后,小车“撞墙之后能意识到自己撞了墙”的速度快了很多,而不是像原来那样在同样的错误上反复被均匀采样慢慢纠正。
但PER不是万能补丁。当随后把人工势场加进来之后,我发现最优参数又发生了变化,这个细节留到后面的混合策略部分一起讲。
5. DQN + 人工势场混合策略:让APF给DQN当“训练助教”
5.1 为什么要把APF请回来说客串
这里有一个看起来有点矛盾的问题:我一开始就是因为APF避障效果不理想才转投强化学习,为什么后来又把它接回来了?
答案是:APF的缺点集中在最终决策层面,而它的优点恰恰集中在强化学习训练脚本最匮乏的探索阶段。APF天然知道该往哪个方向走,虽然它会陷入局部极值、会在窄道里抖,但它给出的方向在大部分时间内是合理的,尤其适合给DQN前期的“无头苍蝇式探索”提供方向性引导。
类比来说,APF像一位熟悉基础知识、但面对复杂考题会犯糊涂的“助教”。DQN刚开始训练时是个完全不懂规则的新生,让助教先手把手带一段路,让新生积累一些合理动作的样本;等新生能力见长,再让助教退场。
5.2 三种混合方式的对比与最终选择
要做到DQN和APF结合,有多种思路。我先后尝试了三种方案,分别记录一下实测感受。
方案A是把APF计算出的“建议方向角度”作为额外维度拼接到状态向量中,作为DQN的附加特征输入。结果:训练有轻微提升,但幅度很有限,瓶颈在于APF方向只是一个标量提示,DQN需要自己慢慢去学着“理解并使用”这个提示,学习成本仍然很高。
方案B是让APF直接计算每个离散动作对应的势场变化量,让DPN的Q值加一个APF修正项,再选动作。这种融合方式效果激进,但风险很大,APF在局部极值处的错误方向会被直接放大并带偏DQN策略,训练后期出现莫名的策略退化。
方案C最终被我用在了主线方案里:在训练阶段的动作选择层做融合,而不是动DQN的Q函数。具体流程如下:
- 根据当前状态计算APF合力方向,映射到最邻近的离散动作,得到“引导动作”。
- 训练中一个回合内,以β概率直接执行这个引导动作。
- 以1-β概率退回标准的epsilon-greedy策略(按DQN当前策略输出动作)。
- β从初期的0.5逐步衰减到0.05,让训练重心逐渐从“教师主导”切换到“学生自主”。
方案C的核心思想是:把APF当作训练阶段的探索引导器,而不是最终决策的一部分。这样APF也坑进局部极值时,DQN尝试其他动作的机会仍然存在,不会被APF的错误一票否决。
5.3 混合策略下的数据流和奖励变化
引入引导探索之后,经验回放缓冲区里的样本成分发生了肉眼可见的变化:训练前100个回合,有效样本(靠近目标或转向合理)的密度比纯DQN高出一大截。平均单回合奖励从纯DQN早期的-60左右直接拉升到-20上下,这个信号的直接意义是:小车在APF“带路”下能有更多机会接触“安全的靠近目标路径”,而不是一上来就撞墙得负分。
但要警惕一个问题:当过强的引导持续太久,DQN的Q函数会退化成一个“对APF行为的简单模仿网络”。我最早把β初始值设到0.8,结果后期把β降下来之后,小车立刻频繁碰壁,最终策略远不如β初始值0.5的版本。原因是前期的Q网络过度拟合了APF的避障轨迹,根本没有学到自己在复杂情况下的判断能力。所以β的衰减节奏应当比epsilon衰减更慢,务必给DQN留下足够的自主试错空间。
此外,混合策略下的PER参数也需要重新调。我原来在单DQN + PER下用α=0.6效果很好,但加上APF引导之后,缓冲区中有效样本不再稀缺,过强的优先级放大反而会引入额外震荡。最终把α降低到0.4,稳定性明显回升。这再次印证了前面那句老话:强化学习算法组合的参数调优,必须放在同一个目标下整体考虑,没有一套配置可以包打天下。
5.4 人工势场参数先用Matlab调好,再给Python用
人工势场法的核心参数是引力系数ξ和斥力系数η,还有一个斥力作用距离阈值ρ₀。引力公式U_att = 0.5ξd²,斥力公式U_rep = 0.5η(1/ρ - 1/ρ₀)²(当ρ≤ρ₀时有效),合力为两者梯度的负向叠加。
这些参数在Python仿真里硬调很痛苦,因为每次修改都需要重跑环境、观测每一步的受力变化,很难判断具体是哪一项参数导致避障失败。我的办法是把参数探索放在Matlab端:直接画出整个地图的势场等高线图、合力方向矢量图,一眼就能看出哪些区域会产生局部极小值,哪些参数下窄道附近的受力方向合理。
Matlab的可视化调试效率极高,通常改一轮参数再画一次图,几十秒就能定出一套在多种地图下都相对合理的参数。调好之后把参数硬编码成一个Python字典传给仿真环境,电位场计算逻辑两端共用同一个物理模型,跑出来的行为一致性很好。这种“Matlab做环境基建、Python做训练演进”的分工贯穿了项目全程,让我少走了大量弯路。
6. 实验对比与调参经验:收敛曲线背后的真实信号
6.1 五种方案对照的实验设计
为了把不同组件的贡献拆开来看,我固定了随机种子、地图文件、训练回合数和评估逻辑,做了五组对照实验:
- 方案A:原版DQN
- 方案B:DQN + PER
- 方案C:DQN + APF引导探索
- 方案D:DQN + PER + APF(三者完整结合)
- 方案E:纯人工势场(不训练,作为基线参考)
每组训练500个回合,每20个回合暂停训练,在固定起点和障碍物布局下进行10次独立评估,记录平均累计奖励、到达成功率、平均到达步数三项指标。
6.2 训练曲线与最终性能对比
实验测得的代表性数据如下:
| 方案 | 稳定点轮次 | 最终平均奖励 | 到达成功率 | 备注 |
|---|---|---|---|---|
| 原版DQN | 约340轮后才初步稳定 | 约-12 | 62% | 后期仍有轻微震荡 |
| DQN + PER | 约280轮稳定 | 约-7 | 74% | 方差明显缩小 |
| DQN + APF | 约180轮基本收敛 | 约+5 | 84% | 前期收敛极快 |
| DQN + PER + APF | 约150轮稳定 | 约+9 | 88% | 综合表现最优 |
| 纯APF | 不适用 | 不适用 | 58% | 复杂地图掉到40%以下 |
纯APF基线在简单地图上表现尚可,换成S型窄道布局后成功率大幅跌落,并且会出现周期性卡在局部极值点无法脱困的行为。原版DQN最终成功率可以接受,但收敛慢、方差大。PER让训练曲线明显更平滑,APF引导则把收敛速度从“缓慢爬坡”变成了“早期拔高”。三者结合方案在收敛速度和最终性能上表现最好,另外它还带来一个意外的收益——换地图之后的泛化能力明显优于其他方案。
6.3 泛化测试:换一张新地图还灵不灵
训练结束后,我换上另一张全新地图(障碍物位置和密度完全不同)做了一次零训练迁移测试。原版DQN在新地图上的到达成功率骤降到35%左右,基本等于“只学到了训练环境的局部模式”;加入PER的版本稍好,约45%;加入APF引导的版本能达到60%左右,因为它训练过程中接触了大量“合理转向避障”的样本,学到了一些可迁移的行为模式而非死记硬背地图;完整结合的方案D达到72%,虽然相比训练地图打了折扣,但明显优于其他对比组。
这个结果让APF引导的意义从“加速收敛”上升到了“提升策略迁移动力”。后续如果要做sim-to-real迁移,或者换到更复杂的地图,这12个百分点的差距可能会被进一步放大。
6.4 一份可以直接抄作业的参数表
最后把项目里经过几百组实验筛选后比较稳定的参数整理成清单。如果你也在Pytorch下复现类似项目,可以直接拿这份参数当作起点,再根据实际环境微调:
| 参数项 | 推荐配置 | 备注 |
|---|---|---|
| 优化器 | Adam | 学习率3e-4 |
| 批量大小 | 128 | 过小噪声大,过大有效样本利用率低 |
| 回放缓冲区 | 20000 | 配合epsilon衰减节奏保证样本流动 |
| 目标网络更新间隔 | 2000步 | 硬拷贝,简单可靠 |
| epsilon | 1.0 → 0.05 | 约30000步线性衰减 |
| APF引导β | 0.5 → 0.05 | 约50000步衰减,慢于epsilon |
| PER α优先级系数 | 0.4 | 单独用PER时可提到0.6,混合方案推荐0.4 |
| PER β偏差修正 | 0.4 → 1.0 | 线性增长至训练结束 |
| 梯度裁剪 | max_norm=10 | 强烈建议加上,防止Q值爆走 |
6.5 训练过程中真正值得盯住的三个指标
我在这个项目里最大的一个认知纠偏是:loss曲线根本不值得过度关注。loss下降并不代表策略变好,很多时候loss在稳步下降,避障成功率却纹丝不动,这是因为Q值正在被压向一个“数值合理但动作区分度不够”的状态。
真正需要盯住的指标是:单回合平均累计奖励、到达成功率、平均碰撞次数。这三项才会直接反映策略质量。我在训练脚本里是这样做的:每训练20个回合就保存一份完整日志,记录这三项指标并打印趋势。当回合奖励稳定上升、碰撞次数持续下降、成功率逐步抬升,才是策略真正健康的信号,而不是loss多好看。
7. 最后说几句体会
整个项目做完,最大的感受是:DQN系列在避障控制上的难点,从来不是网络结构或者某一项算法原理,而是各种组件的“配合节奏”。奖励函数太懒、探索率太急、PER的α太大、APF引导时间太长,任何一个旋钮拧过头,训练曲线立刻会给出惩罚。做这类型项目的正确姿势,是先搭起一个能跑通全流程的最小系统,然后像调音响一样逐项微调,用行为指标而不是loss来引导调参方向。
我整理出来的这套流程——Matlab先做环境动力学验证、Python Pytorch跑DQN训练、PER解决有效样本利用率、APF引导提升前期收敛和训练泛化——每一步都踩过一次以上的坑才走到稳定状态。后面我会把Python和Matlab两个版本的环境代码、训练脚本和参数文件一起打包整理出来,如果你正打算做强化学习避障方向的研究或者毕业设计,可以参考这套实现快速跑起来。建议你先从原版DQN单独跑通,再逐项叠加PER和APF,这样每一步的效果和问题都能看得清清楚楚,比一次性堆叠所有改进方案容易定位问题得多。
