ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

不变量理论与对称性特征工程:从群作用到等变网络落地

不变量理论与对称性特征工程:从群作用到等变网络落地

1. 同一个物体转个角度,模型就不认识了

先说一个很常见的观察:某个三维形状分类或分子性质回归的模型,训练集里的样本朝向基本固定,验证集上表现还不错;一旦把测试样本整体旋转三十度,指标立刻掉一大截。大多数人第一反应是“数据不够,加增强”,于是开始堆旋转副本。但如果你把输入特征打印出来逐维看,常常会发现真正的问题不是数据量,而是特征本身带着坐标系的绝对信息,而任务根本不关心这个信息。这就是不变量理论要处理的事情,也是对称性特征工程的起点。

不变量理论在数学里是一门相当老的学问,研究的是“一个群作用在空间上,哪些函数在群作用下保持不变”,以及“这些不变函数能不能用有限个基本块拼出来”。它的工程价值在于:当你把“不关心的变换”形式化成群作用之后,不变量理论能告诉你三件事——该造多少个特征才够、这些特征之间有没有冗余、以及什么时候你的特征其实分不开不同的样本。对称性特征工程做的就是把这三件事翻译成代码。

这篇东西适合两类人看:一类是做三维视觉、分子性质预测、点云、图结构这类天然带对称性的任务,正在被“换个朝向就崩”折磨的;另一类是纯做表格或多模态特征,想搞清楚“特征构造的完备性”到底该怎么判定的。数学部分我会尽量用工程语言讲,公式只保留能直接变成代码的那些。

1.1 坐标锚点是怎么悄悄混进特征里的

最典型的失误是把原始坐标直接喂给一个全连接网络。对于一个点云X ∈ R^{n×3},如果直接展平成 3n 维向量,模型学到的第一层权重实际上在比较“第一维数值大不大”这种毫无物理意义的东西。你把整个点云绕 z 轴转 90 度,这 3n 维向量的每一维都变了,模型自然崩。

于是很多人退而求其次,做“对齐”:先算协方差矩阵,做特征分解,把主轴转到坐标轴上,再喂网络。这个操作在工程上叫选代表元(gauge fixing),它不等于构造不变量。问题有两层:第一,特征向量的符号是任意的,±v 都是单位特征向量,不同实现、不同数值库、甚至同一份代码在不同硬件上,都可能给出相反符号,导致同一个分子在两次运行里得到镜像翻转的特征;第二,当协方差矩阵有两个特征值接近相等时,主轴方向在数值上极不稳定,一点点扰动就会让对齐结果大幅摆动。你以为是“消掉了旋转”,实际上是“把旋转换成了另一种不稳定的抖动”。

真正的做法是构造在群作用下数值恒等的量。距离、夹角、内积、功率谱这些都属于这一类。它们的共同特征是不依赖任何外部参考方向,只依赖点与点之间的相对关系。

1.2 不变量、等变、协变,这三个词别用混

这三个词在实际项目里经常被互换使用,但对应的实现方式完全不同,混淆会直接导致架构选错。

概念数学表述工程含义典型实现
不变量f(g·x) = f(x)输入变,输出纹丝不动距离矩阵、功率谱、池化聚合
等变f(g·x) = ρ(g)·f(x)输入怎么变,输出按同样规则变群卷积、张量积层、向量神经元
协变通常指等变在几何量上的特例输出的变换规则由几何类型决定向量输出层、球谐系数

判据很简单:如果你的最终任务是标量(能量、类别、打分),输出层要的是不变量;如果中间层需要保留方向信息以便后续继续处理(比如预测每个原子的受力向量),中间层要的是等变,最后再通过内积之类的操作收缩成标量。把该等变的层强行做成不变的,信息会在早期就被压掉,模型容量白白浪费;把该不变的输出做成等变的,你会得到一个随输入旋转而旋转的预测值,毫无意义。

2. 不变量理论的骨架:轨道、不变环、有限生成

要判断“我造的特征够不够”,绕不开这套骨架。它不复杂,但几个概念必须抠清楚。

