Hyperbolic Neural Networks: Guide Complet – Embeddings dans l’Espace Hyperbolique

Hyperbolic Neural Networks: Guide Complet - Embeddings dans l'Espace Hyperbolique

Hyperbolic Neural Networks – Guide complet

Résumé : Les réseaux de neurones hyperboliques représentent une avancée majeure dans la façon dont nous modélisons des données hiérarchiques à l’aide d’embeddings. Contrairement aux espaces euclidiens traditionnels qui peinent à représenter efficacement des structures arborescentes, l’espace hyperbolique — en particulier le modèle de la boule de Poincaré — offre une capacité exponentielle à encoder des hiérarchies complexes avec un nombre de dimensions remarquablement réduit. Un arbre à n niveaux possède 2^n feuilles, alors que la surface euclidienne ne croît que de manière quadratique avec le rayon. Cette inadéquation fondamentale entre la croissance exponentielle des arbres et la croissance polynomiale de l’espace euclidien explique pourquoi les embeddings euclidiens nécessitent des centaines de dimensions pour représenter correctement des hiérarchies modestes. L’espace hyperbolique, caractérisé par sa courbure négative constante, présente une croissance exponentielle de la surface et du volume en fonction de la distance au centre. Dans l’article fondateur de 2018, Maximilian Nickel et Douwe Kiessling ont démontré que les plongements de Poincaré surpassent significativement les méthodes euclidiennes pour représenter des relations hiérarchiques, avec des résultats impressionnants sur des jeux de données comme WordNet et les graphes de connaissances. Ce guide complet explore en profondeur les fondements mathématiques, l’intuition géométrique, l’implémentation pratique et les applications concrètes des réseaux de neurones hyperboliques dans l’apprentissage automatique contemporain.


Principe mathématique

La métrique de la boule de Poincaré

Le modèle de la boule de Poincaré est la représentation la plus couramment utilisée de l’espace hyperbolique en apprentissage automatique. Il s’agit de l’intérieur de la boule unité dans ℝ^n, c’est-à-dire l’ensemble des points x tels que ||x|| < 1. La norme d’un point dans cette boule doit strictement être inférieure à 1, ce qui signifie que tous les embeddings résident à l’intérieur d’une sphère de rayon unité. La frontière de cette boule — la sphère unitaire elle-même — représente l’infini géométrique : à mesure qu’un point s’approche du bord, sa distance au centre tend vers l’infini.

La distance hyperbolique entre deux points x et y dans la boule de Poincaré B^n est définie par la formule suivante :

d_B(x, y) = arcosh(1 + 2 * ||x - y||^2 / ((1 - ||x||^2)(1 - ||y||^2)))

Cette métrique encode la géométrie à courbure négative. Le terme ||x - y||^2 représente la distance euclidienne au carré entre les deux points. Les dénominateurs (1 - ||x||^2) et (1 - ||y||^2) amplifient cette distance lorsque les points s’approchent de la frontière de la boule, reflétant le fait que l’espace se déforme exponentiellement près du bord. La fonction arcosh (argument cosinus hyperbolique inverse) garantit que la distance résultante satisfait les axiomes d’une métrique hyperbolique. La fonction arcosh peut être calculée numériquement via la relation arcosh(z) = log(z + sqrt(z^2 - 1)), ce qui est particulièrement utile lors de l’implémentation en PyTorch ou NumPy.

L’addition de Möbius

Dans l’espace euclidien, l’addition vectorielle standard x + y préserve la structure linéaire. En revanche, dans la boule de Poincaré, l’addition directe de deux vecteurs pourrait produire un résultat en dehors de la boule unité, violant ainsi la contrainte fondamentale ||x|| < 1. Pour remédier à ce problème, on utilise l’addition de Möbius, une opération qui respecte la structure riemannienne de l’espace :

x ⊕_M y = ((1 + 2<x,y> + ||y||^2)*x + (1 - ||x||^2)*y) / (1 + 2<x,y> + ||x||^2 * ||y||^2)

<x, y> désigne le produit scalaire euclidien standard entre x et y. L’addition de Möbius garantit que le résultat reste toujours à l’intérieur de la boule unité. Elle n’est ni commutative ni associative au sens classique, mais elle préserve les propriétés géométriques essentielles de l’espace hyperbolique. Cette opération est fondamentale pour réaliser des transformations linéaires dans l’espace tangent puis les projeter correctement sur la variété. Une propriété remarquable de l’addition de Möbius est qu’elle admet un élément neutre — le vecteur zéro — et que chaque élément possède un inverse, ce qui permet de définir une structure de groupe sur la boule de Poincaré.

L’application exponentielle et l’application logarithmique

L’application exponentielle et son inverse, l’application logarithmique, constituent le pont entre l’espace tangent (qui est euclidien) et la boule de Poincaré (qui est hyperbolique).

L’application exponentielle exp_x : T_x B^n → B^n projette un vecteur v de l’espace tangent au point x vers un point sur la boule de Poincaré. Pour un point à l’origine (x = 0), cette projection s’écrit :

