简介:这份PDF文档围绕深度确定性策略梯度算法在工业机械臂轨迹规划中的应用,使用PyTorch实现,面向想学习强化学习与机器人控制的开发者和研究者。文档共二十九页,先介绍工业机械臂结构、运动学基础与轨迹规划目标,说明传统几何模型、运动学模型和动力学模型在复杂环境、多任务和实时性要求下的局限,再讲解DDPG核心原理,包括策略网络、价值网络、经验回放和目标网络,并给出PyTorch环境搭建、状态空间与动作空间定义、奖励函数设计等内容。代码实现按模块拆分为环境类、网络模型、经验回放缓冲区、噪声生成、训练器、评估器与主程序,便于对照调试;最后通过奖励曲线、轨迹可视化、目标到达率及与传统方法对比,验证算法在机械臂轨迹规划中的有效性和鲁棒性。资源为单个PDF文件,大小二点零七兆字节,支持目录章节跳转与左侧大纲定位,查阅方便。目前已有八十六人学习下载,适合需要完整走通从理论到代码落地的学习者。
1. 为什么是DDPG:机械臂轨迹规划的连续动作空间难题
做机械臂轨迹规划,传统路径是先建运动学模型,再算逆解、查碰撞;这套依赖精确模型的打法,在动态环境里经常被打破。DDPG(深度确定性策略梯度)走的是另一条路:不建模型,让Actor网络直接输出关节速度,Critic网络打分,通过与环境大量交互自主学习最优轨迹,这是机器人运动控制里被反复验证的新范式。这份资源把这条路线用PyTorch完整落地——从强化学习基础、算法原理、环境搭建到Actor-Critic实现和实验评估全流程覆盖。对机器人方向的硕博生、刚转向强化学习的机械臂工程师来说,它可以少走不少弯路;如果你正被「DDPG在PyTorch里怎么写」卡住,这份资料能直接用来复现和做实验对照。
2. 把DDPG拆开看:从公式到四个组件的落地逻辑
2.1 连续动作空间:为什么查表式Q-learning在机械臂上走不通
强化学习里最经典的Q-learning面向离散动作。机械臂的控制量是关节速度或关节角度增量,在连续区间内取值,理论上动作数量无限。要把Q-learning硬套上来只能离散化,把每个关节的角度量化成几十档,六自由度机械臂的动作组合瞬间膨胀到天文数字,而且量化本身的精度损失对轨迹规划是致命的——末端误差几个毫米就是废品。
策略梯度类算法(REINFORCE、A2C)可以处理连续动作,但它们在更新策略时依赖蒙特卡洛采样估计回报,方差大、样本效率低。机械臂仿真的每一步都涉及动力学计算,样本效率低意味着训练时间被拉长到无法接受。DDPG选择了另一个方向:用一个确定性网络直接输出动作,再用另一个网络评价这个动作有多好,把策略梯度里的随机积分替换成确定性的链式求导,方差问题被绕开了,样本效率也上了一个台阶。
这个差异也解释了为什么你在做机械臂轨迹规划、而不是玩Atari游戏时,会优先考虑DDPG而不是DQN:DQN打游戏可以,但要输出六维连续关节控制量,它连动作都表示不了。
2.2 确定性策略梯度:策略网络和价值网络各自学什么
DDPG里有两个网络。Actor网络(策略网络)输入状态 $s$,输出动作 $a=\mu(s|\theta^\mu)$,它的目标是找到一个策略,让Critic给这个动作打高分。Critic网络(价值网络)输入状态 $s$ 和动作 $a$,输出 $Q(s,a|\theta^Q)$,它的目标是预测「在状态 $s$ 下执行动作 $a$,后续能拿多少累积奖励」。
训练Critic用的是回归思路:把目标价值 $y = r + \gamma \cdot Q'(s', \mu'(s'|\theta^{\mu'})|\theta^{Q'})$ 当作标签,让 $Q(s,a)$ 的输出向 $y$ 靠近,损失函数是均方误差。这里最关键的是,$y$ 的计算必须用目标网络(带上标撇号的 $Q'$ 和 $\mu'$),不能用在线网络自己算自己。如果用在线网络自举,目标值会跟着参数一起漂移,训练必然发散——这是很多初跑DDPG的人遇到的第一个玄学问题,后面避坑章节我会展开讲。
训练Actor用的是梯度上升:对每个采样的状态 $s$,计算 $Q(s,\mu(s))$ 对动作 $a$ 的梯度,再乘上 $\mu(s)$ 对 $\theta^\mu$ 的梯度,两个梯度链式相乘,方向就是让动作往评价更高的方向挪。这个乘积用一句话说就是——Critic告诉Actor「往哪个方向改动作分数会变高」,Actor照着做。
2.3 经验回放与目标网络:稳定性的两个支柱
经验回放解决的是样本相关性问题。连续交互产生的状态序列高度相关,直接用这批数据训练,等价于在很窄的分布里反复拟合,Critic会学到一堆局部假象。把 $(s,a,r,s')$ 存进缓冲区,训练时随机采样一批,就把相关性打破了。缓冲区容量常见做法是5万到10万条,太小随机性不够,太大旧策略数据占比高,学习变慢。
目标网络解决的是自举发散问题。软更新的做法是:每个训练步把在线网络参数往目标网络方向挪一小步:
tau = 0.005 for target_param, param in zip(target_net.parameters(), net.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data)这里的tau是软更新系数,取值0.005意味着目标网络在一次更新里只往在线网络移动0.5%,参数变化非常平缓,给训练过程提供了一个几乎静止的靶子。tau设太大会震荡,设太小收敛慢,0.001到0.005是常见区间。注意这里用的是copy_原地赋值,不是=直接赋值,直接赋值会破坏PyTorch的自动求导图关系,训练时梯度会算到目标网络上去。
2.4 DDPG、PPO、TD3怎么选
| 算法 | 采样方式 | 适用场景 | 主要缺点 |
|---|---|---|---|
| DDPG | off-policy(经验回放) | 连续动作、仿真样本获取成本高 | 超参敏感,容易出现Q值过估计 |
| PPO | on-policy | 需要稳定策略更新、环境便宜 | 样本效率比off-policy低 |
| TD3 | off-policy | DDPG的改进,针对过估计问题 | 双Critic结构,实现复杂度高 |
如果仿真环境跑一步代价高、想省样本,DDPG类是首选。如果环境便宜、试错成本低,PPO是安全牌。TD3在DDPG代码基础上加双Critic和目标策略平滑,做机械臂任务如果发现DDPG的Q值明显过估计(表现为Critic loss异常高但reward正常),直接换TD3是顺理成章的事。
2.5 训练主循环:五步压缩成一节
每一步的做法可以压缩成五步:
- 从Actor拿到动作,加探索噪声
- 执行动作,拿奖励和下一个状态
- 把转移元组存进经验回放缓冲区
- 从缓冲区随机采样一个mini-batch
- 依序更新Critic(回归损失)、Actor(策略梯度)、软更新目标网络
这个循环跑起来之后,事情就变成一个工程问题:环境返回什么格式、维度怎么对齐、奖励怎么归一化。接下来的章节就把这些工程细节逐一解决。
3. PyTorch环境搭建:从空环境到能跑DDPG的完整配置
3.1 先花三分钟确认硬件和系统环境
DDPG训练是计算密集任务,纯CPU不是不能跑,只是慢得让人怀疑人生。我一般先确认三件事:显卡是不是NVIDIA的、驱动能不能被操作系统正常加载、磁盘有没有至少10GB空间。Windows、Ubuntu和macOS都能装PyTorch,Ubuntu在深度学习场景下最容易排查驱动问题,因为nvidia-smi一条命令就能看到驱动版本和CUDA版本。
Python版本建议3.9或3.10。深度学习库更新快,太旧的Python版本会卡在一些依赖库的兼容性上。如果电脑上已经装了多个Python版本,后面用conda建独立环境能省掉很多麻烦。
3.2 conda创建独立环境:避免依赖地狱
Anaconda或Miniconda是管理Python环境最顺手的工具。给这个项目单独建一个环境,不污染系统Python,是值得养成的习惯:
conda create -n ddpg_env python=3.9 conda activate ddpg_env建环境时指定python=3.9会装一个干净的解释器,后续所有包都装在这个环境里。激活成功后命令行前面会出现(ddpg_env)前缀,说明已经切进独立环境。这一步做完,后面装什么都不会影响其他项目。
3.3 pip安装PyTorch:CPU版和GPU版两条路线
安装PyTorch最稳妥的方式是去pytorch官网的install页面,它会根据你的操作系统和CUDA版本生成对应的命令。以CUDA 11.3为例,pip安装命令是:
# CPU版,不依赖显卡,先跑通代码用 pip install torch torchvision torchaudio # GPU版,需要预先装好NVIDIA驱动和匹配的CUDA pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113如果电脑没有NVIDIA显卡,或者驱动版本不够新,直接装CPU版不影响跑通DDPG训练流程,只是每个episode的交互和反向传播会慢几倍。对验证算法逻辑来说,CPU版完全够用;对跑长时间训练实验,GPU是必须的。判断该用哪个CUDA版本,在命令行执行nvidia-smi看右上角的CUDA Version即可。
3.4 验证安装:CPU张量计算和GPU可用性
装完先验证,再做其他事:
python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"输出两行:第一行是PyTorch版本号,第二行是GPU是否可用。如果torch.cuda.is_available()返回False,说明GPU版的CUDA没配对。常见原因有三个:装的是CPU版、显卡驱动太旧、CUDA版本和PyTorch不匹配。注意is_available()返回False不会报错,很多人忽略了这一行,后面训练一直在CPU上跑了几小时才发现——这是最常见的隐形坑。
3.5 配套库:numpy、matplotlib、gym
DDPG代码还依赖三个常用库,一次装齐:
pip install numpy matplotlib gymnumpy负责状态和动作的数值运算,matplotlib用来画奖励曲线和轨迹图,gym是OpenAI的强化学习环境接口标准。如果后面要自己写机械臂仿真,可以按gym的接口封装自定义环境,让环境类实现reset()和step()两个方法,DDPG训练代码就不需要改了。
3.6 冒烟测试:模型能不能跑通一次前向
环境装好后的第一件事,不是跑完整训练,而是先跑一个冒烟测试:初始化一个最小的Actor网络,喂一个随机状态向量,确认前向传播不报错。
python -c " import torch from torch import nn class Actor(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, action_dim), nn.Tanh() ) def forward(self, s): return self.net(s) actor = Actor(18, 6) s = torch.randn(1, 18) print(actor(s).shape) "输出torch.Size([1, 6])就说明PyTorch环境、网络定义、张量维度全部就绪。这一步能过滤掉八成环境问题,比直接跑完整训练脚本高效得多。
4. 模型设计与核心代码:状态、奖励、Actor与Critic网络全流程
4.1 状态空间定义:把机械臂姿态、目标与障碍物拼进一个向量
状态空间是DDPG输入分布的基础。对工业机械臂,状态一般包含:关节角度(六自由度就是6维)、末端执行器位置和姿态(3+3维)、目标位置和姿态(3+3维)、障碍物距离(视传感器输出定)。一个有用的设计技巧是:状态里尽量用「差值」而非「绝对目标」。把目标位置减去当前位置得到偏差向量,网络更容易学到「往哪个方向挪」;如果直接给绝对目标坐标,网络在不同目标之间泛化会很困难。
import torch import numpy as np def build_state(joint_angles, ee_pos, ee_ori, target_pos, target_ori, obstacle_dist): # 推荐用相对量:目标与末端的偏差 pos_err = np.array(target_pos) - np.array(ee_pos) ori_err = np.array(target_ori) - np.array(ee_ori) # 关节角度 + 末端位姿 + 偏差 + 障碍物距离 state = np.concatenate([ joint_angles, # 6维 ee_pos, ee_ori, # 6维 pos_err, ori_err, # 6维:相对量 obstacle_dist # 障碍物相关信息 ]).astype(np.float32) return torch.from_numpy(state)参数说明:pos_err和ori_err是核心设计决策——用偏差替代绝对目标值,让网络学的是「我离目标还差多少、下一步该往哪个方向动」,这个映射关系在不同任务目标之间是可迁移的。.astype(np.float32)是必须的,PyTorch默认张量类型是float32,如果喂float64会触发类型不匹配报错。
4.2 动作空间与探索噪声:输出范围怎么映射
动作定义有两种习惯:关节角速度控制和关节角度增量控制。角速度控制适合平滑运动,但需要积分累积;增量控制更直接,网络直接输出每个关节该偏转多少。常见的做法是让Actor输出tanh激活后的[-1, 1]值,然后在环境层映射到真实关节限位:
def map_action_to_joint(action, joint_low, joint_high): # action 是 [-1, 1] 的 tanh 输出 return joint_low + (joint_high - joint_low) * (action + 1.0) / 2.0这里joint_low和joint_high是每个关节的角度或速度下限、上限。比如关节2的活动范围是[-45度, 45度],映射后网络输出0.0就对应中间位置。这个映射如果不做,网络输出的[-1,1]会被环境直接当作关节角度使用,小角度关节会超出物理限位,仿真里表现为机械臂姿态乱飞。
探索噪声:DDPG在确定性动作上加噪声,经验回放里才能覆盖足够广的状态空间。OU噪声在机械臂场景里用得比较多,但高斯噪声更简单,实际效果并不差。一个重要的习惯是训练初期噪声大、后期噪声小,做线性衰减:
class OrnsteinUhlenbeckNoise: def __init__(self, dim, mu=0.0, theta=0.15, sigma=0.2): self.theta = theta self.mu = mu self.sigma = sigma self.dim = dim self.reset() def reset(self): self.x = np.ones(self.dim) * self.mu def sample(self): dx = self.theta * (self.mu - self.x) + self.sigma * np.random.randn(self.dim) self.x += dx return self.x参数说明:theta控制噪声向均值回归的速度,sigma控制噪声幅度。sigma=0.2适合探索初期,训练到后段衰减到0.05左右,策略才不会被噪声带偏。OU噪声的特点是相邻两步的动作有相关性,这对机械臂这种惯性系统更友好,不会出现一步向左一步向右的抖动。
4.3 奖励函数设计:四项拼在一起,量纲必须统一
奖励函数的核心原则:每一项的量纲要接近,否则大尺度项会淹没小尺度项。位置误差用米(量级0.1),姿态误差用弧度(量级0.01),直接相加会导致网络只优化位置不管姿态。常见做法是把各分量归一化:
def compute_reward(current_pos, target_pos, current_ori, target_ori, collision, prev_action, curr_action, tol=0.02): pos_dist = np.linalg.norm(np.array(current_pos) - np.array(target_pos)) ori_dist = np.linalg.norm(np.array(current_ori) - np.array(target_ori)) # 位置与姿态各自归一化到单位量级 r_pos = -pos_dist / 3.0 r_ori = -ori_dist / np.pi r_collision = -100.0 if collision else 0.0 r_smooth = -np.linalg.norm(np.array(curr_action) - np.array(prev_action)) # 稀疏的到达奖励,避免只接近不精确 r_arrive = 100.0 if pos_dist < tol and ori_dist < 0.05 else 0.0 return r_pos + r_ori + r_collision + r_smooth + r_arrive这个设计里r_arrive是关键:如果没有这个稀疏奖励,智能体会学到「靠近目标但永远不精确到达」,因为持续靠近已经能拿到不错的奖励,反而没有动机去精确对准最终位姿。r_smooth惩罚动作突变,防止轨迹出现抖动。这几个分量相加,量纲都在[-1, 1]附近,没有哪一项能完全主导梯度。
4.4 Actor与Critic网络实现:PyTorch里的标准写法
import torch import torch.nn as nn class Actor(nn.Module): """策略网络:状态 -> 动作""" def __init__(self, state_dim, action_dim, hidden_dim=256): super().__init__() self.fc1 = nn.Linear(state_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, action_dim) self.relu = nn.ReLU() self.tanh = nn.Tanh() def forward(self, state): x = self.relu(self.fc1(state)) x = self.relu(self.fc2(x)) return self.tanh(self.fc3(x)) # tanh把输出压缩到[-1, 1] class Critic(nn.Module): """价值网络:状态+动作 -> Q值""" def __init__(self, state_dim, action_dim, hidden_dim=256): super().__init__() self.fc1 = nn.Linear(state_dim + action_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, 1) self.relu = nn.ReLU() def forward(self, state, action): # 状态和动作拼成一个向量再进网络 x = torch.cat([state, action], dim=1) x = self.relu(self.fc1(x)) x = self.relu(self.fc2(x)) return self.fc3(x)Actor的最后一层用tanh是为了把输出限制在[-1, 1],配合前面map_action_to_joint的线性映射。Critic的第一层输入是state_dim + action_dim,因为Q值函数依赖于「状态和动作的组合」,必须拼接后输入。如果Critic只输入状态不输入动作,网络就不知道Q值对应的是哪个动作,训练完全失去意义——这是我见过最多人搞错的地方。
隐藏层hidden_dim取256是常规配置,六自由度机械臂任务下容量足够。如果状态维度很高或者任务复杂,可以加到512,但要注意过拟合和训练变慢的问题。激活函数ReLU是默认选择,换GELU或Swish在部分任务上有提升,但优先保证ReLU基线跑通。
4.5 经验回放与训练主循环:把模块串起来
from collections import deque import random class ReplayBuffer: """经验回放缓冲区""" def __init__(self, capacity=100000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): # 统一存float32,避免采样时维度/类型错误 self.buffer.append(( torch.FloatTensor(state), torch.FloatTensor(action), torch.tensor(reward, dtype=torch.float32), torch.FloatTensor(next_state), torch.tensor(done, dtype=torch.float32) )) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = zip(*batch) return (torch.stack(state), torch.stack(action), torch.stack(reward), torch.stack(next_state), torch.stack(done)) def __len__(self): return len(self.buffer)push里强制torch.FloatTensor转换是值得养成的习惯——环境返回的state经常是numpy数组或内置列表,不转换在训练时会报类型错误。deque(maxlen=capacity)自带容量控制,超出上限自动丢最旧的样本,不用手动管理。
训练主循环的每个训练步,更新顺序必须是:先更新Critic(用目标网络计算目标价值),再更新Actor(让Critic打分更高的方向挪),最后软更新目标网络。顺序不能乱,Actor用了Critic的梯度,如果先更新Actor再更新Critic,Actor拿到的梯度就是旧参数下的值。
# 每步训练核心代码 for step in range(total_steps): # 1. 采动作 + 加噪 + 执行 + 存经验(略) if len(buffer) > batch_size: state, action, reward, next_state, done = buffer.sample(batch_size) # 2. 更新Critic with torch.no_grad(): next_action = target_actor(next_state) target_q = target_critic(next_state, next_action) y = reward + gamma * (1 - done) * target_q q = critic(state, action) critic_loss = nn.MSELoss()(q, y) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() # 3. 更新Actor actor_loss = -critic(state, actor(state)).mean() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() # 4. 软更新目标网络 soft_update(target_actor, actor, tau) soft_update(target_critic, critic, tau)critic_loss用的是MSE回归损失,目标是让Q值预测逼近y。actor_loss取负号是因为PyTorch做梯度下降,要让Q值最大化就得最小化负Q值。done乘以(1 - done)是为了处理终止状态——终止状态没有后续累积奖励。
4.6 关键超参数速查
| 参数 | 常见取值 | 说明 |
|---|---|---|
lr_actor | 1e-4 ~ 1e-3 | Actor学习率,通常比Critic低 |
lr_critic | 1e-3 | Critic学习率 |
tau | 0.005 | 目标网络软更新系数 |
gamma | 0.99 | 折扣因子,越接近1越考虑长远回报 |
batch_size | 64 | 采样batch大小 |
buffer_size | 50000 ~ 100000 | 经验回放容量 |
noise_sigma | 0.2 → 0.05 | 探索噪声,训练后期衰减 |
这些参数跑起来不一定是最优,但作为起点能保证训练不崩。调参的方向是:reward曲线震荡就降学习率或降tau,训练太慢就升学习率或减小buffer,每个episode太长就检查gamma是否让折扣后回报衰减太快。
5. 避坑笔记:不收敛、NaN、维度错位的五个典型翻车现场
5.1 训练震荡不收敛
现象:reward曲线剧烈波动,几千步训练之后还在横跳,看不出上升趋势。
原因:学习率偏高、目标网络更新过快(tau太大)、经验回放缓冲区太小,这三个因素经常一起出现。学习率大了Critic一步迈太远;tau大了目标网络追着在线网络跑,失去了「稳定靶子」的作用;缓冲区太小采样分布太窄,训练数据缺乏多样性。
解决:把lr_actor降到1e-4量级,tau设为0.005,buffer容量至少5万条。如果还震荡,先冻结Actor只训练Critic几百步,让价值网络先稳定下来再联合训练。
5.2 Critic的loss直接变NaN
现象:训练到一半,critic_loss变成nan,奖励也变成inf,训练直接报废。
原因:梯度爆炸。Q值目标y里的奖励项数值过大,或者网络权重初始值不当,反向传播梯度数值溢出。检查reward计算里是否有除零、是否在compute_reward中传入了None导致np.linalg.norm报错。
解决:对Critic和Actor都做梯度裁剪,PyTorch里一行代码:
torch.nn.utils.clip_grad_norm_(critic.parameters(), max_norm=1.0)另外把输入特征做标准化,让每个维度均值为0、方差为1,能大幅降低梯度爆炸概率。max_norm=1.0表示梯度范数超过1就被裁剪到1,防止单步更新过大。
5.3 Actor输出没有被映射回关节限位
现象:仿真里机械臂姿态乱飞,关节角度超出物理允许范围,甚至出现末端执行器穿过地面的情况。
原因:Actor输出[-1, 1]的tanh值,环境却直接把输出当作真实关节角度或角速度。六自由度机械臂每个关节的限位不同,有的关节能转180度,有的只有90度,不做映射就会越界。
解决:在环境模块里对Actor输出做一次线性映射,map_action_to_joint(action, joint_low, joint_high),把[-1, 1]放大到每个关节的实际范围。这个映射函数必须在训练和评估时保持一致,否则训练时的策略在评估时就没法复用。
5.4 维度对不上:mat1 and mat2 shapes cannot be multiplied
现象:一运行训练脚本就报维度错误,提示两个矩阵无法相乘。
原因:最典型的三个——环境返回的state是list类型,torch.FloatTensor把它转成了整个list推导的张量;Critic拼接时torch.cat的维度不对,state和action分别在dim=0和dim=1上拼接;或者是状态向量里混入了int型数据导致类型不匹配。
解决:在push进buffer之前先打印state.shape和action.shape,确认维度一致后再往前推。我一般的做法是在build_state里就用torch.from_numpy(state)把状态统一成torch.Tensor,后续所有环节都基于Tensor操作,不再混用list和numpy。
5.5 奖励一直在涨,目标到达率却为零
现象:reward曲线稳步上升,看起来训练一切顺利,但跑100个测试episode,末端到达目标点的成功率是0。
原因:奖励函数里的距离项把智能体带到了「靠近目标但不精确」的局部最优。持续靠近目标已经拿到了不错的负奖励,精确对准目标需要付出额外的控制代价,智能体觉得不划算。没有稀疏的到达奖励,网络就学不到「精确到达」这个关键动作。
解决:在compute_reward里加一个阈值判断,末端与目标的距离小于容差(如2cm)时给一个大的正奖励,比如+100。除了看奖励曲线,还要在训练过程中定期跑评估,统计目标到达率,这两个指标一起看才不会自欺欺人。
6. 训练完怎么验证:奖励曲线、轨迹可视化与三个评估指标
6.1 奖励曲线与损失曲线的判读
训练结束后的第一件事是画曲线。奖励曲线看三样东西:整体趋势是否上升、波动幅度是否在可接受范围、有没有突然崩塌。损失曲线看Critic是否收敛——如果Critic loss持续居高不下,通常是Q值估计和目标值之间存在系统性偏差。
import matplotlib.pyplot as plt fig, ax = plt.subplots(2, 1, figsize=(10, 8)) ax[0].plot(reward_history, label='episode reward') ax[0].set_xlabel('episode') ax[0].set_ylabel('total reward') ax[1].plot(critic_loss_history, label='critic loss', color='orange') ax[1].set_xlabel('training step') ax[1].set_ylabel('loss') plt.tight_layout() plt.savefig('training_curves.png', dpi=150)奖励曲线可以做滑动平均(窗口10~50),原始曲线太抖会看不清趋势。如果滑动平均后还在震荡,就要回头检查第5章的参数问题。
6.2 轨迹可视化与目标到达率
轨迹可视化能让机械臂的行为直观呈现在眼前。用matplotlib画3D轨迹,把末端执行器每个时间步的位置连成线,对比规划的轨迹和目标点:
from mpl_toolkits.mplot3d import Axes3D fig = plt.figure(figsize=(8, 8)) ax = fig.add_subplot(111, projection='3d') ax.plot(traj_x, traj_y, traj_z, 'b-', linewidth=2, label='DDPG trajectory') ax.scatter(*target_pos, color='r', marker='*', s=200, label='target') ax.set_xlabel('X (m)') ax.set_ylabel('Y (m)') ax.set_zlabel('Z (m)') ax.legend() plt.savefig('trajectory_3d.png', dpi=150)图里如果轨迹是一条平滑的曲线、末端精确落在目标点上,说明策略学到了。轨迹如果在某个点反复打转,说明噪声过大或奖励设计有问题。
评估指标里我认定三个:目标到达率(100次随机初始位置的测试中,末端与目标距离小于阈值的比例,阈值取2cm)、轨迹长度(末端位移的累计值,越短说明路径越高效)、平均速度波动(相邻两步速度差的均值,越小说明运动越平滑)。这三个指标各有侧重,到达率看任务完成度,轨迹长度看效率,速度波动看平稳性。
6.3 部署前把模型导出固定下来
如果这套策略要接到实际控制器或仿真软件里,模型导出是个绕不开的环节。PyTorch的torch.save(actor.state_dict(), "actor.pth")保存权重是最低要求,但部署场景更常见的是导出ONNX,把Actor网络固化成计算图。导出前有个细节要留意:ONNX要求输入维度固定,所以导出时要把状态向量的维度确定好,我之前就因为状态里多了一个没用的常量维度,导出的模型在目标设备上输入尺寸对不上,白白查了半天。没有后悔药,导出前先检查输入输出的形状。
从那以后我每次跑DDPG,都先花十分钟检查四个地方:state的尺度、reward的量级、tau的大小、动作有没有映射回物理限位。这几件事检查完,训练基本就是等曲线走平的事。评估阶段一定跑成功率而不是只看reward,这两者差十万八千里。希望帮到你。
本文还有配套的精品资源,点击获取