迭代学习控制开源代码:从重复轨迹到误差自动收敛的Python实现
简介这份开源代码面向控制领域的研究者、工程师及自动化相关专业学生提供迭代学习控制ILC的完整实现框架可用于理解并实践重复性任务中通过多次迭代优化控制输入的核心思想。资源以zip压缩包形式提供共包含1个文件为.m格式的MATLAB脚本压缩包整体约2KB体积轻量便于快速下载与本地运行。目前已有1246人学习下载具备一定的参考热度。代码内容围绕ILC的关键环节展开涵盖系统模型描述、迭代更新律设计、误差处理与停止准则等模块读者可借此掌握如何依据前一次迭代误差修正控制输入并理解滤波器设计、优化算法在其中的作用。对于希望将ILC应用于机器人、自动化生产线或航空航天场景的读者这份代码可作为算法验证与二次开发的起点帮助梳理迭代学习控制的实现脉络与工程落地思路。1. 迭代学习控制开源代码从“重复跑同一段轨迹”到误差自动收敛如果你做过机械臂轨迹跟踪、电机位置控制或者任何需要“一遍遍重复同一动作”的场景大概率遇到过这个尴尬传统 PID 调得再好每次跑完轨迹总差那么一点换一个工况又得重新调参。迭代学习控制Iterative Learning Control简称 ILC就是冲着这个痛点来的——它不追求一次跑完美而是利用上一轮的误差去修正下一轮的控制量跑得越多越准。这份开源代码把 ILC 的几种经典算法P 型、D 型、PD 型以及带遗忘因子的变体做成了可运行的仿真框架适合正在做运动控制、轨迹跟踪的开发者直接拿来跑通、改参数、换被控对象。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序把这份代码拆开讲一遍。2. 项目定位与技术选型为什么用 Python 而不是 MATLAB2.1 这份代码到底解决了什么问题迭代学习控制的核心思想可以用一句话概括被控系统重复执行同一任务时把上一次的跟踪误差按某种学习律叠加到本次的控制输入上随着迭代次数增加误差逐渐趋近于零。这份开源代码实现的就是这个闭环学习过程它包含三个必备组件被控对象模型、学习律、迭代终止条件。代码里内置的被控对象是一个典型的二阶线性系统模拟的是单关节机械臂或直线电机的动力学。为什么选这个模型因为它足够简单能让你在几秒内跑完 100 次迭代看到收敛曲线同时又保留了 ILC 最核心的数学结构——相对阶为 2需要 D 型或 PD 型学习律才能保证收敛。如果你手头有更复杂的模型比如带摩擦或柔性的系统代码里留了替换接口后面会讲怎么改。适合谁看如果你正在做以下任意一件事这份代码能帮你省掉从零推导公式的时间机械臂重复抓放、数控机床走刀轨迹优化、电机周期性转速跟踪、甚至四足机器人步态学习。它不涉及深度学习不需要 GPU一台普通笔记本就能跑。2.2 技术栈与依赖numpy matplotlib 就够了整个项目没有用任何重型框架核心依赖只有两个# 依赖安装建议用虚拟环境 pip install numpy matplotlibnumpy 负责矩阵运算和迭代循环matplotlib 负责画收敛曲线和误差对比图。为什么不用 MATLAB因为 Python 免费、易集成而且这份代码的目标是让你能直接嵌入到自己的 Python 控制脚本里而不是停留在仿真阶段。代码结构上它分成了三个文件plant.py定义被控对象包含状态更新方程和输出方程ilc.py实现学习律包括 P 型、D 型、PD 型以及带遗忘因子的更新公式simulate.py主仿真脚本负责跑迭代、记录误差、画图这种拆分方式的好处是你换被控对象只改plant.py换学习律只改ilc.py主循环不用动。常见做法是把这三部分写在一个脚本里但那样改起来容易牵一发动全身这份代码的模块化程度对二次开发更友好。2.3 目录结构与关键文件说明下载后解压你会看到这样的目录ilc_open_source/ ├── plant.py # 被控对象模型 ├── ilc.py # 迭代学习律实现 ├── simulate.py # 主仿真入口 ├── config.yaml # 参数配置文件迭代次数、学习增益等 └── results/ # 仿真结果保存目录config.yaml是这份代码的一个亮点它把迭代次数、学习增益、遗忘因子、采样时间这些容易调乱的参数集中管理。我一般会在simulate.py里用yaml.safe_load读进来这样换一组参数不用改代码只改配置文件就行。results/目录默认保存每次仿真的误差范数和收敛曲线图方便你对比不同参数的效果。3. 核心模块拆解从被控对象到学习律的完整链路3.1 被控对象建模二阶系统怎么写成状态空间打开plant.py核心是一个SecondOrderPlant类。它用状态空间形式描述被控对象import numpy as np class SecondOrderPlant: def __init__(self, m1.0, c0.5, k2.0, dt0.01): # m: 质量, c: 阻尼, k: 刚度 self.m, self.c, self.k m, c, k self.dt dt # 状态空间矩阵: x [位置, 速度] self.A np.array([[0, 1], [-k/m, -c/m]]) self.B np.array([[0], [1/m]]) self.C np.array([[1, 0]]) # 只输出位置 def step(self, x, u): # 前向欧拉离散化更新状态 x_next x self.dt * (self.A x self.B.flatten() * u) y self.C x_next return x_next, y这段代码的逻辑很直白A矩阵决定系统自身的动态特性B矩阵决定控制输入如何影响状态C矩阵决定你观测哪个状态。参数m、c、k分别对应质量、阻尼、刚度改这三个值就能模拟不同负载或不同刚度的机械结构。dt是采样时间默认 0.01 秒对应 100Hz 的控制频率这个频率对大多数运动控制场景够用了。注意step方法用的是前向欧拉离散化这是最简单也最常用的离散化方式。如果你对精度要求更高可以换成四阶龙格库塔但那样计算量会大一些。对于 ILC 来说离散化误差只要一致学习律能补偿掉大部分所以前向欧拉通常够用。3.2 学习律实现P 型、D 型和 PD 型的区别在哪ilc.py是整份代码最核心的部分。它定义了三种学习律我用一个基类加三个子类的方式组织class ILCBase: def __init__(self, gain, forget_factor1.0): self.gain gain # 学习增益 self.forget_factor forget_factor # 遗忘因子1.0 表示不遗忘 def update(self, u_prev, e_prev): raise NotImplementedError class PTypeILC(ILCBase): def update(self, u_prev, e_prev): # P 型: u_{k1} u_k L * e_k return self.forget_factor * u_prev self.gain * e_prev class DTypeILC(ILCBase): def update(self, u_prev, e_prev): # D 型: 对误差求差分后再更新 e_diff np.diff(e_prev, prepende_prev[0]) return self.forget_factor * u_prev self.gain * e_diff class PDTypeILC(ILCBase): def __init__(self, kp, kd, forget_factor1.0): super().__init__(kp, forget_factor) self.kd kd # 微分增益 def update(self, u_prev, e_prev): e_diff np.diff(e_prev, prepende_prev[0]) return (self.forget_factor * u_prev self.gain * e_prev self.kd * e_diff)P 型学习律直接用误差乘以增益结构最简单但对相对阶为 2 的系统单独用 P 型可能不收敛因为误差和输入之间差了两个积分环节。D 型对误差求差分相当于引入微分作用能补偿相对阶。PD 型则是两者结合kp和kd分别控制比例和微分项的权重。forget_factor是遗忘因子取值在 0 到 1 之间。当它小于 1 时上一轮的控制量会被“遗忘”一部分这能提高系统对非重复扰动的鲁棒性但代价是收敛速度变慢。我一般会在有噪声或模型不确定的场景下把它设成 0.95 到 0.99纯仿真且模型精确时直接设 1.0。3.3 主仿真循环一次迭代到底发生了什么simulate.py把被控对象和学习律串起来核心循环如下import numpy as np from plant import SecondOrderPlant from ilc import PDTypeILC # 初始化 plant SecondOrderPlant(m1.0, c0.5, k2.0, dt0.01) ilc PDTypeILC(kp0.5, kd0.05, forget_factor1.0) T 2.0 # 单次轨迹时长 N int(T / plant.dt) # 单次迭代的步数 iterations 50 # 总迭代次数 # 期望轨迹正弦波 t np.linspace(0, T, N) y_desired np.sin(2 * np.pi * t) # 存储误差范数 error_norms [] u np.zeros(N) # 初始控制输入全零 for k in range(iterations): x np.zeros(2) # 每轮从零状态开始 y np.zeros(N) for i in range(N): x, y[i] plant.step(x, u[i]) e y_desired - y # 跟踪误差 error_norms.append(np.linalg.norm(e)) u ilc.update(u, e) # 学习律更新控制输入 print(f最终误差范数: {error_norms[-1]:.6f})这段代码的关键点有三个。第一每轮迭代开始时状态x必须重置为零因为 ILC 假设每次任务的初始条件完全相同如果初始状态漂移学习律的收敛性会受影响。第二误差e是整条轨迹上的误差向量不是单个时刻的误差学习律是对整条轨迹做更新。第三u在迭代之间传递这就是“学习”发生的地方——上一轮的控制经验被保留下来用于修正下一轮。跑完 50 次迭代你会看到error_norms从初始的几十降到 0.001 量级收敛曲线在对数坐标下近似一条直线。如果曲线震荡或不下降通常是学习增益太大或遗忘因子设得太小。4. 运行与调参怎么跑通第一组收敛曲线4.1 环境准备与首次运行先把代码下载到本地建议用 Python 3.8 以上版本。如果你用 conda可以这样建环境conda create -n ilc python3.9 conda activate ilc pip install numpy matplotlib pyyaml然后直接运行主脚本python simulate.py --config config.yamlconfig.yaml里默认的迭代次数是 50学习增益kp0.5、kd0.05遗忘因子 1.0。跑完后results/目录下会生成两张图一张是误差范数随迭代次数的变化曲线另一张是最后一次迭代的跟踪轨迹与期望轨迹的对比。如果这两张图正常出来说明环境没问题。常见的一个坑是 matplotlib 后端问题。如果你在服务器上跑没有图形界面需要在脚本开头加matplotlib.use(Agg)否则会报no display错误。这个坑我踩过好几次后来养成习惯所有仿真脚本都强制指定非交互后端。4.2 参数怎么调学习增益与遗忘因子的配合学习增益kp和kd决定了收敛速度但也直接影响稳定性。增益太小收敛慢50 次迭代可能还没降到理想精度增益太大误差曲线会震荡甚至发散。我一般按这个顺序调先把kd设为零只调kp从 0.1 开始每次加 0.1观察误差是否单调下降找到kp的临界值误差开始震荡的那个值然后取它的一半作为基准再加入kd从 0.01 开始慢慢加kd的作用是抑制高频震荡但太大会放大噪声遗忘因子forget_factor的调整逻辑不同。它不影响收敛速度的上限但影响稳态误差。设成 1.0 时理论上误差能降到零设成 0.95 时误差会收敛到一个非零的小值但系统对重复性扰动更鲁棒。如果你发现仿真里误差降不下去先检查遗忘因子是不是被误设成了小于 1 的值。提示调参时把iterations设大一点比如 100这样即使收敛慢也能看到趋势。收敛后再把迭代次数降回 50节省时间。4.3 换一条期望轨迹试试默认的期望轨迹是正弦波你可以改成方波、三角波或任意你需要的轨迹。改的地方在simulate.py里# 方波轨迹示例 y_desired np.sign(np.sin(2 * np.pi * t)) # 三角波轨迹示例 y_desired 2 * np.abs(2 * (t/T - np.floor(t/T 0.5))) - 1换轨迹后学习增益可能需要重新调。方波在跳变点有无限大的导数D 型学习律对跳变敏感容易产生尖峰。这时候要么把kd调小要么在方波跳变处做平滑处理。我一般会在轨迹生成后加一个低通滤波把高频分量压掉这样学习律不会因为跳变而震荡。5. 避坑与常见问题收敛失败时先查这五条5.1 误差曲线不下降反而上升现象跑完 50 次迭代误差范数从 10 涨到 100 甚至更大。原因通常是学习增益超过了稳定边界。ILC 的收敛条件要求学习增益与系统高频增益的乘积小于 1如果kp或kd太大就会破坏这个条件。解决方法是把增益减半再跑如果还是上升就继续减直到误差开始下降。另外检查一下plant.py里的dt是不是被改大了采样时间变大也会降低稳定裕度。5.2 误差降到一定程度就卡住不动现象前 20 次迭代误差降得很快后面几乎不变最终误差停在 0.01 左右。原因通常是遗忘因子小于 1或者被控对象存在非重复扰动。先检查forget_factor是不是 1.0如果不是改成 1.0 再跑。如果已经是 1.0那可能是仿真里引入了随机噪声ILC 对非重复噪声没有抑制能力只能靠滤波或降低学习增益来缓解。5.3 初始几次迭代误差特别大现象第一次迭代误差是 50第二次跳到 200第三次才降下来。这是正常现象因为初始控制输入u是全零第一轮误差完全由系统自由响应决定学习律在第二轮会做一个较大的修正可能过冲。如果你不想看到这个过冲可以把初始u设成期望轨迹的粗略逆模型输出而不是全零。代码里u np.zeros(N)这一行改成u 0.5 * y_desired就能缓解。5.4 换被控对象后完全不收敛现象把plant.py里的m、c、k改成其他值后误差曲线震荡发散。原因是学习增益没有跟着被控对象重新调。不同质量、阻尼、刚度的系统高频增益差别很大原来的增益可能对新对象来说太大了。解决方法是把增益降到原来的十分之一然后慢慢往上加。另外注意如果新对象的相对阶不是 2比如加了积分器变成 3那 D 型学习律可能不够用需要改用高阶学习律或加滤波器。5.5 画图时报错或图是空白的现象脚本跑完没有报错但results/里的图是空白或者只有坐标轴。原因通常是y_desired和y的长度不一致或者error_norms里全是nan。先检查N int(T / plant.dt)算出来的步数是不是和len(t)一致如果不一致np.linspace生成的t和实际仿真步数会对不上。另外在simulate.py里加一行print(np.isnan(error_norms).any())如果输出True说明仿真过程中出现了数值溢出把学习增益调小即可。6. 二次开发把 ILC 接到你的真实控制器上6.1 替换被控对象从仿真模型到真实设备这份代码目前跑的是仿真模型但 ILC 的真正价值在于控制真实设备。要把plant.py换成你的真实控制器核心是改step方法。真实设备通常不给你状态空间矩阵而是给你一个“输入控制量 → 输出测量值”的接口。你可以这样改class RealDevicePlant: def __init__(self, device_interface, dt0.01): self.device device_interface # 你的设备通信接口 self.dt dt def step(self, x, u): # 忽略 x直接发控制量给设备 self.device.send_control(u) time.sleep(self.dt) # 等待一个采样周期 y self.device.read_position() return None, y # 状态不再需要注意真实设备有通信延迟和采样抖动time.sleep的精度可能不够。常见做法是用实时线程或硬件定时器来保证采样周期稳定。如果延迟太大ILC 的收敛性会变差这时候需要在学习律里加一个相位超前补偿或者降低学习增益。6.2 加一个低通滤波器抑制高频噪声的实用技巧真实设备的测量信号里一定有噪声而 D 型学习律对噪声特别敏感因为差分运算会放大高频分量。一个简单有效的办法是在学习律更新前对误差信号做低通滤波from scipy.signal import butter, filtfilt def lowpass_filter(signal, cutoff10, fs100, order2): # cutoff: 截止频率(Hz), fs: 采样频率(Hz) b, a butter(order, cutoff / (fs / 2), btypelow) return filtfilt(b, a, signal) # 在学习律更新前调用 e_filtered lowpass_filter(e, cutoff10, fs100) u ilc.update(u, e_filtered)cutoff的选择取决于你的轨迹频率。如果轨迹主要能量在 1Hz 以下截止频率设 10Hz 就够了如果轨迹变化快可以适当提高但不要超过采样频率的一半。filtfilt是零相位滤波不会引入额外延迟适合离线或准实时场景。如果是在线实时控制只能用单向滤波那就要接受一定的相位滞后。6.3 从固定迭代次数到自动终止省时间的关键改动仿真里跑固定 50 次迭代没问题但真实设备上每多跑一次就多耗一次时间和磨损。更实用的做法是设一个误差阈值当误差范数小于阈值时自动停止threshold 0.001 for k in range(max_iterations): # ... 执行一轮 ... error_norms.append(np.linalg.norm(e)) if error_norms[-1] threshold: print(f第 {k1} 次迭代达到精度要求提前终止) break u ilc.update(u, e)这个改动看起来简单但在真实场景下能省掉大量无效迭代。我一般会把阈值设成期望精度的 1.5 倍留一点余量避免因为单次测量噪声导致误判。另外建议加一个最大迭代次数保护防止误差降不下去时无限循环。6.4 一个容易忽略的细节初始状态对齐ILC 的收敛性证明里有一个前提每次迭代的初始状态必须相同。仿真里x np.zeros(2)保证了这一点但真实设备每次启动时位置可能有微小偏差。这个偏差如果不处理会在迭代中累积导致误差曲线出现平台期甚至发散。我的做法是在每轮迭代开始前先把设备移动到同一个初始位置并等待位置稳定后再开始记录误差。如果设备不支持精确回零可以在学习律里加一个初始状态补偿项把初始误差单独扣除。从那以后我每次把 ILC 往新设备上移植都强制先跑一遍“初始状态重复性测试”——让设备连续回零 10 次记录每次的位置偏差如果偏差超过期望精度的十分之一就先解决机械回零问题再调学习律。这个习惯帮我省掉了至少三次误判为“算法不收敛”的排查时间。希望这份代码和上面的改造思路能帮到你拿到手先跑通仿真再一步步换成自己的设备比一上来就接真实系统要稳得多。本文还有配套的精品资源点击获取