Files
2026-02-28 15:27:59 +08:00

96 KiB

第 6 章:随机近似 (Stochastic Approximation)

在学习 TD 算法之前,我们必须掌握如何“增量式”地更新数据。

1. 增量式均值估计 (Incremental Mean Estimation)

求均值不必等所有数据到齐。如果我们依次收到数据 $x_k$,我们可以通过以下公式不断更新当前的均值估计 $w_{k+1}$:

w_{k+1} = w_k - \alpha_k (w_k - x_k)

2. Robbins-Monro (RM) 算法

当我们想解方程 $g(w) = 0$,但我们不知道 g(w) 的具体表达式,且每次观测到的结果都有随机噪声时(即观测值为 $\tilde{g}(w_k, \eta_k) = g(w_k) + \eta_k$),我们可以使用 RM 算法:

w_{k+1} = w_k - a_k \tilde{g}(w_k, \eta_k)

收敛的魔法条件: 步长 a_k 必须满足 $\sum a_k = \infty$(不能衰减太快,保证能走到终点)且 $\sum a_k^2 < \infty$(最终要衰减到 0,保证能收敛),最经典的步长就是 $a_k = 1/k$。

3. 随机梯度下降 (SGD)

大名鼎鼎的 SGD 其实就是 RM 算法的特例 SGD 用于最小化目标函数 $J(w) = \mathbb{E}[f(w, X)]$。它无需计算真实的期望梯度,而是直接使用单个样本的随机梯度来更新:

w_{k+1} = w_k - \alpha_k \nabla_w f(w_k, x_k)

增量式均值估计 vs 传统均值估计

In [1]:
import numpy as np

# 生成 1000 个随机样本,假设它们是某个状态的奖励
np.random.seed(42)
samples = np.random.normal(loc=10.0, scale=2.0, size=1000)

print("--- 传统方法 (非增量式) ---")
true_mean = np.mean(samples)
print(f"一次性求平均结果: {true_mean:.4f}")

print("\n--- 增量式均值估计 (Incremental) ---")
w_k = 0.0 # 初始猜测
for k, x_k in enumerate(samples, start=1):
    alpha_k = 1 / k  # 步长 (对应公式 1/k)
    # 核心公式: 新估计 = 老估计 - 步长 * (老估计 - 新样本)
    w_k = w_k - alpha_k * (w_k - x_k)
    
    if k in [10, 100, 1000]:
        print(f"收到 {k} 个样本后的均值估计: {w_k:.4f}")

print("\n结论:增量式更新完美逼近了真实均值,且不需要保存历史样本!")
--- 传统方法 (非增量式) ---
一次性求平均结果: 10.0387

--- 增量式均值估计 (Incremental) ---
收到 10 个样本后的均值估计: 10.8961
收到 100 个样本后的均值估计: 9.7923
收到 1000 个样本后的均值估计: 10.0387

结论:增量式更新完美逼近了真实均值,且不需要保存历史样本!

运行一个 Robbins-Monro (RM) 算法

In [11]:
import matplotlib.pyplot as plt
import numpy as np

# 真实函数 (我们假装不知道它的表达式)
def g(w):
    return w**3 - 5

# 带噪声的黑盒观测器
def noisy_observation(w):
    noise = np.random.normal(0, 1) # 标准正态分布噪声
    return g(w) + noise

w_k = 0.0 # 初始猜测 w1 = 0
w_history = [w_k]

# 迭代 100 次
for k in range(1, 101):
    a_k = 0.1 / k # 满足 RM 定理的收敛步长,减小初始步长防止发散
    
    # 观测带噪声的输出
    g_tilde = noisy_observation(w_k)
    
    # RM 核心更新公式
    w_k = w_k - a_k * g_tilde
    w_history.append(w_k)

# 绘图展示收敛过程 (类似图 6.3)
plt.figure(figsize=(8, 4))
plt.plot(range(101), w_history, marker='o', linestyle='-', color='b')
plt.axhline(y=5**(1/3), color='r', linestyle='--', label='True Root (~1.71)')
plt.title('Robbins-Monro Algorithm: Finding root of $w^3 - 5 = 0$ with noise')
plt.xlabel('Iteration index k')
plt.ylabel('Estimated root $w_k$')
plt.legend()
plt.grid(True)
plt.show()

BGD, MBGD, 与 SGD 收敛性对比

In [ ]:
# 目标: 寻找二维平面上一堆散点的中心 (均值)
# 对应优化问题: 最小化均方误差 J(w)
np.random.seed(0)
n_samples = 100
# 在边长 20 的正方形内均匀分布采样,中心(期望)为 [0,0]
X = np.random.uniform(-10, 10, size=(n_samples, 2)) 

# 初始化
w_init = np.array([20.0, 20.0]) # 故意从很远的地方开始
w_sgd = w_init.copy()
w_mbgd_5 = w_init.copy()

dist_sgd = [np.linalg.norm(w_sgd)]
dist_mbgd_5 = [np.linalg.norm(w_mbgd_5)]

# 模拟前 30 步迭代
for k in range(1, 31):
    alpha_k = 1 / k
    
    # SGD: 每次随机抽 1 个样本 [cite: 7173]
    idx_sgd = np.random.choice(n_samples, 1)
    grad_sgd = w_sgd - X[idx_sgd[0]] # 梯度: w - x
    w_sgd = w_sgd - alpha_k * grad_sgd
    dist_sgd.append(np.linalg.norm(w_sgd))
    
    # MBGD (m=5): 每次随机抽 5 个样本
    idx_mbgd = np.random.choice(n_samples, 5)
    grad_mbgd = w_mbgd_5 - np.mean(X[idx_mbgd], axis=0) 
    w_mbgd_5 = w_mbgd_5 - alpha_k * grad_mbgd
    dist_mbgd_5.append(np.linalg.norm(w_mbgd_5))

plt.figure(figsize=(8, 4))
plt.plot(dist_sgd, marker='d', label='SGD (m=1)', alpha=0.7)
plt.plot(dist_mbgd_5, marker='>', label='MBGD (m=5)', alpha=0.7)
plt.title('Distance to True Mean (0,0) over iterations')
plt.xlabel('Iteration step')
plt.ylabel('Distance to mean')
plt.legend()
plt.grid(True)
plt.show()

print("结论:离真值越远,SGD下降得越快;靠近真值时,SGD 会有一定的波动,而 MBGD (使用更多样本) 波动更小。")
结论:离真值越远,SGD下降得越快;靠近真值时,SGD 会有一定的波动,而 MBGD (使用更多样本) 波动更小 [cite: 7129-7134]。
在当前单元格或上一个单元格中执行代码时 Kernel 崩溃。

请查看单元格中的代码,以确定故障的可能原因。

单击<a href='https://aka.ms/vscodeJupyterKernelCrash'>此处</a>了解详细信息。

有关更多详细信息,请查看 Jupyter <a href='command:jupyter.viewOutput'>log</a>。