exp_0(v) = tanh(sqrt(c) * ||v||) * v / (sqrt(c) * ||v||)

où c est le paramètre de courbure. Pour un point x quelconque, on utilise le transport parallèle via l’addition de Möbius :

exp_x(v) = x ⊕_M (tanh(sqrt(c) * ||v||) * v / (sqrt(c) * ||v||))

L’application logarithmique log_x : B^n → T_x B^n effectue l’opération inverse : elle ramène un point y de la boule vers l’espace tangent en x. Pour l’origine :

log_0(y) = (1/sqrt(c)) * arctanh(sqrt(c) * ||y||) * y / ||y||

Ces deux applications sont absolument essentielles dans l’entraînement des modèles hyperboliques. Les gradients sont calculés dans l’espace tangent (où le calcul est euclidien classique), puis projetés sur la variété via l’application exponentielle pour mettre à jour les embeddings. Ce processus est au cœur de la descente de gradient riemannienne, qui remplace simplement la mise à jour euclidienne x = x - η * ∇f(x) par son équivalent riemannien : x = exp_x(-η * ∇^R f(x)), où ∇^R f est le gradient riemannien obtenu en transportant le gradient euclidien dans l’espace tangent via la métrique de Riemann.

Pourquoi l’espace euclidien échoue pour les arbres

Le problème fondamental réside dans la croissance de la surface (ou du volume) en fonction de la distance. Dans un espace euclidien de dimension d, le volume d’une boule de rayon r croît comme r^d — c’est-à-dire de manière polynomiale. Un arbre binaire de profondeur n possède 2^n feuilles : sa croissance est exponentielle. Pour représenter fidèlement cette structure dans ℝ^d, il faut que la dimension d soit suffisamment grande pour que le volume disponible puisse accueillir tous les nœuds sans distorsion excessive. En pratique, cela nécessite des centaines de dimensions pour des hiérarchies modestes.

À l’inverse, dans l’espace hyperbolique, le volume d’une boule de rayon r croît comme e^((d-1)*r) — de manière exponentielle. Cette croissance correspond exactement à celle des arbres, ce qui permet d’encoder des hiérarchies profondes avec seulement 5 à 50 dimensions, au lieu de centaines en euclidien. La différence entre croissance polynomiale et croissance exponentielle du volume est l’argument central qui justifie l’utilisation de la géométrie hyperbolique pour les données hiérarchiques. C’est précisément cette observation théorique qui a motivé Nickel et Kiessling à proposer les plongements de Poincaré dans leur article de NeurIPS 2018.

Le paramètre de courbure c

Le paramètre de courbure c contrôle l’intensité de la courbure négative de l’espace. Une valeur de c = 1 correspond à la courbure standard de la boule de Poincaré unitaire. Des valeurs de c > 1 augmentent la courbure négative, ce qui signifie que la capacité de l’espace croît encore plus rapidement avec la distance. Cela permet de capturer des hiérarchies plus profondes et plus ramifiées. Inversement, lorsque c tend vers 0, l’espace hyperbolique se rapproche de l’espace euclidien plat. Le choix de c est donc un hyperparamètre crucial : une courbure trop élevée peut conduire à des problèmes numériques (les embeddings convergent trop rapidement vers la frontière), tandis qu’une courbure trop faible réduit l’avantage de la géométrie hyperbolique. Dans la pratique, on choisit typiquement c entre 0,5 et 2,0 selon la complexité hiérarchique des données. Certains travaux récents proposent même d’apprendre le paramètre de courbure automatiquement durant l’entraînement, permettant au modèle de s’adapter à la structure intrinsèque des données.


Intuition géométrique

L’analogie de l’arbre et de la surface disponible

Imaginez que vous essayiez de dessiner un arbre binaire complet sur une feuille de papier plane. Au premier niveau, vous avez un nœud racine. Au deuxième niveau, deux nœuds enfants. Au troisième, quatre. Au dixième niveau, 1024 feuilles. Si vous essayez de placer tous ces nœuds sur une surface plane tout en maintenant des distances proportionnelles à leur proximité dans l’arbre, vous rencontrerez rapidement un problème fondamental : la circonférence d’un cercle en géométrie euclidienne croît linéairement avec le rayon (C = 2πr). Il n’y a tout simplement pas assez de périmètre pour accueillir toutes les feuilles à distance égale de la racine. Les nœuds se chevauchent, les distances sont distordues, et la structure hiérarchique est perdue.

Maintenant, imaginez une surface en forme de selle de cheval ou de puce Pringles — une surface à courbure négative. Sur une telle surface, la circonférence d’un “cercle” croît exponentiellement avec le rayon. Il y a toujours assez d’espace pour accueillir davantage de nœuds. C’est exactement comme comparer une feuille de papier plate à une puce Pringles qui s’étend indéfiniment vers l’extérieur. L’espace hyperbolique est fondamentalement cette puce Pringles : il dispose de la place nécessaire pour accueillir l’explosion combinatoire des nœuds dans une hiérarchie. Cette analogie visuelle est le moyen le plus efficace de comprendre pourquoi la géométrie hyperbolique est si bien adaptée aux données arborescentes.

