📑 本页目录(点开跳转)
05 · 等变与不变
⏱ 38 分钟 | ⭐ 转个角度还是同一个分子,而普通网络不知道这件事
🎯 一句话
科学数据自带一种互联网数据没有的结构:对称性。 分子转 30 度还是同一个分子 —— 可你把坐标喂给普通 MLP,它会把「转过的同一个构象」当成全新样本从头学。 上一页说结构预测的输出是一组三维坐标;这一页说为什么那组坐标不能直接拿去回归。
🔄 一、转一下、挪一下,什么变了
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 量级就是浮点噪声,等于零):
raw coords转和挪都变。转 30 度差出 1.13,而坐标本身的 RMS 才 0.8786 —— 同量级。⚠️ 而它正是你最容易顺手喂进模型的东西centered coords减掉质心后挪不动了,但转还是会变dist matrix两个都不动。⭐ 这就是「预测距离矩阵/接触图比预测坐标好」的全部理由centroid和force都跟着一起动,不是不变量 —— ⭐ 最后一行是关键:转过之后的力精确等于F(X) R^T
🧭 二、两个词,一条不能混的线
| 名字 | 意思 | 哪些量是这样 | 混了会怎样 |
|---|---|---|---|
| 不变 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
📖 三条读法:
- 增强确实有用:坐标模型从 0.6521 降到 0.3924。但那是 64 倍数据换来的,而且仍差距离模型 2.5 倍
- ⭐ 距离那一列一个数字都没动 —— 那 64 份旋转副本对它是逐字节相同的行,喂了等于没喂。这就是「结构上免疫」的样子:它不需要那些数据,因为那件事对它根本不存在
- 💀 最后一行最狠:同一个分子摆 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¹⁰ 个样本才谈得上覆盖。⭐ 它给的五条出路里第 ③ 条「加强的归纳偏置」就是本章在做的事 |
✅ 检查点
- 「不变」和「等变」差在哪?各举一个分子上的量。
- 转 30 度之后,
raw coords和dist matrix的最大差分别是多少? - 为什么「每个原子受的力」不能做成旋转不变的?实验里哪一行证明它是等变的?
- 整组坐标平移之后,
raw coords/centered coords/dist matrix哪些变了? - 吃距离的那个模型,为什么加到 64 倍数据既没变好也没变坏?
- 「32 个朝向」那一行的两个数是多少?说明了什么?
- 只用距离矩阵会丢掉什么?怎么补?
👀 答案
- 不变:输出一模一样(能量、溶解度、距离矩阵)。等变:输出跟着做同一个变换(力、坐标、法向量)。不变是等变的特例。
raw coords差 1.13(坐标本身 RMS 才 0.8786,同量级);dist matrix差 4.44e-16,等于零。- 力是向量、有方向,分子转过去力也该转过去;做成不变等于要求它转了不动,那样连「往哪推」都表达不出来。证据是
force vs F(X) R^T: 1.7e-16。 - 只有
raw coords变了(差 1.00e+01,正好是平移量),另两个都是 1e-15。⚠️centroid也跟着挪了 10,它是等变量。 - 那 k 份副本在距离表示下是完全相同的行,喂 64 遍等于把同一批 300 行抄 64 次,所以 RMSE 固定在 0.1592。
- 坐标模型 0.1861,距离模型 4.2e-16;标签自己的标准差是 0.7182 —— 坐标模型输出里约四分之一的变化只是朝向造成的噪声。
- 丢掉手性:分子和它的镜像距离逐格相同。补法是加有方向的量 —— 二面角、叉积、行列式的符号。
🛑 可以停在这里
⚡ 走神救援
先记住这几件事
- 不变表示输出保持不变;等变表示输出随输入按相应规则变化。
- 根据预测的是标量、向量还是坐标,选择合适的特征或结构约束。
- 增强能提供变换样本,却不等于严格满足对称性;距离表示还会丢失手性。
下一节 👉 06-小数据这一关.md