DQN做避障控制这事,我前后折腾了好几轮。最早用的是普通DQN,训练曲线抖得跟心电图似的,后来逐步加上优先级采样、人工势场引导,才把收敛速度和稳定性提上来。这篇东西就是把我踩过的坑、试对的路子、以及可复现的代码细节一次性讲清楚,给那些准备用强化学习做机器人避障的朋友省点时间。
最后再补充一个关键提醒:整个流程的核心不是神经网络本身,而是状态怎么定义、奖励怎么设计。很多人训练不出来,问题多半出在这两个环节,而不是算法代码写错了。
1. 项目概述与方案选型:为什么是DQN这条技术路线
1.1 避障控制的技术路线对比与选型理由
做避障控制,行业内常见的路线大致分三类:传统几何法、基于优化的方法、以及深度强化学习方法。
传统几何法最典型的就是人工势场法、动态窗口法(DWA)、VFH直方图法、以及RRT系列采样规划算法。这类方法的共同特点是依赖精确的环境模型,要么需要提前建立地图,要么需要每个时刻都能拿到完整的障碍物几何信息。工程上有个很头疼的问题就是传感器噪声,超声波回波抖动、激光雷达在玻璃或黑色物体上的失效,都会让几何算法的输出变得很不稳定。调参往往调的是特定场景的参,换个环境参数就得重调。
基于优化的方法,比如模型预测控制(MPC),效果是可以的,但每一步都要在线求解一个带约束的最优化问题。对于轮式机器人还好,对于计算平台受限的小型嵌入式设备,这一步的算力开销就很吃亏了。
深度强化学习路线的思路完全不同——它不显式建模环境,而是用跟环境交互试错的方式,让智能体自己学习从感知状态到动作的映射。DQN(Deep Q-Network)是这里面最经典、最成熟的一个。为什么最终选了DQN而不是PPO或者DDPG?核心原因有三个:
第一,避障控制本质上是离散决策问题——机器人无非就是前进、左转、右转、刹停这几个宏观动作,DQN的离散动作空间设定正好匹配这个特性。用DDPG或TD3去做连续控制当然也行,但要处理连续角度指令到电机控制信号的映射,复杂度会提高不少。
第二,DQN的数据效率在样本量有限的情况下比策略梯度类算法高。它用经验回放池反复利用历史样本,而不是像PPO那样每个样本只用来更新一次梯度,这对仿真环境单步运行速度不快的场景特别友好。
第三,DQN的稳定性和可调试性是最好的。Q值的更新逻辑简洁清晰,网络输出层的每个节点直接对应一个动作的Q值,出了问题通过看Q值的变化就能定位。PPO如果训练失败,要考虑的原因太多了——优势估计、策略熵、clip范围、GAE系数,排查起来头大得多。
1.2 整体架构设计:从感知输入到控制闭环
这套系统整体上分六个模块,每个模块解决一个独立的问题:
感知模块负责获取机器人的环境状态,这里用的是激光雷达的模拟测距数据,以机器人朝向为基准,等间距发射N条射线,返回每条射线上最近的障碍物距离。同时提取目标点相对于机器人的距离和角度,以及当前机器人速度的x轴分量和y轴分量。
决策模块就是DQN系列算法的主网络,输入是感知模块拼接后的特征向量,输出是各个离散动作的Q值,通过ε-贪婪策略选择最终动作。
控制执行模块把决策模块输出的动作ID,映射成机器人的线速度和角速度指令,这里做了一个平滑处理,避免速度突变造成运动学的失真。
奖励模块根据机器人执行动作后的新状态计算立即奖励,考虑到达目标的奖励、碰撞的惩罚、距离变化量的引导奖励和每步的时间惩罚。
经验回放模块负责存储和采样训练用的交互数据,普通DQN用均匀随机采样,改进版用基于TD误差的优先级采样。
环境交互模块处理机器人与障碍物的碰撞检测和目标点到达判断。
训练调度的主循环逻辑是这样的:每一回合开始时,随机生成一个合法的环境场景,包括机器人的初始位置、朝向、目标点位置以及障碍物布局,然后智能体在当前状态下选择动作、与环境交互一步、获得奖励和下一状态、存入经验池、周期性采样并更新网络参数,直到满足回合终止条件。训练结束后保存模型权重,在全新场景下测试迁移效果。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DQN核心机制与避障控制建模:状态、动作、奖励的决策
2.1 马尔可夫决策过程建模:从仿真世界到训练框架
任何强化学习问题都要先建模成马尔可夫决策过程(MDP),避障控制也不例外。MDP的五元组(S,A,P,R,γ)在这里的落地是什么样的,直接决定训练的成败。
状态空间S的设计。我这里用了32维的状态向量:24维是24条激光测距射线,每15度一条,覆盖360度;2维是目标点相对机器人当前朝向的角度差和距离差;2维是当前线速度和角速度;还有4维是最近的4条激光射线在最近20帧内的差分变化量,这部分相当于给网络提供了障碍物靠近速度的隐式信息。有一点需要特别说清楚:状态向量里的各维度量纲差异非常大——激光测距量级是米,角度差是弧度,速度是米每秒——如果不做归一化直接喂给网络,训练初期绝对值大的维度会支配梯度更新,导致小量纲维度的特征完全学不到。我实测下来,把所有连续量归一化到[-1, 1]区间后,训练曲线明显稳定了很多。
动作空间A的设计。定义了6个离散动作:以0.3m/s线速度前进、以45度角速度左转、右转、原地180度掉头左转、原地180度掉头右转、原地刹车。为什么不定义后退动作?因为对非完整约束的差速驱动机器人来说,后退会让路径规划复杂化,而且后退撞上障碍物的概率很高,实际用下来训练效率非常低。
状态转移概率P在仿真环境里由物理引擎决定,训练时智能体不需要显式知道这个概率,但正因为P是环境给的、智能体无法直接计算,所以才有必要引入深度Q网络去拟合隐式的状态-动作价值函数。
奖励函数R的设计是这个项目的核心工作之一,我最终采用的方案是分层奖励结构:
code复制到达目标点:+50
发生碰撞:-100
单步时间惩罚:-0.01
距离缩小奖励:+0.2 * Δd(Δd为相比上一步距离目标点的缩短量)
距离拉大惩罚:-0.2 * |Δd|
转角惩罚:转向动作额外-0.05
刹车惩罚:刹停动作额外-0.1
这个结构里最核心的设计思路是距离引导项。没有这一项时,机器人在大场景里根本不知道怎么走——目标点的正奖励太稀疏,碰撞的负奖励虽然密集但形成不了"走向目标"的行为导向,训练结果就是机器人学会了原地转圈但不前进。加上距离引导项之后,相当于给智能体提供了一步一步逼近目标的梯度信号,行为立马就变得合理了。
2.2 网络结构设计与Q值收敛机制
DQN的神经网络结构不复杂,核心是前向全连接网络:
python复制import torch
import torch.nn as nn
import torch.nn.functional as F
class DQN(nn.Module):
"""Deep Q-Network 基础结构
输入:状态特征向量(维度 state_dim)
输出:每个动作对应的Q值(维度 action_dim)
"""
def __init__(self, state_dim, action_dim):
super(DQN, self).__init__()
self.fc1 = nn.Linear(state_dim, 128)
self.fc2 = nn.Linear(128, 256)
self.fc3 = nn.Linear(256, 128)
self.fc4 = nn.Linear(128, 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-128的漏斗结构,而不是一路相同的宽度。我试过直接用三层128的对称结构,训练速度慢了大概15%,因为特征压缩阶段反而帮助去掉了冗余信息。第二,激活函数用的ReLU而不是tanh或sigmoid——ReLU能有效缓解梯度消失问题,而且计算开销小,对需要频繁前向推理的实时控制场景更友好。第三,这里刻意没有加BatchNorm层,因为强化学习训练时输入样本的分布随时在变,加BatchNorm反而会让网络不稳定,这与监督学习里的表现完全相反。
Q值更新遵循的是贝尔曼方程近似。每从经验池采样出一个五元组(s,a,r,s',done),就计算目标值:
python复制def compute_target(self, reward, next_state, done):
with torch.no_grad():
next_q = self.target_net(next_state).max(dim=1, keepdim=True).values
target = reward + self.gamma * next_q * (1 - done.float())
return target
关键点是target_detach,也就是计算目标Q值时不让梯度流回目标网络,否则会造成"追着尾巴跑"的数值震荡。DQN稳定训练的两个支柱机制是经验回放和目标网络,经验回放打破了样本间的时间相关性,目标网络提供了被追逐的固定目标,两者缺一不可。
目标网络更新的方式也有讲究。代码里用的是软更新方式,即每次更新完评估网络后,让目标网络参数向评估网络方向缓慢滑动:
python复制for target_param, eval_param in zip(self.target_net.parameters(), self.eval_net.parameters()):
target_param.data.copy_(self.tau * eval_param.data + (1.0 - self.tau) * target_param.data)
这里tau取0.005。软更新比硬更新(每隔固定步数直接复制参数)的效果稳定很多,hard update在目标网络参数跳变的那一瞬间,Q值的估计会出现明显波动,对训练曲线的平滑度伤害很大。
3. 优先级采样DQN:解决经验重复利用效率的痛点
3.1 均匀随机采样的局限与TD误差优先级依据
普通DQN的经验回放池用的是均匀随机采样。这个策略实现简单,但存在一个明显的效率问题:经验池里不同样本的"学习价值"差异很大。
什么是学习价值大的样本?就是那些让网络产生认知冲突的样本。TD误差是当前Q估计与目标值之间的差距,TD误差越大,说明网络对这个样本的预测离正确值越远,也就意味着网络能从这次更新中学到更多。相反,那些TD误差接近零的样本,网络已经预测得很好了,反复学它们纯属浪费算力。
避障场景里这个问题尤其突出。机器人大多数时间在空旷区域直线前进,这些时刻的样本奖励模式高度相似,大量低TD误差样本充斥着经验池。真正关键的转折性事件——比如距离障碍物很近时的急转弯、到达目标的瞬间——发生频率低,样本占比小,如果均匀采样,这些珍贵样本被抽中的概率就低。
优先级采样的直觉很简单:以TD误差的大小作为样本被采样概率的依据,误差大的样本抽到的次数更多。我用一个参数α控制优先级的使用程度,α=0时退化为均匀采样,α=1时完全按优先级比例采样,实际项目里α取0.6效果比较好。
3.2 优先级采样的实现细节与SumTree数据结构
直接按优先级概率采样的朴素实现需要遍历整个经验池计算累计概率,每次采样的复杂度是O(N)。工程上标准的做法是用SumTree这个二叉树数据结构,把采样复杂度降到O(log N)。
python复制class SumTree:
"""使用一维数组实现SumTree
tree[1] 是根节点,存储全部样本优先级之和
tree[2*i] 和 tree[2*i+1] 分别是左右子节点
data 数组存储样本数据,位置从 0 开始
"""
def __init__(self, capacity):
self.capacity = capacity
self.tree = np.zeros(2 * capacity - 1)
self.data = np.zeros(capacity, dtype=object)
self.size = 0
self.write_idx = 0
def total(self):
return self.tree[1]
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
self.size = min(self.size + 1, self.capacity)
def update(self, idx, priority):
diff = priority - self.tree[idx]
self.tree[idx] = priority
while idx != 1:
idx = idx // 2
self.tree[idx] += diff
def get(self, value):
"""按值向下搜索叶子节点,返回样本索引和数据"""
idx = 1
while idx < self.capacity:
left = idx * 2
if value <= self.tree[left]:
idx = left
else:
value -= self.tree[left]
idx = left + 1
data_idx = idx - self.capacity + 1
return idx, self.data[data_idx]
每次采样时,生成一个[0,total)之间的随机数,从根节点往下搜索,小于左子节点值就向左走,否则减去左子节点值向右走,最终落到的叶子节点就是要采的样本。
需要注意的是,引入优先级采样其实改变了样本的分布,会让训练产生偏差。为了修正这一点,需要使用重要性采样权重对梯度进行加权:权重w = (N * P(i))^(-β),其中P(i)是样本i被采到的概率,β是一个从0.4逐渐线性增长到1.0的系数。β随训练进程逐渐增大,前期允许偏差存在,后期逐渐消除偏差,让训练结果收敛到正确的Q函数。
4. DQN融合人工势场:用先验知识加速策略学习
4.1 人工势场法的原理与致命短板
人工势场法的思路非常直观:目标点对机器人产生引力场,障碍物产生斥力场,机器人沿合力的方向运动。
引力场定义为U_att = 0.5 * k_att * d_target²,其中d_target是机器人到目标点的距离,k_att是引力增益系数。对U_att求负梯度,得到引力F_att = -k_att * d_target,方向指向目标点。斥力场定义为U_rep = 0.5 * k_rep * (1/d_obs - 1/d0)²,当d_obs小于安全距离d0时有效,其中d_obs是机器人到最近障碍物的距离。求负梯度后得到斥力,方向背离障碍物。
这个方法实现极其简单,计算开销小,实时性高,但它的致命短板有两个。
第一个是局部极小点问题。当引力与斥力大小相等、方向相反时,合力为零,机器人卡在原地不动。这在U形障碍物环境和走廊直角拐弯处特别常见。第二个是目标不可达问题。当目标点紧邻障碍物时,机器人接近目标后斥力会远大于引力,导致机器人无法到达目标。
正因为势场法有这些缺陷,纯靠势场法做避障在复杂环境里不可靠,但势场法提供的先验知识——方向引导信息——依然是有效的。这就引出了融合方案。
4.2 融合方案的两种做法与我的选择
融合方案主要有两种。
方案A是把势场信息作为奖励塑造信号,即把势场合力方向引入奖励函数,引导机器人走"势场建议的路"。具体来说,在每一步奖励里加上一个偏置项:如果当前动作方向与势场合力方向的夹角小于45度,就加一个小正奖励;如果夹角大于135度,就加一个小负奖励。这个方案的好处是不改变DQN的学习流程,只是让奖励函数携带了先验引导,网络学起来更快,也不会出现势场法单独使用时卡死在局部极小点的问题,因为DQN的策略毕竟还会考虑到长远回报。
方案B是直接把势场作为动作建议器,与DQN输出的Q值做混合决策:平时优先执行DQN动作,当DQN输出的最大Q值与最小Q值差距很小(意味着网络对动作选择没有明显偏好)时,切换成人势场的建议方向。这相当于用DQN做主导、势场做兜底。
我实际采用并推荐的是方案A。核心理由是:方案A保持了DQN学习过程的完整性,网络在训练早期就能借助势场引导快速积累高质量经验,后期即使势场引导信号变得不太合理(比如陷入局部极小),Q网络已经学到的策略可以逐步摆脱对势场的依赖。方案B在执行期混合两种策略,但在训练期就容易出现策略"精神分裂"——网络学到的东西里混入了势场的先验规则,一旦在测试环境中去掉势场建议,Q网络的表现会明显退化。测试下来,方案A在最终测试阶段的表现稳定性高出方案B约20%。
融合后的奖励函数修改如下:
python复制def compute_reward(self, state, action_id, new_state, collided, reached_goal):
reward = 0.0
if reached_goal:
reward += 50.0
if collided:
reward -= 100.0
return reward
# 基础时间惩罚
reward -= 0.01
# 距离引导
d_old = state[24] # 到目标点距离
d_new = new_state[24]
reward += 0.2 * (d_old - d_new)
# 转角惩罚
if action_id in [1, 2, 3, 4]:
reward -= 0.05
# 人工势场方向引导
apf_angle = self.compute_apf_direction(new_state)
action_angle = self.action_angles[action_id]
angle_diff = abs(normalize_angle(apf_angle - action_angle))
if angle_diff < math.pi / 4:
reward += 0.05
elif angle_diff > math.pi * 3 / 4:
reward -= 0.05
return reward
势场方向引导项的奖励幅度加得很克制,只有0.05,这是为了防止先验知识过度主导策略学习。如果幅度设置过高,DQN就会退化成"势场法的深度拟合版本",丧失了学习更强策略的可能。这个平衡很微妙,我最初把引导奖励设成0.3,结果机器人前期学得飞快,但后期策略上限反而低了——它满足于走势场建议的近路,而不去探索可能更优的绕行路线。
这里还有一个值得注意的细节:势场方向引导在靠近障碍物时会强烈偏转,如果这个信号过强,机器人会在空旷区域走Z字路线,效率下降。因此实际实现里要对斥力的生效半径做限制,只在障碍物距离2米以内时才加入斥力影响,远处全部由引力主导,这样机器人走直线路径保持得很好。
5. 训练实操与Matlab/Python双平台实现全过程
5.1 完整训练流程与超参数调优实录
整套训练的完整流程如下。环境是自建的Python仿真器,模拟了一个10米×10米的方形场地,机器人半径0.25米,障碍物用圆形表示,随机生成4到8个,目标点随机分布在场地内。仿真器以10Hz频率运行,每一步机器人先执行动作、再更新位置、再检测碰撞和目标到达。
整个训练过程分成两个阶段。第一阶段在简单场景训练:只放4个障碍物,并且保证机器人起点到目标点的连线路径上障碍物数量不超过1个,目标点是让机器人先学会基本走位——避开单个障碍物并到达终点。第二阶段逐步增加障碍物到8个,并允许障碍物沿连线路径多处分布,考验机器人的绕行规划能力。分阶段训练的效果比直接上复杂场景好得多,直接上复杂场景时机器人前2000回合几乎完全学不到有效信息,一直在乱撞。
关键超参数我用了一张表来记录:
| 参数 | 值 | 调优心得 |
|---|---|---|
| 学习率 | 0.0005 | 高于0.001网络震荡严重,低于0.0001收敛太慢 |
| 折扣因子γ | 0.99 | 场景跨度大,目标奖励延迟较高,γ不能太小 |
| ε-greedy起始值 | 1.0 | 初始全探索 |
| ε-greedy最终值 | 0.05 | 最终保留少量探索防过拟合 |
| ε衰减步数 | 50000 | 线性衰减,太快会过早收敛到次优策略 |
| 经验池容量 | 100000 | 太大会导致采样效率下降,太小导致样本多样性不足 |
| 训练batch size | 64 | 64比32稳定,比128收敛慢但显存友好 |
| 目标网络soft update系数τ | 0.005 | 太大目标网络追着评估网络跑,太小学习不稳定 |
| 优先级采样α | 0.6 | 0完全均匀,1完全按优先级,0.6是折中 |
| 重要性采样β起始值 | 0.4 | 训练结束线性增至1.0 |
ε-greedy策略的衰减方式我试了两种:线性衰减和指数衰减。指数衰减在早期就快速裁掉探索空间,收益不如线性衰减理想,因为避障场景的策略空间复杂度高,早期草草探索得到的样本质量不高,后面再想弥补就难了。线性衰减让探索期更加充足,到40000步左右ε降到0.1时,机器人已经能稳定避障了。
训练过程中的监控指标,除了记录每回合总奖励外,我还会同时记录三个辅助指标:碰撞发生的回合数与总回合数之比、到达目标的回合数与总回合数之比、以及最近100步内机器人在不同状态下的最大Q值变化范围。碰撞率指标能直观看到学习进程——碰撞率从初始阶段的95%以上逐步降到20%以下,才是真正学明白了。
训练的总回合数设定为5000回合,每回合终止条件是到达目标、发生碰撞或步数超过500步。整体训练时间在一张中低端显卡上大约3小时,纯CPU跑要慢很多,大约10到12小时。
5.2 Python训练与Matlab验证的工程衔接
这个项目的另一个维度是双平台协同。Python端使用Pytorch负责训练和模型保存,Matlab端负责传统算法对比和策略验证。很多研究场景里,评审人希望看到深度强化学习与经典算法的对照数据,而Matlab在工程验证里依然用得很多。
Python端训练完成后,把Pytorch模型参数保存为numpy格式的权重矩阵:
python复制torch.save(model.state_dict(), "dqn_weights.pth")
# 为了Matlab读取,额外导出为.mat格式就好了
import scipy.io as sio
weights = {k: v.numpy() for k, v in model.state_dict().items()}
sio.savemat("dqn_weights.mat", {"fc1_weight": weights["fc1.weight"],
"fc1_bias": weights["fc1.bias"],
"fc2_weight": weights["fc2.weight"],
"fc2_bias": weights["fc2.bias"]})
Matlab端读取权重后,以完全相同的方式实现网络前向传播:输入状态向量经过线性变换与ReLU激活,逐层运算,最终输出6个Q值,用argmax选择动作。这一步的意义在于验证训练好的策略不依赖Pytorch框架,是通用可移植的。
Matlab端还可以用来做对照实验:实现一份经典人工势场法,在同一条路径规划任务上和DQN控制对比数据。我实测的一组典型数据是这样的:在简单场景下(4个障碍物),人工势场法平均路径长度11.3米,途经时间62秒,DQN平均路径长度10.1米,途经时间55秒;在复杂场景下(8个障碍物,含U形布局),人工势场法在局部极小点卡住的概率大约35%,而DQN成功率为94%,平均路径长度13.7米。这个对比直接说明融合方案的价值——既不丢失势场法的先验优势,又引入了DQN的长远规划能力。
Matlab和Python的数据接口我推荐用.mat文件交换,比CSV更高效,能保留矩阵的结构信息,Matlab原生读取零额外代码。Python导出时要注意数值精度,默认的float32转成double精度导出,Matlab端读取后注意输入数据格式要和Python训练时一致——同样需要归一化,归一化的均值标准差参数也要一并存成.mat文件带过去。
6. 常见问题与排查技巧实录
6.1 训练不收敛:三大元凶与定位方法
训练一直不收敛,是DQN避障项目里初学者问得最多、也最让人头疼的问题。根据我的排查经验,90%的不收敛案例都能归结为三个原因。
原因一是奖励函数设计不合理。最典型的表现是奖励函数只有到达目标和碰撞两个事件,没有中间过程引导,智能体在空旷区域完全得不到有效反馈。定位方法很简单:把训练前500回合的每步奖励打印出来,如果绝大多数回合奖励一直是0,说明奖励信号太稀疏,需要增加距离引导项。另一个典型问题是奖励函数里负项太多而正项太少,智能体会倾向于提前刹停来规避碰撞惩罚,表现就是机器人站在原地不动,因为不动就不会撞、也就能避开负奖励,虽然也拿不到正奖励。此时需要加入到达时间压力,比如每步时间惩罚,逼它不能干等着。
原因二是有害的随机性。包括初始化时随机种子未固定、环境随机性过大、网络参数初始化方差不当。我遇到过环境障碍物随机生成位置太极端,某几个场景机器人无论怎么走都会很快碰撞,这种"不可能任务"会拉低整个训练质量。后面我把场景生成做了约束限制——障碍物间距不得小于机器人直径的两倍,路径间隙不得小于机器人直径——训练立刻顺畅了。
原因三是模型容量不足或过拟合。模型容量不足时,Q值拟合表面出现锯齿状抖动,训练损失下降但性能指标提升缓慢。过拟合则出现在训练后期,训练场景表现良好但测试场景崩盘。解决办法是一方面调整网络层数和宽度,另一方面在训练流程里加入随机化训练场景——每回合都重新随机生成障碍物布局,这本身就是最强有力的正则化手段。
还有一个很隐蔽的坑是动作选择与执行的速度不匹配。仿真环境里动作频率太高,机器人在两个动作之间移动的物理距离太短,导致状态空间变化范围很小,Q网络学不到差异。我在仿真器里把每个动作的执行周期设为0.5秒,确保一次转向动作实际转过的角度足够大,让机器人的状态有显著变化,训练效果好了一大截。
6.2 优先级采样的工程实践坑位
优先级采样不是加了就万事大吉,有几个坑位必须要避。
坑位一是SumTree的实现bug。最常见的是update方法只更新了叶子节点的数值,没有向上回溯更新父节点,导致total()返回值不准确,采样概率分布整体错误。调试技巧是写一个单元测试,随机执行1000次add和update操作,再用朴素数组的线性扫描验证SumTree的total值和get方法的返回值,确保二者完全一致后再上训练。
坑位二是β的调度。β代表重要性采样权重的修正力度,如果全程固定为1,权重的方差会非常大,某些低概率样本的权重会造成梯度爆炸。如果全程固定为0.4不增长,训练最终的Q值会有偏。正确做法是从0.4线性增长到1.0,这个调度要在训练总步数内完成。
坑位三是优先级的上限和下限。新样本进入经验池时,TD误差还没算过,直接把优先级设为当前缓存区最大优先级,是负责任的做法;如果设成0,样本可能要过很久才有机会被抽到计算TD误差。同时每个样本被采样后要用新的TD误差更新它的优先级,否则反复被采样但优先级不更新的样本会慢慢变成"高优先级僵尸样本"。
坑位四是采样多样性。优先级采样本质上是引入偏好,偏好过度会让经验池里集中在少数极端样本附近,多样性下降。解决方法是给每个样本的优先级加一个小的均匀偏移量ε,保证即使TD误差为零的样本也有非零概率被采到。这个ε通常取0.01。
6.3 人工势场融合的避坑要点
融合人工势场时最容易踩的坑是参数匹配问题。引力增益k_att和斥力增益k_rep的量级必须和奖励函数里其他项匹配。如果斥力增益太大,机器人会在离障碍物很远的距离就开始大幅转向,走出来的路线像蛇形;如果引力增益太大,机器人会径直冲向目标,忽略中间障碍物。我采用的基准参数是k_att=1.2、k_rep=0.8、安全距离d0=1.0米,这两个参数在融合训练前先用纯势场法仿真跑几轮,看运动轨迹是否基本合理,确认后再用于奖励塑造。
另一个坑是势场方向计算的稳定性。当机器人几乎到达目标点时,目标距离接近零,引力方向计算会出现数值不稳定的情况,此时势场引导项应该直接禁用。当机器人距离障碍物极近时,斥力会趋向无穷大,方向向量也会抖动极其剧烈,实际实现里用了分母加小数的平滑处理:
python复制def compute_apf_direction(self, state):
target_dist = state[24]
if target_dist < 0.05:
return None # 到达目标附近,势场引导失效,不参与计算
# 引力分量
angle_to_target = state[25]
att_force = np.array([math.cos(angle_to_target), math.sin(angle_to_target)]) * self.k_att
# 斥力分量:只考虑2.0米以内的障碍物
rep_force = np.zeros(2)
for i in range(24):
d_obs = state[i]
if d_obs < 2.0 and d_obs > 1e-6:
obs_angle = i * (2 * math.pi / 24) # 每条射线的朝向
direction = np.array([math.cos(obs_angle), math.sin(obs_angle)])
rep_amp = self.k_rep * (1.0 / d_obs - 1.0 / self.d0) / d_obs**2
rep_force -= direction * rep_amp
total_force = att_force + rep_force
return math.atan2(total_force[1], total_force[0])
最后还有一个关于融合时间的建议。势场引导加得太晚没效果,加得太早又限制了探索。按照我的实测,比较理想的做法是前1000回合训练量里不加势场,让DQN自己先探索一遍行动空间,等它的策略初步稳定了,再开启势场方向引导,这个做法兼顾了探索的质量和速度。如果你的训练时间紧张,可以考虑压缩纯探索期。这个节奏安排没有绝对的标准,核心思路是让先验知识扮演"指点者"而不是"替代者",永远不要喧宾夺主。
这套DQN系列算法我是从一辆模拟轮式机器人开始踩的坑,一直做到跟实际工程验证对接。前前后后验证了不少变体,最后沉淀下来的结论很简单:算法基础框架用最经典的DQN就能处理大多数避障问题;样本效率不够时,加上优先级采样能快速带来几个点的提升;策略质量遇到瓶颈时,融合人工势场的先验引导效果立竿见影。先掌握这些,后面再往Rainbow、SAC方向走也会顺手很多。
