置换不变集合建模:用 DeepSets 处理无序资产集合
组合层面的建模有个被普遍忽视的结构问题:一篮子资产是『集合』不是『序列』——把 8 只股票的特征按任意顺序摊平喂给 MLP,同一个组合换个排列顺序,模型输出就变。DeepSets(Zaheer et al., NeurIPS 2017)给出通用解:先对每只资产独立编码 φ(x_i),再做对称池化(sum/mean),最后 ρ 读出——数学上可表达一切置换不变函数。本文用纯 numpy 从零实现 DeepSets(含手写反向传播),在组合尾部风险聚合任务上诚实实测:DeepSets R²=0.90 优于摊平 MLP 的 0.79 与手工聚合特征的 0.78,置换后输出偏移 1.8e-15(数值零)而 MLP 偏移达目标标准差的 39%;但变集合大小泛化诚实翻车——训练只见 K=8,测试 K=12 时 sum-pooling R² 崩到 -6.2。并拆穿池化随便选/摊平加增广就行/万能逼近=实际可学/变 K 免费泛化/组合建模必用集合网络五类真实陷阱(中阶)。
给你一个持仓组合:8 只股票,每只带一组特征(波动率、动量、流动性、尾部 β)。要预测这个组合整体的某个属性——比如尾部风险。你会怎么把它喂给神经网络?
最常见的做法是摊平:8 只 × 4 维 = 32 维向量,进 MLP。但这里埋着一个结构性 bug:组合是集合,不是序列。「茅台、宁德、腾讯」和「腾讯、茅台、宁德」是同一个组合,可摊平后的 32 维向量完全不同——MLP 对这两个输入给出的预测不一样。同一个组合,换个行序,风险预测就漂移。这不是过拟合,是归纳偏置错配。
结论先放这:DeepSets(Zaheer et al., NeurIPS 2017)证明了任意置换不变函数都能写成 ρ(Σᵢ φ(xᵢ)) 的形式——每个元素先独立过编码器 φ,加和(对称池化)后再过读出网络 ρ。结构上强制了置换不变,一个字节的顺序信息都漏不进去。我用纯 numpy 从零实现(含手写反向传播),在组合尾部风险聚合任务上实测:DeepSets R²=0.90,摊平 MLP 只有 0.79,置换稳定性上 DeepSets 偏移 1.8e-15(机器精度的零)而 MLP 偏移达目标标准差的 39%。但我也把它最疼的短板摆上台面:训练只见过 8 只资产的组合,拿 12 只的组合去测,sum-pooling 版本 R² 直接崩到 -6.2——置换不变 ≠ 集合大小不变,这两个泛化是两回事。
一、为什么「顺序敏感」在金融里是真 bug#
先把问题钉死。摊平 MLP 学到的函数是 ,对输入位置敏感。这在金融组合场景里造成三个实际伤害:
- 同一组合多个预测:K 只资产有 K! 种排列,8 只就是 40320 种。模型对同一个经济对象给出上万个不同输出,你取哪个?
- 样本效率浪费:模型需要从数据里「自己悟出」顺序无关这件事。等价于要求它把 40320 种排列各学一遍——本可以用结构直接送给它的先验,全靠数据硬堆。
- 上线后的隐性不稳定:生产环境里资产列表的顺序往往由上游数据管道决定(按代码排序?按建仓时间?)。管道改个排序逻辑,模型输出集体漂移,而你的回测根本测不出这件事。
有人会说「训练时做随机排列增广不就行了」。可以缓解,但治标不治本——增广只是让模型近似不变,我们在实验里会看到近似的残余误差有多大。
二、DeepSets:结构性解法#
2.1 定理与架构#
Zaheer 等人 2017 年证明:定义在可数域集合上的函数 是置换不变的,当且仅当它能分解为
架构上就三块:
- φ(逐元素编码器):每只资产的特征向量独立过同一个小 MLP,映射到隐空间。参数在所有资产间共享——这就是为什么参数量与 K 无关。
- 对称池化:把 K 个隐向量按元素求和(或求均值)。求和对任何排列都给出同一个结果——不变性在这一步被数学结构锁死,不依赖训练。
- ρ(读出网络):池化后的定长向量过第二个 MLP,输出预测。
熟悉注意力机制的读者会认出亲戚关系:不带位置编码的 Transformer 编码器本质上是置换等变的,加个平均池化就是置换不变——Set Transformer 就是这条路线。DeepSets 是这一族里最简的成员,也因此最适合看清本质。
2.2 纯 numpy 实现#
前向传播非常直白,唯一的「架构魔法」就是中间那行 sum(axis=1):
class DeepSets:
def forward(self, X): # X: (B, K, D) — B 个组合,每个 K 只资产
self.Z1 = X @ self.W1 + self.b1
self.A1 = np.maximum(0, self.Z1)
self.Z2 = self.A1 @ self.W2 + self.b2
self.A2 = np.maximum(0, self.Z2) # φ 输出: (B, K, H)
self.S = self.A2.sum(axis=1) # 对称池化: (B, H) ← 不变性在这里
self.Z3 = self.S @ self.W3 + self.b3
self.A3 = np.maximum(0, self.Z3)
return (self.A3 @ self.W4 + self.b4).ravel()python反向传播里池化层的梯度值得单独说:sum-pooling 的梯度是把上游梯度原样广播给每只资产(np.repeat(dS[:, None, :], K, axis=1)),mean-pooling 则要再除以 K。这个差异看着无害,实际埋着一个坑——sum-pooling 的梯度幅度随 K 线性放大。我第一版训练直接 NaN 发散,把学习率从 0.02 降到 0.004 并加了梯度裁剪才稳住。mean-pooling 用 0.02 训练全程无恙。这是 sum 与 mean 之争的第一回合,后面还有第二回合。
φ 的参数梯度用 einsum 跨 (batch, K) 两个维度聚合:
gW2 = np.einsum("bkh,bkj->hj", self.A1, dZ2) / B # K 只资产共享同一份 φ 参数python2.3 任务:组合尾部风险的非线性聚合#
合成任务设计成「必须看清个体、又必须非线性聚合」才做得好:每个组合 K=8 只资产,每只 4 维特征,目标是
第一项是 log-sum-exp——组合尾部风险由最危险的那几只主导,而不是平均值,这是真实组合风险的典型形态(一只暴雷股就能拖垮整篮子)。训练 6000 个组合,测试 2000 个。