2.1 从“同一物体的不同描述”到轨道

给定一个群 G 作用在空间 X 上,任意一点 x 在群作用下的全部像构成它的轨道(orbit):G·x = {g·x | g ∈ G}。同一个分子在空间中的所有朝向,就是它在旋转群下的一个轨道;同一个点集的所有排列顺序,就是它在置换群下的一个轨道。

不变量就是轨道上的常函数:在同一个轨道上取同一个值,在不同轨道上尽量取不同值。所有 G-不变的多项式构成一个环,记作 R^G,叫不变量环。这个环的结构决定了你的特征空间长什么样:如果它是有限生成的,你只需要算有限个基本不变量;如果它不是,你就得另想办法。

这里有个非常实用的细节值得单独拎出来讲:平移群的不变量只有常数。因为如果 f(x + c) = f(x) 对所有 c 成立,那么 f 的梯度处处为零,f 只能是常数。所以“平移不变”这件事在多项式层面是做不到的,工程上的通行做法是先减去质心,把数据搬到质心坐标系里。这仍然是选代表元,不是不变量,只不过质心这个代表元选得足够稳定(只要点数大于零且权重正),实践中很少出问题。但你要清楚自己在做什么:减质心之后,如果你再做一步“除以包围盒对角线”做尺度归一,那尺度不变性也是靠选代表元实现的,遇到退化构型(所有点共线、所有点重合)就会炸。

2.2 有限生成定理:为什么搜索空间能塌缩

不变量环 R^G 是个可能无限维的东西,但它作为环,往往只需要有限个元素就能生成。这个结论对有限群和紧致群都成立,是整套方法能落地的前提。

它的工程翻译是:你不需要穷举所有可能的不变多项式,只需要找到一组基本不变量,任何其他不变量都是它们的多项式组合。比如置换群 S_n 作用在 n 个变量上,基本不变量就是初等对称多项式 e_1 到 e_n,一共 n 个;任何对称多项式都能写成它们的多项式。这意味着对于 n 个标量的无序集合,用 n 个数字就能无损表达(在多项式意义下),而不是用 n! 种排列。

反过来说,这也给了一个“特征数量下界”的直觉:如果你的群作用把数据压到了 d 维的轨道空间,那你的不变特征至少需要 d 个数才可能分开所有轨道。少于这个数,无论你怎么设计,都会出现两个不同轨道映射到同一个特征向量的情况。

2.3 第一与第二基本定理:够用,还是有多余

第一基本定理说的是:所有不变量都能写成基本不变量的多项式。这保证了你不会漏掉信息。

第二基本定理说的是:这些基本不变量之间存在哪些代数关系。这听起来很抽象,但工程含义非常直接——冗余。如果基本不变量之间有代数关系,那么某些特征组合是恒定的,或者某些特征可以由其他特征算出来,这在数值上表现为严重的共线性。

举个例子:对于三个点的距离矩阵,三条边长 a、b、c 并不独立,它们必须满足三角不等式。如果你把 (a, b, c) 直接作为三个特征输入线性模型,模型会在一个弯曲的可行域上学参数,泛化往往不如把它们换成两个角度加一个尺度。第二基本定理本质上就是在告诉你“可行域长什么样”。

在实操里,判断冗余的廉价方法是算特征矩阵的条件数或者做 SVD 看奇异值衰减。如果发现有特征值接近零的方向,那就是第二基本定理在数值上跟你打招呼了。

2.4 用 Hilbert 级数数一数到底有几个不变量

这是我觉得最被低估的工具。Hilbert 级数把“每个次数上有多少个线性无关的不变量”编码成一个幂级数,系数就是维数。对于有限群,有现成的公式可以直接算:

Φ(t) = (1/|G|) · Σ_{g∈G} 1 / det(I − t·ρ(g))

其中 ρ(g) 是群元素在表示空间上的矩阵。这个式子看起来唬人,实际写代码非常短:

import numpy as np from itertools import permutations def molien_series(group_mats, max_deg=8): """group_mats: list of (d, d) 表示矩阵,返回各次数的维数""" dims = [] for k in range(max_deg + 1): total = 0.0 for M in group_mats: # 用 det(I - tM) 的展开,这里直接对 t^k 系数求和 # 简化处理:数值上取一组 t 做插值也可以 total += 1.0 / np.linalg.det(np.eye(M.shape[0]) - 0.0 * M) dims.append(total) return dims def perm_rep(n, perm): """置换 perm 在 R^n 上的置换矩阵""" P = np.zeros((n, n)) for i, j in enumerate(perm): P[i, j] = 1.0 return P n = 4 mats = [perm_rep(n, p) for p in permutations(range(n))] print(len(mats), "个群元素,阶乘级别增长,n 大了就别硬算了")

真正的实现我会用符号计算,把 det(I − tM) 当作 t 的多项式展开再取倒数,最后按 t 的幂次累加。实际跑下来你会看到一个很漂亮的规律:S_n 的不变量环的 Hilbert 级数是 1 / ∏_{i=1}^{n} (1 − t^i),也就是第 k 次的不变量维数等于“用 1 到 n 的部分凑出 k 的方案数”。这意味着维数增长得很慢——n=4 时,1 次 1 个、2 次 2 个、3 次 3 个,非常节省。

这个数字的实际用途是:当你打算“多加几个不变特征提升表达力”时,先算一算这个次数上究竟有几个独立的不变量。如果你在 2 次多项式里硬塞了 5 个特征,而理论维数只有 2,那多出来的 3 个一定是前面几个的线性组合,纯粹在给优化器添乱。

3. 把不变量真的算出来:三条工程路径

理论告诉你存在,工程要的是怎么算。实际项目里我常用的是三条路径,各有明确的适用边界。

3.1 对称化平均:最直白,但也最贵

给定任意一个函数 f,把它变成不变函数最直接的办法是对整个群取平均:

P f(x) = (1/|G|) · Σ_{g∈G} f(g·x)

这个算子叫对称化平均(也叫 Reynolds 算子),它是个投影:作用一次和多次结果一样。代码就三行:

import numpy as np from itertools import permutations def symmetrize(feature_fn, n_perm): """对置换群做对称化平均""" acc = None count = 0 for p in permutations(range(n_perm)): v = feature_fn(list(p)) acc = v if acc is None else acc + v count += 1 return acc / count # 用法:任何对顺序敏感的特征,平均之后就是顺序无关的 raw = lambda perm: np.array([perm[0] * 2.0 + perm[1]]) # 随便一个不对称的函数 print(symmetrize(raw, 4))

对有限群(置换、有限点群、镜像)这条路非常稳,而且有个额外好处:它不要求你先知道基本不变量长什么样,任何特征都能被它“掰正”。代价是 |G| 的复杂度,S_n 是 n!,n=8 就已经四万多次前向,n=10 直接不可行。

对连续群(旋转),积分换成了 Haar 测度上的积分,一般只能蒙特卡洛采样近似。这里有个坑必须提醒:采样数固定时,方差不随采样数线性下降,而是跟被积函数在群上的变化剧烈程度相关。如果你的特征对小角度旋转很敏感,几十个采样点得到的“平均特征”本身噪声很大,反而不如老老实实用解析不变量。

提示:对称化平均适合放在数据预处理阶段做一次,把整个数据集“掰正”;不适合放在训练循环里每步都算,尤其是群比较大的时候。

3.2 直接构造基本不变量:对称多项式、距离、功率谱

这是性能最好的一条路,因为你直接用解析公式,没有任何采样噪声。

置换群的情况最简单,初等对称多项式或者幂和都能用,numpy里一行就能算:

import numpy as np def elementary_symmetric(x): """输入 n 维向量,返回 n 个初等对称多项式 e_1..e_n""" n = len(x) e = np.zeros(n + 1) e[0] = 1.0 for i, xi in enumerate(x): for k in range(i + 1, 0, -1): e[k] += e[k - 1] * xi return e[1:] # e_1..e_n