La hiérarchie des concepts

Prenons un exemple concret avec des mots organisés hiérarchiquement : animal → mammifère → chien → caniche. Dans un espace hyperbolique, le mot “animal” serait placé près du centre de la boule de Poincaré, car c’est le concept le plus général. “Mammifère” serait un peu plus loin du centre. “Chien” encore plus loin. Et “caniche” se retrouverait très près de la frontière de la boule. La distance hyperbolique entre “animal” et “caniche” serait grande (ce qui est correct : ils sont très éloignés dans la hiérarchie), mais “chien” et “caniche” seraient relativement proches l’un de l’autre — bien plus proches que “animal” et “caniche”.

La beauté de cette représentation réside dans son interprétabilité géométrique immédiate : la distance au centre indique directement le niveau de généralité du concept. Plus un embedding est proche de l’origine, plus le concept qu’il représente est abstrait et englobant. Plus il s’approche de la frontière, plus il est spécifique et particulier. Cette correspondance entre position géométrique et niveau conceptuel n’existe tout simplement pas dans les espaces euclidiens, où tous les points sont traités de manière symétrique sans notion intrinsèque de “centralité” conceptuelle. Un autre avantage de cette approche est que les opérations de raisonnement hiérarchique — comme déterminer si un concept est une sous-catégorie d’un autre — deviennent de simples comparaisons de distances hyperboliques, sans nécessiter de structures de données complexes ou de règles explicites.


Implémentation Python complète

Voici une implémentation complète en PyTorch d’un modèle d’embeddings hyperboliques sur la boule de Poincaré. Le code inclut la classe PoincareBall, la génération de données arborescentes synthétiques, le modèle d’embedding, la boucle d’entraînement avec descente de gradient riemannienne, et la visualisation 2D des résultats.

Classe PoincareBall et opérations fondamentales

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from collections import defaultdict

EPS = 1e-8


class PoincareBall:
    """
    Boule de Poincaré avec courbure négative -c².
    Implémente les opérations fondamentales de géométrie hyperbolique.
    """

    def __init__(self, c=1.0):
        self.c = c
        self.sqrt_c = np.sqrt(c)

    def mobius_add(self, x, y):
        """Addition de Möbius dans la boule de Poincaré."""
        x2 = torch.sum(x * x, dim=-1, keepdim=True)
        y2 = torch.sum(y * y, dim=-1, keepdim=True)
        xy = torch.sum(x * y, dim=-1, keepdim=True)

        num1 = 1 + 2 * self.c * xy + (self.c ** 2) * y2
        num2 = 1 - self.c * x2 + (self.c ** 2) * y2

        num = num1 * x + num2 * y
        den = 1 + 2 * self.c * xy + (self.c ** 2) * x2 * y2

        return num / (den + EPS)

    def dist(self, x, y):
        """Distance hyperbolique entre deux points dans la boule."""
        diff = x - y
        sq_diff = torch.sum(diff * diff, dim=-1, keepdim=True)

        x2 = torch.sum(x * x, dim=-1, keepdim=True)
        y2 = torch.sum(y * y, dim=-1, keepdim=True)

        denom = (1 - self.c * x2) * (1 - self.c * y2)
        arg = 1 + 2 * self.c * sq_diff / (denom + EPS)

        # arcosh(z) = log(z + sqrt(z² - 1))
        arg = torch.clamp(arg, min=1.0 + EPS)
        result = torch.acosh(arg)
        return result / self.sqrt_c

    def exp_map(self, x, v):
        """Application exponentielle : espace tangent → boule."""
        v_norm = torch.sqrt(torch.sum(v * v, dim=-1, keepdim=True) + EPS)

        if self.c == 0:
            return x + v

        tanh_arg = np.sqrt(self.c) * v_norm
        scale = torch.tanh(tanh_arg) / (np.sqrt(self.c) * v_norm + EPS)

        result = x + scale * v
        result = self.project(result)
        return result

    def log_map(self, x, y):
        """Application logarithmique : boule → espace tangent."""
        diff = y - x
        diff_norm = torch.sqrt(torch.sum(diff * diff, dim=-1, keepdim=True) + EPS)

        if self.c == 0:
            return diff

        x2 = torch.sum(x * x, dim=-1, keepdim=True)
        y2 = torch.sum(y * y, dim=-1, keepdim=True)

        denom = (1 - self.c * x2) * (1 - self.c * y2) + EPS
        inner = 1 + 2 * self.c * torch.sum(diff * diff, dim=-1, keepdim=True) / denom
        dist_xy = torch.acosh(torch.clamp(inner, min=1.0 + EPS))

        scale = dist_xy / (np.sqrt(self.c) * diff_norm + EPS)
        return scale * diff

    def project(self, x, max_norm=0.999):
        """Projection dans la boule unité (contrainte de norme)."""
        x = torch.renorm(x, 2, 0, maxnorm=max_norm)
        return x

    def mobius_matvec(self, M, x):
        """Multiplication matrice-vecteur de Möbius pour couches linéaires."""
        Mx = torch.matmul(x, M.T)
        Mx_norm = torch.sqrt(torch.sum(Mx * Mx, dim=-1, keepdim=True) + EPS)

        x2 = torch.sum(x * x, dim=-1, keepdim=True)
        sqrt_1_mc_x2 = torch.sqrt(torch.clamp(1 - self.c * x2, min=EPS))

        scale = torch.tanh(np.sqrt(self.c) * Mx_norm) / (np.sqrt(self.c) * Mx_norm + EPS)
        result = scale * Mx
        return result * sqrt_1_mc_x2

