概述
双曲空间(Hadamard空间)是一种具有负常曲率的连通、单连通、完备的黎曼流形。与欧几里得空间相比,双曲空间具有以下独特性质:
- 指数增长的体积:半径为 的球体积随 增长,而非
- 树结构的自然嵌入:任意树可以等距嵌入到二维双曲空间
- 层次结构的表达能力:能用更少维度表示层次关系
这些特性使双曲空间特别适合表示层次结构数据(如知识图谱、社交网络、文本语义层级)。
双曲空间模型
Poincaré Ball 模型
Poincaré ball 模型是最常用的双曲空间表示,设曲率为 (),则:
黎曼度量:
其中 是欧几里得度量。这意味着距离原点越远的点,局部度量膨胀得越大(类似鱼眼镜头效果)。
Poincaré距离
两点 之间的测地线距离:
等价形式:
当 时,简化为:
Lorentz / Hyperboloid 模型
另一种常用表示是 Lorentz 模型(也称 hyperboloid model):
其中 Lorentz 内积:
Lorentz 模型在数值计算上更稳定,特别适合梯度下降。
模型等价性
两种模型之间存在等距映射:
其中 。
指数映射与对数映射
从原点出发的映射
设 是切空间 中的向量, 为曲率参数。
指数映射(从切空间到流形):
其中 是归一化范数。
对数映射(从流形到切空间):
一般点的映射
对于任意基点 ,利用平行移动将问题归约到原点:
指数映射:
对数映射:
黎曼梯度
在双曲空间中,损失函数 的黎曼梯度为:
注意这个Mobius梯度修正项,它补偿了 Poincaré ball 的非欧几里得度量。
Mobius运算
双曲空间的算术运算需要特殊定义,这些运算称为 Mobius运算。
Mobius加法(平行移动后的加法)
Mobius数乘
Mobius矩阵乘法
设 为矩阵,则:
这允许我们定义双曲线性层。
距离与相似度
Fréchet均值(双曲空间中的”质心”)
一组点 的 Fréchet 均值 定义为:
计算时使用黎曼梯度下降:
相似度计算
在双曲空间中,余弦相似度需要映射到切空间:
双曲空间的曲率选择
曲率参数 控制双曲空间的”弯曲程度”:
| 值 | 特性 | 适用场景 |
|---|---|---|
| 近似欧几里得空间 | 层次结构不明显时 | |
| 标准双曲空间 | 通用设置 | |
| 更紧凑的嵌入 | 层次非常深时 |
实践中, 通常作为可学习参数或通过验证集调优。
与欧几里得空间的关系
泰勒展开联系
当 时,Poincaré ball 度量趋近于欧几里得度量:
因此双曲空间可以视为欧几里得空间的曲率自适应扩展。
随机游走的比较
- 欧几里得空间: 维随机游走的覆盖半径
- 双曲空间:覆盖半径 (更快的探索)
这解释了为什么双曲空间适合层次数据:叶子节点到根节点的距离在对数尺度上。
代码实现
import torch
import torch.nn as nn
class PoincaréBall:
"""Poincaré Ball 模型的核心操作"""
def __init__(self, c=1.0):
self.c = c
def distance(self, u, v):
"""计算Poincaré距离"""
sqrt_c = self.c ** 0.5
d = 2 * self.c * torch.atanh(
torch.norm(u - v, dim=-1) /
(torch.norm(self.c * u - v, dim=-1) + 1e-5)
)
return d
def expmap(self, u, v):
"""从u沿方向v的指数映射"""
v_norm = torch.norm(v, dim=-1, keepdim=True).clamp(min=1e-10)
second_term = (torch.tanh(torch.sqrt(self.c) * v_norm / 2) /
(torch.sqrt(self.c) * v_norm / 2)) * v
return self._mobius_add(u, second_term)
def logmap(self, u, y):
"""从u到y的对数映射"""
diff = self._mobius_add(-u, y)
diff_norm = torch.norm(diff, dim=-1, keepdim=True).clamp(min=1e-10)
return (2 / torch.sqrt(self.c) * torch.atanh(torch.sqrt(self.c) * diff_norm) /
diff_norm) * diff
def _mobius_add(self, u, v):
"""Mobius加法"""
uv = torch.sum(u * v, dim=-1, keepdim=True)
uu = torch.sum(u * u, dim=-1, keepdim=True)
vv = torch.sum(v * v, dim=-1, keepdim=True)
denominator = 1 - 2 * self.c * uv + self.c ** 2 * vv
return (u + v - 2 * self.c * torch.sum(u * v, dim=-1, keepdim=True) * u) / denominator
def mobius_matvec(self, m, u):
"""Mobius矩阵乘法"""
return self.expmap(self._zeros_like(u), torch.einsum('...ij,...j->...i', m, self.logmap(self._zeros_like(u), u)))
def _zeros_like(self, x):
return torch.zeros_like(x)
def riemannian_grad(self, euclidean_grad, x):
"""Mobius梯度修正"""
return ((self.c - torch.norm(x, dim=-1, keepdim=True) ** 2) ** 2 /
(4 * self.c)) * euclidean_grad
class HyperbolicLayer(nn.Module):
"""双曲空间中的线性层"""
def __init__(self, in_features, out_features, c=1.0):
super().__init__()
self.c = c
self.ball = PoincaréBall(c)
self.weight = nn.Parameter(torch.randn(in_features, out_features))
self.bias = nn.Parameter(torch.zeros(out_features))
nn.init.xavier_uniform_(self.weight)
def forward(self, x):
# Mobius矩阵乘法 + 平移
x_mapped = self.ball.mobius_matvec(self.weight, x)
return self.ball._mobius_add(x_mapped, self.bias)
def extra_repr(self):
return f'in_features={self.weight.shape[0]}, out_features={self.weight.shape[1]}, c={self.c}'