简介:这套 MATLAB 代码面向从事非线性动力学、混沌分析与数据驱动建模的研究者和研究生,围绕慢时间尺度动态发现问题,提供基于 SINDy 架构的可复现实验材料。资源包共 10 个文件,含 7 个 .m 脚本、2 个 .mat 数据文件和 1 个 .md 说明文档,压缩包约 24.17MB;脚本覆盖玩具模型数值积分、连续时间 SINDy 模型拟合及粗粒化演化的离散时间映射发现,并涉及滑动窗口 DMD 提取快速周期。数据文件可用于木星、土星、Logistic 混沌等场景的慢发现实验,便于读者复现论文中的计算流程与图形,并对照修改参数开展自己的算例。已有 381 人学习,适合具备 MATLAB 与动力系统基础、希望掌握 SINDy 和 DMD 联合分析方法的读者。
1. 从一段越来越失控的 logistic 序列说起
前阵子帮 A 同学调一份 MATLAB 混沌仿真脚本,logistic 映射取 r=3.99、初值 0.31,跑六千步导出 CSV 直接扔进 SINDy,多项式字典一拟合,系数全乱了套:本该稀疏到只剩两三项的方程,出来十几项全是 1e-2 量级,残差看着还行,一积分就偏到天边。问题不在 SINDy,在于这份数据里同时塞着快尺度混沌和慢尺度演化,普通 SINDy 会把两个尺度当成一个系统去回归,等于让人用一张地图同时画北京和火星。Slow-Discovery 这份开源 MATLAB 代码包就是把这件事拆开做的:先用 logistic 类混沌序列造数据,再识别慢时间尺度流形,最后只在慢流形上跑 SINDy。适合手上有快慢耦合实测数据、想做可解释动力学建模的开发者,也适合想把 SINDy 从教科书例子搬进真实系统的研究者。
2. 项目拆解:SINDy 稀疏回归之外,Slow-Discovery 多做了哪几件事
先摆结论:这项目不是 SINDy 的替代品,而是 SINDy 的前置数据切片器。Brunton 那套稀疏辨识的核心假设是"系统在状态空间里可以用稀疏多项式字典线性表示",这个假设对单尺度干净数据成立,对多时间尺度耦合数据直接翻车。Slow-Discovery 要补的就是这一环。
2.1 项目里到底有哪几块东西
按常见的 MATLAB 科研代码组织方式,这份开源代码包大致可以分成四块:
- 数据生成层:logistic 映射生成器、快慢耦合 ODE 仿真器;
- 预处理层:有限差分求导、平滑滤波、字典构造与列归一化;
- 慢发现层:导数幅值门限切样本、慢流形投影、STLSQ 稀疏回归;
- 可视化层:原始相图、切片前后样本分布图、系数热图、方程重建对照。
这四块对应的是「造数据 → 洗数据 → 切慢流形 → 回归方程」的链路。真正和 SINDy 库拉开差别的就在第三块:慢流形识别。logistic 那部分在这里是配角,负责提供一份快速、确定性、易复现的混沌源,让你在没有实验数据的情况下也能把整条链路走一遍。
2.2 技术选型:为什么是 MATLAB,为什么是 STLSQ
MATLAB 在这类工作里属于常用选择:微分方程求解ode45、movmean滑动平均、pinv伪逆、eig谱分析都是开箱即用;慢流形识别常常要靠可视化看相图才能定参数,MATLAB 的plot/scatter3交互友好,不用自己拼前端。相比之下,Python 的 PySINDy 生态更完整,但这份代码包偏偏选 MATLAB,好处是高校和工程现场装个 MATLAB 就能跑、依赖几乎为零。
稀疏回归算法这边,用 STLSQ(Sequential Thresholded Least Squares)也就够了。相比 LASSO 或 SR3,STLSQ 只有正则参数 λ 和迭代次数两个超参,调起来直观,收敛快。再结合"切慢流形"这种分步策略,本质上是把"多尺度模型辨识"退化成"在正确子空间上的稀疏回归",模型的解释性会好很多。
2.3 它和普通 SINDy 流程的差异点
普通 SINDy 的链路是:数据 → 导数 → 字典 → 回归 → 方程。Slow-Discovery 在"导数"和"字典"之间插了一步,这一步就是慢流形的识别与切片。具体来说:
| 环节 | 普通 SINDy | Slow-Discovery |
|---|---|---|
| 数据 | 全量 | 全量 + 快慢分离 |
| 导数 | 全样本求导 | 全样本求导,用于度量"离开流形程度" |
| 回归样本 | 全量 | 只保留落在慢流形附近的子集 |
| 拟合对象 | 全系统 | 慢尺度子动力学 |
| 字典 | 全状态变量多项式 | 慢变量 + 少量快慢交叉项 |
最后一行是为了保留快变量对慢变量的驱动效应,不然慢流形上只剩纯慢变量方程、物理意义丢了。这个取舍在论文里叫"慢流形上的有效动力学",实现上就是先投影、再用投影后坐标建字典。
3. 核心链路:logistic 混沌序列怎么被整理成 SINDy 能吃的训练集
从数据源到训练集,这段是整个代码包里最好上手、也最容易踩坑的部分。logistic 序列本身不带导数,得靠人工补时间和差分;快慢耦合数据也得先确定哪个是快分量、哪个是慢分量,才能往下做切片。
3.1 logistic 混沌序列生成与快慢耦合数据构造
先看快尺度这一侧。logistic 映射 x_{n+1}=r·x_n·(1−x_n) 在 r 接近 4 时进入完全混沌态,代码很短,但几个参数不能乱:
%% gen_logistic.m —— 生成 logistic 混沌序列 function x = gen_logistic(r, x0, N) % r : 混沌参数,通常取 3.7~4.0 % x0 : 初值,不能取 0 或 1,否则序列恒为 0 % N : 序列长度 if r <= 0 || r > 4 error('r 应在 (0,4] 区间内'); end x = zeros(N,1); x(1) = x0; for n = 1:N-1 x(n+1) = r * x(n) * (1 - x(n)); end end这段没有任何玄学:r=3.99制造满混沌,x0取中间值(如 0.31)避开不动点。注意x0不能取 0、1 和1-1/r,否则序列立即退化,这一步 A 同学当初栽过,跑出来是一条直线还以为代码有问题。
单有快尺度不够,还要把它变成"有慢分量的数据"。常见的做法是用滑动平均把快序列压成慢驱动,再积分成慢变量:
%% gen_multiscale.m —— 快慢耦合数据 r = 3.99; x0 = 0.31; N = 6000; dt = 0.01; x = gen_logistic(r, x0, N); % 快变量 w = 25; xm = movmean(x, w); % 平滑出慢驱动 y = cumsum(0.05 * xm) * dt; % 慢变量:一次积分类比 X = [x, xm, y]; % N×3 的状态矩阵这里的三个参数决定数据性质:w=25是快慢分离的窗口,数值越大慢分量越"懒";dt=0.01只影响慢变量的时间缩放,对快变量混沌无影响;0.05是驱动的幅度系数,调太大会让慢变量跑出发散区。真实系统直接换成实验数据即可,这一段只在复现时用。
3.2 时间序列到状态空间:有限差分补出"导数"这一列
SINDy 需要的不是状态,而是状态导数 dX。ode45输出会给解,但不给导数,必须自己补:
%% finite_diff.m —— 中心差分求导 function dX = finite_diff(X, dt) n = size(X,1); dX = zeros(size(X)); dX(2:n-1, :) = (X(3:n,:) - X(1:n-2,:)) / (2*dt); % 内部点:中心差分,O(dt^2) dX(1, :) = (X(2,:) - X(1,:)) / dt; % 端点前向 dX(n, :) = (X(n,:) - X(n-1,:)) / dt; % 端点后向 end中心差分的误差是 O(dt²),端点用一阶格式误差大一些,但只影响首尾两行,对上千样本的回归可忽略。这个函数有两个血泪细节:第一,dt必须和生成数据时的时间步严格一致,ode45是变步长的,要先interp1重采样到等距网格;第二,带噪数据千万别直接差分——差分是放大器,噪声会被放大 1/dt 倍,常见做法是先用smoothdata或 Savitzky–Golay 平滑一次再差分。
3.3 多项式字典构造与列归一化
字典构造决定了方程的"可表达性",也决定了 STLSQ 能不能收敛。这里给一份低阶版:
%% build_library.m —— 构造多项式字典 function Theta = build_library(X, polyorder) % X : N×d 状态矩阵 % polyorder : 最高阶数,一般取 2,最多 3 n = size(X,1); d = size(X,2); Theta = ones(n,1); % 常数列 for i = 1:d Theta = [Theta, X(:,i)]; % 一次项 end if polyorder >= 2 for i = 1:d for j = i:d Theta = [Theta, X(:,i).*X(:,j)]; % 二次项,含平方项 end end end % 更高阶项按同样模式扩展 end三个变量、二阶多项式,字典列数是 1 + 3 + 6 = 10,还在人眼和pinv都舒服的范围内。习惯上做完之后还要归一化每一列:
colnorm = vecnorm(Theta); colnorm(colnorm == 0) = 1; % 防止零列除零 Theta = Theta ./ colnorm;归一化是玄学吗?不是。STLSQ 的 λ 是绝对值阈值,列范数不一致时小尺度列和常数列的惩罚力度不对等,方程里经常冒出莫名其妙的 β 项。归一化之后 λ 才有一个跨系统可比的量级,通常从 0.1 起步,按 10 的倍数往上下扫。
4. 慢流形识别:把快变量"切掉"之后再回归
这一章是 Slow-Discovery 的立命之本。慢流形的定义很朴素:快变量已经收敛到准稳态、只有慢变量还在演化的那部分状态空间。几何上它是快变量相图里的"低导数带",工程上就是筛样本。
4.1 导数幅值门限法筛出慢流形样本
用快变量导数的欧氏范数度量"离流形有多远",是最简单也最稳的一种判据:
%% slow_manifold_mask.m —— 慢流形样本掩码 function mask = slow_manifold_mask(dX, fast_idx, tol_ratio) % dX : N×d 全状态导数 % fast_idx : 快变量的列下标,比如 [1,2] % tol_ratio : 门限比例,常取 0.1~0.3 f_n = vecnorm(dX(:, fast_idx), 2, 2); % 每个样本点的快导数范数 thresh = tol_ratio * median(f_n); % 中位数比例 mask = f_n < thresh; % 只保留慢流形附近的样本 end用中位数而不是均值,是因为快导数分布本身就长尾,均值会被剧烈混沌期拉偏。tol_ratio是一个要紧的旋钮:0.1 切得最干净但样本少,0.3 样本多但混进一些还没完全收敛的点。我的经验是从 0.2 开始扫,画出"样本保留率 vs 回归系数稳定性"的曲线,一般会看到一个明显的平台区,取平台区中段即可。
4.2 流形投影与慢变量坐标变换
纯靠门限切样本是最朴素的版本,进阶一步是先投影再切片。常见做法是用主成分或核方法把状态空间压到低维慢子空间上:
%% slow_subspace.m —— 用 PCA 找慢子空间 function [P, Z] = slow_subspace(dX, mask, slow_dim) % 在慢流形样本上做主成分,取方差贡献最大的前 slow_dim 个方向 Xs = dX(mask, :); Xs = Xs - mean(Xs, 1); [V, D] = eig(cov(Xs)); [~, ord] = sort(diag(D), 'descend'); P = V(:, ord(1:slow_dim)); % 慢子空间基 Z = dX * P; % 投影后的慢坐标 end这一步不是为了降维打广告,而是为了给后续字典构造一个物理上有意义的坐标系。如果直接拿原始变量做字典,快慢交叉项会把回归带偏;投影到慢子空间后,字典只需用Z的列来搭,排除快变量本身的混沌噪声项。代价是方程的可解释性会变弱,所以常见做法是:先投影看系数是否稳定,若稳定,就把P反变换回原变量再重拟合一次,用原变量方程对外展示。
4.3 STLSQ 稀疏回归与慢时间尺度方程重建
样本切完了,字典搭好了,剩下的就是把 STLSQ 跑起来:
%% stlsq.m —— 序贯阈值最小二乘 function Xi = stlsq(Theta, dX, lambda, max_iter) % Theta : N×p 字典(已列归一化) % dX : N×d 状态导数 % lambda : 稀疏阈值,常用 0.1 起步,按 10 倍量级扫描 % max_iter : 迭代上限,一般 10 次内收敛 Xi = pinv(Theta) * dX; for it = 1:max_iter small = abs(Xi) < lambda; % 低于阈值的项标记为"该删" Xi(small) = 0; for j = 1:size(Xi, 2) big = ~small(:, j); % 当前列的保留项 if any(big) Xi(big, j) = pinv(Theta(:, big)) * dX(:, j); end end end end逻辑很直白:先最小二乘求一个稠密解,把低于 λ 的系数清零,再在保留下来的项上重解最小二乘,循环直到系数不再变化。用pinv而不是\,是为了规避字典列共线(比如同时出现x、x²和x·y时,列之间相关性很高)时\求解失败的情况。lambda是整个流程里最需要调的参数,太小压不稀疏、太大把真项也砍掉。常见做法是画一条 λ–稀疏度曲线:横轴 λ 从 1e-3 扫到 1,纵轴看 X 的每个分量的项数,一般会看到"某项数从 4-5 突然掉到 2-3 再掉到 0"的阶梯,稀疏度稳定在那个平台上的 λ 就是合适的。
跑完以后拿回归出的Xi和真实方程对照,Slow-Discovery 的真正价值才显出来:不切片的 SINDy 会把快慢耦合当成两项叠加,方程里同时出现 x 和它的高阶项,系数互相抵消;切片后快变量项被压掉,剩下来的正是"慢尺度上的有效驱动"。
5. 排错与参数调优:跑 Slow-Discovery 的五个踩坑点
下面五条都是复现时真踩到的,按"现象 → 原因 → 解决"写,顺序按我实际遇到的先后。
5.1 数据与环境类问题
现象一:ode45生成的解直接喂finite_diff,得到的 dX 全是 NaN。原因:ode45是变步长,t的相邻间隔不固定,用dt常数去除会得到不一致的求导,再加上初值点附近步长极小,除以 0 附近的值就会溢出。解决办法是先tq = linspace(t(1), t(end), N)等距重采样:X = interp1(t, X_raw, tq, 'pchip'),再用等距的dt求导。这一步是新手最容易漏的。
现象二:换一份实验数据,gen_logistic里r调到 4.0 直接炸成 0。原因:r=4时 logistic 映射若初值取到 0.5、0、1 这些特殊点,序列会进入 0 轨道或两周期轨道,看起来就像混沌"死掉了"。解决办法是把x0从[0.05, 0.95]中间取并加微小扰动,比如0.31 + 1e-6*randn;r=3.99比4.0更稳,一般不必贴着临界值跑。
现象三:STLSQ 跑完,常数列系数全是 0。原因:字典没做列归一化,常数列范数是 sqrt(N)≈77,状态列的范数是 O(1) 量级,λ=0.1 对状态项是阈值,对常数项就是绝对压制。解决办法是先做 3.3 节里的列归一化,再拟合;或者干脆把常数列去掉、改用中心化后的状态变量。
5.2 数值与算法类问题
现象四:λ 扫描时系数一会儿稳、一会儿全乱,找不到明显平台。原因:样本量本身就在随tol_ratio变化,切得越狠 λ 敏感度越高。问题出在"tol_ratio和lambda同时扫"。解决办法是固定tol_ratio=0.2,先把 λ 扫出平台,再固定 λ 去微调tol_ratio。两个参数做坐标下降式调参,不能一次扫两个轴。
现象五:换一份三变量数据,二次字典拟合非常慢。原因:pinv每次迭代都做一次 SVD,三变量二次字典是 10 列,问题不大;但把polyorder加到 3 或者状态变量加到 6,字典列会爆炸到几十上百列,pinv在噪声情况下会给出病态解。解决办法是把pinv换成带秩截断的pinv(Theta, 1e-8),或直接对Theta做 SVD 截断保留前 r 个主方向;再不行就上 QR 分解的正则化最小二乘。别小看这一步,字典爆炸是这类方法的通病。
6. 二次开发:在 Slow-Discovery 里换系统、换字典的几条路
原项目跑通以后,真正想把它用起来,基本会走三条改造路线:换系统、换字典、换稀疏策略。
6.1 换系统:把gen_multiscale.m替换成自己的数据
大部分人的第一步是"把 logistic 那套换成我自己的实验数据"。最小改动路径是把gen_multiscale.m整个换成一个 IO 函数:
function [X, dt, var_names] = load_my_data(csv_path) raw = readmatrix(csv_path); t = raw(:, 1); X = raw(:, 2:end); dt = median(diff(t)); % 假定等距采样 var_names = {'x','y','z'}; % 只用于画图 end下游的finite_diff、build_library、slow_manifold_mask、stlsq全部不用动。这里唯一的硬约束是采样率必须够高:快变量的混沌周期如果只有 5 个采样点,有限差分误差会大到直接影响 λ 选择,常见的经验是快变量每周期 ≥ 20 个采样点。
6.2 换字典与换稀疏正则:从多项式到傅里叶、从 STLSQ 到 SR3
换字典是最容易出论文的一块。build_library是个纯函数,返回啥用啥,所以把傅里叶项加进去只要改一行:
% 在已有多项式列后面追加一次谐波 for i = 1:d Theta = [Theta, sin(X(:,i)), cos(X(:,i))]; end稀疏正则也是可替换的:stlsq换成 SR3 或 ADMM 版本,接口保持一致(输入Theta、dX、正则参数,输出Xi),整个管线无感切换。我在stlsq基础上加过一个最简单的 L1 版本,就是每次迭代后额外做一次软阈值Xi = sign(Xi) .* max(abs(Xi) - lambda, 0),效果比硬阈值略稳、收敛稍慢。
改造方向对照一下:
| 改造点 | 改哪个文件 | 影响范围 | 常见风险 |
|---|---|---|---|
| 换数据源 | gen_multiscale.m | 下游全线 | 采样率不足、导数放大噪声 |
| 加物理项 | build_library.m | 仅字典 | 列共线、pinv病态 |
| 换稀疏算法 | stlsq.m | 回归结果 | 接口签名不统一 |
| 换慢流形判据 | slow_manifold_mask.m | 样本选择 | tol_ratio需重扫 |
6.3 验证思路:别只看拟合残差
换系统之后怎么判断改造是对的?我自己定过三条硬指标,每次改完都跑一遍:
- 样本保留率:切完流形的样本占比在 15%–60% 之间,太高说明没切干净,太低说明过度切片;
- λ 平台:稀疏度随 λ 的变化有一段稳定区间,系数符号在该区间内不变;
- 前向预测误差:把回归方程丢回
ode45积分,与原始慢变量做逐点误差,相对误差 < 5% 才算没跑偏。
这三条里前两条是数学层面的,第三条是物理层面的,缺一条都可能被"残差小但方程错"骗过去。
写到这里,最后收一句自己的教训:从那以后我每次拿到一份快慢混合数据,都会先不看回归结果、先把"快导数范数随时间的分布"画出来,如果分布本身没有明显的低值平台,说明这份数据要么采样率不够、要么快慢根本没分离,直接进 SINDy 一定白跑。希望这份笔记能帮你在拿到 Slow-Discovery 的第一天就绕过这个坑。
本文还有配套的精品资源,点击获取