十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

PyTorch实现DDPG:机械臂轨迹规划与动力学建模实战

PyTorch实现DDPG:机械臂轨迹规划与动力学建模实战 简介面向机器人控制、深度强化学习与PyTorch实践者的技术文档聚焦DDPG算法在机械臂轨迹规划与动力学建模中的完整落地路径。内容从机械臂运动学与动力学基础出发覆盖拉格朗日和牛顿-欧拉建模方法再逐步展开DDPG的连续动作空间设计、Actor-Critic网络结构、经验回放、目标网络与动作噪声等关键模块并结合Gazebo/ROS仿真给出环境搭建和集成方案。文档为单份48页PDF压缩包仅2.01MB支持目录章节跳转与大纲快速定位图表公式显示完整特别展示了机械臂动力学模型如何与DDPG结合包括惯性矩阵、科里奥利力、重力项计算以及在仿真中定义状态、动作、奖励并完成训练的流程还能参考目标位置固定/随机等实验分析。适合具备一定Python基础、希望从理论走向机械臂轨迹控制实战的读者已有95人学习下载。1. 机械臂轨迹规划里的PyTorchDDPG为什么先做动力学建模设想一个场景让机械臂末端走一段有障碍的S形轨迹传统做法是样条插值加逆运动学加PD伺服。这套路能跟轨迹但负载、阻尼一变就得重新调参。DDPG这类深度强化学习算法尝试学一个从“当前状态”到“关节力矩”的映射把调参变成训练。但直接让真实机械臂随机探索不现实。所以标准路线是先做动力学建模在仿真里训练再迁到真机。下面按这条路线拆解PyTorch怎么实现DDPG动力学模型怎么和轨迹规划任务拼成一个训练闭环以及训练中的关键参数和踩坑点。适合想用RL做机器人控制、不想只会调包跑demo的工程师。2. 动力学建模从刚体方程到可供DDPG训练的环境2.1 机械臂动力学模型要建到什么程度机械臂的运动由刚体动力学方程描述M(q)q̈ C(q,q̇)q̇ g(q) τ。M是惯性矩阵C包含科氏力和离心力g是重力项τ是关节力矩。对DDPG来说这个方程的作用不是拿来推导而是提供一条可反复探索的状态转移路径给定当前关节角、角速度和力矩仿真环境能给出下一时刻的关节状态。这个“下一步状态”就是强化学习的样本来源。实际工程里动力学建模有两种常见做法。第一种是手动推导并写进代码适合两三个自由度的教学模型和快速原型第二种是直接从URDF文件解析用Pinocchio、RBDL或MuJoCo里的mjModel自动生成M、C、g。对轨迹规划任务我更推荐先手动建一个简化的二连杆把DDPG调通再切换到完整模型。因为RL训练调参的变量已经够多动力学模型再出bug很难判断是策略问题还是环境问题。一个容易被忽略的点训练用的动力学模型不需要和真机完全一致但物理量纲必须一致。力矩限幅、关节角范围、积分步长这些参数会直接影响策略的输入输出分布后续真机部署时策略对它们的敏感度比模型误差更高。下面用二连杆模型作为例子参数见表格。参数数值说明连杆长度 l1, l20.6 m, 0.5 m影响工作空间和末端速度连杆质量 m1, m21.2 kg, 0.9 kg简化为集中质量重力加速度 g9.81 m/s²竖直向下力矩限幅 tau_limit±5 N·m对应真实电机输出上限积分步长 dt0.02 s控制周期 50 Hz2.2 用PyTorch环境包一层RK4积分与状态返回常见做法是把机械臂的step逻辑写成和Gym环境一致的接口reset返回初始观测step接收动作并返回下一观测、奖励、done。这样做的好处是后续换算法TD3、SAC不需要改环境。import numpy as np class TwoLinkArm: def __init__(self, dt0.02, tau_limit5.0): self.dt dt self.tau_limit tau_limit self.l1, self.l2 0.6, 0.5 self.m1, self.m2 1.2, 0.9 self.g 9.81 self.q np.zeros(2) self.qd np.zeros(2) self.t 0.0 self.last_tau np.zeros(2) def reset(self): self.q np.array([0.1, 0.1]) self.qd np.array([0.0, 0.0]) self.t 0.0 return self._get_obs() def _target_q(self, t): 目标轨迹定义在关节空间q1 和 q2 随时间变化 return np.array([0.5 0.3 * np.sin(0.5 * t), 1.0 0.2 * np.cos(0.5 * t)]) def _get_obs(self): q1, q2 self.q tg self._target_q(self.t) # 角度用 sin/cos 编码避免 2π 跳变角速度保持原始值 return np.array([np.sin(q1), np.cos(q1), np.sin(q2), np.cos(q2), self.qd[0], self.qd[1], np.sin(tg[0]), np.cos(tg[0]), np.sin(tg[1]), np.cos(tg[1])]) def _accel(self, q, qd, tau): q1, q2 q l1, l2, m1, m2, g self.l1, self.l2, self.m1, self.m2, self.g M11 (m1 m2) * l1**2 m2 * l2**2 2 * m2 * l1 * l2 * np.cos(q2) M12 m2 * l2**2 m2 * l1 * l2 * np.cos(q2) M22 m2 * l2**2 M np.array([[M11, M12], [M12, M22]]) # 忽略科氏力重力按集中质量近似 G np.array([ (m1 m2) * g * l1 * np.cos(q1) m2 * g * l2 * np.cos(q1 q2), m2 * g * l2 * np.cos(q1 q2) ]) return np.linalg.solve(M, tau - G) def step(self, tau): self.last_tau np.clip(tau, -self.tau_limit, self.tau_limit) def f(s): q s[:2] qd s[2:] ddq self._accel(q, qd, self.last_tau) return np.concatenate([qd, ddq]) s np.concatenate([self.q, self.qd]) k1 f(s) k2 f(s 0.5 * self.dt * k1) k3 f(s 0.5 * self.dt * k2) k4 f(s self.dt * k3) s_new s (self.dt / 6.0) * (k1 2 * k2 2 * k3 k4) self.q s_new[:2] self.qd s_new[2:] self.t self.dt done self.t 5.0 reward self._reward() return self._get_obs(), reward, done, {q: self.q.copy()} def _reward(self): tg self._target_q(self.t) # 关节角误差 速度惩罚 力矩惩罚 return -np.linalg.norm(self.q - tg) - 0.05 * np.sum(self.qd**2) \ - 0.01 * np.sum(self.last_tau**2)step里用四阶Runge-Kutta做数值积分比欧拉法在同一步长下相位误差更小。_accel用np.linalg.solve解线性方程组比求逆矩阵数值更稳。状态观测里把目标角度也做了sin/cos编码和当前关节角保持相同的特征尺度网络不需要额外学习角度表示的变换。2.3 动力学建模里容易埋雷的三个细节积分步长不能太大。dt0.02s对应50Hz控制频率训练时没有真实时间约束可以在每个step里做5次子步积分把目标误差再压小一个量级。末端笛卡尔位姿别放观测。轨迹规划用的是关节空间末端位置可以由正运动学算出放进观测会让策略记住冗余信息训练更慢。奖励函数里不要用self.t或done做显式时间惩罚。给episode设长度上限就好时间维度对“追轨迹”任务天然过拟合。3. DDPG机械臂轨迹规划状态空间、动作空间与算法配置3.1 轨迹规划任务怎么翻译成强化学习定义“轨迹规划”在RL语境下通常指给定一条目标轨迹q_d(t)让机械臂在有限时间内到达并跟随。任务被截断成稀疏的目标序列每个episode只跟一段。状态空间取10维两个关节的sin/cos编码、两个关节角速度、目标位置的双通道编码动作空间是2维连续关节力矩。设计项取值说明状态sin(q1), cos(q1), sin(q2), cos(q2), dq1, dq2, 目标点的sin/cos共10维无时间项动作tau1, tau2连续力矩clip到±5距离系数1.0主任务单位是rad速度惩罚系数0.05抑制关节速度振荡力矩惩罚系数0.01省能耗最小优先级为什么要用关节空间而不是笛卡尔空间DDPG输出的是力矩机械臂的控制指令最终也是关节力矩。把状态设成关节角和角速度网络不需要学逆运动学学到的策略更容易泛化到不同末端位置。为什么状态里只放目标位置而不用整条轨迹因为跟踪轨迹可以当作一系列“到达目标点”的子任务。用整条轨迹会让策略依赖“时间”而时间是仿真和真机差距最大的量。奖励设计按优先级排距离项让关节角逼近目标速度项防震荡力矩项省能耗。三个系数要有数量级差别一般距离项最大、速度次之、力矩最小。初始化时可以用随机策略跑500步看三项各自贡献的reward范围把系数归一化到目标数量级。3.2 为什么选DDPG而不是PPO四足机器人控制算法里常见PPO做步态学习但机械臂这类低自由度连续力矩控制我更倾向DDPG。原因是机械臂的力矩控制是连续动作空间任务每步只增加很小的延迟off-policy算法能把历史经验反复利用样本效率更高。PPO在仿真里效果也不错但通常需要并行几十个环境才能发挥优势本地单机跑DDPG的时间成本更低。DDPG的缺点也明确对超参数敏感、Q值会高估。TD3针对后者做了改进双Q网络取min、延迟更新策略、目标策略平滑。如果训练中Q值在后期一路走高而episode reward不动直接换TD3就行。3.3 确定性策略梯度与目标网络DDPG是确定性策略梯度算法核心是同时训练两个网络策略网络µ(s)输出确定性动作Q网络Q(s,a)估计当前状态动作对的值函数。训练目标拆成两部分。Q网络回归目标y r γQ′(s′, µ′(s′))这个target由目标网络算本身不参与梯度回传。策略网络最大化Q值∇θJ ≈ E[∇a Q(s,a)|aµ(s) · ∇θ µ(s)]。目标网络参数用soft updateθ′ ← τθ (1-τ)θ′。这里的τ取0.005左右太小更新慢太大训练不稳。4. PyTorch实现从环境准备到DDPG训练主循环4.1 搭建PyTorch开发环境先创建环境并安装PyTorch。用Anaconda创建独立环境的好处是不会污染系统Python且PyTorch版本切换方便。conda create -n robot_rl python3.10 -y conda activate robot_rl conda install pytorch torchvision pytorch-cuda12.1 -c pytorch -c nvidia版本选择参考PyTorch官网安装前确认CUDA版本。没有GPU也能跑这个任务CPU训练二连杆DDPG单episode约几秒。纯CPU机器把安装命令换成conda install pytorch cpuonly -c pytorch。Linux发行版较新时注意GPU相关依赖与系统glibc的兼容性Windows下偶尔会遇到c10.dll的DLL初始化失败常见原因是环境里混入了不匹配的CUDA运行时用conda重装一套干净环境比手动拷贝DLL可靠。4.2 Actor与Critic网络的基础结构动作范围由tanh限制到[-1,1]再乘上力矩限幅。from torch import nn class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh(), ) self.max_action max_action def forward(self, x): return self.net(x) * self.max_action class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1), ) def forward(self, state, action): return self.net(torch.cat([state, action], dim-1))这里有个细节Critic输入是状态和动作拼接不是仅状态。DDPG的Q必须有动作维度参与否则学不到“动作好坏”。层宽256是经验值二连杆任务64也能收敛但用256的话对后续换成六轴机械臂更有参考性。4.3 训练主循环import torch import numpy as np from collections import deque state_dim 10 action_dim 2 max_action 5.0 gamma 0.99 tau 0.005 actor_lr 3e-4 critic_lr 3e-4 batch_size 256 buffer_size 200_000 buffer deque(maxlenbuffer_size) def add_sample(s, a, r, s2, d): buffer.append((s, a, r, s2, d)) def sample_batch(): indices np.random.randint(len(buffer), sizebatch_size) batch [buffer[i] for i in indices] s, a, r, s2, d map( lambda x: torch.tensor(np.array(x), dtypetorch.float32), zip(*batch) ) return s, a, r.unsqueeze(1), s2, d.unsqueeze(1) actor Actor(state_dim, action_dim, max_action) critic Critic(state_dim, action_dim) target_actor Actor(state_dim, action_dim, max_action) target_critic Critic(state_dim, action_dim) target_actor.load_state_dict(actor.state_dict()) target_critic.load_state_dict(critic.state_dict()) actor_opt torch.optim.Adam(actor.parameters(), lractor_lr) critic_opt torch.optim.Adam(critic.parameters(), lrcritic_lr) mse nn.MSELoss() env TwoLinkArm() episodes 800 noise_std 0.2 noise_decay 0.9995 for ep in range(episodes): state env.reset() ep_return 0.0 done False while not done: s_tensor torch.tensor(state, dtypetorch.float32).unsqueeze(0) action actor(s_tensor).detach().numpy()[0] action noise_std * np.random.randn(action_dim) next_state, reward, done, _ env.step(action) add_sample(state, action, reward, next_state, done) state next_state ep_return reward if len(buffer) batch_size: s, a, r, s2, d sample_batch() with torch.no_grad(): target_q r gamma * (1 - d) * target_critic(s2, target_actor(s2)) q critic(s, a) critic_loss mse(q, target_q) critic_opt.zero_grad() critic_loss.backward() critic_opt.step() actor_loss -critic(s, actor(s)).mean() actor_opt.zero_grad() actor_loss.backward() actor_opt.step() for tp, p in zip(target_actor.parameters(), actor.parameters()): tp.data.copy_(tau * p.data (1 - tau) * tp.data) for tp, p in zip(target_critic.parameters(), critic.parameters()): tp.data.copy_(tau * p.data (1 - tau) * tp.data) if ep % 50 0: print(fepisode {ep}, return {ep_return:.2f})每一步都用当前batch做一次梯度更新batch_size256。actor_loss用-critic(s, actor(s)).mean()是因为PyTorch优化器只能最小化取负号把Q最大化变成最小化。目标网络每步soft update省去周期硬拷贝。关键超参数说明gamma0.99步长0.02s半衰期约70步覆盖整个episode窗口。tau0.005目标网络更新速度太大会出现训练振荡。noise_std0.2探索噪声初始幅度训练前500个episode可以固定之后按指数衰减。衰减太快策略太早变deterministic容易收敛到局部最优。buffer_size200000二连杆任务内存占用不大保留更长历史有利于样本多样性。如果训练到后期出现Q值爆炸把critic_lr降到1e-4同时把tau降到0.001通常能压住。5. 训练后的验证轨迹跟踪误差与动力学仿真细节5.1 从episode return到轨迹质量训练早期return曲线能反映收敛趋势但它掩盖了任务细节。比如在某些初始状态下策略可能绕远路到达目标return一样但轨迹不好。要验证策略需要用仿真环境回放在固定初始状态下让策略跑一条episode记录每一步的关节角与目标轨迹对比。state env.reset() traj [] goals [] while True: s torch.tensor(state, dtypetorch.float32).unsqueeze(0) a actor(s).detach().numpy()[0] state, _, done, info env.step(a) goals.append(env._target_q(env.t)) traj.append(info[q].copy()) if done: break errs np.linalg.norm(np.array(traj) - np.array(goals), axis1) print(fmean error: {errs.mean():.4f}, max error: {errs.max():.4f})5.2 动力学模型本身怎么验证仿真与真机的差距不会在episode reward里体现。常见做法是用正弦扫频激励给每个关节一个变频正弦力矩记录关节角响应再在仿真里用同样输入跑一遍比较幅频和相频。会看重合度在控制频带内是否足够而不是全频段精确。这个check前置到训练之前能省掉大量无用训练时间。另外两个补充技巧子步积分把dt分为5个子步控制周期不变但减少数值积分误差。动作重复真实机械臂的控制周期比仿真长可以对同一动作重复执行2-3次后再采样让策略适应低控制频率。5.3 迁移到真机前要检查的动力学一致性把训练好的策略部署到真机前检查这四项力矩限幅是否一致、关节角范围是否一致、动作是否是关节力矩而不是关节速度、传感器延迟是否在reward设计里被建模。很多“仿真能跑真机不行”的案例最后都落在传感器延迟和摩擦建模上而不是DDPG本身的收敛问题。本文还有配套的精品资源点击获取
返回列表