ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

融合离散动作的TD3算法:自动驾驶决策改造与部署实战

融合离散动作的TD3算法:自动驾驶决策改造与部署实战 简介本资源为自动驾驶端到端行为决策方向的学术论文文档面向从事自动驾驶决策算法研究的研究生、算法工程师及强化学习爱好者聚焦复杂环境下驾驶策略学习效率低、动作平滑性差等痛点。文档围绕融合离散动作的双延迟深度确定性策略梯度算法TD3WD展开系统梳理模仿学习与强化学习两条技术路线的利弊并针对探索效率、Critic网络初始化、连续控制动作抖动等问题提出改进思路。压缩包内仅含1个docx文件约290KB内容涵盖引言、问题描述、算法建模与Carla仿真实验设计等完整章节结构接近正式期刊论文。目前已有94人学习适合希望深入理解TD3、DDPG、DQN等算法在自动驾驶决策中应用与改进的读者参考可帮助快速把握该领域研究脉络与创新切入点。1. 从连续控制到离散动作TD3 在自动驾驶决策里到底卡在哪很多做自动驾驶端到端行为决策的团队第一反应是直接上 TD3Twin Delayed DDPG因为它天生适合连续动作空间——油门、刹车、转向角都是连续量看起来完美匹配。但真正落地时会撞上一个很现实的问题决策层输出的往往不是连续控制量而是离散的语义动作比如“跟车”“左换道”“右换道”“超车”“让行”。如果强行用 TD3 输出连续值再去阈值化动作抖动、边界跳变、策略不稳定会一起冒出来。这就是“融合离散动作的双延迟深度确定性策略梯度算法”要解决的核心矛盾既要保留 TD3 在连续控制上的稳定性和样本效率又要让策略能直接输出离散行为决策。适合谁看已经跑通过 DDPG/TD3 基础代码、正在做端到端决策模块、被动作空间不匹配折磨过的工程师。下面从算法改造、网络结构、训练流程到调参排错把这条路走一遍。2. 融合离散动作的 TD3 算法改造与网络结构设计2.1 为什么不能直接把 TD3 的输出做阈值离散化TD3 的 Actor 输出的是一个连续动作向量 $a \in \mathbb{R}^n$Critic 基于这个连续动作估计 Q 值。如果决策动作是离散的 $K$ 类最粗暴的做法是 Actor 输出连续值后取 argmax 或分桶。问题在于三点第一Critic 的输入是连续动作离散化后的动作在训练时从未被 Critic 精确评估过Q 值估计有系统性偏差第二TD3 的目标策略平滑target policy smoothing会给动作加高斯噪声这个噪声在离散空间里没有意义反而破坏动作语义第三延迟策略更新在离散边界附近会导致策略在两类动作间反复横跳。常见做法是把动作空间重新定义为“离散动作 连续残差”的混合形式或者干脆把 Actor 改成输出离散动作的概率分布Critic 改成对离散动作打分。前者适合换道同时需要微调横向偏移的场景后者适合纯语义决策。下面以纯离散决策为例给出可复现的改造方案。2.2 融合离散动作的 Actor-Critic 网络结构Actor 不再输出连续动作而是输出 $K$ 个离散动作的 logits经过 Gumbel-Softmax 或直接 softmax 采样。Critic 接收状态和离散动作的 one-hot 或 embedding输出 Q 值。关键改动是 Critic 要能处理离散动作输入常见做法是把动作做 embedding 后与状态特征拼接。import torch import torch.nn as nn import torch.nn.functional as F class DiscreteActor(nn.Module): def __init__(self, state_dim, n_actions, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, n_actions) # 输出 logits不是连续动作 ) def forward(self, state): logits self.net(state) return logits # 训练时用 softmax 采样推理时取 argmax class DiscreteCritic(nn.Module): def __init__(self, state_dim, n_actions, hidden256): super().__init__() self.state_net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU() ) self.action_embed nn.Embedding(n_actions, hidden) # 离散动作 embedding self.q_net nn.Sequential( nn.Linear(hidden * 2, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, state, action_idx): s_feat self.state_net(state) a_feat self.action_embed(action_idx) # action_idx 是 [batch] 的整数 q self.q_net(torch.cat([s_feat, a_feat], dim-1)) return q逻辑说明Actor 输出 logits 后训练时用Categorical(logits).sample()采样动作保证探索推理时用argmax保证确定性。Critic 用 embedding 而不是 one-hot是为了在动作数较多时减少参数量并让相似动作共享部分表示。参数说明hidden建议 256 或 512太小欠拟合太大在车载算力上推理延迟明显n_actions是离散决策动作数常见 5 到 9 类。2.3 双 Critic 与延迟更新的保留方式TD3 的核心 trick 要保留两个 Critic 取 min 抑制过估计Actor 延迟更新目标网络软更新。但目标策略平滑要改——离散空间不能加高斯噪声替代做法是对目标动作做 label smoothing 或者以一定概率随机替换为次优动作。class TD3DiscreteAgent: def __init__(self, state_dim, n_actions, gamma0.99, tau0.005, policy_noise0.2, noise_clip0.5, policy_delay2): self.actor DiscreteActor(state_dim, n_actions) self.actor_target DiscreteActor(state_dim, n_actions) self.critic1 DiscreteCritic(state_dim, n_actions) self.critic2 DiscreteCritic(state_dim, n_actions) self.critic1_target DiscreteCritic(state_dim, n_actions) self.critic2_target DiscreteCritic(state_dim, n_actions) self.actor_target.load_state_dict(self.actor.state_dict()) self.critic1_target.load_state_dict(self.critic1.state_dict()) self.critic2_target.load_state_dict(self.critic2.state_dict()) self.policy_delay policy_delay self.total_it 0 def select_action(self, state, exploreTrue): logits self.actor(state) if explore: return torch.distributions.Categorical(logitslogits).sample() return logits.argmax(dim-1)参数说明policy_delay2表示 Critic 更新两次才更新一次 Actor这是 TD3 原论文的推荐值tau0.005是目标网络软更新系数太大目标漂移快太小学习慢。注意离散版本里policy_noise和noise_clip不再直接作用于动作而是用于对目标 Q 做正则或对目标动作做随机替换具体在下一章训练循环里体现。3. 训练循环、经验回放与离散动作的 Q 值更新3.1 离散 TD3 的完整训练步骤训练循环和连续 TD3 骨架一致差别在动作采样、目标 Q 计算和损失函数。目标 Q 不再对连续动作加噪声而是对目标 Actor 输出的 logits 做 softmax 后采样或者用 top-2 动作的加权 Q 值。def train_step(self, replay_buffer, batch_size256): self.total_it 1 state, action, reward, next_state, done replay_buffer.sample(batch_size) # action 是离散索引 [batch] with torch.no_grad(): next_logits self.actor_target(next_state) next_probs F.softmax(next_logits, dim-1) # 目标动作按概率采样替代连续空间的噪声平滑 next_action torch.distributions.Categorical(probsnext_probs).sample() target_q1 self.critic1_target(next_state, next_action) target_q2 self.critic2_target(next_state, next_action) target_q torch.min(target_q1, target_q2) target_q reward (1 - done) * self.gamma * target_q current_q1 self.critic1(state, action) current_q2 self.critic2(state, action) critic_loss F.mse_loss(current_q1, target_q) F.mse_loss(current_q2, target_q) self.critic1_optimizer.zero_grad() self.critic2_optimizer.zero_grad() critic_loss.backward() self.critic1_optimizer.step() self.critic2_optimizer.step() if self.total_it % self.policy_delay 0: logits self.actor(state) probs F.softmax(logits, dim-1) # 用期望 Q 值作为 Actor 损失可微 actor_loss -(probs * self.critic1(state, torch.arange(probs.size(-1)).expand(state.size(0), -1))).sum(dim-1).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # 软更新 for p, tp in zip(self.actor.parameters(), self.actor_target.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data) for p, tp in zip(self.critic1.parameters(), self.critic1_target.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data) for p, tp in zip(self.critic2.parameters(), self.critic2_target.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data)逻辑说明目标 Q 用目标 Actor 采样出的离散动作计算替代连续 TD3 的高斯噪声平滑。Actor 损失用 softmax 概率对 Critic 打分的加权期望这样梯度可以通过概率回传到 logits。注意torch.arange那行是构造所有离散动作索引实际实现时可以用torch.arange(n_actions).unsqueeze(0).repeat(batch_size, 1)更清晰。3.2 经验回放里离散动作的存储与采样离散动作在回放缓冲区里存成整数索引比存 one-hot 省内存采样后直接喂给 Critic 的 embedding 层。常见坑是采样时忘了把 action 转成long类型embedding 层会报类型错误。存储字段数据类型维度说明statefloat32[state_dim]观测特征含自车速度、前车距离等actionint64[1]离散动作索引0 到 K-1rewardfloat32[1]即时奖励换道成功给正碰撞给大负next_statefloat32[state_dim]下一时刻观测donefloat32[1]终止标志碰撞或到达目标为 1参数说明回放缓冲区容量建议 10 万到 100 万太小容易过拟合近期经验太大在车载内存上不现实。batch size 256 是常见起点显存不够降到 128但训练会更抖。3.3 奖励函数设计与离散动作的对应关系奖励函数直接决定策略会不会退化成“永远跟车”。常见做法是分层奖励安全项、效率项、舒适项、任务项。换道动作只有在安全间隙足够时才给正奖励否则给负。def compute_reward(state, action, next_state, collision, lane_change_safe): reward 0.0 if collision: return -100.0 # 碰撞大负 reward 0.1 * next_state[0] # 速度项鼓励快 if action in [1, 2]: # 换道动作 if lane_change_safe: reward 5.0 else: reward - 10.0 # 不安全换道重罚 reward - 0.01 * abs(next_state[3]) # 横向加速度惩罚舒适性 return reward逻辑说明速度项鼓励效率换道安全项约束策略不要乱换横向加速度惩罚舒适性。参数说明碰撞惩罚 -100 要远大于其他项否则策略会冒险换道正奖励 5.0 和负奖励 -10.0 的不对称是为了让策略保守。4. 仿真环境接入、训练排错与典型失败模式4.1 在 CarSim/PreScan/VTD 联合仿真里跑通训练自动驾驶端到端决策训练离不开仿真。常见组合是 CarSim 做车辆动力学PreScan 或 VTD 做场景和传感器Python 侧用 gym 接口封装成环境。关键是状态接口要对齐仿真输出的自车速度、加速度、前车距离、车道线曲率等要拼成固定维度向量。import gym from gym import spaces import numpy as np class AutoDriveEnv(gym.Env): def __init__(self): self.action_space spaces.Discrete(5) # 跟车、左换、右换、超车、让行 self.observation_space spaces.Box(low-np.inf, highnp.inf, shape(12,), dtypenp.float32) self.sim None # 这里接 CarSim/VTD 的 Python API def step(self, action): # 把离散动作翻译成仿真控制指令 cmd self._action_to_control(action) self.sim.send_control(cmd) next_state self.sim.get_state() collision self.sim.check_collision() reward compute_reward(self.state, action, next_state, collision, self.sim.lane_change_safe()) done collision or self.sim.reach_goal() self.state next_state return next_state, reward, done, {}逻辑说明_action_to_control把离散决策翻译成仿真能接受的油门/刹车/转向或轨迹点。参数说明状态维度 12 是示例实际按仿真输出调整动作数 5 对应常见决策集合可按场景扩展。4.2 训练不收敛的 4 个排查方向第一检查 Critic 损失是否爆炸。离散 TD3 里如果 target Q 用了 softmax 采样但没 detach梯度会回传到目标网络导致发散。第二检查动作索引是否越界embedding 层索引超范围会直接报错或静默取错。第三检查奖励尺度如果碰撞惩罚和速度奖励差两个数量级Critic 很难拟合。第四检查目标网络软更新是否真的执行了常见 bug 是只更新了 Actor 目标网络忘了 Critic。提示训练初期先关掉换道动作只用跟车和刹车跑通确认 Q 值能收敛后再放开全部动作。这样能把动作空间问题和网络问题分开定位。4.3 离散动作抖动与策略震荡的处理策略在两类动作间反复横跳通常是 Q 值差异太小或探索噪声太大。处理方式有三种一是增大 Critic 网络容量或加 LayerNorm让 Q 值区分度更高二是降低采样温度训练后期把 softmax 温度从 1.0 降到 0.5三是加动作切换惩罚在奖励里对连续两步不同动作扣分。def action_switch_penalty(prev_action, action): if prev_action is not None and prev_action ! action: return -0.5 # 切换动作的固定惩罚 return 0.0参数说明惩罚系数 -0.5 是起点太大会导致策略不敢换道太小不起作用。建议先用 -0.2 跑一轮看换道频率。5. 从训练到部署离散 TD3 策略的推理优化与验证技巧训练收敛只是第一步部署到车载或仿真闭环里还要过推理延迟和策略一致性两关。离散 TD3 的 Actor 推理很简单就是一次前向取 argmax但要注意训练和推理的预处理必须完全一致否则状态分布偏移会让策略失效。一个实用技巧是把 Actor 导出成 ONNX 或 TorchScript在目标平台上做延迟测试。常见做法是先用 PyTorch 的torch.jit.trace把 Actor 固定成推理图再用 ONNX Runtime 跑 benchmark。import torch actor DiscreteActor(state_dim12, n_actions5) actor.eval() dummy_input torch.randn(1, 12) traced torch.jit.trace(actor, dummy_input) traced.save(discrete_actor.pt) # 推理时 loaded torch.jit.load(discrete_actor.pt) with torch.no_grad(): logits loaded(torch.tensor(state, dtypetorch.float32).unsqueeze(0)) action logits.argmax(dim-1).item()逻辑说明torch.jit.trace把动态图固定成静态图去掉训练相关分支推理更快。参数说明dummy_input的维度必须和实际状态维度一致否则 trace 出来的图输入不匹配。注意 trace 不支持控制流如果 Actor 里有 if 分支要改成 mask 形式。验证策略一致性时建议在仿真里跑固定随机种子对比训练时和部署后的动作序列。如果动作分布差异超过 10%大概率是状态归一化参数没同步。另一个技巧是记录 Q 值分布部署时如果 Q 值整体偏低说明遇到了训练分布外的状态需要触发安全兜底策略。注意离散 TD3 的探索只在训练阶段用部署时必须关掉采样用 argmax。如果部署时还带随机性换道决策会不可复现安全验证没法做。最后给一个调参顺序先固定奖励函数跑通跟车再放开换道动作然后调policy_delay和tau最后调网络宽度和 batch size。每次只改一个参数记录 Q 值曲线和动作分布比一次性调一堆参数靠谱得多。本文还有配套的精品资源点击获取
返回列表