Génération de données hiérarchiques synthétiques

def generate_tree_data(depth=5, branching=3, seed=42):
    """
    Génère des paires de nœuds avec leurs relations hiérarchiques.

    Args:
        depth: profondeur maximale de l'arbre (0 à depth)
        branching: nombre de branches par nœud
        seed: graine aléatoire pour reproductibilité

    Returns:
        all_head, all_tail, all_label, node_depths, num_nodes
    """
    np.random.seed(seed)

    # Assigner des IDs aux nœuds par parcours BFS
    parent_child_edges = []
    node_depths = {0: 0}

    queue = [0]
    next_id = 1

    while queue:
        current = queue.pop(0)
        current_depth = node_depths[current]

        if current_depth < depth:
            for _ in range(branching):
                new_id = next_id
                next_id += 1
                node_depths[new_id] = current_depth + 1
                parent_child_edges.append((current, new_id))
                queue.append(new_id)

    num_nodes = next_id

    # Paires positives (liens parent-enfant dans l'arbre)
    pos_head = [e[0] for e in parent_child_edges]
    pos_tail = [e[1] for e in parent_child_edges]
    pos_label = torch.ones(len(pos_head), dtype=torch.float32)

    # Paires négatives (nœuds non liés)
    all_nodes = list(range(num_nodes))
    neg_pairs = set()
    max_neg = len(parent_child_edges) * 2

    attempts = 0
    while len(neg_pairs) < max_neg and attempts < max_neg * 10:
        h, t = np.random.choice(all_nodes, 2, replace=False)
        if (h, t) not in parent_child_edges and (t, h) not in parent_child_edges:
            if (h, t) not in neg_pairs and (t, h) not in neg_pairs:
                neg_pairs.add((h, t))
        attempts += 1

    neg_head = [p[0] for p in neg_pairs]
    neg_tail = [p[1] for p in neg_pairs]
    neg_label = torch.zeros(len(neg_pairs), dtype=torch.float32)

    # Combiner les paires positives et négatives
    all_head = pos_head + neg_head
    all_tail = pos_tail + neg_tail
    all_label = torch.cat([pos_label, neg_label])

    # Ajouter des négatifs de même niveau (plus difficiles à discriminer)
    same_level_pairs = []
    by_level = defaultdict(list)
    for nid, d in node_depths.items():
        by_level[d].append(nid)

    for d, nodes_at_d in by_level.items():
        if len(nodes_at_d) >= 2:
            for _ in range(min(len(nodes_at_d), 10)):
                i, j = np.random.choice(nodes_at_d, 2, replace=False)
                if (i, j) not in neg_pairs and (j, i) not in neg_pairs:
                    same_level_pairs.append((i, j))

    if same_level_pairs:
        sl_head = [p[0] for p in same_level_pairs]
        sl_tail = [p[1] for p in same_level_pairs]
        sl_label = torch.zeros(len(same_level_pairs), dtype=torch.float32)
        all_head += sl_head
        all_tail += sl_tail
        all_label = torch.cat([all_label, sl_label])

    return all_head, all_tail, all_label, node_depths, num_nodes

Modèle d’embedding hyperbolique

class HyperbolicEmbeddingModel(nn.Module):
    """Modèle d'embeddings sur la boule de Poincaré."""

    def __init__(self, num_nodes, embedding_dim=5, c=1.0):
        super().__init__()
        self.manifold = PoincareBall(c=c)
        self.embedding_dim = embedding_dim
        self.num_nodes = num_nodes
        self.c = c

        # Initialisation des embeddings (proche de l'origine)
        self.embeddings = nn.Parameter(
            torch.randn(num_nodes, embedding_dim) * 0.01
        )

    def forward(self, head_idx, tail_idx):
        """Calcule la similarité hyperbolique entre paires de nœuds."""
        head_emb = self.embeddings[head_idx]
        tail_emb = self.embeddings[tail_idx]

        # Distance hyperbolique
        dist = self.manifold.dist(head_emb, tail_emb)

        # Score: plus la distance est faible, plus la similarité est grande
        # On utilise exp(-d²) comme mesure de similarité
        similarity = torch.exp(-dist.squeeze(-1) ** 2)
        return similarity, dist


