简介这份代码包围绕 SINDy 方法与慢时间尺度动态发现展开面向非线性动力学、数据驱动建模及科学计算方向的学习者与研究者可用于复现 Bramburger、Dylewsky 与 Kutz 在 Physical Review E2020中的数据和图形。内容以 MATLAB 脚本为核心配合示例数据与说明文档覆盖玩具模型数值积分、连续时间 SINDy 模型拟合、粗粒度演化的离散时间映射发现以及木星、土星、混沌和 Logistic 等场景的慢发现实验适合用来理解多尺度系统中快速周期提取与慢动态预测的完整流程。压缩包共 10 个文件包含 7 个 .m 脚本、2 个 .mat 数据文件和 1 个 .md 说明文档整体约 24.17MB脚本与数据配套便于直接运行和对照修改。已有 380 人学习下载。读者可据此获得从数据生成、模型发现到预测验证的可复现实验链路并借助说明文档与脚本结构快速定位各实验模块理解滑动窗口 DMD 求快速周期、SINDy 辨识控制方程以及慢预测映射的衔接方式为论文复现和后续课题扩展提供参考。1. 慢与快的错觉logistic 混沌轨迹里为什么能挖出一条低维慢流形logistic 映射大概是很多人写下的第一行混沌代码x - r*x*(1-x)三行就能画出分岔图。但它也最容易被误读——它是一维离散递推本身没有任何“快慢”之分。真正反直觉的地方在于一旦把这个非线性项塞进带快变量的耦合系统数值轨迹里就会自发长出一条低维慢流形慢变量的有效方程甚至是一个四项多项式而 SINDy 能从几十个候选项里把它精确挑出来。Slow-Discovery 这套系统开源的 MATLAB 实现瞄准的正是这件事从多尺度数据里同时发现慢坐标和它的约化动力学而不是靠人眼看相图猜结构。搞懂它等于搞懂了“数据驱动建模”里最难的一环——为什么必须先分离时间尺度稀疏回归才可能给出可解释、可外推的方程。2. Logistic 映射的相空间可发现性从何而来在谈“发现慢动态”之前得先确认一件事待发现的结构确实存在于数据里而不是算法幻觉。logistic 映射之所以适合当试金石是因为它的分岔结构、Lyapunov 指数、不动点位置全都有解析解任何数值流程跑偏了都能立刻对照解析值发现。这一章把“数据从哪来、导数怎么估、快慢系统怎么造”三件事拆开因为它们恰好是后续所有翻车的源头。2.1 不动点、周期倍增与参数扫描的数值实现映射f(x) r*x*(1-x)有两个不动点x*0和x*1-1/r。稳定性由导数决定f(x) r*(1-2x)在非零不动点处f(x*) 2-r。当|2-r| 1即1 r 3时不动点稳定r 3时f -1发生 flip 分岔周期从 1 变 2此后每经过一次周期倍增参数区间长度按 Feigenbaum 常数约 4.669 收缩直到r ≈ 3.5699进入混沌。这些数字不是背景知识而是后面验证代码正确性的标尺参数扫描图如果画不出r3处的分叉后面的 SINDy 结果一律不可信。参数扫描的标准写法要区分“丢弃瞬态”和“保留稳态”两段否则图上会混入初值路径的伪影% 参数扫描每个 r 丢弃 nTrans 步瞬态再记录 nKeep 步 rList 2.4:0.001:4.0; nTrans 2000; % 瞬态步数需远大于 1/|log|f|| nKeep 200; % 每个参数留下的稳态点数 X zeros(numel(rList), nKeep); for k 1:numel(rList) r rList(k); x 0.4; % 初值固定便于复现 for n 1:nTrans x r*x*(1-x); end for n 1:nKeep x r*x*(1-x); X(k,n) x; end end plot(rList, X, ., MarkerSize, 1); % 分岔图nTrans的取值不是拍脑袋不动点附近的收敛速度由|f|决定越接近r3|f|越接近 1收敛越慢瞬态需要越长。用 2000 步对r3.5足够但对接近周期窗口边界的参数仍可能残留实际排查时我会把nTrans翻倍再跑一遍看分岔图是否发生位移——位移说明瞬态没丢干净。Lyapunov 指数同样可以顺手算出来它衡量的是“可发现性”的反面指数越大轨迹对初值越敏感从单条轨迹重建动力学的难度越高。% 沿轨迹估计最大 Lyapunov 指数 x 0.4; lambda 0; N 100000; for n 1:N x r*x*(1-x); lambda lambda log(abs(r*(1-2*x))); % 每步累积 log|f| end lambda lambda / N;lambda 0就是混沌的判据。这里有个容易被忽略的细节log|f|在x接近 0 或 1 时会掉到负无穷附近浮点下会给出-Inf累积几十万步后整个均值就废了。稳妥做法是给abs(r*(1-2*x))加一个 1e-12 的下限或者只在|f|1e-8的步上累积并记录有效步数。2.2 用有限差分构造导数矩阵噪声如何毁掉一切连续时间 SINDy 的核心等式是Xdot Θ(X)Ξ右边是候选函数库乘系数左边是状态导数。库矩阵可以从状态直接算导数却只能估。最常用的是二阶中心差分边界退化为单侧差分function Xdot diffDeriv(X, dt) % X: T x n按行是时间样本dt: 采样间隔 [T, n] size(X); Xdot zeros(T, n); Xdot(2:T-1,:) (X(3:T,:) - X(1:T-2,:)) / (2*dt); % 内部中心差分 Xdot(1,:) (X(2,:) - X(1,:)) / dt; % 首行前向 Xdot(T,:) (X(T,:) - X(T-1,:)) / dt; % 末行后向 end中心差分的截断误差是O(dt^2)看起来二阶挺体面但它对噪声的放大是致命的差分把噪声幅度除以dt方差放大1/dt^2。采样越密dt越小导数估计反而越脏。这就是为什么很多人把采样率调到很高之后SINDy 反而一个系数都恢复不出来——不是算法不行是导数被噪声淹没了。常见的实现思路是两条一是先平滑再求导Savitzky-Golay 滤波在窗口内做低阶多项式拟合能在压噪声的同时保留峰形二是根本不解连续时间方程改用离散时间形式X(k1) Θ(X(k))Ξ直接拿相邻样本当回归目标彻底绕开差分。后者对 logistic 这种本来就是离散映射的系统尤其自然代价是方程形式变成了映射而不是微分方程物理含义要重新解释。2.3 多尺度耦合系统的构造快慢分离怎么落到代码单靠 logistic 映射造不出“慢时间尺度”必须引入快慢耦合。一个足够小、又能解析求解的最小模型是dx/dt -a*x b*y^2 慢变量 dy/dt (-y x^2) / eps 快变量eps 1eps越小y收敛到x^2的速度越快。令dy/dt 0得到慢流形y x^2代回慢方程就得到约化动力学dx/dt -a*x b*x^4。这个四次多项式就是 SINDy 应该恢复出来的“真值”它同时包含了原始 logistic 型二次项经流形替换后升幂和线性衰减项结构足够简单又足以区分“恢复对了”和“凑巧拟合上了”。function dX fastSlow(~, X, p) % X [x; y]p.a 慢衰减率p.b 耦合强度p.eps 时间尺度比 x X(1); y X(2); dX [ -p.a*x p.b*y^2 ; % 慢方程 (-y x.^2) / p.eps ]; % 快方程eps 决定刚性 end积分要用ode15s而不是ode45eps 1e-3时系统已经是刚性的ode45会把步长压到eps量级跑完一段慢时间要几百万步。ode15s用隐式方法步长由慢时间尺度决定代价是每步要解线性方程组。采样输出时还要注意odeset(MaxStep, ...)的上限如果只按慢时间尺度给输出点快瞬态会被完全跳过——做“慢发现”时这正是想要的但做参数辨识时就会漏掉快变量的信息两种目标要分开设置。提示eps不要一上来就设成 1e-6。快慢比过大时ode15s的误差控制会把有效精度吃掉慢流形上的残差反而变大。先用 1e-2 到 1e-3 把流程跑通再逐步收紧。3. SINDy 的稀疏回归内核Θ(X)ΞXdot 到底在解什么SINDy 常被描述成“用稀疏回归发现方程”这话没错但漏掉了它真正的设计取舍它假设动力学在候选函数张成的空间里是稀疏的于是把“找方程”变成“在过完备字典里做变量选择”。这个转换一旦成立问题就从非线性拟合退化成了带阈值的最小二乘代价是候选库的设计、列归一化和阈值调度成了决定成败的三个旋钮。3.1 候选函数库的设计与量纲归一化库矩阵Θ(X)的每一行对应一个采样时刻每一列对应一个候选函数。对单变量x取到四阶库就是[1, x, x^2, x^3, x^4]双变量再加交叉项x*y, x^2*y, ...。列数随阶数和变量数组合爆炸n个变量、p阶多项式的列数是C(np, p)五变量四阶就已经 126 列而样本可能只有几百行——这就是典型的“宽矩阵”回归解不唯一必须靠稀疏性约束挑出唯一解。function Theta poolData(X, polyorder) % X: T x n 状态polyorder: 多项式最高阶 [T, n] size(X); Theta ones(T, 1); % 常数项先行 for k 1:polyorder % 单项式用索引组合生成所有 k 阶项 idx nchoosek(1:n, k); % 允许重复的组合需自行展开 for j 1:size(idx,1) Theta [Theta, prod(X(:, idx(j,:)), 2)]; end end end这段代码只是示意nchoosek不允许重复索引实际要展开成带重复的组合多重集组合。更重要的是库建好后必须先做列归一化把每列的 2-范数缩到 1否则x的量级是 1e0、x^4的量级可能只有 1e-3同一个阈值对两列的实际惩罚差了三个数量级阈值要么把小项全砍光要么把大项全留下。归一化后解出的系数再按归一化因子还原这一步漏掉系数数值全是错的但稀疏结构可能看起来“挺合理”属于最难发现的那类 bug。3.2 STLSQ阈值迭代为什么收敛到稀疏解序贯阈值最小二乘STLSQ的流程非常朴素先解一次普通最小二乘把绝对值小于阈值lambda的系数置零再用剩下的列重解最小二乘重复到支撑集不再变化。它本质上是L0正则的一个贪心近似比L1更容易控制“到底留几项”代价是没有全局最优保证初值和阈值调度会影响结果。function [Xi, iter] stlsq(Theta, Xdot, lambda, maxIter) % Theta: T x p 候选库已列归一化Xdot: T x n 导数 % lambda: 硬阈值maxIter: 最大迭代次数 [~, p] size(Theta); Xi Theta \ Xdot; % 初始普通最小二乘 for iter 1:maxIter small abs(Xi) lambda; % 找出该砍的系数 if ~any(small(:)), break; end % 支撑集不变则停 XiNew Xi; for j 1:size(Xdot, 2) keep ~small(:, j); % 当前列保留的候选 if ~any(keep), XiNew(:, j) 0; continue; end XiNew(keep, j) Theta(:, keep) \ Xdot(:, j); % 仅在支撑集上重解 XiNew(~keep, j) 0; end if isequal(XiNew ~ 0, Xi ~ 0), Xi XiNew; break; end Xi XiNew; end endlambda的量级取决于数据尺度常见做法是先跑一遍最小二乘看系数分布把阈值设在“大系数和小系数之间的空隙”里一般取最大系数的 1% 到 10%。如果系数分布没有明显空隙说明库设计有问题——要么缺了关键项要么存在高度共线的冗余列比如x和x^2在x ∈ [0,1]上相关系数能到 0.97这时阈值怎么调都是在两个几乎等价的解之间跳。注意Xi的稀疏结构稳定不代表系数稳定。共线列的系数会在两次迭代间大幅摆动而支撑集不变判断收敛要看系数的相对变化不能只看非零位置。3.3 用最小快慢系统验证系数恢复把第 2 章的fastSlow跑起来用慢采样采样间隔远大于eps只记录x就能验证整条链路。真值是dx/dt -a*x b*x^4库取到四阶理论上应该恢复出两个非零系数其余全为零。p struct(a, 0.3, b, 0.5, eps, 1e-3); opts odeset(RelTol,1e-8,AbsTol,1e-10,MaxStep,0.05); [t, X] ode15s((t,X) fastSlow(t,X,p), [0 40], [0.8; 0.64], opts); % 只保留慢变量并降采样到慢时间尺度 xSlow X(1:20:end, 1); dt t(20) - t(1); % 降采样后的间隔 Theta poolData(xSlow, 4); Theta Theta ./ vecnorm(Theta); % 列归一化 Xi stlsq(Theta, diffDeriv(xSlow, dt), 0.05, 10);跑通之后会看到恢复出的系数按归一化因子还原后接近(-0.3, 0.5)而三次项系数在阈值以下被清零。这里有个隐蔽的坑降采样比例必须让快变量在一个采样间隔内充分衰减eps/a是快时间尺度采样间隔要显著大于它否则慢流形假设不成立恢复出来的四次项系数会系统性偏大——因为残余的快瞬态被回归当成了慢动力学的一部分。4. 慢时间尺度发现把快变量“积掉”的工程实现前三章建立的流程有个隐含前提慢变量已经知道了。真实场景里观测量往往是多个状态的混合哪几个是慢的、慢流形长什么样都得从数据里判断。Slow-Discovery 这一层的价值就在这儿——它不假设尺度分离已知而是先用谱信息把快慢拆开再在慢子空间上做稀疏回归。这一步做错后面 SINDy 拟合得再漂亮也是拟合了错误的对象。4.1 时间尺度分离的量化特征值、快慢比与谱间隙最直接的判据是局部 Jacobian 的特征值。在慢流形上的任意一点求J ∂f/∂X实部绝对值最小的那几个方向对应慢模态最大的对应快模态两者的比值就是时间尺度分离度。上一节的模型在慢流形y x^2上快方向特征值约-1/eps慢方向约-a 4b*x^3分离度大约1/(a*eps)。这个数小于 10 的时候所谓“慢流形”只是一条被快动力学轻微拉扯的曲线约化误差会肉眼可见地进入系数。实际操作中 Jacobian 拿不到解析式只能用数据估对状态做局部线性拟合或者用有限差分构造ΔXdot/ΔX。更稳的办法是看谱间隙——把轨迹上所有点的局部 Jacobian 特征值收集起来按实部分布排序找最大的那道间隙。间隙明显比如差两个数量级以上说明快慢可分间隙模糊就该老老实实做全系统辨识而不是硬套慢流形。4.2 慢流形上的约化动力学与拟合目标的选择约化动力学的严格形式是dx/dt f(x, h(x))其中h是慢流形映射。工程上没人真的去解h常见的替代是两条一是直接对慢变量做 SINDy让稀疏回归顺便把h的影响吸收进有效系数里二是先拟合h比如用多项式回归y ≈ c0 c1*x c2*x^2再把h代回慢方程。前者简单但有效方程的系数不再有原始物理含义外推到慢流形之外会崩后者多一步可解释性更好。选择的关键在于“你要拿这个方程干什么”。如果只是做短期预测或控制器设计有效方程足够了如果想从系数反推物理参数就必须显式建模h否则b会被流形的曲率污染。我一般会两种都跑一遍看系数是否随采样窗口漂移——漂移大的那个版本说明它把不该吸收的东西吸收进去了。4.3 多尺度采样的三个参数采样率、窗口与对齐采样率的选择有个反直觉的约束既要大于快时间尺度否则慢流形不成立又要小于慢时间尺度否则慢动态被欠采样。可用的窗口是eps/a dt 1/a两个边界差着1/eps倍。eps 1e-3时窗口有三个数量级还算宽裕eps 1e-5时窗口窄到几乎没有容错这时候更稳的做法是用多尺度采样——快采样段估导数慢采样段拟合动力学两段数据在时间上对齐后联合回归。对齐这件事经常被忽略。ode15s输出的时间点是不等距的直接拿相邻样本差当导数dt在每行都不一样diffDeriv里那个固定dt就错了。必须先插值到等距网格或者把dt逐行传进去。插值本身会引入平滑相当于隐式低通滤波对慢发现是好事对快瞬态识别是坏事得按目标取舍。5. 避坑与排查跑不出正确系数的五种典型翻车理论推完真正花时间的永远是调试。下面五条是按“现象 → 原因 → 解决”整理的高频问题基本覆盖了从数据准备到结果解释的主要失败模式。现象一稀疏回归返回全零或者一个系数都不砍。原因几乎总是归一化和阈值不匹配库没做列归一化各列量级差几个数量级同一个lambda对大项形同虚设、对小项一刀切或者lambda相对系数尺度设得过大过小。解决方法是先打印最小二乘解的系数绝对值排序看有没有明显的数量级断层把lambda设在断层中间同时确认归一化后每个系数的还原因子都记录下来了。现象二系数符号反了或者量级差了 2 到 10 倍。常见原因是导数估计的系统性偏差。中心差分对二次项是无偏的但对高阶项有O(dt^2)的截断误差x^4项的系数会被系统性低估另一种情况是时间单位没统一t用秒、dt用毫秒系数正好差 1000 倍。排查方法是把数据对半切开两段分别拟合如果系数不一致多半是导数估计或采样对齐的问题而不是稀疏回归本身。现象三换一个采样率稀疏结构完全变了。这是离散 SINDy 和连续 SINDy 混用的典型症状。离散形式X(k1) Θ(X(k))Ξ的系数依赖采样间隔连续形式不依赖如果用离散形式拟合再拿系数去解释连续方程采样率一变结论就变。解决办法是明确建模对象要从数据恢复微分方程就必须用连续形式并保证导数估计质量要的是预测器离散形式更稳但别把系数当物理参数解读。现象四训练残差极小外推两步就发散。残差小只说明拟合了样本内的流形不代表方程对。慢流形假设被破坏时最典型eps不够小快变量没有完全弛豫回归把残余快模态吸收进了慢方程样本内拟合得很好一旦外推到快模态被激发的区域立刻崩。验证方法是留出一段初始瞬态不参与拟合用拟合出的方程从同一初值重新积分看它能否复现那段瞬态——能复现说明方程抓到了动力学不能说明只是曲线拟合。现象五加点噪声非零项的位置就开始跳。STLSQ 是硬阈值噪声让系数在阈值附近抖动时支撑集会整项进出。解决方法是做集成对数据分块或加 bootstrap 重采样跑几十次 SINDy统计每一项被选中的频率只有高频入选的项才保留系数取中位数。这一步多花几分钟但能把“偶然入选”的伪项滤掉尤其在候选库列数远多于样本数时几乎是必需的。提示排查顺序建议固定为“先看导数、再看归一化、最后调阈值”。反过来做会在一个本来就错的导数上反复调参调到怀疑人生。6. 设计取舍先分离尺度再回归还是端到端硬拟合把整条链路走完会发现 Slow-Discovery 的核心主张其实是一个取舍它宁可多花一步做尺度分离和慢坐标识别也不愿意把原始多尺度数据直接丢给稀疏回归。这个选择在工程上并不总是最优值得摊开对比。维度端到端直接回归先分离尺度再回归实现复杂度低库建好就能跑高需要谱分析或慢坐标估计样本需求大快慢模态混在一起较小慢子空间维度低系数可解释性差快模态被吸收进慢项好系数对应约化动力学外推稳定性差离开训练流形易发散较好前提是流形假设成立失败模式残差小但方程错难察觉分离度不足时约化误差可见表格里最有信息量的是最后一行。端到端方法的失败是“静默”的——残差小、图好看、系数也像模像样但方程是错的分离尺度的方法失败时谱间隙会直接告诉你分离度不够错误是“可见”的。对做机理分析的人来说可见的失败远比静默的失败有价值。验证方法上我习惯做两件事。第一件是“闭环重积分”用恢复出的方程从训练数据的初值重新积分和原始轨迹对比看偏差是否随时间指数增长——增长速率应该接近最大 Lyapunov 指数如果远大于它说明方程引入了虚假的不稳定性。第二件是“参数扰动”把eps调大 10 倍重跑整个流程如果恢复出的慢方程系数基本不变说明分离是真实的如果系数跟着大幅漂移说明之前的“慢流形”只是恰好在这个eps下看起来像流形。% 闭环验证用恢复的系数重新积分对比原始慢轨迹 fRec (t, x) Xi(2)*x Xi(5)*x.^4; % 常数项 一次项 四次项 [tv, xv] ode15s((t,x) fRec(t,x), [t(1) t(end)], xSlow(1)); err abs(interp1(tv, xv, t(1:20:end)) - xSlow.); % 若 err 的增长率显著大于 Lyapunov 指数方程不可信这段验证代码里Xi的下标依赖库的列顺序常数、一次、二次、三次、四次换一种库构造方式就要跟着改所以务必把列名和索引一起存下来别只存系数矩阵——这是我踩过最蠢的一次坑隔了两周回头完全对不上号。真正让我改变工作习惯的是一次在快慢比只有 5 左右的系统上硬套慢流形约化拟合残差 1e-4闭环预测三步就发散查了两天才发现谱间隙根本不够。从那以后我每次跑 SINDy都强制先做一遍尺度分离检查和导数残差自检两步都过了才开始调阈值——希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?