注意这里的输入必须是已经排好序或者本身无序的量,否则你算的其实是“按位置排列的对称多项式”,对输入顺序仍然敏感。

旋转群的情况,最常用的是成对距离和球谐功率谱。成对距离的完备性有个精确结论:n 个带标签的点的完整距离矩阵,在欧氏等距变换下唯一确定这组点(允许反射)。所以完整的距离矩阵是一组完备不变量,只是它有 O(n²) 个元素,冗余极大。

球谐功率谱走的是另一条路:先把局部环境展开成球谐系数,再对磁量子数求模方,得到旋转不变的功率谱:

p_l = Σ_{m=-l}^{l} |c_lm|²

这个量对旋转严格不变,而且对每个 l 只产出一个数,从 O(n²) 压到了 O(l_max)。代价是不完备:功率谱相同不代表构型相同,这是它的固有缺陷,不是实现问题。实际项目里我一般把距离特征和功率谱特征拼起来用,前者保完备性,后者保紧致性和平滑性。

构造方式完备性计算复杂度可微性适用场景
完整距离矩阵完备(对欧氏等距)O(n²) 存储在重合点处不可微小规模点云、分子
排序后的距离多重集不完备(存在同距异构)O(n² log n)排序处不可微快速粗筛、去重
球谐功率谱不完备O(n·l_max²)处处可微大规模、需要梯度
初等对称多项式完备(对置换)O(n²)可微无序标量集合
对称化平均特征取决于基函数O(G)

3.3 完备性与“分不开”的判定:什么时候该上 Gröbner 基

有一定规模的项目会遇到这个问题:你的不变特征在下游任务上表现平平,你怀疑是特征分不开不同样本,但又不确定。这时候可以正经判定一下。

思路是这样的:把两组样本的不变量向量都算出来,如果两组样本属于同一个轨道,那么它们的不变量向量必须完全相同;反过来,如果不变量向量相同但样本不同轨道,那就是分离失败,说明你的特征不完备。对于紧致群作用在多项式环上,不变量确实能分离闭轨道——这是理论保证。但你的特征未必是“完整的不变量环”,可能只是其中几个,那分离失败就是必然的。

真要判定,可以用Gröbner 基做消元:把“不变量相等”写成多项式方程组,用消元判断它是否有解、解空间维数是多少。这套工具在符号计算库里都有现成实现。实践中我不会每次都跑,只在两种情况下用:一是发现模型在某些样本对上持续给出相同的预测,怀疑特征塌缩;二是设计新特征时想验证“加了这几个之后是否真的增加了分离能力”。

更轻量的替代方案是做轨道距离检验:对每一对样本,分别枚举群作用,取两组轨道之间的最小距离。如果最小距离接近零,说明它们几乎在同一轨道上;如果最小距离远离零但不变量特征却相同,那就是特征不完备。这个检验对离散群可以直接跑,对连续群用优化求解最小距离,成本略高但思路清晰。

3.4 数值路径:核方法与随机特征

当群太大、解析不变量又不够完备时,还有一条中间路线:不变核。定义 k(x, y) = k₀(g·x, h·y) 对任意 g, h 成立,那么基于这个核的模型天然具备不变性。最常见的形式是对群做平均:

k(x, y) = (1/|G|²) Σ_{g,h} k₀(g·x, h·y)

这本质上还是在做对称化平均,只不过平均的是核而不是特征,好处是核函数可以用 RBF 这类平滑函数,数值上更稳,而且不需要显式构造特征向量。代价是预测时要对每个支持向量算一遍群平均,推理成本上去了。

还有一条是随机特征近似:用随机投影把高维不变特征压到低维,靠 Johnson-Lindenstrauss 那套保证内积近似。这条路我在特征维度很高(比如完整距离矩阵展开后上万维)的时候用过,压缩到几百维后精度损失在可接受范围内,训练速度快了一个数量级。缺点是随机性带来的方差需要多种子平均来压,别指望单次结果稳定复现。

4. 落到模型结构上:三类对称性的具体特征工程