def train_hyperbolic_model(num_nodes, all_head, all_tail, all_label,
                           embedding_dim=5, c=1.0, lr=0.005,
                           epochs=200, batch_size=256):
    """Entraînement avec descente de gradient riemannien."""

    model = HyperbolicEmbeddingModel(num_nodes, embedding_dim, c)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    loss_fn = nn.BCELoss()

    all_head = torch.tensor(all_head, dtype=torch.long)
    all_tail = torch.tensor(all_tail, dtype=torch.long)

    losses = []

    for epoch in range(epochs):
        # Mélanger les données
        perm = torch.randperm(len(all_head))
        all_head_shuffled = all_head[perm]
        all_tail_shuffled = all_tail[perm]
        all_label_shuffled = all_label[perm]

        epoch_loss = 0.0
        n_batches = 0

        for i in range(0, len(all_head), batch_size):
            h = all_head_shuffled[i:i + batch_size]
            t = all_tail_shuffled[i:i + batch_size]
            l = all_label_shuffled[i:i + batch_size]

            optimizer.zero_grad()

            similarity, dist = model(h, t)
            loss = loss_fn(similarity, l)

            loss.backward()

            # Projection riemannienne : on clippe les gradients
            # et on projette les embeddings dans la boule après chaque mise à jour
            optimizer.step()

            # Projeter les embeddings dans la boule
            with torch.no_grad():
                model.embeddings.data = model.manifold.project(
                    model.embeddings.data, max_norm=0.999
                )

            epoch_loss += loss.item()
            n_batches += 1

        avg_loss = epoch_loss / n_batches
        losses.append(avg_loss)

        if (epoch + 1) % 20 == 0:
            print(f"Époque {epoch + 1}/{epochs} | Perte: {avg_loss:.4f}")

    return model, losses

Visualisation 2D des embeddings

def visualize_embeddings_2d(model, node_depths, num_nodes,
                            title="Embeddings Hyperboliques"):
    """Visualise les embeddings 2D sur le disque de Poincaré."""

    # Extraire les embeddings 2D
    emb = model.embeddings.detach().numpy()

    # Si embeddings > 2D, utiliser PCA pour réduire à 2D
    if emb.shape[1] > 2:
        from sklearn.decomposition import PCA
        pca = PCA(n_components=2)
        emb_2d = pca.fit_transform(emb)
        # Remettre dans la boule après PCA
        norms = np.linalg.norm(emb_2d, axis=1, keepdims=True)
        max_norm = norms.max()
        if max_norm > 0:
            emb_2d = emb_2d / (norms + 1e-8) * 0.95
    else:
        emb_2d = emb

    # Créer le graphique
    fig, ax = plt.subplots(1, 1, figsize=(8, 8))

    # Cercle unité (frontière de la boule)
    theta = np.linspace(0, 2 * np.pi, 100)
    ax.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=2,
            label="Frontière de la boule")

    # Colorier par profondeur
    max_depth = max(node_depths.values()) if node_depths else 0
    colors = plt.cm.viridis(np.linspace(0.1, 0.9, max_depth + 1))

    depth_label_map = {
        0: ("Racine (profondeur 0)", 'red', 200),
        1: ("Profondeur 1", 'orange', 120),
        2: ("Profondeur 2", 'green', 100),
        3: ("Profondeur 3", 'blue', 80),
    }
    labeled = set()

    for node_id in range(num_nodes):
        depth = node_depths.get(node_id, 0)
        x_val, y_val = emb_2d[node_id]

        if depth in depth_label_map:
            label, color, size = depth_label_map[depth]
            if label not in labeled:
                ax.scatter(x_val, y_val, c=color, s=size, zorder=3,
                           label=label, edgecolors='darkgray')
                labeled.add(label)
            else:
                ax.scatter(x_val, y_val, c=color, s=size, zorder=3,
                           edgecolors='darkgray')
        else:
            if "Profondeurs > 3" not in labeled:
                ax.scatter(x_val, y_val, c='purple', s=60, zorder=2,
                           label="Profondeurs > 3", edgecolors='darkgray')
                labeled.add("Profondeurs > 3")
            else:
                ax.scatter(x_val, y_val, c='purple', s=60, zorder=2,
                           edgecolors='darkgray')

    ax.set_title(title, fontsize=14)
    ax.set_xlabel("Dimension 1")
    ax.set_ylabel("Dimension 2")
    ax.set_aspect('equal')
    ax.grid(True, alpha=0.3)

    # Légende unique
    handles, labels = ax.get_legend_handles_labels()
    by_label = dict(zip(labels, handles))
    ax.legend(by_label.values(), by_label.keys(), loc='upper center',
              bbox_to_anchor=(0.5, -0.05), ncol=3)

    plt.tight_layout()
    plt.savefig("hyperbolic_embeddings_poincare_disc.png", dpi=150,
                bbox_inches='tight')
    plt.show()

    # Statistiques des normes
    norms = np.linalg.norm(emb_2d, axis=1)
    print(f"\nStatistiques des normes d'embeddings:")
    print(f"  Moyenne: {norms.mean():.4f}")
    print(f"  Min:     {norms.min():.4f}")
    print(f"  Max:     {norms.max():.4f}")

    # Vérifier que la norme augmente avec la profondeur
    depth_norms = defaultdict(list)
    for nid, depth in node_depths.items():
        depth_norms[depth].append(norms[nid])

    print(f"\nNorme moyenne par profondeur:")
    for d in sorted(depth_norms.keys()):
        print(f"  Profondeur {d}: {np.mean(depth_norms[d]):.4f} "
              f"(n={len(depth_norms[d])})")


