🏠 总目录📚 本教程 05 · 等变与不变 ← →
📑 本页目录(点开跳转)

05 · 等变与不变

⏱ 38 分钟 | ⭐ 转个角度还是同一个分子,而普通网络不知道这件事


🎯 一句话

科学数据自带一种互联网数据没有的结构:对称性。 分子转 30 度还是同一个分子 —— 可你把坐标喂给普通 MLP,它会把「转过的同一个构象」当成全新样本从头学。 上一页说结构预测的输出是一组三维坐标;这一页说为什么那组坐标不能直接拿去回归。


🔄 一、转一下、挪一下,什么变了

同一个构象,转一下 / 挪一下,什么变了量旋转平移原始坐标变了1.13e+00变了1.00e+01中心化坐标变了9.26e-01没变1.11e-15距离矩阵没变4.44e-16没变1.33e-15质心变了2.08e-01变了1.00e+01能量没变4.44e-16没变4.44e-16受力跟着转3.59e-01没变9.44e-16← 这两行两边都不变 = 不变量← 变了,但 F(XR) = F(X)Rᵀ = 等变表里是变换前后的最大差;1e-15 这个量级就是浮点误差,等于没变
先看「没变」那几格:距离矩阵和能量对两种变换都免疫,所以它们能直接当预测目标。⭐ 再看高亮的受力那一行 —— 它在旋转下确实变了,但变得恰好跟着一起转。「不变」和「等变」的分界就在这一行上。
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(size=(6, 3))                       # 6 个原子的一个构象
t = np.deg2rad(30); c, s = np.cos(t), np.sin(t)
R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])  # 绕 z 轴转 30 度
XR = X @ R.T                                      # ⭐ 同一个分子,只是换个朝向
XT = X + np.array([10.0, -4.0, 2.5])              # ⭐ 同一个分子,只是挪个位置

ctr = lambda P: P - P.mean(0)                                     # 相对质心的坐标
dm = lambda P: np.sqrt(((P[:, None] - P[None]) ** 2).sum(-1))     # 两两距离矩阵
E = lambda P: np.exp(-dm(P)[np.triu_indices(len(P), 1)]).sum()    # 只依赖距离的能量

def F(P):                                         # 每个原子受的力 = -dE/dx
    d = dm(P) + np.eye(len(P))                    # 对角线加 1,避开除零
    return (np.exp(-d)[..., None] * (P[:, None] - P[None]) / d[..., None]).sum(1)

print("coord RMS (scale)  = %.4f" % np.sqrt((X ** 2).mean()))
print("%-16s %-11s %-11s" % ("quantity", "rotate", "translate"))
for name, f in [("raw coords", lambda P: P), ("centered coords", ctr), ("dist matrix", dm),
                ("centroid", lambda P: P.mean(0)), ("energy", E), ("force", F)]:
    g = lambda P: np.max(np.abs(np.asarray(f(P)) - np.asarray(f(X))))
    print("%-16s %-11.2e %-11.2e" % (name, g(XR), g(XT)))
print("force vs F(X) R^T:  %.1e   <- equivariant" % np.abs(F(XR) - F(X) @ R.T).max())

真实输出(两列都是「变换前后的最大差」):

coord RMS (scale)  = 0.8786
quantity         rotate      translate
raw coords       1.13e+00    1.00e+01
centered coords  9.26e-01    1.11e-15
dist matrix      4.44e-16    1.33e-15
centroid         2.08e-01    1.00e+01
energy           4.44e-16    4.44e-16
force            3.59e-01    9.44e-16
force vs F(X) R^T:  1.7e-16   <- equivariant

📖 怎么读(1e-15 量级就是浮点噪声,等于零):


🧭 二、两个词,一条不能混的线

名字 意思 哪些量是这样 混了会怎样
不变 invariant 输入变了,输出一模一样 能量、溶解度、「是不是酶」、距离矩阵 什么都不做:六个自由度全靠数据硬学,学到的一大半是冗余
等变 equivariant 输入变了,输出跟着做同一个变换 力、预测出的坐标、速度、法向量 做成不变:力只剩一个与朝向无关的数,连「往哪推」都表达不出来

不变是等变的特例 —— 作用在输出上的那个变换恰好是「什么都不做」。

⭐ 动手前先问一句:我要预测的这个量,把输入转 30 度之后,它变不变? 这不是生物学问题,是建模问题 —— 它直接决定你该用哪种表示。


🌫️ 三、平移是最容易被忽略的那个

旋转大家都记得,平移常常没人管。看上表的 translate 列:坐标整整差了 10,而距离一动没动。

