非负矩阵分解 NMF:把资产收益矩阵压成可解释的基与权重
PCA 的成分有正有负,你没法把 PC1 读成『40% 科技 + 30% 消费』——因为它允许负载荷,本质是方差最大的正交方向,不是构成。NMF 强制 W、H 全非负,分解出的每个基就是一个可加的『主题』,每个资产是这些主题的非负叠加,读起来像占比。合成实验:12 个资产由 3 个非负时间主题混合而成,NMF 乘法更新 400 次把重构误差从随机初值压到 2.536,还原的时间基激活与真实基相关 0.82~0.92、权重矩阵相关 0.80~0.91;PCA 拿同样数据,58.3% 的载荷是负的,PC1 根本没法解释成资产构成。秩选择靠误差肘部:K 从 1→2→3 时误差 17.48→10.98→2.52 断崖下跌,K=3 之后拐平(2.52→2.51→2.30)精确对上真实秩。纯 numpy 实现 Lee-Seung 乘法更新,拆穿 NMF 唯一解/成分自动对齐/负值数据也能用/秩越大越好/基一定有金融意义五类陷阱(中阶)。
先说结论:PCA 能压缩,但压出来的成分你读不懂;NMF 牺牲一点数值最优性,换来一组能当『主题构成』读的基。 差别的根源只有一条约束——非负。PCA 的载荷有正有负(本文实验里 58.3% 的 PCA 载荷是负的),所以 PC1 可能是「+0.4 科技 −0.3 消费 +0.2 能源」,这种带减号的组合在物理上没有「构成」的意义。NMF 强制 W、H 全非负,每个基就是一个可加的时间模式,每个资产是这些基的非负叠加,H 的每一列直接读作「这个资产由各主题占多少」。

一、问题:降维不等于可解释#
量化里降维几乎是反射动作——几百只资产、几十个因子,先 PCA 抽几个主成分再说。但 PCA 解决的是「用最少的正交方向解释最多方差」,它从没答应过你「这些方向能读懂」。
矩阵分解的通用形式是把一个 T × N 的数据矩阵 V(T 个时间点、N 个资产)近似成两个小矩阵的乘积:
V ≈ W · H
(T×N) (T×K) (K×N)plaintextW是T × K:K 个基(成分)随时间的激活强度H是K × N:每个资产在这 K 个基上的权重
PCA、ICA、NMF 都是这个框架,区别只在对 W、H 加什么约束:
| 方法 | 约束 | 成分含义 |
|---|---|---|
| PCA | W 列正交、方差递减 | 方差最大的正交方向(可正可负) |
| ICA | 各成分统计独立 | 相互独立的源(可正可负,符号/幅度不定) |
| NMF | W ≥ 0 且 H ≥ 0 | 可加的『部分/主题』(全非负) |
NMF 的杀手锏就是最后那行:非负 = 只能相加、不能相减。这逼着模型学出「构成型」的表示——就像把一张脸拆成鼻子、眼睛、嘴(可加的部件),而不是拆成一堆有正有负的鬼影(PCA 的特征脸)。
二、合成实验:3 个主题,12 个资产#
为了能验证还原效果,我构造一个「真相已知」的场景:3 个潜在时间主题(各自是非负的高斯脉冲叠加,模拟事件驱动的活跃度爆发),12 个资产各自主要挂在一个主题上、带一点跨主题泄漏。
import numpy as np
rng = np.random.default_rng(7)
T, N, K = 300, 12, 3
def burst(centers, width, amp):
t = np.arange(T); s = np.zeros(T)
for c, a in zip(centers, amp):
s += a * np.exp(-0.5*((t-c)/width)**2)
return s
W_true = np.zeros((T, K))
W_true[:,0] = 0.4 + burst([60,180], 25, [1.2,1.0])
W_true[:,1] = 0.4 + burst([120,240], 30, [1.1,1.3])
W_true[:,2] = 0.4 + burst([30,150,270], 18, [0.9,0.9,1.0])
H_true = np.array([ # 每行一个主题→各资产权重
[1.0,0.9,0.8,0.7, 0.1,0.05,0.1,0.05, 0.15,0.1,0.05,0.1],
[0.1,0.05,0.1,0.15, 1.0,0.9,0.85,0.8, 0.1,0.05,0.1,0.05],
[0.05,0.1,0.05,0.1, 0.1,0.1,0.05,0.1, 1.0,0.95,0.85,0.9],
])
V_clean = W_true @ H_true
V = np.maximum(V_clean + rng.normal(0, 0.05, V_clean.shape), 0.0) # 保持非负python注意最后一行的 np.maximum(..., 0):NMF 只接受非负输入。这里数据天然是「活跃度 / 成交强度 / 换手率」这类恒非负的量,正好适配。收益率有正有负,不能直接喂 NMF——这是第一个必须警惕的坑,后面细说。
三、算法:Lee-Seung 乘法更新#
NMF 最经典的求解器是 Lee & Seung 的乘法更新(multiplicative update)。它的美妙之处在于:只要初值非负,每一步更新都是「乘以一个非负比值」,所以 W、H 永远保持非负,不需要投影、不需要裁剪。
目标是最小化 Frobenius 范数 ‖V − WH‖²,更新规则:
def nmf(V, K, iters=400, seed=0):
r = np.random.default_rng(seed)
W = r.uniform(0.1, 1.0, size=(V.shape[0], K))
H = r.uniform(0.1, 1.0, size=(K, V.shape[1]))
eps = 1e-9
errs = []
for _ in range(iters):
H *= (W.T @ V) / (W.T @ W @ H + eps) # 更新 H
W *= (V @ H.T) / (W @ H @ H.T + eps) # 更新 W
errs.append(np.linalg.norm(V - W @ H))
return W, H, errs
W, H, errs = nmf(V, K=3, iters=400, seed=3)python分子分母都是非负矩阵,比值非负,乘上原本非负的 W/H 依然非负——约束天然满足。这个更新可以证明让目标函数单调不增(不会变大)。

