Koopman算子数据驱动建模:Chemostat模型EDMD回归与验证
2026/9/15 2:24:21 网站建设 项目流程

简介:一个基于Koopman算子理论对Chemostat模型进行数据驱动建模的Matlab代码包,适合自动化、生物工程、数学及相关专业本科生用于课程设计、期末大作业或毕业设计。程序采用参数化编程,结构清晰,注释详细,支持在Matlab 2014/2019a/2021a中直接运行,并附带可复现的示例数据。包内共9个文件,其中6个.m脚本分别负责Monod动力学、PRBS激励生成、RBF近似、Koopman模型构建与RMSE误差评估等环节,另含2张可视化结果图与1份Markdown说明文档,整体压缩后仅116KB,轻量易部署。目前已有372人学习使用,适合想快速上手数据驱动建模与非线性系统辨识的读者。通过该代码可掌握从激励设计、数据采集、Koopman字典构造到模型验证的完整流程,并可直接迁移到其他生物反应器或连续发酵过程的建模场景中。

1. 用Koopman算子理论做数据驱动建模,Chemostat模型是最合适的落点

用Koopman算子理论做数据驱动建模,最常被问的一句话是:非线性系统凭什么能放进一个线性矩阵里?Chemostat模型是最合适的答案——菌体X和底物S两个状态,Monod方程把比生长速率和底物浓度拧成一条饱和曲线,泰勒展开的线性模型只在小邻域成立。

Koopman的思路是换空间看问题:把状态x提升到一组观测函数ψ(x),高维观测空间里非线性演化变成线性算子K的重复作用。K不靠推导,靠快照数据拟合,这正是数据驱动建模的含义。

下文按“模型→提升→回归→验证→体检”的顺序拆解这套流程,代码全部用MATLAB裸函数实现,不依赖封装库,适合生物过程建模工程师,也适合刚接触Koopman算子的研究生。

2. Chemostat模型的状态方程与Koopman算子理论的衔接点

2.1 二维状态方程、Monod动力学与五个关键参数

Chemostat是在恒定搅拌、恒定进排料的连续培养装置上抽象出来的模型,对应理想CSTR生物反应器,状态只有两个:生物量浓度X(g/L)和限制性底物浓度S(g/L)。物料平衡写成

dX/dt = μ(S)·X − D·X

dS/dt = D·(S_in − S) − μ(S)·X/Y

比生长速率μ(S)取Monod形式:μ(S) = μ_max·S / (K_s + S)

五个参数一个都不能省,下面这组值是示例用的默认设置。

参数符号示例值单位含义
最大比生长速率μ_max0.3h⁻¹底物饱和时的最快生长速度
半饱和常数K_s0.1g/Lμ达到μ_max/2时的底物浓度
产率系数Y0.4g/g每消耗1g底物生成的菌体量
稀释率D0.1h⁻¹进料流量与体积之比,也是控制输入
进料底物浓度S_in5g/L新鲜培养基的底物浓度

模型的非线性全在μ(S)·X这一项:S远大于K_s时μ趋近μ_max,系统接近线性;S和K_s同数量级时饱和曲线弯得最厉害,正好是平衡点线性化误差最大的区域。数据驱动建模的目标,就是不依赖平衡点,直接用轨迹数据把整个区域的动力学装进一个线性框架。

对应的MATLAB函数如下,后面所有数据生成都调它:

function dx = chemostatODE(~, x, p) % 状态 x = [X; S],参数 p 为结构体,字段名见上表 X = x(1); S = x(2); mu = p.mu_max * S / (p.Ks + S); % Monod 比生长速率 dx = zeros(2,1); dx(1) = mu * X - p.D * X; % 菌体:生长 − 排料流失 dx(2) = p.D * (p.Sin - S) - mu * X / p.Y; % 底物:进料 − 消耗 − 排料 end

注意:D一旦超过μ_max,稀释速度快于菌体最大生长速度,系统会走向“洗出”(washout),X衰减到0。训练数据只覆盖D<μ_max的稳定工况时,拟合出的K在这些区域没有外推能力。

2.2 把ODE变成离散映射F再谈Koopman算子

Koopman算子理论作用的对象不是连续时间微分方程,而是离散映射 x_{k+1}=F(x_k)。对Chemostat来说,F就是间隔Δt的流映射:给定当前状态,把ODE从t积分到t+Δt,取末端。MATLAB里用ode45跑一步就能实现F的一次求值。