理论讲完,讲讲我实际怎么把它缝进模型里。按对称性类型分,做法差别挺大。

4.1 置换对称:池化、重心化和它们各自的隐含假设

点集、邻居列表、无序分子描述符,处理置换对称的标准套路是“逐元素变换 + 聚合”:

f(X) = ρ( Σ_{x∈X} φ(x) )

其中 φ 是逐元素的编码器,ρ 是后续网络。这个结构对置换严格不变,因为求和与顺序无关。

聚合函数的选择比很多人想的要重要:

  • sum:保留基数信息,但输出量级随元素个数线性增长,样本点数差异大时会带来尺度问题。
  • mean:归一化了量级,但丢掉了基数。两个点和一个点如果均值相同,模型分不开。
  • max:只保留每个维度上的极值,信息损失最大,对离群点敏感,但在点云分割这类任务上意外地好用。

一个常见错误是:前面用了 sum 聚合,后面接了一个 BatchNorm。BatchNorm 在 batch 维度上做统计,意味着同一个样本的输出会依赖于同批次的其它样本。这在训练时可能还能忍,推理时一旦 batch 组成变化(比如最后一个不满的 batch),结果就漂了。对不变特征来说这是毁灭性的,因为它破坏了你辛苦构造的“输入怎么变我都不变”的承诺——现在输入不变,但批次变了,输出还是变。

我的做法是:不变特征的归一化一律用不依赖 batch 的统计量,或者把归一化挪到特征构造之前、在原始数据上做,并且把统计量固化成常量写进配置。

还有一个隐藏很深的坑:归约顺序的浮点不确定性。在 GPU 上做 scatter/gather 或者并行求和,求和顺序不固定,导致同一个输入两次运行的结果在 1e-6 量级上有差异。单独看无所谓,但如果下游有阈值判断、去重、或者 argmax,这点差异足够让结果翻转。我在一个去重流程里被这个坑过一次:两个理论上完全相同的构型算出了差 1e-7 的特征向量,然后被判定为不同样本,导致数据集里出现重复项。解决办法是在比较前做量化(比如保留 6 位小数)或者用容差比较,而不是直接用==。

4.2 连续旋转:距离、球谐与局部环境描述符

旋转不变特征的构造,业内已经形成了相当成熟的几个套路,我按使用频率排一下:

成对距离最基础,也最容易被误用。前面说过,完整距离矩阵对带标签的点集是完备的,但一旦你把距离排序或者取上三角后丢掉对应关系,完备性就没了。经典的“从无序两两距离重建点集”问题已知存在非唯一解。所以如果你的点是可区分的(比如分子里不同元素),老老实实保留带标签的完整矩阵;如果点不可区分,那排序后丢失的信息本来就是不可恢复的,只能接受不完备。

局部环境描述符是分子和材料领域的标准做法。基本思路是:对每个中心原子,把邻居的位置分布展开成径向部分和角向部分。径向部分统计不同距离壳层上有多少邻居,角向部分统计三体夹角分布。这类描述符对旋转、平移、置换都不变,而且计算复杂度是 O(n·k²)(k 是邻居数),非常实用。

球谐功率谱更适合需要平滑性和可微性的场景。实现要点在于:先把邻居相对位置转成球坐标,用球谐函数在 (θ, φ) 上展开,得到每个 l 的一组系数,再对 m 求模方。这套东西的上手门槛主要在于球谐函数的数值实现——自己写容易在极点附近出数值问题,建议直接用成熟库。

注意:球谐功率谱对反射也是不变的(因为取模方抹掉了符号信息),这在分子手性任务是致命的。如果你要区分手性对映体,必须额外引入手性敏感的量(比如三阶标量三重积),单靠功率谱永远分不开。

4.3 等变网络:让中间层跟着转,输出层才不动

不变量特征工程的另一个极端是等变网络。核心思想是不把不变性压到输入侧,而是让每一层都保持等变,直到最后一层才收缩成标量。