# --- Exemple d'utilisation complet ---
if __name__ == "__main__":
    print("=" * 60)
    print("Entraînement d'embeddings hyperboliques sur un arbre synthétique")
    print("=" * 60)

    # Générer les données
    all_head, all_tail, all_label, node_depths, num_nodes = generate_tree_data(
        depth=4, branching=3, seed=42
    )
    print(f"Nombre total de nœuds: {num_nodes}")
    print(f"Nombre de paires d'entraînement: {len(all_head)}")
    print(f"Profondeur maximale: {max(node_depths.values())}")

    # Entraîner le modèle
    model, losses = train_hyperbolic_model(
        num_nodes=num_nodes,
        all_head=all_head,
        all_tail=all_tail,
        all_label=all_label,
        embedding_dim=2,  # 2D pour visualisation directe
        c=1.0,
        lr=0.005,
        epochs=200,
        batch_size=128
    )

    # Visualiser les embeddings sur le disque de Poincaré
    visualize_embeddings_2d(model, node_depths, num_nodes,
                           title="Arbre synthétique - Disque de Poincaré")

    # Tracer la courbe de perte
    plt.figure(figsize=(10, 4))
    plt.plot(losses)
    plt.title("Courbe de perte durant l'entraînement")
    plt.xlabel("Époque")
    plt.ylabel("Perte (BCE)")
    plt.grid(True, alpha=0.3)
    plt.savefig("training_loss.png", dpi=150)
    plt.show()

Hyperparamètres

Le choix des hyperparamètres est crucial pour obtenir des résultats satisfaisants avec des réseaux de neurones hyperboliques. Chaque paramètre influence directement la qualité des embeddings appris et la stabilité de l’entraînement.

  • Courbure (c) : Généralement comprise entre 0,5 et 2,0. Une courbure plus élevée (c ≈ 2,0) est adaptée aux hiérarchies très profondes et fortement ramifiées. Une courbure plus faible (c ≈ 0,5) convient mieux aux structures moins arborescentes. La courbure peut également être apprise automatiquement durant l’entraînement, ce qui permet au modèle de déterminer lui-même le niveau optimal de courbure négative. Dans leur article original, Nickel et Kiessling ont utilisé une courbure fixe de c = 1,0, mais de nombreux travaux ultérieurs ont montré qu’ajuster ce paramètre est souvent la première étape pour optimiser les performances.
  • Dimension de l’embedding (d) : 5 à 50 dimensions suffisent généralement pour les embeddings hyperboliques, ce qui représente une compression considérable par rapport aux 100-500 dimensions requises par les méthodes euclidiennes. Pour des visualisations, on utilise d = 2. Pour des tâches de production sur des hiérarchies complexes (WordNet, Freebase), d = 10 à 20 est souvent optimal. La règle générale est que la dimension hyperbolique optimale est environ 10 fois inférieure à la dimension euclidienne équivalente pour une qualité d’embedding comparable.
  • Taux d’apprentissage (η) : 0,001 à 0,01. Des taux plus élevés peuvent causer des instabilités numériques car les gradients dans l’espace tangent peuvent être amplifiés par la projection sur la variété. Il est recommandé d’utiliser un scheduler de taux d’apprentissage décroissant ou un optimiseur adaptatif comme Adam. Un taux initial de 0,005 avec une décroissance linéaire est un bon point de départ pour la plupart des applications.
  • Contrainte de la boule (max_norm) : Typiquement fixée à 0,999 pour garantir que les embeddings restent strictement à l’intérieur de la boule unité. Cette projection est appliquée après chaque étape de gradient. Sans elle, les embeddings pourraient dépasser la frontière, produisant des valeurs NaN dans le calcul de la distance hyperbolique. La valeur exacte de cette contrainte doit être suffisamment proche de 1 pour ne pas limiter l’expressivité du modèle, mais suffisamment éloignée pour éviter les problèmes numériques.

5 avantages majeurs