定义观测函数ψ(x)(标量函数),Koopman算子K满足 (Kψ)(x)=ψ(F(x))。这个定义把“当前状态上的观测值”搬到“下一时刻状态上的观测值”,而K在函数空间上是线性的。一个非线性映射F,在无穷维函数空间里被一个线性算子精确表示,代价是维度无穷。实际使用只能选N个观测函数ψ₁,…,ψ_N,把K投影到有限维,这就是EDMD(扩展动态模态分解)要做的事。

Chemostat的状态维数只有2,初学阶段最容易犯的错是“状态小所以观测库可以很小”。观测库要覆盖μ(S)·X这个有理函数项在目标区域里的变化。μ(S)=μ_max·S/(K_s+S)在物理域S>0上没有奇点,多项式可以一致逼近它,所以多项式基是这里最省事的选择——比随机傅里叶特征少两个超参数,窄域内效果也够好。想从连续时间角度继续深入,还可以讨论Koopman生成子(generator),但对数据驱动任务,离散EDMD是更低门槛的入口:只需要快照对和一次最小二乘,不需要微分观测。

2.3 EDMD的拟合目标:让K近似满足Ψ_y ≈ K·Ψ_x

把快照对写成矩阵:Ψ_x每一列是ψ(x_k),Ψ_y每一列是ψ(x_{k+1})。如果字典正好张成K的不变子空间,就存在K使Ψ_y=K·Ψ_x精确成立;一般情况下取最小二乘解

K = Ψ_y · Ψ_x⁺

其中Ψ_x⁺是伪逆。这就是EDMD的全部核心:没有迭代优化、没有反向传播,一次最小二乘投影。伪逆计算基于SVD,Ψ_x的奇异值会告诉你哪些观测方向在数据里有足够能量、哪些接近零而不可辨识——奇异值掉到机器精度以下的方向,对应的K元素没有数据支撑,强行保留只会放大噪声。后面章节的MATLAB代码全部围绕“构造Ψ_x、Ψ_y → 求K → 用K做多步推演”三步展开,调参不再是试运气,而是判断“字典有没有覆盖轨迹经过的区域”。

3. 用MATLAB从Chemostat仿真数据回归Koopman矩阵

3.1 数据生成:多初值扫轨迹,拼出快照对

EDMD要的数据是“快照对”,即前后两步的状态对(x_k, x_{k+1})。Chemostat是ODE模型,先用ode45生成连续轨迹,再按固定间隔Δt采样。最常见的坑是只用一条轨迹训练:单轨迹只覆盖一维流形,K在垂直方向的行为完全没有约束,换个初值预测就飘。我一般扫至少5个初值,把平衡点两侧都覆盖到。

p.mu_max = 0.3; p.Ks = 0.1; p.Y = 0.4; p.D = 0.1; p.Sin = 5; dt = 0.2; % 采样间隔,约为时间常数的1/50 Tend = 40; % 单条轨迹时长,h X0s = [0.6 1.2 1.8 2.4 3.0]; % 5个不同初值 Xall = []; Yall = []; for k = 1:numel(X0s) [t, x] = ode45(@(t,x) chemostatODE(t,x,p), 0:dt:Tend, [X0s(k) p.Sin]); Xall = [Xall; x(1:end-1,:)]; % 当前时刻快照 Yall = [Yall; x(2:end,:)]; % 下一时刻快照(间隔 dt) end nS = size(Xall, 1);

时间尺度的选择有讲究:Chemostat主时间常数约1/D=10h,dt取0.2h,一条轨迹里有约50个采样点去分辨一次典型瞬态,快照对包含足够可学信息。dt太小会让相邻快照几乎相同,Ψ_x各列高度共线,伪逆病态;dt太大则快照对之间丢失中间动态,拟合出的K只对大步长有效。我习惯先按时间常数的1/50量级试,再看验证误差决定是否加密。

初值方面,除了X0,也可以把S0做扰动,比如S0=S_in±1,让快照点在状态空间铺得更开。另外确保至少有一条轨迹经过稳态附近:常数观测1要靠稳态数据校准,轨迹全从瞬态开始、在到达稳态前就截断的话,K(1,:)那一行的拟合会偏。数据生成这一步的目标不是“模拟得像”,而是让快照对覆盖关心的状态区域,K的适用范围完全由这一步决定。

3.2 多项式观测库:liftPoly的实现与维度计算

观测函数库是EDMD里唯一的“模型结构”选择。二维Chemostat用全Monomial基就够,度从1到3依次扩张。实现时两条铁律:字典第一行必须是常数观测1,相当于线性模型里的偏置;原始状态X、S必须原样放进字典前几行,这样推演结束后才能直接读回状态。

