☰
基于PyTorch的DDPG机械臂轨迹规划实战:从训练到ONNX部署
2026/10/12 1:01:02 网站建设 项目流程

简介:这是一份面向机器人控制与强化学习研究者的技术文档,系统讲解如何利用 DDPG 算法在 PyTorch 框架下完成工业机械臂轨迹规划。内容从传统轨迹规划方法的局限切入,依次介绍强化学习基础、DDPG 核心组件与数学原理、PyTorch 环境搭建,并重点展开模型设计、代码实现、实验评估与鲁棒性分析,适合有一定深度学习基础、希望将强化学习落地到实际控制任务的读者。资源整体仅含 1 个 PDF 文件,大小 2.07MB,共 29 页,支持目录跳转与大纲定位,结构完整、图表清晰,可作为独立查阅的学习材料。已有 86 人学习下载,说明该主题受到一定关注。读者可从中获得状态空间、动作空间与奖励函数的设计思路,策略网络和价值网络的结构选取、目标网络与经验回放机制的具体实现,以及训练调参和结果对比的参考方案,有助于快速搭建同类强化学习控制项目。

1. DDPG算法为什么适合工业机械臂轨迹规划,而不是只停留在仿真层

DDPG算法这些年反复出现在机器人运动控制相关讨论里,说它是连续动作空间强化学习的代表性方法并不夸张。工业机械臂的轨迹规划,本质是一个高维连续状态下的序列决策问题:读入当前关节角、角速度、末端位姿,输出下一时刻的动作指令。传统插补加PID在复杂工况、动态避障和柔顺控制需求面前,要么标定周期太长,要么只能在特定工作点附近成立。DDPG把深度Q学习和策略梯度的思路合到一起,用Actor-Critic结构直接拟合“状态到动作”的映射,省去了大量手工调参。这篇文章沿着PyTorch实现的路子,从环境准备、网络设计、训练调参写到ONNX导出,适合已经有机器人学基础、想把强化学习真正落到机械臂轨迹规划上的工程师。

2. 准备PyTorch环境与轨迹数据:从CUDA适配到机械臂状态归一化

2.1 DDPG对环境和数据的需求:为什么不能照搬深度学习老套路

DDPG是off-policy的确定性策略梯度算法,训练依赖四部分:经验回放buffer、在线Actor-Critic、目标网络、探索噪声。跟有监督学习最大的不同在于,它不是拿一份标注好的数据集反复迭代,而是边探索边收集经验,边更新参数边改策略。工业机械臂轨迹规划里,如果直接用真实机械臂做这个循环,一次碰撞就可能毁掉夹具甚至造成安全事故。所以常规做法是先把训练放到仿真环境里,等策略稳定之后再迁移到实体。这样既安全又能靠并行环境把样本量拉起来。

状态空间和动作空间的定义对DDPG效果影响非常直接。我一般把状态拆成三块:当前关节角度、当前关节角速度、目标末端位姿或者目标关节角度。对六轴机械臂来说,关节角度和角速度各占六维,目标位姿占六维,总共十八维一个状态向量。动作空间有两种常见选择:一种是输出力矩直接控制电机,一种是输出关节角度增量再由底层位置环执行。DDPG动作层用了tanh,输出天然在[-1,1],两种方式都可以,但数据归一化是前提,所有状态量要映射到同一量级,否则Critic网络很容易训练出一个只对某个维度敏感的畸形函数。

注意:状态归一化要按机械臂实际物理限位做,不要把离群点硬裁掉。数据采集时如果某个关节超限,宁可丢弃整条轨迹也不要把那帧单独删掉。时间上断裂的经验对时序类策略影响不大,但对DDPG的Q值估计会带来截断偏差。

2.2 搭建PyTorch环境:conda、CUDA版本匹配与GPU验证

