随机化 NLA
通过线性草图(SA)压缩高维问题,ε-子空间嵌入保证几何结构。SHRT 提供 O(n log n) 的快速应用。
CS267 · 第 17 课
随机化算法在内部使用随机性来加速计算、提升可扩展性或增强可靠性。本课程分为两部分:第一部分介绍随机化数值线性代数(Randomized NLA),通过线性草图(sketching)将高维问题压缩到低维子空间;第二部分介绍随机化在数据科学与 HPC 中的应用,包括加权采样、数据流中的基数估计,以及 Luby 的并行最大独立集算法。
随机化算法通过引入随机性,在保持可控误差的前提下显著降低计算成本。与传统确定性算法不同,随机化算法的正确性通常以概率保证(例如 (ε, δ) 近似)。本课程涵盖两类核心应用:
随机化算法的本质是以精度换速度。通过精心设计的随机变换,我们可以在不丢失关键信息的前提下,将问题规模从 n 降低到 d(目标维度),从而获得数量级的加速。
线性草图的核心思想是将原始问题通过一个随机线性变换 S 压缩到低维空间。给定一个高维向量 x ∈ ℝⁿ,其草图为 Sx ∈ ℝᵐ,其中 m ≪ n。
典型形式:对于矩阵 A ∈ ℝⁿˣᵈ,构造草图 SA,其中 S ∈ ℝᵐˣⁿ 是一个随机矩阵。草图保留了 A 的列空间的关键几何性质。
一个矩阵 S 被称为 A 的 ε-子空间嵌入(ε-subspace embedding),如果对所有 x ∈ ℝᵈ 满足:
(1 - ε) ||Ax||² ≤ ||SAx||² ≤ (1 + ε) ||Ax||²
这保证了草图 SA 保留了 A 的列空间中所有向量的范数(即内积和距离)至多 ε 倍的相对误差。
可以将其理解为"近似保距":高维空间中的任意向量在随机投影后,其长度几乎不变。这与 Johnson-Lindenstrauss 引理密切相关,但子空间嵌入要求对所有向量同时成立,而不仅仅是单个向量。
定理(Oblivious Subspace Embedding, OSE):设 S ∈ ℝᵐˣⁿ 的每个元素独立地取值为 ±1/√m(等概率)。如果 m = O(d²/ε²),那么 S 以至少 2/3 的概率成为任意固定 d 维子空间的 ε-子空间嵌入。
朴素随机 ±1 矩阵需要 m = O(d²/ε²),这在 d 较大时开销过高。后续我们将介绍如何通过结构化矩阵将 m 降低到 O(d log d / ε²) 甚至 O(d/ε²)。
实际应用中,我们需要在以下性质之间权衡:
| 矩阵类型 | m 的阶 | 应用代价 SA | 并行性 |
|---|---|---|---|
| 稠密随机 ±1 | O(d²/ε²) | O(nmd) | 良好 |
| SHRT(Subsampled Randomized Hadamard Transform) | O(d²/ε²) | O(nd log n) | 良好 |
| CountSketch | O(d²/ε²) | O(nnz(A)) | 优秀 |
SHRT(Subsampled Randomized Hadamard Transform) 将 S 分解为三个算子的乘积:
S = PHD
应用代价仅为 O(n log n)(由 FHT 决定),远优于稠密矩阵的 O(nm)。
import numpy as np
from scipy.linalg import hadamard
def shrt_sketch(A, m):
"""应用 SHRT 到矩阵 A,返回 m 行的草图"""
n, d = A.shape
# Step 1: 随机符号翻转
D = np.random.choice([-1, 1], size=n)
AD = A * D[:, np.newaxis]
# Step 2: 快速 Hadamard 变换
H = hadamard(n) / np.sqrt(n)
AHD = H @ AD
# Step 3: 随机采样 m 行
idx = np.random.choice(n, size=m, replace=False)
return AHD[idx, :] / np.sqrt(m / n)
为了在并行环境中高效应用 SHRT,可以使用 Block SHRT(BSHRT)。核心思想是将矩阵按行分块,每块独立应用 SHRT,然后合并结果。BSHRT 采用 1D 外积算法:
A_i。BSHRT 将通信从全局 Hadamard 变换(需要 all-to-all)降低为仅在所有进程间交换 m×d 的草图矩阵,通信量显著减少。
对于超定最小二乘问题 min_x ||Ax - b||₂,经典解法需要 O(nd²) 时间。Sketch-and-Solve 方法:
SA 和 Sb(S 是子空间嵌入)。x* = min_x ||SAx - Sb||₂。(ε, δ) 保证:以至少 1-δ 的概率,||Ax* - b||₂ ≤ (1+ε) · min_x ||Ax - b||₂。
import numpy as np
def sketch_and_solve(A, b, m):
"""Sketch-and-Solve 求解最小二乘问题"""
n, d = A.shape
# 构造高斯随机草图矩阵
S = np.random.randn(m, n) / np.sqrt(m)
SA = S @ A
Sb = S @ b
# 求解压缩后的问题
x_star, _, _, _ = np.linalg.lstsq(SA, Sb, rcond=None)
return x_star
Sketch-and-Solve 对病态矩阵(ill-conditioned)敏感。实践中常结合迭代精化(iterative refinement)或使用 CountSketch 等更鲁棒的草图方法。
许多应用需要从离散分布 p = (p₁, p₂, ..., pₙ) 中按权重采样。常用方法:
| 方法 | 预处理 | 单次采样 | 备注 |
|---|---|---|---|
| 逆变换采样(Inverse Transform) | O(n) | O(log n)(二分查找) | 简单通用 |
| 拒绝采样(Rejection Sampling) | O(1) | O(1) 期望 | 需要提议分布 |
| 别名方法(Alias Method) | O(n) | O(1) | 最优单次采样 |
别名方法通过将概率分布重构为 n 个"桶",每个桶最多包含两个元素,实现 O(1) 采样:
import numpy as np
class AliasMethod:
"""别名方法:O(n) 预处理,O(1) 采样"""
def __init__(self, probs):
n = len(probs)
self.prob = np.zeros(n)
self.alias = np.zeros(n, dtype=int)
scaled = np.array(probs) * n
small, large = [], []
for i, p in enumerate(scaled):
(small if p < 1.0 else large).append(i)
while small and large:
s, l = small.pop(), large.pop()
self.prob[s] = scaled[s]
self.alias[s] = l
scaled[l] = scaled[l] + scaled[s] - 1.0
(small if scaled[l] < 1.0 else large).append(l)
for idx in large + small:
self.prob[idx] = 1.0
def sample(self):
i = np.random.randint(len(self.prob))
return i if np.random.rand() < self.prob[i] else self.alias[i]
给定一个数据流,估计其中不同元素的数量(cardinality estimation),要求空间远小于 n。
Probabilistic Counting with Stochastic Averaging (PCSA) 的核心思想:
反复抛硬币,出现 k 次连续正面的概率是 2^{-k}。如果最多看到 k 次连续正面,大约抛了 2^k 次。类似地,哈希值的末尾 0 的个数反映了已见元素的数量级。
HyperLogLog 使用调和平均(harmonic mean) 替代 PCSA 的几何平均,显著降低异常值的影响:
误差率:O(1/√m),空间复杂度:O(m log log n)。
import hashlib
import numpy as np
def rho(x):
"""计算二进制表示中末尾 0 的个数 + 1"""
if x == 0:
return 64
n = 0
while (x & 1) == 0:
n += 1
x >>= 1
return n + 1
class HyperLogLog:
"""HyperLogLog 基数估计器"""
def __init__(self, b=10):
self.b = b # 桶的指数
self.m = 1 << b # 桶的数量
self.registers = np.zeros(self.m, dtype=np.int32)
self.alpha = self._correction(self.m)
def _correction(self, m):
if m == 16: return 0.673
if m == 32: return 0.697
if m == 64: return 0.709
return 0.7213 / (1.0 + 1.079 / m)
def add(self, item):
h = int(hashlib.md5(str(item).encode()).hexdigest()[:16], 16)
idx = h >> (64 - self.b)
self.registers[idx] = max(self.registers[idx], rho(h))
def estimate(self):
Z = np.sum(2.0 ** (-self.registers))
E = self.alpha * self.m ** 2 / Z
return int(E)
PCSA 和 HyperLogLog 天然支持并行化:
all-reduce(按位 OR)合并所有进程的草图。
from mpi4py import MPI
def parallel_hll(local_stream, b=10):
"""并行 HyperLogLog 基数估计"""
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
# 每个进程本地估计
hll = HyperLogLog(b)
for item in local_stream:
hll.add(item)
local_regs = hll.registers.copy()
# all-reduce: 按位取最大值(等价于位图的 OR)
global_regs = np.zeros_like(local_regs)
comm.Allreduce([local_regs, MPI.INT],
[global_regs, MPI.INT],
op=MPI.MAX)
# 在 rank 0 上计算最终估计
if rank == 0:
hll.registers = global_regs
return hll.estimate()
return None
可合并草图(mergeable sketches)是并行和分布式环境中的关键原语。它们允许分片处理数据后高效合并,无需保留原始数据。
问题:给定无向图 G = (V, E),找一个极大独立集(即一组互不相邻的顶点,且无法再添加任何顶点)。
贪心 MIS 算法顺序复杂度为 O(n),但并行跨度(span)为 O(n)——本质上是顺序的。
Luby 算法(1986) 通过随机化实现 O(log n) 跨度:
每个顶点有约 1/(deg(v)+1) 的概率成为局部最小值。期望中,每轮至少移除常数比例的顶点,因此期望在 O(log n) 轮后终止。
import random
from collections import defaultdict
def luby_mis(n, adj):
"""Luby 的并行 MIS 算法"""
in_graph = set(range(n))
mis = set()
while in_graph:
# Step 1: 为每个顶点分配随机标签
labels = {v: random.random() for v in in_graph}
# Step 2: 局部最小值加入 MIS
chosen = set()
for v in in_graph:
if all(labels[v] < labels[u] for u in adj[v] if u in in_graph):
chosen.add(v)
mis.update(chosen)
# Step 3: 移除 chosen 及其邻居
to_remove = set(chosen)
for v in chosen:
to_remove.update(u for u in adj[v] if u in in_graph)
in_graph -= to_remove
return mis
复杂度分析:
经典的"球入箱"问题展示了随机化的力量:
O(ln n / ln ln n)。O(ln ln n)。仅仅增加一个选择(从 1 到 2),最大负载就从 Θ(ln n / ln ln n) 降低到 Θ(ln ln n)——这是指数级的改进!这一原理广泛应用于负载均衡、哈希表设计、路由网络等场景。
import random
def balls_into_bins(n, two_choices=False):
"""模拟 balls into bins 实验"""
bins = [0] * n
for _ in range(n):
if two_choices:
i, j = random.randrange(n), random.randrange(n)
idx = i if bins[i] <= bins[j] else j
else:
idx = random.randrange(n)
bins[idx] += 1
return max(bins)
# 示例:n=10000 时
n = 10000
print(f"纯随机: {balls_into_bins(n, False)}")
print(f"2-choices: {balls_into_bins(n, True)}")
| 策略 | 最重箱负载(n 球入 n 箱) | 改进 |
|---|---|---|
| 纯随机 | Θ(ln n / ln ln n) | 基线 |
| Power of 2 Choices | Θ(ln ln n) | 指数改进 |
| Power of d Choices | Θ(ln ln n / ln d) | 进一步改进 |
通过线性草图(SA)压缩高维问题,ε-子空间嵌入保证几何结构。SHRT 提供 O(n log n) 的快速应用。
将大规模最小二乘问题压缩为 (ε, δ) 近似问题,以可控误差换取显著加速。
别名方法实现 O(1) 采样;逆变换与拒绝采样各有适用场景。
HyperLogLog 用调和平均改进 PCSA,O(m log log n) 空间实现 O(1/√m) 误差。
Luby 算法通过随机标签实现 O(log n) 跨度,打破顺序贪心算法的瓶颈。
微小的额外随机性(2 个选择 vs 1 个)带来指数级负载均衡改进。
问题 1:ε-子空间嵌入保证什么性质对所有 x ∈ ℝᵈ 成立?
问题 2:SHRT (Subsampled Randomized Hadamard Transform) 中 S = PHD 的三个算子按顺序分别是?
问题 3:关于 HyperLogLog 与 PCSA 的比较,以下哪项是正确的?
问题 4:Luby 的并行 MIS 算法中,一个顶点 v 加入独立集的条件是什么?
问题 5:在 "balls into bins" 问题中,"power of 2 choices" 策略相比纯随机策略,最重箱的负载从 Θ(ln n / ln ln n) 降低到多少?