1. Capacité exponentielle pour les hiérarchies — L’atout principal des réseaux de neurones hyperboliques réside dans leur capacité à représenter des structures hiérarchiques avec un nombre de dimensions logarithmique par rapport au nombre de nœuds. Là où un embedding euclidien nécessiterait des centaines de dimensions pour encoder correctement une hiérarchie de 150 000 entités (comme WordNet), un embedding hyperbolique de 5 à 20 dimensions suffit. Cette efficacité dimensionnelle provient directement de la croissance exponentielle du volume dans l’espace hyperbolique, qui correspond exactement à la croissance exponentielle du nombre de nœuds dans un arbre. En termes plus concrets, un embedding hyperbolique de dimension 5 peut représenter une hiérarchie de profondeur 20 avec une distorsion négligeable, alors qu’un embedding euclidien de même dimension serait totalement incapable de le faire.

2. Meilleure généralisation sur les données arborescentes — Les embeddings hyperboliques préservent naturellement les relations de parenté et d’enfant au sein des hiérarchies. Les expériences de Nickel et Kiessling (2018) ont montré que les embeddings de Poincaré surpassent les méthodes euclidiennes de 10 à 50 % en termes de précision sur des tâches de prédiction de liens hiérarchiques. Cette supériorité est particulièrement marquée pour les hiérarchies profondes où la structure arborescente domine. De plus, les modèles hyperboliques nécessitent moins de données d’entraînement pour atteindre une performance équivalente, ce qui est un avantage considérable lorsque les données annotées sont rares ou coûteuses à obtenir.

3. Interprétabilité géométrique — Contrairement aux embeddings euclidiens où la position dans l’espace n’a pas de signification intrinsèque, dans l’espace hyperbolique, la distance au centre de la boule encode directement le niveau de généralité du concept. Un mot comme “être vivant” sera placé près de l’origine, tandis qu’un mot comme “caniche toy de race champion” se retrouvera près de la frontière. Cette interprétabilité facilite l’analyse et le débogage des modèles. Elle permet également de visualiser et de comprendre les décisions du modèle, ce qui est de plus en plus important dans les applications critiques où la transparence est requise.

4. Compression dimensionnelle extraordinaire — Des recherches ont démontré que 5 à 10 dimensions hyperboliques peuvent capturer autant d’information hiérarchique que 50 à 100 dimensions euclidiennes. Cette compression se traduit directement par des modèles plus légers en mémoire, plus rapides en inférence et plus efficaces en stockage. Pour les systèmes embarqués ou les applications nécessitant un faible encombrement mémoire, c’est un avantage décisif. Dans le contexte des modèles de langage à grande échelle où chaque dimension compte, réduire la taille des embeddings de 90 % tout en préservant la qualité est un gain considérable en termes de ressources computationnelles et de latence.

5. Métrique de distance naturelle pour la similarité hiérarchique — La distance hyperbolique d_B(x, y) fournit une mesure de similarité intrinsèquement adaptée aux relations hiérarchiques. Deux concepts proches dans la hiérarchie auront une distance hyperbolique faible, tandis que deux concepts éloignés auront une distance grande, même si leurs caractéristiques superficielles sont similaires. Cette propriété est inatteignable en géométrie euclidienne sans un nombre prohibitif de dimensions. Par exemple, “chat” et “tigre” sont deux félins mais l’un est domestique et l’autre sauvage : la distance hyperbolique entre “animal” et “tigre” sera plus grande qu’entre “mammifère” et “tigre”, reflétant correctement la structure hiérarchique sous-jacente.


4 limites importantes

1. Complexité mathématique de la géométrie riemannienne — La mise en œuvre d’opérations de base comme l’addition, la multiplication matricielle ou la normalisation nécessite des reformulations non triviales (addition de Möbius, multiplication de Möbius, projection). Le calcul des gradients suit des règles de différentiation sur les variétés riemanniennes, bien plus complexes que les dérivées euclidiennes standard. Cette complexité augmente la probabilité d’erreurs d’implémentation et rend le débogage plus difficile. De plus, la communauté de l’apprentissage profond est largement familiarisée avec les espaces euclidiens, ce qui signifie que les ingénieurs doivent apprendre un nouveau corpus mathématique pour travailler efficacement avec la géométrie hyperbolique. Comprendre la métrique de Riemann, les symboles de Christoffel et les géodésiques demande un investissement significatif en temps et en apprentissage.

2. Absence de support GPU natif dans la majorité des frameworks — Bien que PyTorch permette d’implémenter des opérations hyperboliques sur GPU, la plupart des frameworks d’apprentissage profond ne proposent pas de couches hyperboliques optimisées nativement. Des bibliothèques spécialisées comme Geoopt ou HyperML comblent partiellement ce manque, mais elles restent moins matures et moins optimisées que les couches euclidiennes standard. Cela peut entraîner des temps d’entraînement significativement plus longs, en particulier pour les grandes architectures. Les opérations comme l’addition de Möbius ou la projection sur la variété ne bénéficient pas des kernels CUDA optimisés disponibles pour les opérations linéaires classiques, ce qui crée un goulot d’étranglement computationnel pour les embeddings de grande taille.