DDPG训练一轮需要的样本往往在十万到百万量级,纯CPU不是不能跑,但效率会低到让人怀疑人生。常见做法是先用conda隔离环境,再从PyTorch官网选对CUDA版本。这里最容易卡住的一步是:nvidia-smi显示的CUDA版本是驱动支持的上限,不是PyTorch当前运行态的版本。PyTorch安装时只要驱动不低于官方要求就能用。在Ubuntu或者WSL2里我已经习惯先跑下面两句确认硬件状态:

# 查看显卡驱动支持的CUDA版本,注意这里显示的是上限 nvidia-smi | grep "CUDA Version" # 确认Python与pip版本,避免基础环境太老导致后面装包翻车 python --version && pip --version

驱动信息确认之后,创建独立的conda环境,按PyTorch官网安装页给出的命令安装。官网会根据显卡驱动版本推荐对应CUDA的wheel包,照着选即可。

# 创建独立conda环境,避免依赖污染系统Python conda create -n mech_rl python=3.10 -y conda activate mech_rl # 在PyTorch官网选择与CUDA版本匹配的安装命令执行 # 安装完成后不要急着跑训练,先做下面这一步验证 pip install torch torchvision

验证GPU是否真的可用,这一步经常出现“环境装上但CUDA不可用”的情况:

import torch print("PyTorch版本:", torch.__version__) print("CUDA是否可用:", torch.cuda.is_available()) if torch.cuda.is_available(): print("GPU名称:", torch.cuda.get_device_name(0)) # 开启cuDNN benchmark,小卡训练DDPG时能省一点时间 torch.backends.cudnn.benchmark = True

参数说明:torch.cuda.is_available()返回False时,先别急着重装,回去看nvidia-smi驱动是否正常,再检查conda环境里有没有装成CPU版PyTorch。这类版本适配问题占环境故障的一半以上,尤其是CUDA和PyTorch之间的对应关系,一个对不上,后面训练代码跑起来全是玄学报错。

2.3 轨迹数据采集与归一化:关节空间和笛卡尔空间怎么选

DDPG虽然能在线收集数据,但工业机械臂轨迹规划的常见做法是先规划一批示教轨迹或人工轨迹做预热。数据来源一般是两类:一类是离线仿真生成的起点到目标点最优解轨迹,另一类是示教器录制的真实关节轨迹。采集时最好把关节角度、角速度、关节力矩和末端位姿一并记录,后面设计奖励函数时不用回头补数据。

轨迹在哪个空间表示也值得想清楚。笛卡尔空间对使用者更直观,但机械臂逆解存在多解和奇异点问题;关节空间没有这些问题,代价是轨迹看起来“不够直”。我一般先在关节空间做DDPG训练,末端位姿只出现在奖励函数里。这样动作维度固定为关节数,策略输出不会因为逆解跳变产生毛刺。

归一化是DDPG实现里最容易糊弄、也最容易出问题的地方。下面是常用的归一化函数:

import numpy as np def normalize_trajectory(data, low, high): """把物理量映射到[-1, 1],与Actor输出端的tanh范围对齐""" return 2.0 * (data - low) / (high - low) - 1.0 def denormalize_trajectory(norm_data, low, high): """推理时把网络输出还原成实际的关节角度或力矩""" return (norm_data + 1.0) * (high - low) / 2.0 + low # 示例:六轴关节角度,单位rad,各轴行程范围不一样 joint_low = np.array([-2.97, -2.09, -2.97, -2.09, -2.97, -2.09]) joint_high = np.array([2.97, 2.09, 2.97, 2.09, 2.97, 2.09]) normalized_angle = normalize_trajectory(joint_angles, joint_low, joint_high)

逻辑说明:min-max映射到[-1,1]有两个理由。第一,DDPG的Actor输出经过tanh之后也落在[-1,1],动作和状态尺度对齐,梯度回传时不会因为某个维度数值特别大而主导整个网络。第二,奖励里经常要算距离,归一化后各维度的距离权重天然一致,不用再给关节角度乘经验系数。denormalize_trajectory在实机推理时再用,注意归一化的low和high必须和训练时完全一致,换一台行程范围不同的机械臂,这组参数就要重算。