⚠️ 结构文件里的绝对坐标是任意的 —— 原点由当初解析结构时的坐标系决定,同一条链在两份文件里 可以落在完全不同的地方。你不减质心,模型第一层就在学「这东西落在盒子哪个角」,而那个角不携带信息。

⭐ 中心化只要一行 P - P.mean(0),是这一章性价比最高的一件事。它只解决平移,解决不了旋转。


📉 四、数据增强顶不上(这一条最反直觉)

「转不变?那训练时随机转几次不就行了。」这条路能走,但走得很贵。

同一批 300 个构象,每个随机摆 k 个朝向当成 k 个样本,标签是一个只依赖两两距离的能量。 两个模型只差在吃什么:一个吃中心化坐标(转了就变),一个吃两两距离(转了不变)。

import numpy as np
tri = np.triu_indices(6, 1)
dst = lambda P: np.sqrt(((P[:, None] - P[None]) ** 2).sum(-1))[tri]  # 表示 B:距离,转了不变
crd = lambda P: (P - P.mean(0)).ravel()                              # 表示 A:坐标,转了就变

def rot(r):                                       # 随机旋转矩阵
    Q, _ = np.linalg.qr(r.normal(size=(3, 3)))
    return Q if np.linalg.det(Q) > 0 else Q * np.array([-1., 1, 1])

def make(seed, k):                                # 300 个构象,每个随机摆 k 个朝向
    rc, rr = np.random.default_rng(seed), np.random.default_rng(77)
    A, B, Y = [], [], []
    for _ in range(300):
        P = rc.normal(size=(6, 3))
        y = np.exp(-dst(P)).sum()                 # ⭐ 标签只依赖距离 -> 和朝向无关
        for _ in range(k):
            Q = P @ rot(rr).T
            A.append(crd(Q)); B.append(dst(Q)); Y.append(y)
    return np.array(A), np.array(B), np.array(Y)

def fit(X, y, n=300):                             # 随机 ReLU 特征 + 岭回归,当小网络用
    r = np.random.default_rng(5)
    m, s = X.mean(0), X.std(0) + 1e-9
    W = r.normal(size=(X.shape[1], n)) / np.sqrt(X.shape[1]); b = r.uniform(-1, 1, n)
    f = lambda Z: np.maximum((Z - m) / s @ W + b, 0)
    F = f(X); w = np.linalg.solve(F.T @ F + 1e-3 * len(y) * np.eye(n), F.T @ y)
    return lambda Z: f(Z) @ w

TA, TB, yte = make(999, 1)                        # 测试集:300 个没见过的构象
err = lambda p, Z: np.sqrt(((p(Z) - yte) ** 2).mean())
print("baseline (always predict the mean) RMSE = %.4f" % yte.std())
for k in [1, 4, 16, 64]:
    A, B, y = make(1234, k)
    pa, pb = fit(A, y), fit(B, y)
    print("rot/conf=%-3d n=%-6d coords %.4f | dists %.4f" % (k, len(y), err(pa, TA), err(pb, TB)))

r = np.random.default_rng(2024)                   # 同一个分子摆 32 个朝向,预测飘不飘
P = r.normal(size=(6, 3)); Qs = [P @ rot(r).T for _ in range(32)]
print("same molecule, 32 orientations: coords std = %.4f | dists std = %.1e"
      % (pa(np.array([crd(q) for q in Qs])).std(), pb(np.array([dst(q) for q in Qs])).std()))

真实输出:

baseline (always predict the mean) RMSE = 0.7182
rot/conf=1   n=300    coords 0.6521 | dists 0.1592
rot/conf=4   n=1200   coords 0.4152 | dists 0.1592
rot/conf=16  n=4800   coords 0.4034 | dists 0.1592
rot/conf=64  n=19200  coords 0.3924 | dists 0.1592
same molecule, 32 orientations: coords std = 0.1861 | dists std = 4.2e-16

📖 三条读法:

  1. 增强确实有用:坐标模型从 0.6521 降到 0.3924。但那是 64 倍数据换来的,而且仍差距离模型 2.5 倍
  2. ⭐ 距离那一列一个数字都没动 —— 那 64 份旋转副本对它是逐字节相同的行,喂了等于没喂。这就是「结构上免疫」的样子:它不需要那些数据,因为那件事对它根本不存在
  3. 💀 最后一行最狠:同一个分子摆 32 个朝向,坐标模型的预测标准差 0.1861,而标签自己的标准差才 0.7182 —— 它输出的「信号」里有四分之一只是朝向

⚠️ 增强补不上的原因:三维旋转有 3 个自由度,靠采样去「覆盖」它,样本量得按指数走。


🧱 五、三档做法,够用就停