3. Généralisation limitée de certaines opérations — De nombreuses opérations fondamentales de l’apprentissage profond, comme l’attention, les convolutions ou les fonctions d’activation non linéaires, ne se généralisent pas naturellement aux variétés hyperboliques. Les couches d’attention hyperbolique, par exemple, nécessitent des formulations ad hoc qui ne bénéficient pas des optimisations éprouvées des versions euclidiennes. La fonction ReLU, ubiquiste en apprentissage profond euclidien, n’a pas d’équivalent direct sur une variété riemannienne. Cette limitation restreint l’applicabilité des réseaux hyperboliques à des architectures spécifiques et empêche le transfert direct des avancées récentes (comme les architectures Transformer) vers le domaine hyperbolique. Les chercheurs travaillent activement à développer des équivalents hyperboliques de ces opérations, mais le domaine est encore jeune.

4. Visualisation restreinte à 2D ou 3D — La visualisation des embeddings hyperboliques n’est intuitivement compréhensible qu’en 2D (le disque de Poincaré) ou en 3D (la boule de Poincaré). Au-delà, le cerveau humain ne peut pas appréhender directement la géométrie hyperbolique. Pour les embeddings de dimension supérieure (d > 3), il faut soit projeter en 2D avec une ACP (avec perte d’information), soit se fier uniquement aux métriques numériques. Cette limitation rend l’analyse exploratoire plus difficile que pour les embeddings euclidiens, où des techniques comme t-SNE ou UMAP fournissent des visualisations de qualité même pour des dimensions élevées. La projection PCA en 2D d’embeddings hyperboliques peut en effet distordre les distances de manière significative, rendant l’interprétation visuelle peu fiable.


4 cas d’usage concrets

1. Embeddings de la hiérarchie WordNet — WordNet est une base lexicale de la langue anglaise contenant plus de 150 000 synsets (ensembles de synonymes) organisés en une hiérarchie sémantique complexe de relations hypernymie/hyponymie. Plonger cette hiérarchie dans un espace hyperbolique permet de capturer la structure taxonomique avec une fidélité remarquable. Les embeddings de Poincaré ont montré qu’avec seulement 5 dimensions, ils surpassent les embeddings euclidiens de 100 dimensions en termes de reconstruction de la hiérarchie. Les concepts généraux comme “entity” se retrouvent naturellement au centre de la boule, tandis que les concepts spécifiques comme “persian_cat” migrent vers la frontière. Les distances hyperboliques entre synsets corrèlent fortement avec les longueurs de chaîne dans l’arbre de WordNet, démontrant que l’espace hyperbolique capture fidèlement la structure taxonomique.

2. Complétion de graphes de connaissances — Les graphes de connaissances comme Freebase, DBpedia ou Wikidata contiennent des millions d’entités et de relations. Un sous-ensemble important de ces relations sont hiérarchiques de type “est_un_type_de” (is_a, subclass_of, part_of). En apprenant des embeddings hyperboliques pour les entités de ces graphes, on améliore significativement la prédiction de liens manquants. Les modèles hyperboliques surpassent les méthodes euclidiennes comme TransE ou ComplEx sur les tâches de complétion de graphes avec une forte composante hiérarchique. L’incorporation de la distance hyperbolique dans la fonction de score des modèles de complétion de graphes permet de mieux discriminer les vrais liens des faux, en particulier pour les entités situées en bout de chaîne hiérarchique.

3. Apprentissage automatique de taxonomies — À partir de corpus textuels bruts (articles scientifiques, pages web, documentation technique), il est possible d’apprendre automatiquement une taxonomie de concepts en utilisant des embeddings hyperboliques. Les modèles de langage pré-entraînés peuvent extraire des paires de concepts liés hiérarchiquement, puis un modèle d’embedding de Poincaré apprend la structure taxonomique sous-jacente. Cette approche est particulièrement utile pour la découverte de nouvelles catégories dans des domaines en évolution rapide comme la biologie (nouvelles espèces, nouveaux gènes) ou l’informatique (nouvelles technologies, nouveaux frameworks). Contrairement aux méthodes de clustering euclidiennes qui nécessitent de spécifier a priori le nombre de clusters, l’approche hyperbolique découvre naturellement la structure hiérarchique et le nombre de catégories à chaque niveau.

4. Analyse des réseaux sociaux et professionnels — Les réseaux sociaux comme LinkedIn ou les organigrammes d’entreprise présentent une structure hiérarchique naturelle : PDG → vice-président → directeur → manager → employé. En apprenant des embeddings hyperboliques pour les profils professionnels, on peut automatiquement inférer la position hiérarchique des individus, détecter des structures organisationnelles atypiques ou prédire des relations de reporting non documentées. La distance au centre de la boule indique directement le niveau hiérarchique : les dirigeants se retrouvent au centre, tandis que les employés opérationnels se placent vers la périphérie. Cette propriété est particulièrement utile pour la cartographie d’organisations complexes, la détection de fraude dans les réseaux professionnels ou l’analyse de la mobilité ascendante au sein d’une entreprise.


Voir aussi


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.