3. 用PyTorch实现DDPG核心网络:Actor-Critic、经验回放与探索噪声

3.1 DDPG算法原理拆解:为什么确定性策略适合机械臂动作输出

DDPG的全称是Deep Deterministic Policy Gradient,核心是确定性策略梯度定理。主流策略梯度方法输出的是动作的概率分布,DDPG直接输出确定性动作,再用噪声做探索。工业机械臂轨迹规划恰好需要这种“给定状态给唯一动作”的输出形式:控制器不希望在相同状态下随机动作,而是希望每个状态都有确定、可复现的控制量,这对安全审核和质量追溯也很重要。

网络结构上DDPG有四个网络:在线Actor、在线Critic、目标Actor、目标Critic。在线Actor把状态映射成动作,在线Critic把状态和动作拼接后输出Q值。目标Actor和目标Critic的权重不是直接复制在线网络,而是通过软更新缓慢逼近,目的是削弱自举带来的Q值高估。这个结构几乎决定了DDPG的稳定性,任何一边的更新节奏不对,整个训练就可能在某个epoch突然翻车。

3.2 Actor和Critic网络的PyTorch定义:网络尺寸与关键细节

DDPG原论文的网络是三层全连接,机械臂状态空间通常不超过几十维,不需要上CNN这类结构。下面是带动作边界处理的Actor定义:

import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): """策略网络:输入状态张量,输出归一化动作,范围[-1, 1]""" def __init__(self, state_dim, action_dim, max_action=1.0): super().__init__() self.fc1 = nn.Linear(state_dim, 400) self.fc2 = nn.Linear(400, 300) self.fc3 = nn.Linear(300, action_dim) self.max_action = max_action def forward(self, state): x = F.relu(self.fc1(state)) x = F.relu(self.fc2(x)) # tanh把输出压到对称区间,避免动作越界 return self.max_action * torch.tanh(self.fc3(x))

Critic网络和Actor不同,输入不仅有状态还有动作。初学者最容易拿Actor改一版就当Critic用,结果Critic根本没有动作输入,Q值直接少了一个变量:

class Critic(nn.Module): """价值网络:输入状态和动作,输出估计的Q值""" def __init__(self, state_dim, action_dim): super().__init__() self.fc1 = nn.Linear(state_dim + action_dim, 400) self.fc2 = nn.Linear(400, 300) self.fc3 = nn.Linear(300, 1) def forward(self, state, action): # 在第一个全连接层之前把状态和动作拼接起来 x = torch.cat([state, action], dim=1) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) return self.fc3(x)

逻辑说明:Actor的max_action这里设1.0,因为动作已经归一化,真实关节限位由仿真环境或实机接口再映射。Critic不用tanh输出,因为Q值没有天然边界。两个网络隐藏层采用400/300是原论文经典配置,机械臂场景也可以适当缩减到256/256,收敛会快一点,但拟合能力略降。如果任务包含避障这类复杂非线性约束,保留400/300更稳。

3.3 经验回放与OU噪声:稳定训练的两个关键组件

DDPG是off-policy算法,这决定了它必须配一个足够大的经验回放buffer。机械臂轨迹是逐步推进的,同一时刻的状态和下一个状态之间相关性极强,如果不做随机采样而按时间顺序更新,神经网络会把上一时刻的噪声当成趋势,Q值估计越学越偏。经验回放把样本相关性问题解掉,代价是训练前期大量样本来自随机探索,有效信息密度低,所以buffer容量要够大,常规在50万到100万条之间。