三、实验一:K=8 主场对拆#
四个模型同台:手工聚合特征(mean/std/max 拼接 + Ridge)、摊平 MLP-concat(128 隐层×2)、DeepSets mean-pool、DeepSets sum-pool。
| 模型 | 测试集 R² |
|---|---|
| 均值/std/max 特征 + Ridge | 0.779 |
| MLP-concat(摊平) | 0.786 |
| DeepSets sum-pool | 0.865 |
| DeepSets mean-pool | 0.900 |

三个观察:
- DeepSets 比摊平 MLP 高出 0.11 个 R²。差距不是来自容量(MLP 参数更多),而是来自结构先验:MLP 得用参数模拟「顺序无关」,DeepSets 把这部分容量全省下来学聚合函数本身。
- 手工聚合特征(0.779)已经不弱——mean/std/max 是三个合法的置换不变统计量,方向是对的,输的是表达力:log-sum-exp 介于 mean 和 max 之间的「软最大」形态,固定统计量拼不出来,φ+池化可以学出来。
- mean-pool 这局赢了 sum-pool(0.900 vs 0.865),主要是 sum 版本训练更难(学习率被迫压低 5 倍)。K 固定时两者理论表达力等价,差的是优化难度。

四、实验二:置换稳定性——1.8e-15 对 39%#
对同一批 200 个测试组合,随机打乱资产行序 50 次,测输出偏移:
| 模型 | 置换后输出偏移 |
|---|---|
| DeepSets sum-pool | max 1.8×10⁻¹⁵(浮点加法交换误差,数值零) |
| MLP-concat | mean 0.193 ≈ 39% 的目标标准差 |