技术上,等变层的作用是把不同类型的几何量(标量、向量、二阶张量)按群的表示论规则组合起来,组合系数由 Clebsch-Gordan 系数给出。用起来的感觉是:输入一批向量特征,输出一批新的向量特征,整个过程中特征的方向语义保持一致。

什么时候该用它,什么时候该用不变特征?我的判断标准是:

情况推荐方案理由
任务输出是标量,且数据量中等不变特征 + 普通网络实现简单,调试成本低
任务输出是标量,但数据量大、精度要求高等变网络参数量少、样本效率高
任务输出是向量/张量(力、位移、流场)等变网络不变特征无法给出方向
需要解释性、需要人工检查特征不变特征每个维度有明确物理含义
手性相关任务等变网络 + 手性不变量纯不变特征容易丢失手性

等变网络的代价是工程复杂度高、调试困难。我见过不少项目在这个方向上投入了大量时间,最后发现任务本身对方向根本不敏感,用距离特征加一个普通 MLP 就能达到同样效果。先确认方向信息对任务有用,再上等变。

4.4 数据增强与硬约束:两者不是替代关系

很多人把数据增强当成不变性约束的廉价替代。这个理解有偏差。

数据增强是隐式的正则化:它让模型在训练分布上见到变换后的样本,从而倾向于学到不变的模式,但它不提供任何保证。模型的假设空间里仍然存在非不变的解,只是这些解的损失更高。而硬约束(不变量特征、等变结构)是把非不变的解从假设空间里直接删掉,这是本质区别。

连续群的增强尤其尴尬:你没法穷举所有旋转角度,只能采样有限个。采样太稀,模型在没见过的角度上仍然会崩;采样太密,训练成本爆炸。有个经验规律:当群的有效维度越高(比如 SO(3) 是三维的),增强的边际收益衰减得越快。二维旋转(绕单轴)用 36 个角度覆盖还算可行,三维旋转用几千个采样点都未必够。

我的组合策略通常是:用不变量特征做骨架,用增强补平滑性。骨架保证大方向不会错,增强让模型在特征空间里对小的数值扰动更鲁棒。纯靠增强省事的项目,我还没见过在分布外旋转上表现好的。

5. 踩坑实录:几个把不变性做没了的瞬间

这一节是这篇东西里我最想写的部分,因为下面每一个坑我都真踩过,而且每个坑的共同特点是“代码跑得通、指标看着还行、但不变性已经没了”。

5.1 归一化写在不变特征之后

这是最常见的一个。流程是:算距离特征 → 除以最大值归一化 → 送进网络。看起来没问题,但当输入被旋转时,距离特征理论上完全不变,归一化因子也不变,结果应该一样。问题出在归一化因子如果在批次上算:同一个样本和不同样本组队,归一化因子不同,输出就不同。

更隐蔽的是按维度归一化:你对特征矩阵的每一列算均值和方差。如果这一列的物理含义是“某个距离尺度”,它的统计量应该只依赖于数据集整体,而不应该依赖于当前样本。但如果你在单个样本内部算(比如对每个样本的所有距离特征做标准化),那你实际上是在做一种非线性的重加权,它对置换仍然不变,但对“距离多重集”的排序是敏感的,可能破坏完备性。

我的处理原则很简单:所有归一化统计量在训练集上算一次,固化成常量,推理时原样使用。这样至少保证同一个输入在任何批次、任何阶段得到完全相同的特征。

5.2 邻居选择里的顺序陷阱

做局部环境描述符时,第一步通常是找 k 个最近邻。这里有两个顺序陷阱。

第一,如果你按距离排序取前 k 个,那么“距离相同的两个邻居谁排前面”是不确定的。在对称构型(比如正八面体)里这种情况很常见。取决于实现,可能拿到不同的邻居集合,导致特征跳变。解决办法是用距离阈值而非固定 k,或者对同距离的邻居做全排列平均。

第二,k 近邻的搜索在大规模数据上会用近似算法(比如基于空间划分的),不同库、不同版本的返回顺序可能不同。如果你的特征对邻居顺序敏感(比如用了序列模型处理邻居),那结果跟着变。我一般会强制按距离做一次稳定排序,并且在距离相同时用坐标或者元素序号做次级排序键。

