为了深入理解fMRI分析的原理,还是要学习一些微分方程与动力系统的东东,这是我的学习笔记,内容还没有很好的整理。
视频在这里1-Differential Equations and Dynamical Systems Overview_哔哩哔哩_bilibili
课程的原始地址在这里:ME 564 - Mechanical Engineering Analysis (washington.edu)
1. Overview
线性代数是微分方程的一部分,如果可以的话,大学课程应该将这两部分一起传授。这门课程除了基本的模型介绍以外,还会结合Matlab/Python的代码来进行具体的实现。
这门课程会介绍以下几个主题:
- 微积分:简单回顾微积分的计算,重点是泰勒展开
- 简单常微分方程(Simple Ordinary Differential Equations, ODEs):即,可以用于描述很多系统(如最常见的兔子吃草问题)
- 常微分方程组(System of ODEs):即,常见的问题如在兔子吃草过程中引入其他动物(比如狼)
- 特征值与特征向量
- 非线性系统与混沌:即,常见的问题如行星运动
- 数值与计算:用Python或者Matlab
2. 微积分回顾:导数
导数反映的是一个函数对于独立变量的变化率。其定义如下:
链式法则(Chain Rule):
例子:和,则:
3. 向量和矩阵建模
【一个天气模型】在这个模型中,天气有三种状态,分别是多云(cloudy)
、下雨(rainy)
和晴天(nice)
,已知:
上面公式表示一个概率,例如第一条表示如果前一天的天气为多云时,则第二天的天气为多云的概率是0.5,后面以此类推。此时,我们可以用一个向量来表示当前的天气情况:
上式中表示当前的天气为晴天。则第二天的前期状态可以根据之前的概率得到:
不难看出,为:
此时,可以换一种形式来表示:
clear all, close all, clc
A = [0.5 0.5 0.25;
0.25 0.0 0.25;
0.25 0.5 0.5];
xtoday = [1; 0; 0];
for k=1:50
k
xtomorrow = A * xtoday
xtoday = xtomorrow;
end
运行上述代码可以看到,这个模型中天气会收敛到一个稳定的状态,。
有意思的事情是,这个稳态值和矩阵特征向量与特征值的关系。可以用eig
函数来得到矩阵的特征值与特征向量:
[T,D]=eig(A)
T =
-0.6667 -0.7071 0.4082
-0.3333 0.0000 -0.8165
-0.6667 0.7071 0.4082
D =
1.0000 0 0
0 0.2500 0
0 0 -0.2500
此时如果对第一个特征值对应的特征向量进行归一化处理,可以得到:
T(:,1)/sum(T(:,1))
ans =
0.4000
0.2000
0.4000
可以看到其最终结果就是系统的稳态分布,因此可以用这种方法来计算得到稳态分布,而不是使用迭代的方法。
上面过程的Python代码如下:
import numpy as np
import matplotlib.pyplot as plt
A = np.array([[0.5, 0.5, 0.25],[0.25, 0, 0.25],[0.25, 0.5, 0.5]])
xtoday = np.array([[1], [0], [0]])
the_weather = np.zeros((3,50))
for k in range(50):
the_weather[:,k] = xtoday[:,0]
xtomorrow = A@xtoday
xtoday = xtomorrow
print(k)
print(xtomorrow)
plt.plot(the_weather.transpose())
plt.grid(True)
4. 简单常微分方程和它的指数解
简单常微分方程的形式:
【例子】假设在一个草原上有只兔子,兔子的增长速度与兔子的数量成正比。这个问题就可以描述为上面的公式:
其中,表示增长率,是时间的函数。后面我们还会延续这个模型,引入一些其他的动物。
4.1 微分方程求解
要想计算这个微分方程,有很多种方法:
【method 1】
根据上式,不难得到以下结论:
易知上述微分方程的差分形式为:
5. 使用幂级数求解微分方程
对于常微分方程,假设函数可以写成幂级数的形式,可知:
则
同时:
的对应系数应该相等,因此:
所以:
6. 泰勒级数和幂级数
一个函数可以在一个点(在处光滑)被泰勒展开:
如果在特定的点(即)展开,上述公式也可以写为如下形式:
【例1】当时,其泰勒展开为:
【例2】当时,其泰勒展开为:
Matlab代码如下:
clear all, close all
x = -10:0.01:10;
y = sin(x);
plot(x, y, 'k', 'LineWidth',2)
axis([-10 10 -10 10])
grid on, hold on
%% 一阶泰勒展开
P = [1 0];
yT1 = polyval(P, x);
plot(x, yT1, 'b--', 'LineWidth', 1.2)
%% 三阶泰勒展开
P = [-1/factorial(3) 0 1 0];
yT3 = polyval(P, x);
plot(x, yT3, 'r--', 'LineWidth', 1.2)
%% 五阶泰勒展开
P = [1/factorial(5) 0 -1/factorial(3) 0 1 0];
yT5 = polyval(P, x);
plot(x, yT5, 'g--', 'LineWidth', 1.2)
%% 七阶泰勒展开
P = [-1/factorial(7) 0 1/factorial(5) 0 -1/factorial(3) 0 1 0];
yT7 = polyval(P, x);
plot(x, yT7, 'm--', 'LineWidth', 1.2)
%% 九阶泰勒展开
P = [1/factorial(9) 0 -1/factorial(7) 0 1/factorial(5) 0 -1/factorial(3) 0 1 0];
yT9 = polyval(P, x);
plot(x, yT9, 'c--', 'LineWidth', 1.2)
Python的泰勒展开结果如下:
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.special import factorial
x = np.linspace(-10, 10, 2000)
y = np.sin(x)
plt.plot(x, y, 'k', linewidth=2)
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.grid(True)
# 一阶泰勒展开
P = [1, 0]
yT1 = np.polyval(P, x)
plt.plot(x, yT1, 'b--', linewidth=1.2)
# 三阶泰勒展开
P = [-1/factorial(3), 0, 1, 0]
yT3 = np.polyval(P, x)
plt.plot(x, yT3, 'r--', linewidth=1.2)
# 五阶泰勒展开
P = [1/factorial(5), 0, -1/factorial(3), 0, 1, 0]
yT5 = np.polyval(P, x)
plt.plot(x, yT5, 'g--', linewidth=1.2)
# 七阶泰勒展开
P = [-1/factorial(7), 0, 1/factorial(5), 0, -1/factorial(3), 0, 1, 0]
yT7 = np.polyval(P, x)
plt.plot(x, yT7, 'm--', linewidth=1.2)
# 九阶泰勒展开
P = [1/factorial(9), 0, -1/factorial(7), 0, 1/factorial(5), 0, -1/factorial(3), 0, 1, 0]
yT9 = np.polyval(P, x)
plt.plot(x, yT9, 'c--', linewidth=1.2)
7.指数函数的泰勒级数和欧拉公式
本节课利用泰勒级数推导出欧拉公式,过程如下:
得到上述公式后,后面的计算会变得更加容易
8. 用二阶常微分方程描述谐振子
此处,我们引入了一个物理学中的常见概念——谐振子,并使用微分方程来对其状态进行建模。
问题描述如下:在一个光滑平面上,有一个质量为 的物体,其一端用弹簧与墙壁相连,弹簧的系数为k,物体偏移静止状态(即除支撑力外,不受任何弹簧弹力)的位移为,则可知:
令,,则:
求解上述方程的方法有很多种:
【Method 1】瞪眼法
因为,,结合前面的泰勒展开结果,充分考虑这些初始条件,则可以猜测
则
而上述结果满足之前的,于是瞪眼成功。
对于一般的和,
【Method 2】泰勒级数展开
因此
将其带回,可得:
【Method 3】瞪眼法2
一个函数求导之后仍然是自身的函数,最容易想到的就是指数函数,因此不妨猜测是一种指数函数。
将其代入微分方程,可得:
所以:
【Method 4】Suspend variable
在该方法中,引入一个新的变量
令:
则:
9. 二阶ODE,阻尼震荡器
新的问题变成了添加了一个阻尼的震荡子,如上图所示,此时模型变为:
令
所以:
看到这个模型,不难继续要使用瞪眼法,令:
所以:
这个方程被称为特征方程。
其解为:
最终
还可以将模型写为矩阵形式:
下面我们在Matlab中对这个过程进行模拟,需要注意的是:
看懂了上式就可以对上述过程进行编码了,具体代码如下
clear all; close all;
w = 2*pi;
d = 0.25;
% dt = Ax,此处和文中给出的公式并不一致,相当于k=.5,但是只要知道这是在做一些系统初始化的设置即可
A = [0 1; -w^2 -2*d*w];
dt = .01; % time step
T = 10; % amount of time to integrate
% 初始位置在2处,初始速度为0
x0 = [2; 0];
xF(:,1) = x0;
tF(1) = 0;
for k=1:T/dt
tF(k+1) = k*dt;
xF(:,k+1) = (eye(2) + dt*A)*xF(:,k);
end
plot(tF, xF(1,:), 'k')
% 使用龙格库塔(Runge Kutta)得到的结果
[t, xGood] = ode45( @(t, x) A*x, 0:dt:T, x0);
hold on
plot(t, xGood(:,1), 'r')
xlabel('Time (s)')
ylabel('Position (m)')
legend('Forward Euler', 'ODE45(RK4)')
figure
plot(xGood(:,1),xGood(:,2),'k')
xlabel('Position [m]')
ylabel('Velocity [m/s]')
import numpy as np
from matplotlib import pyplot as plt
from scipy.integrate import solve_ivp
# play around with different 'd' and 'dt' to see various behavior!
w = 2 * np.pi # natural frequency
d = .25 # damping ratio
# spring-mass-damper system
A = np.array([[0, 1], [-w ** 2, -2 * d * w]]) # \dot{x} = Ax
dt = 0.01 # time step
T = 10 # amount of time to integrate
n = int(T / dt)
t = np.linspace(0, T, n)
x0 = [2, 0] # initial condition (x=2, v=0)
# iterate forward Euler
xF = np.zeros((2, n))
xF[:, 0] = x0
for k in range(n - 1):
xF[:, k + 1] = (np.eye(2) + dt * A) @ xF[:, k]
# compute better integral using built-in python code
# 4th-order Runge Kutta
def linear_ode(t, x):
return A @ x # @ symbol for matrix-vector product here
linear_ode_solution = solve_ivp(linear_ode, (0, T), x0, t_eval=t)
xGood = linear_ode_solution.y
plt.figure(figsize=(20, 4))
plt.subplot(1, 2, 1)
plt.plot(t, xF[0, :], 'k')
plt.plot(t, xGood[0, :],'r')
plt.xlabel('Time [s]')
plt.ylabel('Position [m]')
plt.legend(['Forward Euler', 'ODE45 (RK4)'])
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(xF[0, :], xF[1, :], 'k')
plt.plot(xGood[0, :], xGood[1, :], 'r')
plt.xlabel('Position [m]')
plt.ylabel('Velocity [m/s]')
plt.legend(['Forward Euler', 'ODE45 (RK4)'])
plt.grid(True)