简介:一份围绕水下自主航行器动态避障的深度强化学习实现文档,面向机器人学、自动化控制与深度学习方向的科研人员和工程师。内容系统梳理了基于DRL的端到端避障框架,重点包括IMM-EKF障碍物状态预测、DQN多行为网络调用、DDPG-PID水平面控制以及SumTree-DDPG三维避障算法,并配有详细可运行代码及逐段解释,覆盖环境依赖配置、IMMEKF类实现、模型概率更新等关键环节,便于读者快速复现。压缩包仅含1个docx文档,约51KB,属于精炼型技术资料。文档同时对比了传统方法与DRL方法在避障成功率、路径最优性和能耗效率上的差异,给出单步决策时间小于0.5秒的实测指标;目前已有106人学习,适合用于AUV避障算法开发、仿真验证及梳状搜索巡检、自主回收等任务的安全优化。
1. 基于深度强化学习的AUV动态避障:从仿真到水池试验的落地路径
AUV(自主水下航行器)在近底作业、管网巡检、海洋牧场监测等场景里,最让人头疼的并不是导航精度,而是动态障碍物——迎面游来的鱼群、漂移的渔网、突然出现的潜水员,这些目标声呐回波弱、运动轨迹不确定,传统人工势场法和动态窗口法在二维平面还能勉强应付,一旦把俯仰、深度变化纳入三维空间,很容易陷入局部极小值或者干脆规划失败。深度强化学习刚好补上这个缺口:它不依赖全局地图,而是让AUV在交互中学会“看到什么就怎么躲”的端到端策略,输出的直接是前向速度、垂向速度和艏摇角速度,天然适合三维动态避障。
这篇文章要解决的,是“怎么把强化学习真正用起来”的问题。我会基于多模型感知增强的思路——前视声呐点云加单目视觉深度估计做多模态融合,结合PPO算法给出一个可运行的三维避障训练闭环,并给出关键代码和参数。内容适合两类人:一类是正在做AUV避障算法选型、被仿真结果和实物差距折磨的研究生,另一类是想要在水下机器人平台上快速搭建验证系统的工程师。理论部分我会压缩到够用,重点放在状态空间设计、奖励塑形、训练稳定性和实机迁移这些真正决定成败的细节上。
2. 问题建模与算法选型:为什么三维动态避障会逼疯传统算法
2.1 传统避障算法在三维水下的三个死穴
先说说为什么不用经典方法。人工势场法在二维静态环境里很好用,引力斥力一叠加,路径就出来了,但水下环境里动态障碍物的运动速度常常和AUV自身速度一个量级,势场法要求实时更新斥力场,计算量还能忍,问题是它没有速度维度的概念——障碍物朝你冲过来和离你远去,在势场里的表现是一样的,经常出现“原地抖动半天然后撞上去”的笨拙行为。动态窗口法(DWA)稍好一些,它在速度空间里采样并评价轨迹,但评价函数里的目标朝向、速度、安全距离这几个系数的调节非常依赖场景,换个障碍物密度就要重新调参,而且DWA的采样空间是按二维运动学模型设计的,加入垂向速度后就变成三维采样,维数诅咒直接让实时性崩溃。
还有一个更隐蔽的问题:传统算法需要知道障碍物的精确位置和运动状态。水下环境里前视声呐的每帧点云稀疏且带大量噪点,多波束图像和光学图像之间的时延也不同步,你连“障碍物到底在哪”都没法准确回答,基于确定性状态的规划算法自然无从谈起。深度强化学习恰好是端到端方式——它直接从感知输入映射到控制指令,感知误差被当作状态随机性消化在网络里,不需要显式维护环境模型。这就是为什么近年在AUV避障研究里,DRL的论文占比直线上升。
2.2 DRL选型对比:DDPG、PPO、SAC到底选哪个
确定了方向之后,下一个问题是算法族选型。水下避障场景的三个特点决定了选择范围:第一,动作空间是连续量(前向速度、垂向速度、艏摇角速度),所以Q-learning这类离散动作算法直接排除;第二,训练成本高——每一条训练轨迹都要在仿真环境里跑几十上百步,样本效率必须高;第三,部署端的计算资源有限,策略网络的推理时间要控制在10毫秒以内。
DDPG是很多人的第一反应,毕竟它是连续控制最经典的actor-critic算法。实际用过就会知道,DDPG对超参数极其敏感,学习率、噪声方差、软更新系数tau稍有不适配,critic的Q值估计就开始发散,而且经验回放池里旧策略产生的数据和新策略差距大了之后,训练波动特别大。我自己的血泪经验是DDPG在AUV避障这种高维感知任务上,前两百万步基本都在“碰运气”,运气好能收敛,运气不好直接翻车。
PPO则是当前工程落地最稳的选择。它用clip机制限制策略更新的步幅,天然抗超参数扰动,在两百万步内基本能稳定收敛。虽然样本效率理论上不如SAC,但水下仿真器的物理步长动辄0.1到0.5秒,一条完整轨线的数据量本来就不大,PPO的on-policy特征反而成了一种保护——策略每次更新都是基于最新数据分布的。SAC的熵系数自动调节机制理论上最优,但在多维连续动作空间里,双Q网络加熵正则化的复杂度会让调参变成一场灾难。
我最终选择PPO作为基线算法。下面是我在仿真里用的PPO超参数起点,来自多次实验的经验值,适配AUV避障场景:
| 参数名 | 取值 | 说明 |
|---|---|---|
| clip参数epsilon | 0.2 | 策略更新的最大偏差,太大不稳定,太小收敛慢 |
| 学习率actor | 3e-4 | 用Adam优化器,训练步数长了可以按线性衰减 |
| 学习率critic | 1e-3 | critic收敛速度可以比actor快,它的任务更简单 |
| GAE lambda | 0.95 | 优势估计的衰减因子,越小偏差越大但方差小 |
| 更新轮数K_epochs | 10 | 每条数据采完后重复利用次数,过高容易过拟合 |
| mini-batch大小 | 128 | 每轮更新时的样本批次 |
| 熵系数lambda_entropy | 0.01 | 鼓励探索的权重,AUV任务不需要太激进 |
| 训练环境数n_envs | 16 | 并行环境数量,太少训练慢,太多卡显存 |
2.3 状态空间设计:多模型感知增强的融合策略
状态空间设计是整个系统最核心的部分。我之前看很多AUV避障论文,状态输入直接用声呐的点云原始数据,效果很差——前视声呐每帧只有几百个点,而且分布极不均匀,近处密集远处稀疏,网络很难学到有效的特征。我采用的做法是“多模型感知增强”。
感知层做成三路输入。第一路是前视声呐点云,做体素化处理以后输入一个轻量的3D卷积网络,提取障碍物的空间分布特征;第二路是单目前视相机,用DepthNet估计深度图,再和声呐数据做空间对齐——两套传感器在AUV坐标系里标定之后,把声呐点云投影到图像平面做个校验,声呐在浊水中可靠但分辨率低,视觉在清水里分辨率高但受浊度影响大,两者互为冗余;第三路是AUV自身的状态量,包括当前的三维位置、姿态角、线速度、角速度,以及目标点的相对位置。
这三路特征在融合层拼接起来,输入到PPO的策略网络。你可能要问:为什么不直接把所有状态拼一起?我的经验是,不同模态的特征尺度差太多——声呐体素特征是稀疏的,深度图特征是稠密的,直接拼接会让网络在早期训练阶段过于依赖数值占主导的那一路。我用的做法是分别过一层MLP映射到128维的embedding空间,再做元素级相加。这一层设计从代码上长这样:
# 多模型感知融合层 class FusionLayer(nn.Module): def __init__(self, sonar_dim=512, vision_dim=256, state_dim=15, embed_dim=128): super().__init__() self.sonar_embed = nn.Sequential( nn.Linear(sonar_dim, 256), nn.ReLU(), nn.Linear(256, embed_dim) ) self.vision_embed = nn.Sequential( nn.Linear(vision_dim, 128), nn.ReLU(), nn.Linear(128, embed_dim) ) self.state_embed = nn.Linear(state_dim, embed_dim) # 模态权重, 初始相等 self.modality_weights = nn.Parameter(torch.ones(3) / 3) def forward(self, sonar_feat, vision_feat, state): sonar_vec = self.sonar_embed(sonar_feat) vision_vec = self.vision_embed(vision_feat) state_vec = self.state_embed(state) # 加权融合, 权重随训练自动学习 weights = torch.softmax(self.modality_weights, dim=0) return weights[0] * sonar_vec + weights[1] * vision_vec + weights[2] * state_vec这段代码的关键在于modality_weights是可学习的参数。训练初期声呐和视觉特征都不稳定,权重大概会收敛到差不多的值;训练收敛后,哪个模态对避障决策的判别力更强,它的权重就会自动提高。在浑浊水域的数据上跑,你会发现声呐权重越来越高,视觉权重被压下去,这就是自适应融合在做实事。如果你在实机部署时发现某一类传感器常掉线,把这个权重改成固定值0.7/0.2/0.1会更稳,可学习的版本在传感器骤变时会有短暂的退化期。
3. 三维避障算法落地:PPO主循环与奖励塑形的关键设计
3.1 AUV运动学模型与动作空间映射
AUV的避障决策必须在运动学层面闭环。简化处理的话,我使用6自由度模型中的水平面三自由度加深度控制,动作空间定义为三个连续量:前向速度指令u(0.5到2.0 m/s)、垂向速度指令w(-0.8到0.8 m/s)、艏摇角速度指令r(-0.5到0.5 rad/s)。横摇和纵摇假设由内部的姿态控制器稳定在0附近——大多数AUV本体在设计中都有这两个通道的机械自稳机制,直接忽略对避障逻辑影响不大。
强化学习输出的动作经过一个限幅层之后,进入AUV的动力学模型。仿真环境里我用的是简化的二阶惯性模型加上白色噪声扰动,噪声幅值大约是控制输入的5%,用来模拟海流和模型不确定性:
# AUV运动学仿真步进 def auv_step(state, action, dt=0.1, current_vel=[0.1, 0.05, 0]): # state: [x, y, z, yaw, u, v, w, r] # action: [u_cmd, w_cmd, r_cmd] 已经过限幅 u_cmd, w_cmd, r_cmd = action # 一阶惯性延迟, 时间常数2s u = state[4] + (u_cmd - state[4]) * dt / 2.0 v = state[5] + (0.0 - state[5]) * dt / 2.0 w = state[6] + (w_cmd - state[6]) * dt / 2.0 r = state[7] + (r_cmd - state[7]) * dt / 5.0 # 噪声扰动 u += np.random.normal(0, 0.05 * u_cmd) # 运动学积分 x += (u * np.cos(state[3]) - v * np.sin(state[3]) + current_vel[0]) * dt y += (u * np.sin(state[3]) + v * np.cos(state[3]) + current_vel[1]) * dt z += (w + current_vel[2]) * dt yaw += r * dt return np.array([x, y, z, yaw, u, v, w, r])这里要说明两点。第一,一阶惯性模型虽然简单,但它模拟了AUV在指令变化时的动态响应延迟,这个延迟对强化学习训练至关重要——如果环境是理想运动学模型,策略会学会输出非常激进的动作指令,仿真里能完成任务,上了实机之后物理响应跟不上就直接撞了。第二,current_vel参数模拟的是恒定海流,这实际上是在逼策略学会针对海流扰动做补偿,我试过不加海流训练出来的策略,在流速大于0.5m/s的环境里会系统性跑偏。所以这个参数别设成0,甚至可以在训练过程中每隔几万步随机变更一次海流方向,培养策略的泛化能力。
3.2 PPO训练主循环与feed_dict设计
状态输入这块需要仔细设计。外界传进来的原始数据中,声呐是点云、视觉是RGB图像、状态是15维向量,如果把原始数据直接给网络,内存会被大量占用且训练效率低下。我在.env和agent之间定义了一个标准接data dict:
# 训练主循环核心逻辑 obs_sonar = env.reset() # 声呐点云, 体素化后 (512,) obs_vision = env.reset_vision() # 深度图, 降采样后 (64, 64) obs_state = env.reset_state() # 15维状态向量 for step in range(MAX_STEPS): # 多模态特征提取 sonar_feat = sonar_encoder(obs_sonar) # (batch, 256) vision_feat = vision_encoder(obs_vision) # (batch, 256) # PPO策略网络推理 fused = fusion_layer(sonar_feat, vision_feat, obs_state) # (batch, 128) dist = policy_net(fused) # 输出多维正态分布 action = dist.sample() # 执行动作, 获得反馈 obs_sonar, obs_vision, obs_state, reward, done = env.step(action) # 存入buffer buffer.push(sonar_feat.detach(), vision_feat.detach(), obs_state, action, reward, done) if buffer.size() > UPDATE_THRESHOLD: update_ppo(buffer) buffer.clear()这段代码的结构是:先把多模型感知压缩成特征提取,再融合推理动作。其中要特别注意的是buffer里存的是降维后的特征向量,而不是原始点云和图像——这个设计在长时间训练时能省下可观内存,而且特征层面的回放比像素层面的回放在训练稳定性上更好。如果想把整个流程换成端到端(原始图像直接进网络),那需要把batch下采样到32以下,否则训练速度和显存都撑不住。
3.3 奖励函数设计:别让AUV变成一个莽夫
奖励函数是三维避障里最玄学的部分,也是决定策略行为风格的根。我最初用的奖励设定是:到达目标点给+50,碰撞给-50,上一帧到下一帧的进度差给一个小的正向激励。结果训练出来的策略非常莽——它学会贴着障碍物走,因为这样路径最短,进度激励最高,碰撞惩罚虽然高但概率低,期望收益反而更大。这个现象在强化学习里叫“奖励黑客”。修复的办法是加入安全距离的连续惩罚项。
最终采用的奖励形式是稀疏引导加密集安全约束的组合:
def compute_reward(state, next_state, obstacle_map, goal, action): # 1. 碰撞惩罚(稀疏) if check_collision(obstacle_map, next_state): return -100.0, True # done=True # 2. 距离目标奖励(稀疏) dist_now = np.linalg.norm(state[:3] - goal) dist_next = np.linalg.norm(next_state[:3] - goal) if dist_next < 1.0: return +50.0, True # 3. 安全距离惩罚(密集) min_dist = get_min_distance_to_obstacle(next_state, obstacle_map) safe_dist = 3.0 # 米, 安全距离阈值 if min_dist < safe_dist: safety_penalty = -5.0 * (safe_dist - min_dist) / safe_dist else: safety_penalty = 0.0 # 4. 进度激励(密集) progress = (dist_now - dist_next) * 2.0 # 5. 能量惩罚, 抑制频繁急转 energy_penalty = -0.05 * abs(action[2]) # 艏摇角速度惩罚 return progress + safety_penalty + energy_penalty, False奖励函数的关键权重在safety_penalty上。-5.0的系数经过实验验证能让策略把“保持3米安全距离”作为优先任务学习起来。progress奖励乘以2.0是保证在没有障碍物的简单环境里策略能快速学会直行。energy_penalty这个项看似不起眼,实际非常重要,没有它策略会在障碍物之间高频率打舵,实机上伺服电机根本跟不住。
我踩过的一个坑是,碰撞惩罚从-50改成-100之后,训练初期策略会变成“原地罚站”——AUV发现任何动作都可能碰撞,干脆停在原地不动,因为停留时碰撞概率低但收益也低,期望收益比乱动要高。解决办法是给状态引入一个“最小前进速度”约束,或者把进度奖励的系数放大,逼它必须往前。我的做法是加了一个动作屏蔽层:前向速度指令小于0.3m/s时,强制设定为0.3m/s。这个细节直接影响了训练初期策略能否正常探索。
4. 动态障碍物处理与感知增强:三维避障算法的实战实现
4.1 动态障碍物建模与运动预测的坑
真实水下的动态障碍物——巡游的鱼群、漂流的锚链、往复运动的ROV——它们的运动模式差异太大,想让强化学习策略逐一学会应对每一种模式不现实。我采用的做法是两段式:感知层做障碍物运动的短时预测,规划层把预测后的未来位置当作“伪静态障碍物”来避让。
短时预测部分,我维护了每个障碍物过去6帧的位姿数据,用一个简单的线性运动模型预测未来0.5秒和1.0秒后的位置:
# 基于历史轨迹的障碍物运动预测 class DynamicObstaclePredictor: def __init__(self, history_len=6, predict_horizons=[0.5, 1.0]): self.history = deque(maxlen=history_len) self.predict_horizons = predict_horizons def update(self, obstacle_position): self.history.append(obstacle_position) def predict(self): if len(self.history) < 3: return None # 数据不足, 不预测 traj = np.array(self.history) # 用最小二乘拟合直线运动模型 t = np.arange(len(traj)) # 帧索引 coeffs = np.polyfit(t, traj, deg=1) # 逐坐标拟合 predictions = {} for h in self.predict_horizons: future_t = len(traj) - 1 + h / 0.2 # 假设帧间隔0.2s predicted_pos = np.polyval(coeffs, future_t) predictions[h] = predicted_pos return predictions实现上不复杂,但有几个细节值得说。第一,预测只覆盖0.5到1.0秒的短时范围,更长的预测误差太大,反而会误导策略。第二,如果障碍物轨迹拟合残差很大,说明它不是直线运动,此时要降低预测权重,让策略更依赖当前位置的即时避让。第三,障碍物速度越快,预测时间窗要越短——一个以5节速度游动的目标,0.5秒后它可能已经离预测位置好几米远了。我的经验是在仿真中将障碍物速度范围设计在0.2到2.0m/s之间,让策略学会对不同速度的障碍物调用不同的避让时机。
4.2 体素化声呐点云的处理流程
前视声呐(FLS)点云处理是整个感知增强里最容易被低估的一步。原始点云有大量的旁瓣干扰和混响噪点,训练策略时如果把噪点也当障碍物,策略会学习到“对着空气躲闪”这种神经质行为。我用的流程是:先做幅度阈值分割,保留信噪比高于8dB的点;再做DBSCAN聚类,把同一目标的散点聚合成cluster,每个cluster只保留质心和半径;最后把质心坐标投影到以AUV为中心的球坐标系,做成一个固定大小的体素栅格。
体素化的分辨率设置有讲究。栅格太小(比如0.5m)会导致点云过于稀疏,卷积网络提取不到有效特征;栅格太大(比如5m)会让近处的避障精度完全丢失。我用的是非均匀体素——近处分辨率高、远处分辨率低,这样既保住了近距离避障需要的精度,又不让感知场的范围受限。
# 非均匀体素化声呐点云 def sonar_voxelization(points, auv_pos, auv_yaw): # points: (N, 3) 原始声呐点云 # 将点云转换到AUV坐标系 rel_pos = points - auv_pos # 旋转到AUV体坐标系(简化,省略旋转矩阵) x_rel, y_rel, z_rel = rel_pos[:,0], rel_pos[:,1], rel_pos[:,2] # 非均匀体素划分: 近处0.5m格子, 远处2m格子 voxel_grid = np.zeros((32, 32, 16)) # 8m x 8m x 4m 范围 for i in range(len(x_rel)): d = np.sqrt(x_rel[i]**2 + y_rel[i]**2) if d < 4.0: # 近处精细分辨率 vx = int(x_rel[i] / 0.5 + 16) # 中心为16 vy = int(y_rel[i] / 0.5 + 16) elif d < 12.0: # 远处粗分辨率 vx = int(x_rel[i] / 2.0 + 6) vy = int(y_rel[i] / 2.0 + 6) else: continue vz = int(z_rel[i] / 0.25 + 8) # 垂直方向统一0.25m if 0 <= vx < 32 and 0 <= vy < 32 and 0 <= vz < 16: voxel_grid[vx, vy, vz] = 1.0 return voxel_grid.flatten() # (512,)这段代码的输出维度是512,对应前面FusionLayer里sonar_dim=512的设定。注意远处粗分辨率格子的位置偏移和近处不同,这是为了让视野范围足够宽又不牺牲近处精度。实际使用中,我把整个非均匀体素网格的范围扩大到水平面12米×12米、垂向4米,对应的格子数量不变但分辨率分布变了,覆盖范围更适合AUV巡航速度下的避障反应时间。
4.3 深度图像与声呐点云的时空对齐
视觉和声呐的融合难点不在算法,而在时间同步和空间对齐。水下环境中,声呐扫描一帧需要100到300毫秒,摄像头一般在20到30帧每秒,两种数据不是同一时刻采到的。如果你直接把最新一帧声呐和最新一帧图像扔给融合网络,感知结果在时间上是错位的——声呐显示障碍物在左边,视觉显示障碍物已经在正前方,策略网络会困惑。
我采用的做法是维护一个感知缓存队列,声呐和视觉帧各自带时间戳,融合前用最近时刻的帧做插值对齐:
# 多传感器时间对齐与融合输入准备 class PerceptionBuffer: def __init__(self, max_delay=0.5): self.sonar_buffer = deque(maxlen=10) self.vision_buffer = deque(maxlen=30) self.max_delay = max_delay def align_and_fuse(self, current_time): # 找到离当前时间最近的声呐帧和视觉帧 sonar_frame = None vision_frame = None for s in reversed(self.sonar_buffer): if abs(s.timestamp - current_time) < self.max_delay: sonar_frame = s break for v in reversed(self.vision_buffer): if abs(v.timestamp - current_time) < self.max_delay: vision_frame = v break # 时间差过大则丢弃该帧, 避免用过期数据 if sonar_frame is None or vision_frame is None: return None time_gap = abs(sonar_frame.timestamp - vision_frame.timestamp) if time_gap > 0.15: # 两个传感器帧间隔过大, 用声呐为主视觉为辅 fuse_mode = 'sonar_dominant' else: fuse_mode = 'normal' return { 'sonar': sonar_frame.voxel, 'vision': vision_frame.depth_map, 'mode': fuse_mode }对齐之后还要做空间融合。声呐点云和深度图在像素坐标系里不是一一对应的,需要先用AUV的刚体变换矩阵把声呐点云投影到相机坐标系,再根据相机的内参矩阵去查深度图的对应像素。实现时这个变换矩阵需要经过一次标定,如果AUV到相机的安装角偏差超过3度,投影误差就会超过0.5米,融合效果会大打折扣。我在实机调试时发现,安装角最好在每次布放前做一次快速验证——AUV不动,把一个圆柱形靶标放到不同方位,看声呐点和深度图的匹配残差,超过0.3米就要重启标定流程。
5. 避坑指南:训练不收敛、仿真与实物差异大、奖励渐变失效的排查路径
5.1 训练不收敛:先查状态归一化,再查时序依赖
这是我在做AUV避障时遇到的第一个大坑。训练了50万步,reward曲线一条水平线,策略毫无起色。最初怀疑是网络容量不够,把隐藏层加宽加深之后依旧没有变化。后来逐层检查输入数据分布,发现声呐体素化后的特征值范围是0到1,视觉深度图的范围是0到80米,状态里的坐标范围是-50到50米,三个模态的数值尺度差了上百倍,网络早期训练的梯度几乎全被数值大的状态项主导。
解决方法是给每个模态的输入都在进网络之前做一次normalization。声呐体素特征本身是0和1不用动,深度图除以最大深度80,状态向量做Z-score标准化——用训练早期采集的均值和方差固定下来再做归一化,不要在训练过程中滚动更新,否则非平稳分布会让策略网络时好时坏。标准化之后,收敛速度肉眼可见地加快了。如果你训练时发现reward稳定在某个值不再上升,建议先去打印每一层输入输出的均值和方差,85%的概率是数据尺度问题。
时序依赖是第二个隐蔽因素。PPO算法在理论上是按马尔可夫假设推导的,它默认当前状态已经包含了做决策所需的全部信息。但AUV的环境里,声呐帧之间、障碍物的动态变化都有时间相关性。如果你只把单帧感知塞给策略网络,它实际上是一个部分可观测环境,PPO很容易在复杂场景下学歪。解决方法是把最近4帧的状态拼接起来输入网络,或者在特征提取层后面加一个GRU循环头。我用的是状态向量拼接最近4帧的做法,简单且对网络改动小,效果优于GRU头。
5.2 碰撞惩罚的幅度选择:为什么减少惩罚反而学得更好
前面提到过碰撞惩罚设为-100会导致策略罚站,把碰撞惩罚改成-30之后训练能动起来了,但还是很容易让策略把“靠近障碍物”当作可接受行为。这个度很难拿捏。我的经验是:惩罚幅度的绝对值要小于单步最大进度奖励乘以一个安全裕度之后的值——如果每步最大进度奖励是2.0,一个典型的避障过程要走大约120步才能到达目标点,总累计正向收益是240,那么碰撞惩罚设为-50到-80这个区间比较合理。罚太重,策略保守到动不了;罚太轻,策略莽撞到不怕撞。
另外要注意的是,碰撞惩罚的下发应该伴随一个truncate信号,而不是episode自然结束信号。我给环境里加了一个flag,把碰撞结束标记为“truncate”,PPO算法的实现里不会把这个终局当作真正的episode结束——这样GAE优势估计不会因为提前终止而引入偏差。很多同学发现训练出来的价值函数在终点附近严重低估,就是因为把这类提前终止误当成了普通终端状态处理。
5.3 仿真转实物的系统迁移:泛化能力的三个磨刀石
仿真训练出来的策略直接搬到实机上,十次有九次要翻车。问题不出在算法,而出在仿真和现实的差距——AUV模型的惯性参数不准、执行机构的响应延迟比仿真里大、海流扰动和传感器噪声比仿真的白噪声模型复杂得多。我提高迁移成功率的方法是domain randomization技术,训练时随机化一批环境物理参数。
具体做法有四处。第一,随机化AUV的质量和惯性矩,加±15%的扰动;第二,随机化执行机构的时间常数,从1.5秒到3.5秒之间随机采样;第三,随机化海流的方向和流速,方向0到360度均匀分布,流速0到0.8m/s范围;第四,随机化传感器噪声方差,声呐点云在体素化后随机翻转10%到20%的体素,视觉深度图随机加零均值的椒盐噪声。这四种随机化能让策略学到更具鲁棒性的行为模式。
实机部署时还有一个伪技巧值得分享:策略推理的输入不做数据增强,但状态量要加一阶低通滤波。AUV的姿态传感器和DVL(多普勒测速仪)在水下工作时噪声比仿真大很多,直接喂给策略会让动作输出伴随高频抖动。对状态向量做截止频率为1Hz的低通滤波,动作平滑度提升明显且几乎没有决策迟滞感。我用的是一阶IIR滤波器,alpha系数取0.3,在5Hz的控制回路下效果很好。
5.4 奖励函数渐变失效:当策略学会“钻空子”之后
强化学习训练出来的策略会深挖奖励函数里的漏洞,这在避障任务里尤其明显。比如我早期设置“距离目标小于1米给+50”,策略学会了在目标点附近绕圈,每次进入1米范围就给+50,累计刷出的奖励比到达目标点本身还高。修复方法是把稀疏奖励改成“只有第一次接近目标才触发”,并且在触发后加入一个dead zone——一旦进入目标点2米以内,奖励函数停止输出,直接进入episode结束状态。
另一个常见的奖励漏洞是“安全距离惩罚”被策略利用成“快速冲过危险区”。因为安全距离惩罚是和碰撞概率强相关的,策略发现高速直线冲过一个障碍物群时,惩罚累计很小,而进度奖励很高,于是一遍遍地表演高速穿障。我的对策是把安全距离惩罚从线性改成二次函数形式——越靠近障碍物惩罚增长越陡。实现了这个非线性的惩罚之后,策略很快就学会了绕行而不是硬穿。
6. 进阶优化技巧:从仿真验证到硬件在环测试的最后一公里
6.1 用仿真器对比测试策略鲁棒性
仿真器选型上,我现在常用的是Gazebo加UUV Simulator插件,它和ROS2的接口很成熟,能直接读取AUV的位姿并接受速度指令。训练和测试共用同一个环境模型,但训练时开启domain randomization,测试时关闭,这样能评估策略的域间泛化能力。在测试集上我用四个难度层级——静止障碍物、低速动态障碍物、高速随机方向障碍物、混合湍流环境,每个层级跑50个回合,统计碰撞率、平均到达时间、最小安全距离三个指标。
具体的评价指标表格如下:
| 评价指标 | 计算方式 | 合格标准 |
|---|---|---|
| 碰撞率 | 碰撞回合数/总测试回合数 | 低于5% |
| 平均到达时间 | 从起点到目标点的平均时间 | 不超过无障碍物路径时间的1.5倍 |
| 最小安全距离 | 整条路径上与障碍物的最小距离 | 不低于1.5米 |
| 动作平滑度 | 相邻动作差值的标准差 | 小于0.1 |
我在一轮实验中,策略在训练环境测试集上的碰撞率是2%,可以说是很理想了;换到新的地图环境里碰撞率直接飙到17%。根本原因是我训练时的地图障碍物布局太规整,策略学会了“从两个障碍物的间隙穿过去”这种依赖特定空间分布的行为,而不是抽象的“避开任意凸形物体”。修复方案是训练地图改成随机生成障碍物场景,每次都打乱位置和数量,逼策略学泛化特征。这也是我强烈建议你在仿真验证阶段做的一件事:障碍物布局必须走随机化,否则评估结果没有参考价值。
6.2 模型压缩与推理加速
策略网络在仿真里跑得再欢,部署到AUV的计算板卡上才是真章。我用的计算平台是NVIDIA Jetson AGX Xavier,整机功耗在30W以内,对这个平台来说,一个大号的PPO策略网络(输入512+256+15,三层隐藏层各256,输出3维动作)推理一次大约需要8到14毫秒,勉强满足控制周期需求,但在浮点精度和电源波动下偶尔会超时。
我做了两步压缩。第一步是网络蒸馏:用训练好的PPO大网络作为teacher,训练一个输入输出映射关系相同的student网络,隐藏层从256×3压缩成128×2,参数减少了将近70%。第二步是半精度推理:PyTorch的JIT保存脚本模式下用torch.jit.script编译模型,再把权重转成float16。压缩后推理时间稳定在2到3毫秒。蒸馏这一步的损失函数直接对齐teacher和student的输出分布就行:
# 策略网络蒸馏训练 import torch import torch.nn.functional as F teacher_model.eval() # 冻结teacher参数 student_model.train() # 收集训练数据: 随机采样的状态输入 states = collect_states_from_buffer() # (N, 785) with torch.no_grad(): teacher_dist = teacher_model(states) # Normal分布 # 蒸馏损失 = 动作均值MSE + KL散度 action_mean_loss = F.mse_loss(student_model(states).mean, teacher_dist.mean) kl_loss = torch.distributions.kl_divergence( student_model(states), teacher_dist ).mean() total_loss = action_mean_loss + 0.5 * kl_loss total_loss.backward() optimizer.step()蒸馏训练2000个step就够用了,student网络的精度损失大约在3%以内——碰撞率从2%升到2.8%左右,完全可接受。
6.3 最后的建议:从仿真到水池试验的验收流程
我的习惯是,在真正布放前先做一轮水池试验。水池里布设几个不同深度的障碍物浮标,AUV在同一路径上往返跑十次,用地面站实时记录避障动作。验收重点有两个:第一,策略是否表现出“预防性避让”行为——即检测到障碍物时提前3到5米就开始转向,而不是等到逼近了才急转;第二,垂向维度的避让是否合理——很多策略会倾向于用深度变化绕开障碍物,但在真实水下,深度变化往往意味着接触不同的温跃层和海流条件,合理的策略应该优先水平面转向,垂向作为后备方案。
我遇到过最典型的坑是,在水池试验里策略的表现和仿真测试结果判若两人——碰撞率稳稳为0,但运动轨迹非常怪异,几乎每个障碍物都要先俯冲再拉起,像是在水底下画正弦曲线。检查发现是仿真环境里没有对垂向速度的能耗做惩罚,策略学到了“垂直面机动比水平面转向更方便”的捷径。我建议在训练时把垂向速度动作限制在±0.5m/s以内,并且增加一个垂向动作的能量惩罚项,系数设成艏摇惩罚的两倍,能有效纠正这类行为。
最后再分享一个个人习惯:每次训练结束,我都会把策略的参数和它对应的仿真环境配置hash值存在一起,因为换一个随机种子重新训练,策略行为会有细微差别,没有hash记录的话,过两周你可能完全想不起来当前这个策略是在什么条件下训出来的。这算是我踩过太多次“这是什么版本”的坑之后养成的习惯。如果你是在做工程交付或者毕业设计,建议也这样做,能省掉大量重复调参的时间。希望这篇文章能帮你在AUV动态避障这条路上少走几个弯路。
本文还有配套的精品资源,点击获取