问题引入¶
- 布朗运动(Brownian motion,1827年):观察到水中大颗粒(如花粉微粒)呈现随机游动。
- 爱因斯坦在1905年解释布朗运动:水分子反复碰撞颗粒的宏观表现。
- 微观确定与宏观随机:
- 模拟相互碰撞的圆盘
- 每个圆盘严格遵循牛顿定律
- 但仅观察单一圆盘时,其轨迹呈现高度随机性
准备工作¶
In [1]:
import timeit # 用于计时
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, IntSlider
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC"]
可视化随机游走¶
随机游走 (Random Walk) 模型
- 定义:模拟物体在时空中的随机运动过程。
- 规则:在每个离散时间步,物体朝随机方向移动固定距离。
- 目标:在二维空间中模拟并可视化其运动轨迹。
In [2]:
def walker2d_step():
directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]
return directions[np.random.randint(4)]
def trajectory_2d(N):
x, y = 0, 0
positions = [(x, y)]
for _ in range(N):
dx, dy = walker2d_step()
x += dx
y += dy
positions.append((x, y))
return positions
_traj2d = trajectory_2d(10)
_N_val = 1
import math
@interact(N=IntSlider(min=1, max=6, step=1, value=1, description="N:"),
t=IntSlider(min=1, max=100, step=1, value=1, description="t (%):"))
def plot_walk2d(N, t):
global _traj2d, _N_val
if N != _N_val:
_N_val = N
_traj2d = trajectory_2d(10**N)
traj = _traj2d
t = int(math.ceil(len(traj) * t / 100))
xs = [p[0] for p in traj[:t]]
ys = [p[1] for p in traj[:t]]
fig, ax = plt.subplots(figsize=(8, 8))
ax.plot(xs, ys, alpha=0.5, linewidth=2)
ax.scatter([xs[0]], [ys[0]], c="red", s=100, zorder=5, label="起点")
ax.scatter([xs[-1]], [ys[-1]], c="green", s=100, zorder=5, label="终点")
all_xs = [p[0] for p in traj]
all_ys = [p[1] for p in traj]
ax.set_xlim(min(all_xs) - 1, max(all_xs) + 1)
ax.set_ylim(min(all_ys) - 1, max(all_ys) + 1)
ax.set_aspect("equal")
ax.legend()
plt.show()
interactive(children=(IntSlider(value=1, description='N:', max=6, min=1), IntSlider(value=1, description='t (%…
观察结论:这种宏观的数学动力学,在定性上极其相似于物理中硬盘碰撞的杂乱运动。
为什么要使用随机游走建模?¶
随机过程的优势:
- 规避数据爆炸:无需获取海量微观数据来精确描述复杂系统。
- 本质洞察:通过简化的概率假设,更容易提取系统的宏观物理法则。
典型应用场景:
- 金融:股票价格的涨跌波动
- 环境:污染物在空气中的扩散
- 生物:中性基因在种群中的传播
简单随机游走¶
- 最简模型:在一维整数坐标上,每步向左或向右跳跃。
- 数学抽象:
- 每一步是独立的随机变量。
- 取值为 $\pm 1$,概率均为 $1/2$。
- 本质:抛硬币实验(伯努利随机变量)的线性缩放映射。
Python:基准测试¶
寻找最快的 $\pm 1$ 生成方式:
- 模拟需要运行数百万次的核心循环。
- 微小的性能差异将被无限放大。
- 工具:Python 内置
timeit模块。 - 原理:重复运行代码片段并统计耗时分布。
In [3]:
def step1():
return np.random.choice([-1, +1])
def step2():
return 2 * int(np.random.random() < 0.5) - 1
def step3():
return 2 * np.random.randint(0, 2) - 1
def step4():
return int(np.sign(np.random.randn()))
In [4]:
n = 1000000
print("step1() (np.random.choice):")
result1 = timeit.timeit("step1()", globals=globals(), number=n)
print(f" 平均时间: {result1/n*1e6:.3f} 微秒")
print("\nstep2() (np.random.random < 0.5):")
result2 = timeit.timeit("step2()", globals=globals(), number=n)
print(f" 平均时间: {result2/n*1e6:.3f} 微秒")
print("\nstep3() (np.random.randint):")
result3 = timeit.timeit("step3()", globals=globals(), number=n)
print(f" 平均时间: {result3/n*1e6:.3f} 微秒")
print("\nstep4() (np.sign(np.random.randn)):")
result4 = timeit.timeit("step4()", globals=globals(), number=n)
print(f" 平均时间: {result4/n*1e6:.3f} 微秒")
step1() (np.random.choice): 平均时间: 3.193 微秒 step2() (np.random.random < 0.5): 平均时间: 0.188 微秒 step3() (np.random.randint): 平均时间: 1.004 微秒 step4() (np.sign(np.random.randn)): 平均时间: 0.322 微秒
计算随机游走轨迹¶
- 轨迹记录:从初始位置 $x=0$ 开始,记录连续迈步后的累积位置序列。
In [5]:
def walk1D(N):
x = 0
xs = [x]
for i in range(N):
x += step2()
xs.append(x)
return xs
In [6]:
fig, ax = plt.subplots(figsize=(10, 6))
for i in range(10):
ax.plot(walk1D(100), alpha=0.5, linewidth=2)
plt.show()
使用类实现的随机游走¶
需求拓展:支持 1D、2D 甚至多维的各种游走规则。
最佳实践:使用面向对象编程中的类与多态。
- 建立抽象基类
Walker。 - 统一行为接口:获取位置、生成步长、更新状态。
In [7]:
from abc import ABC, abstractmethod
class Walker(ABC):
@abstractmethod
def position(self):
pass
@abstractmethod
def step(self):
pass
@abstractmethod
def update(self, step_val):
pass
In [8]:
class Walker1D(Walker):
def __init__(self, pos=0):
self.pos = pos
def position(self):
return self.pos
def step(self):
return step2()
def update(self, step_val):
return Walker1D(self.pos + step_val)
In [9]:
class Walker2D(Walker):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def position(self):
return (self.x, self.y)
def step(self):
directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]
return directions[np.random.randint(4)]
def update(self, step_val):
return Walker2D(self.x + step_val[0], self.y + step_val[1])
In [10]:
def trajectory(walker, N):
positions = [walker.position()]
for i in range(N):
step_val = walker.step()
walker = walker.update(step_val)
positions.append(walker.position())
return positions
In [11]:
trajectory(Walker2D(0, 0), 10)
Out[11]:
[(0, 0), (-1, 0), (-2, 0), (-2, 1), (-2, 2), (-2, 3), (-2, 2), (-1, 2), (-1, 1), (0, 1), (0, 2)]
随机游走的数学原理:随机变量之和¶
- 在时间 $n$ 的位置 $S_n$,是 $n$ 个随机步长之和: $$S_n = X_1 + \cdots + X_n$$
- $X_i$:第 $i$ 步的独立同分布 (independent and identically distributed, i.i.d., iid, 或 IID) 随机变量。
- 核心难点:虽然每步 $X_i$ 独立,但中间状态 $S_n$ 与 $S_{n-1}$ 有状态依赖。
快速计算:累积和¶
首先,生成一组独立的随机步长向量:
In [12]:
steps = np.random.choice([-1, +1], size=10)
steps
Out[12]:
array([-1, -1, -1, -1, -1, -1, -1, 1, 1, -1])
轨迹等价于部分和 (Partial Sums): $$\begin{align} S_1 & = X_1 \\ S_2 &= X_1 + X_2 \\ S_3 &= X_1 + X_2 + X_3 \end{align}$$
- NumPy 的
cumsum(前缀和/扫描) 函数可直接、高效地计算整条轨迹:
In [13]:
np.cumsum(steps)
Out[13]:
array([-1, -2, -3, -4, -5, -6, -7, -6, -5, -6])
可视化验证:
In [14]:
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.cumsum(steps), "o-", linewidth=2, markersize=8)
plt.show()
从轨迹到概率分布的演化¶
视角切换:
- 运行成千上万次模拟
- 观察空间中的概率分布
主方程 (Master Equation):
- 设 $p_i^{(t)}$ 为时间 $t$ 位于位置 $i$ 的概率。
- $t+1$ 时刻位于 $i$ 的概率,必然来自上一时刻其相邻位置的流入: $$p_i^{(t+1)} = \frac{1}{2} (p_{i-1}^{(t)} + p_{i+1}^{(t)})$$
设整条一维空间的概率分布为向量 $\mathbf{p}^{(t)}$,定义演化函数:
In [15]:
def evolve(p):
p_new = np.zeros_like(p)
for i in range(1, len(p) - 1):
p_new[i] = 0.5 * (p[i-1] + p[i+1])
p_new[0] = 0
p_new[-1] = 0
return p_new
数学观察:卷积
- 这个邻域平均操作,本质上是一维离散卷积。
- 类似图像处理中的“模糊/平滑滤波”。
两项基本设定:
边界条件 (Boundary Conditions)
- 强制设两端为 0。
- 物理意义:吸收边界(粒子游走到边界即消失/逃逸)。
初始条件 (Initial Condition)
- $t=0$ 时的向量 $\mathbf{p}^{(0)}$。
- 设定:所有粒子从系统中心点出发(中心概率为 1,其余为 0)。
In [16]:
def initial_condition(n):
p0 = np.zeros(n)
p0[n // 2 + 1] = 1
return p0
概率演化的可视化呈现¶
In [17]:
def time_evolution(p0, N):
ps = [p0.copy()]
p = p0.copy()
for i in range(N):
p = evolve(p)
ps.append(p.copy())
return ps
In [18]:
p0 = initial_condition(101)
ps = time_evolution(p0, 100)
In [19]:
ps[2]
Out[19]:
array([0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0.25, 0. , 0.5 , 0. , 0.25, 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. ])
In [20]:
@interact(t=IntSlider(min=0, max=100, step=1, value=0, description="时间步 t:"))
def plot_distribution(t):
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(ps[t], linewidth=2)
ax.set_ylim(0, 1)
plt.show()
interactive(children=(IntSlider(value=0, description='时间步 t:'), Output()), _dom_classes=('widget-interact',))
In [21]:
M = np.array(ps)
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(M, aspect="auto", origin="upper", cmap="hot")
plt.colorbar(im)
plt.show()
本讲小结¶
- 随机游走模型:从布朗运动的物理背景出发,建立了随机游走的数学模型——每步朝随机方向移动固定距离,宏观呈现随机性。
- 建模动机:随机建模可规避数据爆炸,以简化的概率假设提取系统的宏观法则,广泛应用于金融、环境、生物等领域。
- 面向对象设计:利用抽象基类
Walker与多态,统一了 1D/2D 游走的接口(position、step、update),使轨迹计算函数trajectory适用于任意维度的游走者。 - 概率分布演化:从单轨迹转向概率分布视角,主方程 $p_i^{t+1} = \frac{1}{2}(p_{i-1}^t + p_{i+1}^t)$ 描述了概率随时间的演化,其本质是一维离散卷积;通过吸收边界条件与中心初始条件,可视化了概率分布从尖峰逐步扩散的过程。