左图:400 次迭代重构误差单调下降,最终收敛到 ‖V − WH‖_F = 2.536。右图后面讲秩选择。
四、结果:基被干净还原#
NMF 分解出来后有个恼人的问题——成分顺序和缩放是不定的。W·H = (WD)·(D⁻¹H) 对任意非负对角阵 D 都成立,所以学出来的「基 1」不一定对应真实的「主题 1」。要跟真相对比,必须先按相关性把成分排列对齐、再归一化缩放:
from itertools import permutations
best = None
for p in permutations(range(K)):
c = sum(np.corrcoef(H[p[i]], H_true[i])[0,1] for i in range(K))
if best is None or c > best[0]:
best = (c, p)
perm = best[1]
Hn = H[list(perm)]; Wn = W[:, list(perm)]
for k in range(K): # 归一化:H 行和为 1
s = Hn[k].sum() + 1e-9
Hn[k] /= s; Wn[:,k] *= spython对齐 + 归一化之后,还原质量很硬:
- 时间基激活 W 与真实基相关:0.918 / 0.834 / 0.821
- 权重矩阵 H 与真实权重相关:0.845 / 0.905 / 0.804

灰线是真实主题激活,彩线是 NMF 还原——两次爆发的位置、相对高度都对上了。这意味着 NMF 不只是压缩,它真的把「哪个主题在什么时候活跃」拆了出来。
五、真正的价值:可解释性#
这才是 NMF 值钱的地方。把 NMF 的权重矩阵 H 和 PCA 的载荷并排放:

左边 NMF 的 H:全是非负数,每一列(资产)读起来就是「这个资产 = 基1 占 x + 基2 占 y + 基3 占 z」,像一张成分表。资产 0-3 明显主要挂基 1,资产 4-7 挂基 2,资产 8-11 挂基 3——聚类结构一目了然。
右边 PCA 的载荷:58.3% 是负数。PC1 上某个资产是 +0.41、另一个是 −0.33,你怎么解释「这个资产在主成分 1 上是负构成」?在方差分解的语境里这没问题,但在「我想知道这个组合由哪些主题构成」的语境里,负载荷是灾难。
一句话:PCA 回答『方差在哪』,NMF 回答『构成是什么』。 做因子聚类、板块归因、组合主题拆解时,你要的是后者。
六、秩选择:误差肘部#
K(成分数)怎么定?扫一遍,看重构误差的肘部:
ranks = range(1, 8); rec = []
for kk in ranks:
_, _, e = nmf(V, kk, iters=250, seed=5)
rec.append(e[-1])python实测最终误差随 K 变化:
| K | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 误差 | 17.48 | 10.98 | 2.52 | 2.51 | 2.30 | 2.26 | 2.14 |
从 K=2 到 K=3 误差断崖式下跌(10.98 → 2.52),K=3 之后几乎拐平——这个肘部精确对上了真实秩。右图那条曲线就是它。这是实践中定 K 的主力方法(配合领域先验)。
已知偏差(Limitations and Bias)#
- 合成数据是理想剧本:真实数据里主题不会这么干净可分,泄漏更严重、主题数未知、还可能随时间漂移。这里的相关 0.8+ 是在「真相已知且噪声很小」下取得的,实盘还原度会明显下降。
- NMF 无全局唯一解:目标函数非凸,不同随机初值收敛到不同局部最优。本文用固定 seed 报告单次结果,稳健做法是多起点跑、按重构误差挑最好的,并检查基的稳定性。
- 成分顺序/缩放不定:
W·H = (WD)(D⁻¹H)的等价性意味着「基 1」是谁纯属随机,必须靠对齐才能和先验对照——这不是 bug,是 NMF 的内禀不定性。 - 秩肘部有时不干净:本例 K=3 之后误差没有完全平(还在缓慢降),真实数据肘部往往更模糊,单看误差容易过拟合,需结合成分可解释性和稳定性交叉判断。
结果解读(Result Interpretation)#
为什么 NMF 在量化里被低估又被误用。 它的定位很清楚:当你的数据天然非负(成交量、换手率、波动强度、情绪计数、持仓占比),并且你要的是「构成/主题」而非「方差方向」时,NMF 几乎是唯一给你可加解释的降维。本文实验里它把 3 个主题干净拆开、权重读起来像占比表,这是 PCA 给不了的。
但它的适用边界很窄。 三个最常见的翻车点:其一,直接喂收益率——收益有负值,NMF 要么报错要么逼你做 max(0) 截断,等于把所有下跌信息扔掉,分解出的「主题」失去意义;正确做法是分解非负派生量(如 |收益|、成交额、波动率),或改用允许负值的 PCA/ICA。其二,把基当成真实因子——NMF 找的是「能重构数据的非负基」,不保证有金融含义,本文能对上真相是因为数据就是这么造的,真实数据里一个基可能混了两个概念。其三,唯一解幻觉——非凸 + 缩放不定,别人复现你的「基 2」很可能是你的「基 1」,报告结果必须说明初值、迭代、对齐方式。
和 PCA/ICA 的分工。 要方差压缩、要正交、数据有正负 → PCA;要还原相互独立的驱动源、不在乎符号 → ICA;数据非负、要可加的构成解释 → NMF。三者不是替代关系,是针对不同问题的三把刀。用错场景,再干净的还原也是错的。