档 做法 代价
① 换成不变特征 距离矩阵、接触图、键角、二面角 最便宜,一行就能上;⚠️ 只能预测不变量
② 训练时随机变换(增强) 上面实验的左列 永远只是「大概不变」;测试时通常还得靠 TTA 兜
③ 结构上等变 中间量分成标量和向量两类,向量只被允许跟着一起转 实现复杂、算得慢;数据少又必须输出方向时才值

⭐ 绝大多数任务停在 ① 就够,只有当你要预测的东西本身带方向(力、位移、朝向)才不成立。 🗓️ 第 ③ 档的正式名字是 E(3) / SE(3) 等变网络,属于「几何深度学习」这一支;群表示、球谐函数 那套推导这一章不展开,想深入去找几何深度学习的专门材料。⚠️ 哪个实现最强半年换一批,记判据别记模型名。

💀 ① 不是免费的:距离矩阵丢掉了手性。 一个分子和它的镜像,两两距离逐格相同,可它们是两个东西。 只喂距离,模型分不出左右手;要分就得加上有方向的量(二面角、叉积、行列式的符号)。 ⭐ 你换来的不变性,可能顺手把你需要的信息也抹掉了。

🧪 一条两分钟的自检:同一个输入随机变换 20 次跑你的模型 —— 声称不变的,预测标准差该在 1e-15 量级(是 0.1 就说明它在拿朝向当特征);声称等变的,比 f(RX) 和 f(X) @ R.T。 它抓到的多半是你代码里的 bug:忘了中心化、增强只作用在输入没作用在标签、法向量没跟着转。


🔗 这一章连到哪里

去哪 为什么
机器学习与深度学习基础 · 12 CNN:处理图像 卷积是同一个思想的老朋友:同一个核滑遍全图(权重共享),物体挪了输出跟着挪。⭐ 那一章统一叫「平移不变」,是把「卷积层等变 + 池化后不变」合起来说;它还写了这个假设什么时候害你
机器学习与深度学习基础 · 10 正则化全家桶 那一章把数据增强的本质写成「告诉模型这些变化下答案不变 —— 把先验知识注入进去」。⭐ 本章是同一件事的另一条路:与其告诉它,不如让它结构上做不到
数据这一关 · 14 数据泄漏的七种来源 增强出来的副本不是独立样本。那一章判泄漏的第三问「训练集和测试集之间有没有同一个东西跨过去」正好卡住它;实测「⑤ 重复样本跨集」AUC 0.745 掉到 0.605
机器学习的数学原理 · 05 维度灾难 为什么「多转几次就学会了」不成立:它的实验 4 里 10 维要 10¹⁰ 个样本才谈得上覆盖。⭐ 它给的五条出路里第 ③ 条「加强的归纳偏置」就是本章在做的事

✅ 检查点

  1. 「不变」和「等变」差在哪?各举一个分子上的量。
  2. 转 30 度之后,raw coords 和 dist matrix 的最大差分别是多少?
  3. 为什么「每个原子受的力」不能做成旋转不变的?实验里哪一行证明它是等变的?
  4. 整组坐标平移之后,raw coords / centered coords / dist matrix 哪些变了?
  5. 吃距离的那个模型,为什么加到 64 倍数据既没变好也没变坏?
  6. 「32 个朝向」那一行的两个数是多少?说明了什么?
  7. 只用距离矩阵会丢掉什么?怎么补?
👀 答案
  1. 不变:输出一模一样(能量、溶解度、距离矩阵)。等变:输出跟着做同一个变换(力、坐标、法向量)。不变是等变的特例。
  2. raw coords 差 1.13(坐标本身 RMS 才 0.8786,同量级);dist matrix 差 4.44e-16,等于零。
  3. 力是向量、有方向,分子转过去力也该转过去;做成不变等于要求它转了不动,那样连「往哪推」都表达不出来。证据是 force vs F(X) R^T: 1.7e-16。
  4. 只有 raw coords 变了(差 1.00e+01,正好是平移量),另两个都是 1e-15。⚠️ centroid 也跟着挪了 10,它是等变量。
  5. 那 k 份副本在距离表示下是完全相同的行,喂 64 遍等于把同一批 300 行抄 64 次,所以 RMSE 固定在 0.1592。
  6. 坐标模型 0.1861,距离模型 4.2e-16;标签自己的标准差是 0.7182 —— 坐标模型输出里约四分之一的变化只是朝向造成的噪声。
  7. 丢掉手性:分子和它的镜像距离逐格相同。补法是加有方向的量 —— 二面角、叉积、行列式的符号。

🛑 可以停在这里

⚡ 走神救援

先记住这几件事

下一节 👉 06-小数据这一关.md

打卡记录保存在你的浏览器里,首页能看到总进度