function Psi = liftPoly(x, deg) % x: n×nS 快照矩阵;返回 N×nS 观测矩阵 n = size(x,1); nS = size(x,2); Psi = [ones(1,nS); x]; % 常数项 + 原始状态 if deg >= 2 b2 = {}; for i = 1:n for j = i:n b2{end+1} = x(i,:).*x(j,:); % X^2, X*S, S^2 end end Psi = [Psi; vertcat(b2{:})]; end if deg >= 3 b3 = {}; for i = 1:n for j = i:n for kk = j:n b3{end+1} = x(i,:).*x(j,:).*x(kk,:); % X^3, X^2*S, ... end end end Psi = [Psi; vertcat(b3{:})]; end end

循环里i≤j、j≤kk的约束是为了不重复枚举X·S和S·X这类同一项。维度计算:deg=1时N=3,deg=2时N=6,deg=3时N=10。N是Koopman矩阵K的尺寸,观测库变大拟合误差一定下降,但K的未知元素按N²增长,快照数nS不足时就开始过拟合。对2状态系统,deg=3通常够用,deg=4带来的提升远小于扩大数据区域带来的提升。

3.3 EDMD回归:一行伪逆命令得到K

有了快照对和字典,拟合就剩一行代码。MATLAB的mrdivide(/)解的是最小二乘问题,使‖K·Ψ_x−Ψ_y‖F最小,等价于K=Ψ_y·pinv(Ψ_x),但对秩亏情形的容差处理更稳。

deg = 3; PsiX = liftPoly(Xall', deg); % N×nS PsiY = liftPoly(Yall', deg); K = PsiY / PsiX; % K = PsiY * pinv(PsiX) 的最小二乘写法 % 病态明显时改用岭回归: % lam = 1e-6; % K = PsiY * PsiX' / (PsiX * PsiX' + lam * eye(size(PsiX,1)));

拟合完先做两个体检。一是看K的第一行:字典第一行是常数观测1,1经过映射还是1,所以K(1,:)应当近似等于[1,0,…,0],偏差超过1e-3就说明数据或字典有问题。二是看max(abs(eig(K)))是否超过1,超过1说明K在重复作用时发散,多半是数据覆盖不足或秩亏。

数值尺度上有个容易忽略的点:S量级约5 g/L、X约0.6~3 g/L,deg=3后S³≈125、X³≈27,差值放大会让Ψ_x条件数变大。训练前把X、S归一化到零均值单位方差,条件数能降一个数量级,具体做法放在4.2节。

4. Koopman模型的多步推演验证与观测库参数调优

4.1 从任意初值推演整条轨迹

K拟合出来后,验证方式是开环推演:给定新初值x0,反复左乘K,在前N个观测里读回状态,得到整条预测轨迹。只有用训练时没见过的初值做验证,结果才有意义。

function xPred = koopmanPredict(K, x0, nPred, deg) % 从 x0 推演 nPred 步;状态维数由 x0 自动确定 n = numel(x0); psi = liftPoly(x0(:), deg); % 初始观测向量,N×1 xPred = zeros(n, nPred+1); xPred(:,1) = x0(:); for k = 1:nPred psi = K * psi; % 观测空间里线性演化一步 xPred(:,k+1) = psi(2:n+1); % 状态占据观测向量第2到第n+1行 end end

推演时每步只做一次矩阵乘向量,nPred=100步也就是100次乘法。这是Koopman方法相对ODE积分最大的实际收益:模型离线拟合好之后,在线推演没有任何ODE求解开销。注意训练时若做了归一化,x0也要先归一化、读回的状态要反归一化,否则前一两步就有明显偏置误差。

验证脚本把预测轨迹和ode45真解放在一起,记录每个时刻的RMSE:

x0 = [1.5, 5]; % 训练时没用过的初值 nPred = 75; % 推演 15 h(dt=0.2) xPred = koopmanPredict(K, x0, nPred, 3); [~, xTrue] = ode45(@(t,x) chemostatODE(t,x,p), 0:dt:nPred*dt, x0); err = sqrt(sum((xPred - xTrue').^2, 1)); % 每个时刻的欧氏误差

一个常见误用是拿训练初值去验证,误差当然小,但反映的是插值能力而不是外推能力。Koopman模型本质上是数据区域内的插值器,验证初值要离训练集远一些,最好处于不同的平衡点邻域,才算检验了模型的泛化边界。对一组验证初值重复这个流程,取每个时刻误差的最大值或90分位数作为汇总指标,比单条轨迹的RMSE更接近实际使用场景。

4.2 观测库的3个必调参数:阶数、常数项与归一化

阶数deg。deg=1相当于对快照对做线性回归,K只有3×3,是最朴素的数据驱动线性模型;deg=2加入X²、XS、S²,RMSE通常出现最大的一次下降;deg=3再降一截但幅度变小。调参做法是写个for循环把deg∈{1,2,3,4}各拟合一次,画同一验证集上的RMSE-步数曲线,选曲线尾部不再明显下降的最小deg。不要在deg上贪多,N=10以上每加一项观测,K的未知数多出约20个(按N²涨),快照数不够时噪声会被拟合进去。

常数项。字典第一行必须放常数1,相当于线性模型的偏置。删掉它,K无法表达系统的非零平衡点,推演会持续漂移。检查方法还是看K(1,:):拟合正常时它应当极端接近[1,0,…,0];偏差超过1e-3,说明数据没覆盖到稳态附近,或者字典构造有误。

归一化。这是收益最大且最容易被跳过的一步。X和S量级不同,高次项的数值差异直接放大Ψ_x条件数。做法是先把所有快照按状态标准化,再提升、回归:

Z = [Xall; Yall]; % 快照按行堆叠,用于统计 m = mean(Z, 1); sd = std(Z, 0, 1); % 每状态的均值与标准差 XallN = (Xall - m) ./ sd; % 训练快照标准化 YallN = (Yall - m) ./ sd; % 目标快照同样标准化 % 之后用 liftPoly(XallN', deg)、liftPoly(YallN', deg) 求 K % 预测时: x0N = (x0 - m) ./ sd; 推演后 xN = psi(2:n+1) 再反归一化 % 恢复: x = xN .* sd + m;

判断要不要归一化有个简单标准:打印cond(PsiX),大于1e6就做。标准化之后deg=3、deg=4的拟合稳定性明显改善。

4.3 与泰勒线性化和BiLSTM/NARX的边界对比

Koopman EDMD在方法谱系里的位置,用一张表说清楚:

方案非线性处理方式多步外推边界训练成本可解释性
Koopman EDMD提升到观测空间后线性观测库覆盖的区域一次最小二乘可做谱分析
平衡点泰勒展开一阶截断平衡点小邻域解析求雅可比
NARX / BiLSTM端到端非线性映射训练分布内迭代训练加调参

工程评估里,泰勒展开输在适用范围:D从0.1改成0.15,平衡点移动,原雅可比失效,要重新推导。BiLSTM这类网络赢在免特征工程,输在训练成本和可解释性:如果你在MATLAB里用BiLSTM做过SOC估计这类时间序列回归,会明显感受到数据量、训练轮数和随机种子对结果的影响;Koopman没有这些超参数,K可以直接特征值分解,哪些模态快衰减、哪些慢,一目了然。Koopman夹在两者中间:比泰勒展开适用域大,比神经网络省数据和算力,代价是观测库设计——也就是4.2节那三个参数。

提示:当控制输入D也参与建模时,快照对要扩展成(x_k,u_k)形式,K变成输入仿射的扩展Koopman模型,这一步是EDMD往控制方向走的自然延伸。

5. Koopman矩阵的两个快速体检:谱检查与推演误差曲线

5.1 看一眼特征值,断定K会不会发散

K拟合完先别急着看RMSE,先打印特征值:

ev = eig(K); [maxAbsEv, idx] = max(abs(ev)); fprintf('最大特征值模: %.6f, 对应特征值: %.4f + %.4fi\n', ... maxAbsEv, real(ev(idx)), imag(ev(idx)));

对本例的Chemostat参数,D=0.1时系统存在稳定平衡点,离散映射F是压缩的,K的特征值应全部落在单位圆内或单位圆上。单位圆上的特征值来自常数观测:常数是K的特征函数,特征值精确等于1。其余特征值模小于1,模的负对数除以dt就是对应模态在连续时间下的衰减率;幅角反映模态是否振荡。如果max|λ|明显大于1,重复左乘K必然发散,不必看误差曲线就能判定模型不可用。

5.2 用误差曲线的形状区分“字典不够”和“实现有bug”

推演误差曲线比单点RMSE的信息量大得多。对验证集每个初值单独画err随步数的曲线,按形状分类:

  • 前2步误差就跳到0.1以上:实现问题,优先查读回行号和归一化是否对称——训练标准化了预测没标准化,或者两者顺序反了。
  • 误差平滑但持续增长:轨迹正在离开训练区域,字典覆盖不足。对策是增加训练初值,把验证轨迹所在区域纳入快照集,或者提高deg。
  • 误差缓慢增长但收敛后有偏:字典够用,常数项未准确表达平衡点偏移,检查K(1,:)是否偏离[1,0,…,0]。
  • 误差先降后升:初值位置不同导致的正常现象,评价指标以末尾时刻误差为准。

提示:把特征值检查、K(1,:)检查和误差曲线画进同一个validateKoopman脚本,每次拟合完先跑它再谈精度,能省掉大量无效调参。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询