简介:面向机器人控制与强化学习研究者的技术文档《机器人动态控制:PyTorch强化学习DDPG算法在六轴机械臂轨迹规划中的实时仿真方案》,系统讲解如何借助PyTorch实现DDPG算法并完成六轴机械臂轨迹规划的实时仿真。文档从六轴机械臂运动学基础入手,依次阐述轨迹规划目标、强化学习与策略梯度、DDPG算法原理,再到PyTorch框架下的网络设计与训练流程,最后给出仿真环境搭建、奖励函数设计、结果可视化和实时性优化等完整路径,适合深度强化学习入门者及机械臂轨迹规划项目开发者参考。资源为单一PDF文件,共30页,压缩包大小1.89MB,支持目录章节跳转及阅读器大纲定位,内容完整、图表正常,便于快速查阅与对照学习。目前已有98人学习,可用作课题研究、课程设计或仿真实验的实用参考资料。
1. 六轴机械臂轨迹规划为什么需要 DDPG
六轴机械臂的轨迹规划从来不是单点求解问题。一个静态目标点可以用解析逆解加插值直接算,但目标点移动、负载变化或障碍物挪位后,传统方法要重新建模、重新标定,现场改参数的成本非常高。DDPG(Deep Deterministic Policy Gradient)是少数能把连续控制策略直接学出来的强化学习算法:输入由关节角度、角速度、末端误差拼成的状态,输出关节力矩或位姿增量,轨迹由奖励函数而不是运动学方程驱动。PyTorch 的动态计算图和自动求导把这类算法从论文公式变成可跑通的训练循环,周期比手动推导快得多。这份方案完整覆盖 D-H 参数建模、状态动作空间设计、DDPG 训练和实时仿真验证,适合正在做机械臂轨迹规划、强化学习落地,或者想给传统控制找一条数据驱动替代路线的工程师。
2. 状态动作空间建模:从 D-H 运动学到 DDPG 的输入输出设计
DDPG 不会凭空理解“六轴机械臂”这个物理对象。训练开始前,必须先把正运动学、状态向量、动作向量和奖励函数定义清楚。这一步做错,后面调多少超参数都救不回来。
2.1 D-H 参数与正运动学:先解决“机械臂在哪儿”
机械臂的每个相邻关节坐标系之间都能用一个 4×4 齐次变换矩阵描述,四个 D-H 参数分别是关节角度theta、连杆偏移d、连杆长度a和连杆扭转角alpha。把六个关节的矩阵连乘,就得到末端执行器在基座坐标系下的位姿。
import numpy as np def dh_transform(theta, d, a, alpha): """计算相邻关节的齐次变换矩阵""" return np.array([ [np.cos(theta), -np.sin(theta) * np.cos(alpha), np.sin(theta) * np.sin(alpha), a * np.cos(theta)], [np.sin(theta), np.cos(theta) * np.cos(alpha), -np.cos(theta) * np.sin(alpha), a * np.sin(theta)], [0, np.sin(alpha), np.cos(alpha), d], [0, 0, 0, 1] ]) def forward_kinematics(theta_list, d_list, a_list, alpha_list): """六轴机械臂正运动学,返回末端位姿矩阵""" T = np.eye(4) for i in range(6): T = T @ dh_transform(theta_list[i], d_list[i], a_list[i], alpha_list[i]) return T这段代码的输入是六个关节角度和机械臂的 D-H 参数表,每循环一次就左乘一个新的关节变换矩阵,最终得到末端执行器的 4×4 位姿矩阵。D-H 参数表可以直接从机械臂产品手册里查,UR5、AUBO i5、自研六轴都适用。正运动学本身不参与 DDPG 的反向传播,但它承担两个关键任务:一是用来计算奖励里的末端误差,二是训练结束后用来验证策略生成的轨迹是否真的到达目标点。
2.2 状态空间与动作空间的定义
六轴机械臂轨迹规划中,状态向量不能只放关节角度。只看角度,策略分不清机械臂是在向目标靠近还是远离。我一般会把状态拼成 24 维,并做归一化处理。
| 状态分量 | 维度 | 作用 |
|---|---|---|
| 关节角度 q | 6 | 当前位形描述 |
| 关节角速度 qdot | 6 | 惩罚突变、判断运动方向 |
| 末端位置误差 tcp_pos - goal | 3 | 给出笛卡尔空间的学习信号 |
| 末端姿态误差 | 3 | 六轴任务的末端朝向约束 |
| 上一时刻动作 a_prev | 6 | 避免控制量高频抖动 |
动作空间有两种常用选择:一种是关节力矩,直接驱动机械臂动力学模型;另一种是关节位置增量或关节速度增量,底层再挂一个 PID 伺服环。对实时仿真来说,我倾向于用位置增量作为动作输出,因为 DDPG 的探索噪声作用在位置域更平稳,不容易一上来就把仿真模型打飞。动作需要归一化到 [-1, 1],再乘上每个关节允许的最大变化量。
2.3 奖励函数:让机械臂自己知道“好轨迹”长什么样
奖励函数是这条路线的隐性超参数。只给稀疏奖励,比如“到了给 100,没到给 0”,六轴机械臂的高维状态空间里几乎不可能收敛。常见的做法是 Dense Reward:末端误差越大扣分越多,动作变化过于剧烈也扣分,到达目标点后给一个较大的正向奖励。
def compute_reward(tcp_pos, target, action, prev_action, done, timeout=False): dist = np.linalg.norm(tcp_pos - target) # 末端与目标的欧氏距离 action_change = np.linalg.norm(action - prev_action) # 动作突变程度 reward = -3.0 * dist - 0.05 * action_change if dist < 0.02: reward += 100.0 # 进入目标容差范围 if timeout and dist >= 0.02: reward -= 50.0 # 超时且未到达,给惩罚 return reward这段代码把奖励拆成三项:距离惩罚项让策略有方向性地逼近目标,动作变化惩罚项抑制关节抖动,到达项提供稀疏的“强成功信号”。实际操作时要注意量纲,末端误差是米,动作变化量是弧度,两者直接相加会出现大数吃小数,建议先各自归一化再调权重。奖励数值的量级还会影响 Critic 网络的学习难度,累计奖励动辄上千的设定会让 Q 值估计很不稳定。
2.4 为什么是 DDPG 而不是 PPO 或 Q-learning
机械臂轨迹规划的动作空间是连续的,Q-learning 需要对动作做离散化,六个关节即使每个关节只分 5 档,也要产生 5 的 6 次方个动作组合,维度灾难直接卡死训练。PPO 虽然能处理连续动作,但它是 on-policy 算法,每次策略更新后旧经验全部作废,在仿真器里一步一采样的成本很高。DDPG 是 off-policy 的确定性策略梯度算法,配合经验回放可以反复利用历史数据,样本效率明显占优。和 TD3 的关系也要看清:DDPG 的 Q 过估计问题在 TD3 里被裁剪双 Critic 解决,先跑通 DDPG,再迁到 TD3 只改动几十行代码。对这份实时仿真方案,DDPG 是性价比最高的起点。
3. PyTorch 实现 DDPG:Actor-Critic 网络、经验回放与训练循环
PyTorch 在这类任务里的价值不在“模型定义”本身,而在动态计算图让目标网络、软更新、梯度裁剪这些机制都能直接在训练循环里看到、改到。下面这套实现是我在六轴机械臂仿真里常用的一版,隐藏层 256,足以表达六个关节的耦合关系。
3.1 Actor 与 Critic 网络结构
import torch import torch.nn as nn class Actor(nn.Module): """策略网络:输入状态,输出确定性动作""" def __init__(self, state_dim, action_dim, max_action, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) self.max_action = max_action def forward(self, state): return self.net(state) * self.max_action class Critic(nn.Module): """价值网络:输入状态+动作,输出 Q 值""" def __init__(self, state_dim, action_dim, hidden=256): super().__init__() self.fc1 = nn.Linear(state_dim + action_dim, hidden) self.fc2 = nn.Linear(hidden, hidden) self.fc3 = nn.Linear(hidden, 1) def forward(self, state, action): x = torch.cat([state, action], dim=1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)Actor 网络的最后一层是 Tanh,把输出限制在 [-1, 1],再乘上max_action得到实际动作范围。max_action可以是关节最大角速度,也可以是单步最大位置增量。Critic 网络把状态和动作拼在一起输入,输出一个标量 Q 值,用来评估“当前状态下执行这个动作到底好不好”。隐藏层数量不建议盲目加深,六轴机械臂的状态维度并不高,256 维的两层全连接已经能捕捉关节间的耦合关系,更深的网络只会放大 Q 值震荡。
3.2 经验回放缓冲区与采样
DDPG 的 off-policy 特性依赖经验回放。仿真里的五元组(state, action, reward, next_state, done)被存进缓冲区,训练时随机采样一个小批量,打散样本之间的时间相关性。
import random from collections import deque import numpy as np class ReplayBuffer: def __init__(self, capacity=1_000_000): self.buffer = deque(maxlen=capacity) def push(self, s, a, r, s2, done): self.buffer.append((s, a, r, s2, float(done))) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) s, a, r, s2, d = map(np.array, zip(*batch)) return (torch.FloatTensor(s), torch.FloatTensor(a), torch.FloatTensor(r).unsqueeze(1), torch.FloatTensor(s2), torch.FloatTensor(d).unsqueeze(1))缓冲区容量我一般给到 100 万条,对应几十分钟的仿真经验。容量太小,旧经验被快速覆盖,网络会反复学习同一批样本,策略容易过拟合到近期状态;容量太大,训练初期采到大量还没探索出有效动作的陈旧样本,Q 值更新会滞后。done转成float是必要的,后面计算目标 Q 值时需要它做截断,布尔值参与矩阵乘法会出错。
3.3 训练循环与目标网络软更新
class DDPGAgent: def __init__(self, state_dim, action_dim, max_action): self.actor = Actor(state_dim, action_dim, max_action) self.critic = Critic(state_dim, action_dim) self.target_actor = Actor(state_dim, action_dim, max_action) self.target_critic = Critic(state_dim, action_dim) self.target_actor.load_state_dict(self.actor.state_dict()) self.target_critic.load_state_dict(self.critic.state_dict()) self.actor_opt = torch.optim.Adam(self.actor.parameters(), lr=1e-4) self.critic_opt = torch.optim.Adam(self.critic.parameters(), lr=1e-3) self.gamma = 0.99 self.tau = 0.005 def select_action(self, state, noise=0.1): state = torch.FloatTensor(state.reshape(1, -1)) action = self.actor(state).detach().cpu().numpy().flatten() return np.clip(action + np.random.normal(0, noise, size=len(action)), -1, 1) def update(self, batch, batch_size=256): s, a, r, s2, d = batch # 目标 Q 值:用目标网络计算下一状态的动作和价值 with torch.no_grad(): next_a = self.target_actor(s2) target_q = r + self.gamma * (1 - d) * self.target_critic(s2, next_a) # 更新 Critic:最小化当前 Q 与目标 Q 的误差 current_q = self.critic(s, a) critic_loss = nn.MSELoss()(current_q, target_q) self.critic_opt.zero_grad() critic_loss.backward() self.critic_opt.step() # 更新 Actor:最大化 Critic 给出的 Q 值 actor_loss = -self.critic(s, self.actor(s)).mean() self.actor_opt.zero_grad() actor_loss.backward() self.actor_opt.step() # 软更新目标网络:theta_target = tau * theta_main + (1 - tau) * theta_target with torch.no_grad(): for p, tp in zip(self.actor.parameters(), self.target_actor.parameters()): tp.data.mul_(1 - self.tau).add_(self.tau * p.data) for p, tp in zip(self.critic.parameters(), self.target_critic.parameters()): tp.data.mul_(1 - self.tau).add_(self.tau * p.data)训练循环的逻辑分四步。Critic 更新用的是目标网络算出的target_q,这相当于给价值函数设置了一个缓慢移动的靶子,避免 Q 值一步跳到离谱的估计上。Actor 更新方向是朝向 Q 值增大的方向,也就是说策略要让 Critic 认为当前动作组合“更有价值”。软更新系数tau很关键,tau太大会让目标网络跟主网络同频振荡,tau太小则目标网络长期落后,我一般从 0.005 起步。训练时不必每一步都调用update,物理仿真里通常每 2 到 4 个仿真步更新一次 Critic,Actor 更新频率再低一点,训练会更稳定。
3.4 超参数参考表
| 超参数 | 推荐值 | 调整方向 |
|---|---|---|
| Actor 学习率 | 1e-4 | 过大策略振荡,过小收敛慢 |
| Critic 学习率 | 1e-3 | 一般比 Actor 高一个数量级 |
| 折扣因子 gamma | 0.99 | 长轨迹任务用 0.99,短任务可用 0.95 |
| 软更新系数 tau | 0.005 | 目标网络跟随过快会不稳定 |
| 批次大小 batch_size | 256 | 128 到 512 之间按显存调 |
| 经验回放容量 | 1e6 | 经验过旧会引入偏差 |
| 探索噪声标准差 | 0.1~0.3 | 训练后期衰减到 0.05 以下 |
学习率是 DDPG 里最敏感的一组参数。Critic 学习率通常比 Actor 高,因为它要快速逼近 Q 值,而 Actor 只需要顺着价值函数的梯度方向缓慢走。探索噪声可以直接用高斯噪声,也可以用 Ornstein-Uhlenbeck 噪声,后者会让动作在时间上更平滑,但对六轴机械臂仿真来说,高斯噪声加动作限幅已经足够,OU 噪声反而多一个参数要调。
4. 实时仿真方案设计:环境搭建、主循环与性能保障
DDPG 训练收敛只是第一步,把训练好的策略放进实时仿真回路,才会遇到真正的工程问题:控制频率够不够、训练任务会不会阻塞控制循环、状态和动作的尺度有没有做好配准。
4.1 仿真平台选择与机械臂模型导入
常见的仿真平台有三类。PyBullet 加载 URDF 最方便,适合快速验证 DDPG 控制逻辑;MuJoCo 的物理精度更高,适合需要关节力矩和接触力精细建模的任务;CoppeliaSim 带完整的传感器和场景编辑器,适合做视觉引导抓取这类复合任务。实时仿真的方案里我一般用 PyBullet 起步,先在无碰撞场景里跑通算法,再迁到 MuJoCo 做高保真验证。
import pybullet as p p.connect(p.GUI) robot = p.loadURDF("ur5.urdf", useFixedBase=True) joint_ids = [] for i in range(p.getNumJoints(robot)): info = p.getJointInfo(robot, i) if info[2] != p.JOINT_FIXED: joint_ids.append(info[0])这段代码把 URDF 模型加载进 PyBullet,然后筛出所有可动关节的 ID。useFixedBase=True适合六轴机械臂固定在基座上的场景,如果要做移动机械臂,这个参数要改成False。拿到关节 ID 后,状态读取和动作下发都依赖这些 ID,所以要先确认顺序和 URDF 里的 joint name 对得上。
4.2 实时交互主循环
实时仿真主循环和离线训练循环没有本质区别,但每一步都要考虑频率和延迟。
def realtime_loop(agent, env, replay, args): state = env.reset() for step in range(args.max_steps): action = agent.select_action(state, noise=args.noise) next_state, reward, done, info = env.step(action) replay.push(state, action, reward, next_state, done) if len(replay.buffer) >= args.batch_size: batch = replay.sample(args.batch_size) agent.update(batch) state = next_state if done: state = env.reset()主循环的关键在于把动作从 [-1, 1] 映射到仿真器能接受的真实值。比如 PyBullet 里如果你用的是关节位置控制,动作要乘以max_action再加到当前关节角上;如果用的是关节力矩控制,动作要换算成扭矩范围。环境里的物理步长一般固定为 1/240 秒,但 DDPG 的控制频率可以低于物理频率,比如每 4 个物理步执行一次策略推理,剩下 3 步由底层 PID 保持上一时刻目标位置,这样既节省算力,也让轨迹更平滑。
4.3 状态归一化与动作限幅
机械臂状态里同时存在角度、角速度、位置误差,量纲差异很大。不归一化直接喂给网络,值较大的维度会主导梯度,训练初期容易让 Actor 盲目调整个别关节。
def build_state(q, qdot, tcp_pos, goal_pos, prev_action): state = np.concatenate([ q / np.pi, qdot / (2 * np.pi), (tcp_pos - goal_pos) / 1.0, prev_action ]) return state.astype(np.float32)这里角度除以 pi,角速度除以 2pi,位置误差除以 1 米,是为了让所有特征大致落在同一个量级。动作限幅也要做在环境侧,不能只在 Actor 输出端乘max_action。很多仿真崩溃的根因是探索噪声把动作推到限位之外,机械臂模型在仿真器里被硬掰到奇异位形。我在环境里会再加一道np.clip,确保每个关节的角度变化都在安全范围内。
4.4 实时性保障:训练任务与控制任务分离
训练过程中最容易被忽视的问题是:agent.update在控制主线程里执行时,一次反向传播可能让控制循环卡顿几十毫秒。六轴机械臂实时仿真的控制频率通常在 100Hz 以上,卡顿会直接导致仿真失真。
@torch.no_grad() def select_action_fast(self, state): state = torch.as_tensor(state, dtype=torch.float32).unsqueeze(0) action = self.actor(state).squeeze(0).cpu().numpy() return np.clip(action, -1, 1)推理阶段必须用torch.no_grad(),这一步能去掉计算图的保存开销,把推理时间压到毫秒级。训练更新则放到另一个线程,主线程只做状态读取、动作推理、仿真 step,训练线程从经验回放里采样并更新网络参数。两个线程之间需要给 ReplayBuffer 加锁,或者用queue.Queue把新产生的样本异步传给训练线程。硬件上,如果训练用 GPU,可以只把 Critic 更新放到 GPU,Actor 推理留在 CPU,这样能躲开 GPU 推理的启动延迟,整体延迟反而更低。
5. 实验评估与调试:累积奖励、轨迹误差与收敛陷阱
训练收敛不等于轨迹规划成功。DDPG 的累计奖励曲线可能稳步上升,但机械臂的实际轨迹仍然可能抖动、绕路甚至根本到不了目标。要判断算法是否真正有效,必须同时看累计奖励、轨迹误差和成功率三个指标。
5.1 评价指标与计算
def evaluate_trajectory(traj, goal, tolerance=0.02): final_err = np.linalg.norm(traj[-1][:3] - goal) avg_err = np.mean([np.linalg.norm(s[:3] - goal) for s in traj]) success = final_err < tolerance return {"final_err": final_err, "avg_err": avg_err, "success": success}final_err衡量最终到达精度,avg_err衡量整条轨迹的逼近质量,success用容差判定是否完成目标任务。评估时要固定随机种子,关闭探索噪声,让 Actor 以纯确定性策略连续跑多个随机起始位形。如果final_err达标而avg_err很大,说明机械臂是最后时刻冲刺到目标点的,轨迹质量并不好,中间过程可能有明显绕路。
5.2 训练曲线分析与收敛判断
我一般会同时绘制累计奖励、Critic loss、Actor loss 三条曲线。Critic loss 持续下降而累计奖励不涨,通常是奖励尺度太小,Q 值的学习信号被淹没;Actor loss 下降但 Critic loss 反而上升,大概率是 Q 过估计开始显现,这时候先调小两个学习率,再看目标网络的tau是否偏大。
绘制曲线本身并不复杂,关键是看形态。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 4)) plt.plot(reward_history, label="cumulative reward") plt.xlabel("episode") plt.ylabel("reward") plt.legend() plt.savefig("ddpg_reward_curve.png", dpi=120)在训练早期,累计奖励曲线会有较长的平台期,这并不代表策略没学东西,而是 Critic 还在积累足够准确的 Q 值。真正需要警惕的是训练中期出现的“突然崩溃”,也就是奖励曲线已经稳住又开始剧烈回退。这种情况通常是探索噪声衰减过快,策略过早确定性化,失去了对环境变化的适应能力。
5.3 一个低成本验证技巧:冻结策略回放轨迹
训练结束后,先不急着调参,做一个固定策略回放测试。把 Actor 参数保存下来,设置固定随机种子,在仿真里从同一组初始关节角度和目标位置出发,记录每一步状态、动作和末端位置。然后检查这三件事:动作序列有没有高频抖动,末端轨迹有没有突变,目标点附近有没有反复穿越。这个测试成本很低,但能快速区分“算法没收敛”和“收敛到错误策略”这两种情况。
检查动作抖动时,我一般看相邻两步动作差的绝对值之和,如果超过正常步长的五倍,说明奖励函数里的动作变化惩罚项权重太低,或者探索噪声没有在评估阶段关闭。检查轨迹突变时,把记录到的末端轨迹按照时间顺序画出来,突变处通常对应某个关节进入了接近奇异位形的区域,这时应该给关节角加上范围惩罚,而不是继续堆学习率。这个技巧能帮你在一小时内排查掉一半“DDPG 不收敛”的误判,剩下的一半再去查状态归一化和奖励权重。
本文还有配套的精品资源,点击获取