39% 是什么概念:同一个组合,仅仅换了输入行序,MLP 的风险预测平均漂移了目标波动的四成。如果这是生产里的风险模型,上游管道从「按代码排序」改成「按权重排序」,全部组合的风险读数集体跳变——而且没有任何报错。DeepSets 的 1.8e-15 则纯粹是浮点加法顺序的舍入残差,工程意义上就是零。这一局不是「赢多少」的问题,是定性差异:一个是结构保证,一个是靠运气。
五、实验三:变 K 泛化——诚实翻车现场#
DeepSets 的宣传语之一是「天然支持变长输入」:φ 参数与 K 无关,K=5 和 K=12 的组合都能直接喂。能喂 ≠ 能预测对。训练只用 K=8,直接测 K=5 和 K=12:
| 池化 | K=5 | K=8(训练同分布) | K=12 |
|---|---|---|---|
| sum-pool | -0.106 | 0.865 | -6.23 |
| mean-pool | 0.176 | 0.900 | -0.228 |
全线崩溃,sum-pool 尤其惨烈。原因拆开看非常清晰:
- sum-pool 的池化输出量级随 K 线性增长。ρ 网络在训练时只见过「8 只资产的和」这个量级,K=12 时输入整体放大 1.5 倍,直接把 ρ 推出训练分布,外推爆炸(R²=-6.2 意味着比瞎猜均值差 6 倍)。
- mean-pool 量级稳定,所以崩得温和些,但依然是负的 R²。因为目标函数本身随 K 变化:log-sum-exp 项在 K 变大时系统性上移(更多资产 = 更大的软最大值),这个「K 效应」模型从未见过——它学的是 K=8 条件下的聚合函数,不是关于 K 的规律。
这个翻车恰恰是本文最值钱的实验:置换不变是对「顺序」的不变性,不是对「规模」的泛化能力。文献里的补救方案(把 K 作为 ρ 的显式输入、用 K 归一化的池化、训练时混合多种 K)都有效,但没有一个是免费的——都要求你训练时就见过或建模过 K 的变化。
六、金融场景的落点#
哪些量化问题真的是「集合建模」问题:
- 组合层面预测:给定持仓集合预测组合风险/容量/拥挤度——本文任务的直接对应物。
- 截面选股的上下文建模:单只股票的 alpha 依赖「今天整个候选池长什么样」(拥挤度、行业分布)。把候选池当集合编码成上下文向量,拼到每只股票的特征上——这是 DeepSets 的置换等变变体。
- 订单簿聚合:一侧挂单是(价格,数量)对的集合,天然无序(同价位归并后),集合编码比按档位摊平更符合结构。
- 另类数据:一家公司关联的新闻集合、供应商集合、专利集合——全是变长无序输入。
反过来,不该用的场景同样明确:K 线序列(时间顺序就是信息,用了置换不变等于亲手删掉最重要的结构)、有明确主次结构的输入(如「本股 + 对照组」,应该用非对称架构)。
七、五个真实陷阱#
陷阱一:「sum 和 mean 池化随便选一个」。 本实验里两回合各有输赢:固定 K 时 mean 好训练(sum 梯度随 K 放大,学习率被迫降 5 倍才不发散);变 K 时 mean 崩得温和(sum 的池化量级随 K 漂移,R² 崩到 -6.2)。理论上 sum 表达力更强(能计数),实践上 mean 更稳。默认 mean,确定需要「计数类」信息时再换 sum 并配好归一化。
陷阱二:「摊平 MLP 加排列增广就等价了」。 增广只能把顺序敏感度压小,压不到零,而且代价是有效样本量被排列稀释。本实验 MLP 未做增广时偏移 39%;文献中充分增广后典型残余在百分之几量级——对风险模型这仍是不可忽略的抖动。结构保证(1.8e-15)与统计近似之间隔着一个量级鸿沟。
陷阱三:「万能逼近定理 = 实际学得出来」。 DeepSets 定理说的是存在性——存在 φ 和 ρ 能表达任意置换不变函数,且隐维度理论上可能需要随集合大小增长。实际有限宽度 + SGD 下,本实验 R²=0.90 而非 1.0,剩下的 0.1 里既有噪声也有优化不到位。定理给的是入场券,不是保票。
陷阱四:「支持变长输入 = 变 K 免费泛化」。 本文最响亮的翻车:K=8 训练、K=12 测试,sum-pool R²=-6.2。架构上能接受任意 K 只说明前向传播不报错,分布上 ρ 从未见过那个量级的池化向量。生产里组合规模会变的话,训练集必须覆盖 K 的分布,或显式把 K 建模进去。
陷阱五:「组合建模必须上集合网络」。 手工聚合特征(mean/std/max + Ridge)拿到了 0.779,只比摊平 MLP 低 0.007,而它零训练成本、完全可解释、天然置换不变。如果你的聚合结构接近简单统计量,先用手工特征打底;DeepSets 的增量价值(本实验 +0.12 R²)只在聚合函数确实复杂非线性(如软最大化)时才兑现。先便宜后昂贵,永远是对的顺序。
八、收尾#
DeepSets 用一行 sum(axis=1) 把「组合是集合不是序列」这个先验焊进了架构:K=8 主场上 R²=0.90 对摊平 MLP 的 0.79,置换稳定性上机器零对 39% 漂移——结构先验的价值实打实。但变 K 泛化的崩溃(-6.2)同样实打实地提醒:架构解决的是它声称解决的那一个不变性,别替它许诺更多。
对量化研究员,行动建议压缩成三句:组合层面的模型输入若是无序资产列表,摊平 MLP 就是在裸奔,至少换成置换不变结构或做严格的排列增广测试;池化先用 mean;组合规模会变的话,把 K 的分布纳入训练设计——这件事架构不会替你做。