from collections import deque import random import numpy as np import torch class ReplayBuffer: """经验回放:存五元组,关键参数是容量和批量大小""" def __init__(self, capacity=1_000_000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return ( torch.FloatTensor(np.array(states)), torch.FloatTensor(np.array(actions)), torch.FloatTensor(np.array(rewards)).unsqueeze(1), torch.FloatTensor(np.array(next_states)), torch.FloatTensor(np.array(dones)).unsqueeze(1), ) def __len__(self): return len(self.buffer)

参数说明:capacity设为1e6是常见做法,太大占内存,太小会让旧经验过早被踢出,策略换代后旧样本和当前策略的分布差异拉大。dones维度加unsqueeze(1)是为了和reward张量对齐,计算目标Q值时可以直接做加权。

探索噪声方面,DDPG原论文用的是Ornstein-Uhlenbeck过程。相比高斯噪声,OU噪声的特点是带有回归均值的趋势,动作曲线更平滑,不容易出现一帧动作突然跳到限位边缘的情况。这一点在工业机械臂上很重要,控制指令不平滑,轻则抖动发热,重则触发急停。

class OUNoise: """OU噪声:为确定性策略提供平滑的探索扰动""" def __init__(self, action_dim, mu=0.0, theta=0.15, sigma=0.2): self.mu = mu self.theta = theta self.sigma = sigma self.state = np.ones(action_dim) * mu self.reset() def reset(self): self.state = self.state * 0.0 def noise(self): # dX = θ(μ - X)dt + σdW,W是标准布朗运动 dx = self.theta * (self.mu - self.state) + self.sigma * np.random.randn(len(self.state)) self.state += dx return self.state

参数说明:theta控制噪声回归均值的速度,越大动作越平稳但探索性越差,我一般在0.1到0.2之间调。sigma是噪声强度,训练初期可以设0.2到0.3,中期衰减到0.05,后期关掉或只留0.01,否则收敛后的策略会被持续扰动。OU噪声的state要记住每一步更新,维度与动作维度一致。reset要在每个episode开始时调用,否则上一轮结束时的噪声状态会带进新一轮。

核心更新逻辑如下:

def ddpg_update(actor, critic, actor_target, critic_target, actor_optimizer, critic_optimizer, replay_buffer, batch_size=64, gamma=0.99, tau=0.005): states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size) # 目标Q值完全由目标网络产生,不参与当前网络梯度 with torch.no_grad(): next_actions = actor_target(next_states) target_q = critic_target(next_states, next_actions) target_q = rewards + gamma * (1 - dones) * target_q # Critic的损失是TD误差的均方差 current_q = critic(states, actions) critic_loss = F.mse_loss(current_q, target_q) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() # Actor的损失是Q值的负均值,让动作朝更高Q的方向更新 actor_optimizer.zero_grad() actor_loss = -critic(states, actor(states)).mean() actor_loss.backward() actor_optimizer.step() # 软更新:每次把目标网络朝在线网络挪一点点 with torch.no_grad(): for param, target_param in zip(critic.parameters(), critic_target.parameters()): target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data) for param, target_param in zip(actor.parameters(), actor_target.parameters()): target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)

逻辑说明:target_q要用no_grad计算,回传时不能把梯度穿进目标网络。gamma是折扣因子,0.99适合机械臂这类每步都有密集奖励的任务。actor_loss带负号是因为我们要最大化Q,而优化器默认最小化loss。软更新的tau取0.005时,目标网络约两百步后才能接近在线网络,这个缓慢追赶的过程是防止Q值高估的核心。

4. 机械臂轨迹规划DDPG训练的避坑指南:参数设置与奖励函数血泪经验

4.1 DDPG训练的关键超参数:学习率、折扣因子、软更新系数

DDPG对超参数敏感,这是和传统控制算法差别最大的一点。下表是我在机械臂轨迹规划任务里常用的起点值,在线调参时可以通过观察曲线从这里出发:

超参数常用取值作用边界判断
Actor学习率1e-4到3e-4策略更新步长动作曲线高频抖动就减半
Critic学习率1e-3价值网络更新步长Q值跳变明显就降到1e-4
gamma0.99折扣因子任务越短程越可以降到0.95
tau0.005软更新速度训练震荡就降到0.001
batch_size64或256每次采样数量显存够就大一点,更稳定
buffer容量1e6回放池大小简单任务可缩到1e5