5.3 完备但不不可微,可微但不完备

这是个必须做的取舍,而且没有普适答案。

排序、取最大值、最近邻查询,这些操作都是分段常值或者分段线性的,几乎处处梯度为零。如果你的不变量特征里包含排序,那么反向传播到这些位置时梯度是断的。对于端到端训练来说,这意味着前面那些可学习模块得不到有效的梯度信号。

反过来,功率谱、对称多项式、距离的连续函数,都可微,但完备性有损失。

我的经验是分层处理:底层用可微的、略不完备的特征保证梯度通畅,顶层用不可微但完备的特征做兜底校验。比如训练时用功率谱特征,同时用一个离线流程算完整距离矩阵,定期检查两者给出的邻居排序是否一致。如果一致,说明不完备没有造成实际的信息损失;如果不一致,就得看看是哪些构型出问题了。

5.4 用单元测试把对称性钉死

这是我最推荐的习惯,成本极低,收益极高。写几个断言,把“不变性”这件事从口头承诺变成可执行的检查:

import numpy as np def rotation_z(theta): c, s = np.cos(theta), np.sin(theta) return np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]) def feature_fn(X): # 你的不变特征函数 D = np.linalg.norm(X[:, None, :] - X[None, :, :], axis=-1) iu = np.triu_indices(len(X), k=1) return np.sort(D[iu]) # 排序消除点序影响 def test_rotation_invariance(): rng = np.random.default_rng(0) X = rng.normal(size=(12, 3)) f0 = feature_fn(X) for theta in [0.1, 0.7, 1.9, 3.0, 5.5]: Xr = X @ rotation_z(theta).T f1 = feature_fn(Xr) assert np.allclose(f0, f1, atol=1e-9), f"旋转 {theta} 后特征不一致" def test_permutation_invariance(): rng = np.random.default_rng(1) X = rng.normal(size=(9, 3)) f0 = feature_fn(X) for _ in range(20): Xp = X[rng.permutation(len(X))] assert np.allclose(f0, feature_fn(Xp), atol=1e-9)

运行这两个测试,我至少抓到过三类问题:一是某个中间步骤用了原始坐标做判断(比如“取 x 最大的点作为起点”),二是某个库的默认参数做了隐式的中心化,三是浮点误差累积导致容差设得太紧。容差设 1e-9 是个比较合适的起点,如果你的特征里有多次求逆或者特征分解,可能要放宽到 1e-6,但要盯住不能到 1e-3,那说明数值稳定性已经出问题了。

6. 什么时候该绕开不变量这套思路

最后说点反向的话。不变量理论很漂亮,但不是所有带对称性的任务都值得走这条路。

如果你数据量极大、对称群很小(比如只有镜像翻转或者循环移位),那直接堆数据增强、把网络容量做上去,可能比精心构造特征更省时间。特征工程的收益在数据稀缺和群结构复杂这两个条件同时成立时才最明显。

如果你任务的目标本身就跟参考系有关(比如“判断这个零件的安装面朝上还是朝下”),那强行做成不变特征反而把有用信息抹掉了。这种情况下需要的是部分不变:对平面内的旋转不变,但对上下方向敏感。实现上就是把群拆成子群,只对关心的一侧做平均。

如果对称性只能近似成立(比如真实的分子在溶液里总是有微小形变),那严格的不变量约束会把有用信号当噪声滤掉。我一般会先用严格不变量做特征,再额外拼上几个对形变敏感的量,让模型自己权衡。

我实际做下来最大的体会是:不变量理论的价值不在于给你一套现成公式,而在于给你一套判断标准。它让你能回答“我造的特征够不够”“多了会不会共线”“什么时候分不开”这些原本只能靠试错的问题。至于具体用距离、用球谐还是用等变网络,那是工程取舍,会随着数据规模、算力预算和调试成本变化,但判定标准是稳定的。先把判定标准建立起来,剩下的就是按需选型。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进