概述

双曲空间(Hadamard空间)是一种具有负常曲率的连通、单连通、完备的黎曼流形。与欧几里得空间相比,双曲空间具有以下独特性质:

  1. 指数增长的体积:半径为 的球体积随 增长,而非
  2. 树结构的自然嵌入:任意树可以等距嵌入到二维双曲空间
  3. 层次结构的表达能力:能用更少维度表示层次关系

这些特性使双曲空间特别适合表示层次结构数据(如知识图谱、社交网络、文本语义层级)。


双曲空间模型

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}'

参考