简介:这份资源面向机器人控制、强化学习方向的开发者与研究生,提供一套基于MATLAB Simulink的自适应机器人控制算法实现代码,用于解决机器人在未知或变化环境中策略难以实时调整的问题。压缩包共127个文件,以106个m脚本为核心,辅以jpg、png、gif等仿真截图与演示动图、fig图形界面文件及html、txt说明文档,整体约336KB,体积轻量便于快速部署。代码围绕经验回放与学习算法展开,包含episodic_learn、episodic_replay、replay、learn等模块,并配有startuprl、cleanuprl启动清理脚本与Changelog更新记录,可帮助读者理解DQN或DDPG类算法与Simulink仿真环境的结合方式。目前已有853人学习下载,适合希望掌握强化学习控制落地流程、借鉴经验回放与自适应控制实现思路的读者参考。
1. 从一条 Simulink 曲线说起:强化学习控制机器人到底在控什么
如果你在 MATLAB 里跑过一个两连杆机械臂的 Simulink 模型,大概率见过这样的曲线:关节角度在目标值附近来回振荡,PID 参数调了三天,超调压下去了,响应又慢得像蜗牛。这不是你调参水平的问题,而是固定增益控制器面对非线性、时变负载时的结构性局限。强化学习控制要解决的,正是这个「控制器自己学会怎么调自己」的问题——让智能体通过与环境反复交互,自己找出一套随状态变化的控制策略,而不是靠人手工整定一组固定参数。
这个方向适合两类人:一是做机器人控制、手里已经有 Simulink 被控对象模型、想引入学习型控制器的工程师;二是做强化学习算法、想找一个有明确物理约束和实时性要求的落地场景的研究者。它不要求你从头搭一个机器人,但要求你能把「被控对象」「奖励函数」「训练循环」这三件事在 Simulink 环境里串起来。下面按我实际做过的路径,从环境搭建讲到训练收敛,再到部署时的坑,一层层拆开。
2. 把 Simulink 被控对象接进 RL 训练循环:环境接口怎么搭
2.1 为什么选 Simulink 而不是纯 Python 环境
很多人第一反应是用 Gym 搭一个机械臂模型,但纯 Python 写的动力学和真实系统差距很大,尤其是摩擦、柔性关节、执行器饱和这些效应,手写模型很难还原。Simulink 的优势在于:你手里可能已经有一个经过验证的被控对象模型,它可能来自 Simscape Multibody,也可能来自实验辨识得到的传递函数。直接复用这个模型,训练出来的策略迁移到实机时偏差会小很多。
MATLAB 从 R2019a 开始提供 Reinforcement Learning Toolbox,它和 Simulink 的集成方式是通过rlSimulinkEnv函数,把 Simulink 模型包装成一个符合 RL 接口的环境对象。核心思路是:Simulink 模型负责一步仿真推进,RL 工具箱负责观测、动作、奖励的交换。你不需要把模型导出成 C 代码再包一层,直接在 MATLAB 工作区里就能跑训练。
2.2 搭建最小可训练环境的三步操作
第一步,准备 Simulink 模型。模型里需要有两个关键端口:一个输入端口接收动作信号,一个输出端口输出观测信号。动作通常是关节力矩或位置增量,观测通常是关节角度、角速度、末端位置。模型里还要有一个停止条件,比如仿真时间到或者状态越界。
第二步,在 MATLAB 里定义观测和动作的规格。下面是一个两连杆机械臂的示例:
% 观测空间:4个关节状态(角度+角速度),归一化到[-1,1] obsInfo = rlNumericSpec([4 1], 'LowerLimit', -1, 'UpperLimit', 1); obsInfo.Name = 'jointStates'; % 动作空间:2个关节的力矩,范围[-10,10] N·m actInfo = rlNumericSpec([2 1], 'LowerLimit', -10, 'UpperLimit', 10); actInfo.Name = 'torque'; % 创建Simulink环境,模型名称为myRobotArm env = rlSimulinkEnv('myRobotArm', 'myRobotArm/RL Agent', obsInfo, actInfo); % 设置仿真时间,每个episode最多跑5秒 env.ResetFcn = @(in) setVariable(in, 'Tsim', 5);这段代码里,rlSimulinkEnv的第一个参数是模型名,第二个参数是 RL Agent 模块在模型中的路径。ResetFcn用来在每个 episode 开始时重置仿真变量,比如把初始关节角度随机化,增加训练多样性。
第三步,定义奖励函数。奖励函数决定了智能体学什么。一个常见的做法是:位置误差越小奖励越高,控制力矩越大惩罚越重。在 Simulink 里可以用 MATLAB Function 模块实现:
function reward = computeReward(q, qd, q_ref, tau) % q: 当前关节角度, q_ref: 目标角度 pos_error = q - q_ref; % 位置误差惩罚,权重100 r_pos = -100 * (pos_error' * pos_error); % 速度惩罚,抑制振荡,权重1 r_vel = -1 * (qd' * qd); % 控制力矩惩罚,权重0.01 r_tau = -0.01 * (tau' * tau); % 总奖励 reward = r_pos + r_vel + r_tau; end奖励权重的选择是训练成败的关键。位置误差权重太低,机器人到不了目标;太高,智能体会用巨大的力矩去冲目标,导致振荡甚至发散。我一般先让位置误差权重占主导,训练到能稳定到达目标后,再逐步加大力矩惩罚,让动作变平滑。
2.3 训练循环的启动与监控
环境搭好后,选一个算法开始训练。对于连续动作空间,DDPG 和 TD3 是常用选择。TD3 比 DDPG 多了一对 Critic 网络和延迟更新,训练稳定性更好,适合机器人控制这种对平滑性要求高的场景。
% 创建TD3智能体 agent = rlTD3Agent(obsInfo, actInfo); % 训练选项:最多跑2000个episode,每个episode最多500步 trainOpts = rlTrainingOptions(... 'MaxEpisodes', 2000, ... 'MaxStepsPerEpisode', 500, ... 'ScoreAveragingWindowLength', 20, ... 'Verbose', true, ... 'Plots', 'training-progress'); % 开始训练 trainingStats = train(agent, env, trainOpts);训练过程中要盯住两个指标:平均奖励曲线和 episode 步数。平均奖励上升但步数不降,说明智能体在「磨洋工」,奖励设计可能有问题;步数骤降但奖励没升,可能是提前触发了越界停止条件。我习惯每跑 200 个 episode 存一次 agent,防止训练后期崩溃导致前功尽弃。
3. 奖励函数与网络结构:两个决定收敛速度的变量
3.1 奖励塑形的三个实用技巧
奖励函数不是越复杂越好。我见过有人把末端轨迹误差、关节力矩、能量消耗、动作变化率全塞进去,结果智能体学了两千个 episode 还在原地抖。原因是奖励信号太密,智能体分不清哪个是主要目标。实用做法是分层:先只给位置误差,让智能体学会「到达」;到达率稳定后,再加入速度惩罚抑制振荡;最后加入力矩惩罚让动作平滑。每次只加一项,观察平均奖励是否还能继续上升。
另一个技巧是使用「稀疏奖励+课程学习」。对于抓取、装配这类接触任务,全程给密集奖励反而会让智能体学会「慢慢靠近但不接触」。可以先在目标点附近给奖励,然后逐步缩小奖励区域,逼着智能体精确到达。
第三个技巧是奖励归一化。不同任务的奖励量级可能差几个数量级,把奖励缩放到 [-1,1] 或 [0,1] 区间,能让不同任务共用一套网络超参数。MATLAB 的 RL 工具箱不自动做这件事,需要你在奖励函数里手动除一个尺度因子。
3.2 Actor 和 Critic 网络的层数与激活函数选择
TD3 的 Actor 网络输出连续动作,Critic 网络评估状态-动作对的价值。对于 4 维观测、2 维动作的机械臂任务,网络不需要太深。我常用的配置是:Actor 两个隐藏层,每层 128 个神经元,激活函数用 ReLU;Critic 两个隐藏层,每层 256 个神经元,输出层线性。观测和动作分别输入 Critic 的两个分支,在第二层隐藏层拼接。
% Actor网络:4维观测 -> 128 -> 128 -> 2维动作 actorNet = [ featureInputLayer(4, 'Normalization', 'none') fullyConnectedLayer(128) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(2) tanhLayer % 输出限制在[-1,1],再乘以动作范围 ]; % Critic网络:观测和动作分别输入,中间拼接 obsPath = [ featureInputLayer(4) fullyConnectedLayer(256) reluLayer ]; actPath = [ featureInputLayer(2) fullyConnectedLayer(256) reluLayer ]; commonPath = [ concatenationLayer(1, 2) fullyConnectedLayer(256) reluLayer fullyConnectedLayer(1) ]; criticNet = layerGraph(); criticNet = addLayers(criticNet, obsPath); criticNet = addLayers(criticNet, actPath); criticNet = addLayers(criticNet, commonPath); criticNet = connectLayers(criticNet, 'relu_1', 'concat/in1'); criticNet = connectLayers(criticNet, 'relu_2', 'concat/in2');层数再深,训练时间会明显增加,但性能提升有限。我试过 256-256 的 Actor,在同样任务上比 128-128 收敛慢约 30%,最终奖励只高不到 5%。对于实时性要求高的场景,浅网络反而更实用。
3.3 经验回放缓冲区的大小与采样策略
TD3 默认的经验回放缓冲区大小是 100 万条。对于 Simulink 仿真,每条经验包含一步的状态、动作、奖励、下一状态,100 万条大约占几百 MB 内存。如果仿真步长是 0.01 秒,100 万条相当于 2.7 小时的仿真数据。实际训练中,我一般设 20 万到 50 万条,既能保证样本多样性,又不会让内存爆掉。
采样策略上,TD3 默认均匀采样。如果任务有明显的「关键阶段」(比如接近目标时的精细调整),可以用优先经验回放,给 TD 误差大的样本更高采样概率。MATLAB 的 RL 工具箱不直接支持优先回放,需要自己改rlReplayMemory的采样逻辑,或者用rlTD3Agent的ExperienceBuffer参数调整。
4. 训练不收敛、抖动、过冲:排查清单与避坑记录
4.1 现象:平均奖励曲线剧烈震荡,方差比均值还大
原因通常是 Critic 网络过估计。TD3 用两个 Critic 取最小值来缓解,但如果两个 Critic 初始化太接近,或者学习率太高,过估计依然会发生。解决方法是把 Critic 学习率降到 Actor 的 1/2 到 1/10,同时增大目标网络软更新系数TargetSmoothFactor,从默认的 0.005 降到 0.001,让目标网络变化更慢。
4.2 现象:机器人到达目标后持续抖动,关节力矩高频切换
这是奖励函数里速度惩罚和力矩惩罚权重不匹配的典型表现。速度惩罚太小,智能体不介意高速冲过目标再拉回来;力矩惩罚太小,智能体用大幅值动作反复修正。我一般把速度惩罚权重设为位置误差权重的 1/10,力矩惩罚设为 1/100,然后根据抖动幅度微调。如果抖动依然存在,可以在动作输出后加一个一阶低通滤波器,截止频率设为控制频率的 1/5。
4.3 现象:训练初期智能体一动不动,episode 很快结束
大概率是动作空间范围设得太小,或者奖励函数在初始状态就给了很大的负值。检查动作的LowerLimit和UpperLimit,确保它们覆盖了让机器人动起来所需的最小力矩。另外,奖励函数里如果用了q - q_ref的平方,初始误差大时奖励会非常负,智能体可能直接放弃。可以在训练初期把奖励裁剪到 [-10, 10],避免梯度爆炸。
4.4 现象:Simulink 仿真速度极慢,训练一天才跑几百个 episode
Simulink 默认的求解器是变步长,对于 RL 训练这种需要固定步长交互的场景,变步长会导致每步仿真时间不可控。把求解器改成固定步长ode4,步长设为 0.01 秒,仿真速度能提升 5 到 10 倍。另外,关闭 Simulink 模型里的示波器和数据记录,这些可视化模块在训练时会拖慢仿真。
4.5 现象:训练好的策略在实机上完全失效
仿真到实机的差距主要来自三个方面:传感器噪声、执行器延迟、模型参数偏差。在仿真训练时加入这些扰动,能显著提升迁移效果。具体做法:在观测信号上叠加高斯噪声,信噪比设为 20 dB;在执行器输出后加一个 10 毫秒的传输延迟;把被控对象的摩擦系数、质量参数随机化 ±10%。这些扰动在 Simulink 里用 Band-Limited White Noise 模块和 Transport Delay 模块就能实现。
5. 从仿真到部署:策略导出与实时性验证的一个具体技巧
训练收敛后,下一步是把 Actor 网络导出成可部署的形式。MATLAB 的 RL 工具箱支持把 Actor 网络导出为 ONNX 格式,然后用 ONNX Runtime 在 C++ 或 Python 里加载。但这里有一个容易被忽略的问题:训练时观测做了归一化,部署时如果忘了同样的归一化,策略输出会完全错误。
我一般会在导出前,把 Actor 网络和观测归一化层合并成一个单独的dlnetwork对象。具体做法是:在 Actor 网络前面加一个featureInputLayer,把归一化的均值和标准差作为固定参数写进去,然后用assembleNetwork合并。这样导出的 ONNX 模型自带归一化,部署时只需要把原始观测喂进去。
% 假设obsMean和obsStd是训练时统计的均值和标准差 obsMean = [0.5; 0.1; 0.3; 0.05]; obsStd = [0.2; 0.15; 0.25; 0.1]; % 构建带归一化的Actor网络 normLayer = [ featureInputLayer(4, 'Normalization', 'zscore', ... 'Mean', obsMean, 'StandardDeviation', obsStd) ]; actorLayers = [ fullyConnectedLayer(128) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(2) tanhLayer ]; % 合并并导出 fullNet = [normLayer; actorLayers]; lgraph = layerGraph(fullNet); dlnet = dlnetwork(lgraph); exportONNXNetwork(dlnet, 'robotArmActor.onnx');导出后,在目标平台上用 ONNX Runtime 加载,输入一个 4 维向量,输出 2 维动作。实时性验证时,重点测两个指标:单次推理耗时和端到端控制周期。单次推理在普通 x86 上应该在 1 毫秒以内,在嵌入式 ARM 上可能在 5 到 10 毫秒。如果控制周期是 10 毫秒,推理耗时必须小于 5 毫秒,否则需要裁剪网络或用量化。
我自己的习惯是:每次导出 ONNX 后,先在 MATLAB 里用同样的测试数据跑一遍,把输出和原始 Actor 网络的输出对比,误差超过 1e-4 就说明归一化或层合并出了问题。这个检查花不了两分钟,但能省掉在实机上调试半天的麻烦。另外,部署前一定在仿真里做一次「零噪声」测试,确认策略在理想条件下能稳定到达目标,再逐步加噪声和延迟,观察性能退化曲线。如果退化太快,说明训练时的扰动还不够,需要回到第 4 章重新加扰动训练。
希望帮到你。
本文还有配套的精品资源,点击获取