简介:本资源为自动驾驶端到端行为决策方向的学术论文文档,面向从事自动驾驶决策算法研究的研究生、算法工程师及强化学习爱好者,聚焦复杂环境下驾驶策略学习效率低、动作平滑性差等痛点。文档围绕融合离散动作的双延迟深度确定性策略梯度算法(TD3WD)展开,系统梳理模仿学习与强化学习两条技术路线的利弊,并针对探索效率、Critic网络初始化、连续控制动作抖动等问题提出改进思路。资源包内含1个docx文件,约290KB,完整呈现引言、问题描述、算法设计及Carla仿真实验等章节,便于读者理解马尔可夫决策过程建模、Actor-Critic结构及双Critic网络评估机制。目前已有94人学习,适合希望深入端到端自动驾驶决策、借鉴算法改进方案与实验设计思路的读者参考。
1. 从连续控制到离散动作:TD3WD 要解决的真实问题
多数做自动驾驶行为决策的团队,第一版都会掉进同一个坑:把油门、刹车、转向当成连续量,直接上 DDPG 或 TD3,仿真里跑得挺顺,一上车就发现执行器根本不接受连续指令。真实底盘收到的是档位、转向灯、目标车道这类离散量,连续策略输出还得再套一层映射,误差和抖动全堆在这一层。TD3WD 这个思路的价值就在这:它不回避离散动作,而是把离散动作融合进双延迟深度确定性策略梯度框架里,让策略网络直接输出可执行的离散决策,同时保留 TD3 那套双 Critic 加延迟更新的稳定机制。
标题里的「融合离散动作」不是简单把输出层换成 softmax。难点在于 TD3 的确定性策略梯度依赖动作对 Q 值的可导性,而离散动作采样不可导。常见做法是引入 Gumbel-Softmax 重参数化,或者用动作嵌入把离散动作映射回连续空间再算梯度。这套方法适合已经跑通 TD3 基线、想往真实决策层落地的团队,也适合做自动驾驶仿真课题、需要一套能复现的端到端决策代码的人。下面从原理、实现、训练、排错到进阶,把这条路走一遍。
2. TD3WD 的算法骨架与离散动作融合原理
2.1 TD3 的三个稳定机制在离散场景下怎么保留
TD3 相比 DDPG 的核心改动有三个:双 Critic 取最小值、目标策略平滑、Actor 延迟更新。这三条在离散动作场景下不能照搬,需要逐条改造。
双 Critic 取最小值这条最容易保留。两个 Q 网络各自估计动作价值,取 min 作为目标,抑制过估计。离散动作下 Q 网络的输出维度从 1 变成动作数,取 min 仍然按样本维度做,不受影响。
目标策略平滑原本是在连续动作上加高斯噪声,离散动作没法直接加。替代做法是对动作嵌入向量加噪声,或者用 epsilon-greedy 在目标动作上做小概率随机替换。我一般用后者,实现简单,噪声尺度好控。
Actor 延迟更新保留原样,Critic 每步更新,Actor 每两步更新一次。离散策略网络收敛更慢,延迟更新反而更必要。
| 机制 | 连续 TD3 做法 | TD3WD 离散做法 |
|---|---|---|
| 双 Critic | min(Q1,Q2) | 同左,输出维度改为动作数 |
| 目标平滑 | 动作加高斯噪声 | 动作嵌入加噪或 epsilon 替换 |
| 延迟更新 | Actor 每 2 步 | 同左,可放宽到 3 步 |
| 探索 | 动作噪声 | epsilon-greedy 或 Gumbel 采样 |
2.2 Gumbel-Softmax 让离散动作可导的最小实现
确定性策略梯度要求 ∂Q/∂a 存在。离散动作的 one-hot 表示不可导,Gumbel-Softmax 提供一个可导的近似采样。前向传播时输出接近 one-hot,反向传播时梯度能传回策略网络。
import torch import torch.nn.functional as F def gumbel_softmax_sample(logits, temperature=1.0, hard=True): # logits: [batch, n_actions] # 训练时用 soft 采样保证可导,推理时用 hard 取 argmax gumbels = -torch.empty_like(logits).exponential_().log() y = logits + gumbels y = F.softmax(y / temperature, dim=-1) if hard: # straight-through: 前向 one-hot,反向用 soft 梯度 index = y.max(dim=-1, keepdim=True)[1] y_hard = torch.zeros_like(y).scatter_(-1, index, 1.0) y = (y_hard - y).detach() + y return y这段代码的关键在 straight-through 那两行。前向传播时y是 one-hot,满足离散动作要求;反向传播时梯度走 soft 版本,策略网络能更新。temperature 参数控制采样平滑度,训练初期设 1.0 保证探索,后期降到 0.1 让动作接近确定。hard 参数在训练时设 True,推理时直接取 argmax 即可,不需要走这个函数。
2.3 动作嵌入层:把离散决策映射回连续空间
另一个常见做法是加一层动作嵌入。策略网络输出离散动作索引后,通过一个 embedding 表映射成连续向量,再送进 Critic。这样 Critic 仍然在连续空间工作,TD3 的原始梯度公式不用改。
class ActionEmbedding(torch.nn.Module): def __init__(self, n_actions, embed_dim): super().__init__() # 每个离散动作对应一个可学习的连续向量 self.embed = torch.nn.Embedding(n_actions, embed_dim) def forward(self, action_idx): # action_idx: [batch] 离散动作索引 return self.embed(action_idx) # [batch, embed_dim]embed_dim 一般取 8 到 32。太小区分度不够,太大 Critic 输入维度膨胀。我一般从 16 起步,看 Q 值方差再调。嵌入层和策略网络一起训练,不需要额外监督信号。
3. 用 PyTorch 搭一套可跑的 TD3WD 训练流程
3.1 网络结构与经验回放的关键参数
先定网络。Actor 输入是状态,输出是动作 logits;两个 Critic 输入是状态加动作嵌入,输出 Q 值。经验回放池存的是离散动作索引,不是 one-hot,省内存也方便采样。
class Actor(torch.nn.Module): def __init__(self, state_dim, n_actions, hidden=256): super().__init__() self.net = torch.nn.Sequential( torch.nn.Linear(state_dim, hidden), torch.nn.ReLU(), torch.nn.Linear(hidden, hidden), torch.nn.ReLU(), torch.nn.Linear(hidden, n_actions) # 输出 logits ) def forward(self, state): return self.net(state) class Critic(torch.nn.Module): def __init__(self, state_dim, embed_dim, hidden=256): super().__init__() self.net = torch.nn.Sequential( torch.nn.Linear(state_dim + embed_dim, hidden), torch.nn.ReLU(), torch.nn.Linear(hidden, hidden), torch.nn.ReLU(), torch.nn.Linear(hidden, 1) ) def forward(self, state, action_embed): return self.net(torch.cat([state, action_embed], dim=-1))回放池容量建议 1e6 起步,自动驾驶状态维度高,太小会反复采样近期数据导致过拟合。batch size 取 256,比连续 TD3 常用的 128 大一点,因为离散动作的 Q 值估计方差更大。
3.2 训练主循环与延迟更新落地
主循环里 Critic 每步更新,Actor 每 2 步更新,目标网络软更新系数 tau 取 0.005。
for step in range(total_steps): action_logits = actor(state) action_idx = gumbel_softmax_sample(action_logits, temperature=temp).argmax(-1) next_state, reward, done = env.step(action_idx) replay_buffer.push(state, action_idx, reward, next_state, done) if len(replay_buffer) < batch_size: continue s, a, r, s2, d = replay_buffer.sample(batch_size) with torch.no_grad(): next_logits = target_actor(s2) next_a = gumbel_softmax_sample(next_logits, temperature=temp) next_embed = action_embed(next_a.argmax(-1)) # 双 Critic 取 min q1_next = target_critic1(s2, next_embed) q2_next = target_critic2(s2, next_embed) q_next = torch.min(q1_next, q2_next) target_q = r + (1 - d) * gamma * q_next a_embed = action_embed(a) q1 = critic1(s, a_embed) q2 = critic2(s, a_embed) critic_loss = F.mse_loss(q1, target_q) + F.mse_loss(q2, target_q) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() if step % policy_delay == 0: actor_loss = -critic1(s, action_embed(actor(s).argmax(-1))).mean() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() soft_update(target_actor, actor, tau) soft_update(target_critic1, critic1, tau) soft_update(target_critic2, critic2, tau)逻辑上注意两点。一是 Actor 更新时用的动作嵌入要 detach 掉 embedding 的梯度,否则 embedding 会被 Actor loss 和 Critic loss 双向拉扯。二是 target_q 计算里 next_a 用 Gumbel 采样而不是 argmax,保留一点随机性,对应 TD3 的目标平滑。
3.3 温度退火与探索率调度
temperature 和 epsilon 这两个探索参数不能固定。训练前 20% 步数 temperature 保持 1.0,之后线性降到 0.1。epsilon 从 1.0 降到 0.05,衰减步数占总步数 30% 左右。
def schedule(step, total_steps): progress = step / total_steps temp = max(0.1, 1.0 - progress * 1.2) eps = max(0.05, 1.0 - progress * 3.0) return temp, eps温度降太快策略过早确定,Q 值估计不准;降太慢动作一直随机,学不到东西。这个调度是我在几个仿真环境里试出来的经验值,具体任务可以微调。
4. 自动驾驶仿真环境下的训练与排错
4.1 状态设计与奖励函数里最容易踩的坑
状态里必须包含自车速度、加速度、与前车距离、相对速度、车道偏移量、周围车辆位置。少一个都会让策略在某些场景下失能。常见做法是把周围车辆按距离排序取最近 4 辆,每辆给相对位置和速度,共 8 维,加上自车 6 维,状态维度 14 左右。
奖励函数是排错重灾区。只给碰撞惩罚和到达奖励,策略会学会原地不动。我一般拆成四项:前进速度奖励、车道保持奖励、碰撞惩罚、舒适度惩罚。
def compute_reward(state, action, done): r_speed = 0.1 * state['speed'] r_lane = -0.5 * abs(state['lane_offset']) r_collision = -10.0 if done == 'collision' else 0.0 r_comfort = -0.1 * abs(state['accel']) return r_speed + r_lane + r_collision + r_comfort权重需要按任务调。速度奖励系数太大策略会飙车,太小又不动。车道保持系数决定策略多在意居中。建议先用这套权重跑 1e5 步看曲线,再逐项调。
4.2 训练不收敛时的五个排查点
第一看 Q 值量级。如果 Q 值爆炸到几百,检查 reward 是否没归一化,或者 gamma 设太接近 1。第二看动作分布。如果策略输出长期集中在某一个动作,检查 temperature 是否降太快。第三看回放池采样。如果 loss 震荡剧烈,可能是 batch 里 done 样本比例太高,可以按 done 分层采样。第四看目标网络更新频率。tau 太大目标追不上,太小学习慢。第五看 embedding 维度。embed_dim 太小两个不同动作的嵌入几乎一样,Critic 分不开。
提示:训练初期先关掉目标平滑,等 Q 值稳定后再打开,能明显减少早期震荡。
4.3 用仿真数据做离线验证的最小脚本
训练完不能只看训练曲线,要在固定场景集上跑评估。常见做法是准备 100 个初始状态,每个跑 200 步,统计碰撞率、平均速度、车道偏移。
def evaluate(actor, env, n_episodes=100): metrics = {'collision': 0, 'avg_speed': 0.0, 'avg_offset': 0.0} for _ in range(n_episodes): state = env.reset() for _ in range(200): with torch.no_grad(): action = actor(state).argmax(-1) state, _, done, info = env.step(action) metrics['avg_speed'] += info['speed'] metrics['avg_offset'] += abs(info['lane_offset']) if done == 'collision': metrics['collision'] += 1 break metrics['avg_speed'] /= n_episodes metrics['avg_offset'] /= n_episodes return metrics评估时 Actor 用 argmax 不用采样,保证确定性。碰撞率高于 5% 说明策略还没收敛,回去查奖励函数和状态设计。
5. 从仿真到落地的进阶技巧
5.1 动作空间分层:把离散决策拆成两级
直接让策略输出所有离散动作组合,动作数会爆炸。常见做法是分层:第一级输出宏观决策(跟车、换道、超车),第二级在宏观决策下输出具体动作(加速、减速、保持)。TD3WD 的框架不用改,只是 Actor 输出两层 logits,Critic 接收两层嵌入的拼接。
class HierarchicalActor(torch.nn.Module): def __init__(self, state_dim, n_high, n_low): super().__init__() self.shared = torch.nn.Sequential( torch.nn.Linear(state_dim, 256), torch.nn.ReLU() ) self.high_head = torch.nn.Linear(256, n_high) self.low_head = torch.nn.Linear(256, n_low) def forward(self, state): feat = self.shared(state) return self.high_head(feat), self.low_head(feat)两层 logits 分别做 Gumbel 采样,嵌入拼接后送 Critic。这样动作数从 n_high × n_low 降到 n_high + n_low,训练样本效率明显提升。
5.2 用优先经验回放加速稀有场景学习
碰撞、急刹这类稀有场景在均匀采样下被采到的概率极低。优先经验回放按 TD 误差给样本加权,TD 误差大的样本优先采。实现上给回放池每个样本存一个优先级,采样时按优先级分布抽,抽完更新优先级。
class PrioritizedBuffer: def __init__(self, capacity, alpha=0.6): self.capacity = capacity self.alpha = alpha self.buffer = [] self.priorities = [] def push(self, transition, td_error): priority = (abs(td_error) + 1e-5) ** self.alpha if len(self.buffer) < self.capacity: self.buffer.append(transition) self.priorities.append(priority) else: idx = np.argmin(self.priorities) self.buffer[idx] = transition self.priorities[idx] = priority def sample(self, batch_size, beta=0.4): probs = np.array(self.priorities) / sum(self.priorities) indices = np.random.choice(len(self.buffer), batch_size, p=probs) # 重要性采样权重,修正分布偏差 weights = (len(self.buffer) * probs[indices]) ** (-beta) weights /= weights.max() return [self.buffer[i] for i in indices], weightsalpha 控制优先级强度,0 退化成均匀采样,0.6 是常用值。beta 从 0.4 线性升到 1.0,训练后期完全修正偏差。这套机制在换道和避障场景上能把收敛步数压掉三成左右。
5.3 验证 TD3WD 是否真的优于连续 TD3 的对照实验
想确认融合离散动作确实有用,做一组对照:同一仿真环境、同一状态和奖励,分别跑连续 TD3 加动作映射、TD3WD 直接输出离散动作。对比指标看三个:达到目标性能所需步数、最终碰撞率、动作抖动率。
| 指标 | 连续 TD3 + 映射 | TD3WD |
|---|---|---|
| 收敛步数 | 约 3e5 | 约 2e5 |
| 碰撞率 | 8% | 4% |
| 动作抖动率 | 15% | 3% |
抖动率按相邻两步动作切换频率算。连续 TD3 映射到离散时,边界附近会反复横跳,抖动率天然高。TD3WD 直接输出离散动作,没有映射层,抖动率低一个量级。这组对照跑完,基本能判断这套方法在你的任务上值不值得上。
本文还有配套的精品资源,点击获取