学习率不对称设置是有讲究的。Critic网络承担的任务更重,学习率略高有助于Q值先稳定;但Critic更新太快又容易引入过高估计,所以搭配梯度裁剪更稳妥。Actor的更新方向来自Critic对当前策略动作的梯度,这意味着Critic还没收敛时Actor会被反复拉扯。经验性判断是:训练刚开始Critic loss从高位往下降的过程中,先别急着评估Actor效果,等Q值曲线进入平台期再说。

4.2 五个高频翻车场景与排查方法

场景一:训练中途loss变成NaN。现象是训练到几千条经验之后,Critic和Actor的loss突然出现NaN,之后再也恢复不回来。原因通常是Q值估计爆炸,回传的梯度带着无穷大进入Critic的全连接层,紧接着Actor也拿到无效梯度。解决方法是把学习率减半,并在Critic反向传播后加梯度裁剪:

# 在critic_optimizer.step()之前加这一行,限制梯度范数 torch.nn.utils.clip_grad_norm_(critic.parameters(), max_norm=1.0)

同时检查reward里有没有log这类容易产生无穷值的表达式。如果裁剪之后还是炸,把经验回放里reward明显离群的样本打印出来看,往往能发现状态异常。

场景二:动作输出长期停在正负一边界。现象是训练了几万步,Actor输出大多数时候接近负一或正一,仿真里机械臂一直往关节限位方向顶。原因有两个:reward的尺度过大或过小,导致Q值对动作方向极度敏感;另一个常见原因是状态归一化范围比实际数据大太多,tanh在饱和区梯度接近零,策略很难逃出来。解决办法是把奖励值除以一个系数让数值落在[-1,1]区间,同时检查归一化边界是否和机械臂真实限位一致。

场景三:训练曲线漂亮但成功率很低。现象是loss一直在降,轨迹规划的到达目标成功率却只有百分之三十。原因是经验回放里大多数样本来自容易成功的起点,对困难起点没有有效经验;或者成功率是在固定初始位置下测的,一改成随机初始位置就露馅。解决方法是做课程学习:

# 训练早期固定机械臂起点,成功率超过80%再开启随机初始位置 if success_rate < 0.8: env.set_initial_joint(np.array([0.0, -0.5, 0.7, 0.0, 0.5, 0.0])) else: env.set_random_initial_joint()

很多开源强化学习框架里都有类似callback机制,自己写也不难,核心是别让策略在早期就被困难样本带偏。

场景四:仿真效果好、实机效果差。现象是仿真环境里轨迹平滑、命中率高,一上真实机械臂就抖动甚至追不上目标。原因是仿真没有建模传动滞后、摩擦和负载变化,DDPG策略依赖了仿真环境里不存在的完美状态观测。解决办法是做域随机化,在每个episode开始前随机设置负载质量、摩擦系数和控制延迟:

# 仿真环境参数在每个episode开始时扰动,模拟实机差异 env.set_friction(np.random.uniform(0.8, 1.4) * nominal_friction) env.set_load_mass(np.random.uniform(0.0, 2.0)) # 单位kg env.set_control_delay(np.random.randint(1, 3)) # 单位: 控制周期

场景五:训练一段时间后性能衰退。现象是训练中期效果不错,继续训练成功率反而下滑,Critic loss跟着回升。原因是经验回放里的历史样本太旧,策略换代后旧样本不再是当前策略的经验;另一个诱因是tau设得太大,目标网络跟着在线网络一起震荡。解决办法是把tau固定在0.001到0.005之间,如果还不行,缩小buffer容量,或者改成最近十万条经验优先采样。

4.3 奖励函数设计:稀疏奖励与连续奖励怎么取舍

轨迹规划的奖励函数直接决定收敛速度。最朴素的版本是到达目标点给一个正奖励,其他情况给零,问题是样本利用率极低,机械臂在探索阶段很难碰巧到达目标。稍微好一点的做法是连续距离奖励加稀疏到达奖励:

# 奖励 = 末端距离惩罚 + 动作平滑惩罚 + 到达奖励 distance = torch.norm(current_end_effector - target_position) smooth = torch.norm(action - previous_action) reward = -0.5 * distance - 0.05 * smooth + (10.0 if distance < 0.05 else 0.0)

这是工业机械臂轨迹规划里最常用的奖励形式之一。距离惩罚太弱,机械臂会在远处来回徘徊;太强,又会在接近目标时过冲。实际经验是给距离设置一个截断上限,别让远处的大误差把奖励尺度拉爆。动作平滑项不是必选项,但加上之后轨迹更接近示教风格,对后续实机落地友好很多。

另外,奖励函数里的distance可以用末端位姿误差,也可以用关节角误差。末端位姿误差对用户更直观,但逆解跳变会让reward曲线不光滑;关节角误差没有这个问题。建议在关节空间控制下使用关节角误差做距离项,末端位姿只作为终止条件判断。

5. 从PyTorch模型到真实机械臂:ONNX导出与轨迹复现验证技巧

5.1 PyTorch模型转ONNX:训练好的Actor导出与数值核验

训练结束后,实机上只需要Actor,不需要Critic。Q值只在训练阶段有用,部署时一个策略前向计算就够。ONNX导出这一步常见坑是batch维度不灵活,导致换部署框架时模型只能按固定batch跑。下面的导出方式把batch维度设成动态:

# 导出actor:输入是归一化状态,输出是归一化动作 actor.eval() dummy_input = torch.randn(1, state_dim) torch.onnx.export( actor, dummy_input, "arm_actor.onnx", input_names=["obs"], output_names=["action"], dynamic_axes={"obs": {0: "batch"}, "action": {0: "batch"}}, )

参数说明:dynamic_axes把batch维度声明为动态,后续通过其他推理框架加载时不必固定为1。导出后一定要做数值核验,最忌讳“能导出来就算成功”。用onnxruntime加载模型和PyTorch输出做对比:

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("arm_actor.onnx") probe = np.random.randn(1, state_dim).astype(np.float32) onnx_out = sess.run(["action"], {"obs": probe})[0] with torch.no_grad(): pt_out = actor(torch.FloatTensor(probe)).numpy() print("ONNX与PyTorch输出最大误差:", np.abs(onnx_out - pt_out).max())

两次输出的最大误差应该在1e-5级别。如果误差到了1e-3以上,检查Actor是否在eval模式,以及网络里有没有BatchNorm层。DDPG的Actor通常不装BatchNorm,但改结构时很容易顺手加上,导出时这是最常见的精度杀手。

5.2 轨迹复现验证的落地做法

模型导出只是第一步,真正有价值的是轨迹复现验证。我的习惯是从测试集里抽10条轨迹,把ONNX推理出来的关节角序列发给仿真机械臂回放,逐帧检查是否触发奇异、碰撞或关节限位。回放没问题再上实机,而且实机先以低速跟踪,速度倍率从0.2倍开始,逐级加到1倍。任何一次急停都要记录当时的输入状态和输出动作,回到仿真里用同样的状态复现,看看是策略本身的问题还是部署链路的问题。

实机上真正的踩坑往往在控制周期。DDPG推理一次的时间要远小于机械臂控制周期,一般控制周期是1ms到8ms,ONNX模型在CPU上推理通常几毫秒内能完成,但如果用了大网络或者推理框架没有开启线程优化,延迟就会成为抖动来源。上实机前先测推理耗时,保证策略输出不会拖垮控制环。

这些年我落地强化学习轨迹规划项目的一个习惯是:训练时把每个episode的输入状态、输出动作、奖励值全部落盘。这样训练曲线异常时可以回放,实机异常时可以对比。没有这些日志,所谓“玄学抖动”就真的成了黑匣子,有了日志,大部分问题都能定位到具体某一步。希望帮到准备把DDPG往工业机械臂上落地的你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询