GCN Semi-Supervisé : Guide Complet — Classification sur Graphes avec Peu d’Étiquettes

GCN Semi-Supervisé : Guide Complet — Classification sur Graphes avec Peu d'Étiquettes

GCN Semi-Supervisé : Classification de Nœuds avec Peu d’Étiquettes

Résumé

Les Graph Convolutional Networks (GCN) semi-supervisés représentent l’une des avancées les plus élégantes du machine learning appliqué aux données relationnelles. Contrairement aux méthodes classiques qui nécessitent des milliers d’échantillons étiquetés, le GCN semi-supervisé parvient à classifier l’ensemble des nœuds d’un graphe en n’utilisant qu’une infime fraction de labels — parfois une seule étiquette par classe. Cette approche semi-supervisée repose sur un mécanisme fondamental : la propagation de l’information le long des arêtes du graphe, permettant aux représentations apprises de quelques nœuds connus d’influencer la classification de leurs voisins, puis des voisins de leurs voisins, et ainsi de suite. Ce guide complet explore le principe mathématique du GCN, son intuition profonde, son implémentation pratique avec PyTorch Geometric, ainsi que ses avantages, limites et cas d’usage concrets. Vous découvrirez comment structurer un pipeline d’entraînement semi-supervisé, choisir les hyperparamètres optimaux, et visualiser les embeddings appris grâce à t-SNE. Si vous travaillez avec des données en réseau — publications scientifiques citant d’autres articles, molécules où les atomes sont reliés par des liaisons chimiques, ou réseaux sociaux — le GCN semi-supervisé est l’outil qu’il vous faut.

Principe Mathématique du GCN Semi-Supervisé

La Couche de Convolution sur Graphe

Le cœur du GCN semi-supervisé repose sur une opération de convolution adaptée aux graphes. Là où une convolution classique (comme dans les réseaux de neurones convolutifs pour les images) agrège l’information des pixels voisins selon une grille régulière, la convolution sur graphe doit fonctionner sur une topologie arbitraire. La formulation proposée par Kipf & Welling (2017) exprime une couche GCN comme suit :

H⁽ˡ⁺¹⁾ = σ(D̃⁻¹/² Ã D̃⁻¹/² H⁽ˡ⁾ W⁽ˡ⁾)

Décomposons chaque terme de cette équation fondamentale :

  • Ã = A + I est la matrice d’adjacence du graphe à laquelle on a ajouté des self-loops (boucles sur chaque nœud, représentées par la matrice identité I). L’ajout de ces boucles permet à chaque nœud de conserver une partie de sa propre représentation lors de l’agrégation de ses voisins — sans cette astuce, l’information se disperserait uniquement vers les voisins et le nœud « oublierait » sa propre caractéristique.
  • est la matrice de degrés de Ã, c’est-à-dire une matrice diagonale où chaque élément D̃ᵢᵢ vaut la somme des éléments de la ligne i de Ã. Cette matrice capture le nombre de connexions de chaque nœud (y compris le self-loop).
  • D̃⁻¹/² est la racine carrée inverse de cette matrice de degrés. Ce terme normalise l’agrégation : sans cette normalisation symétrique, les nœuds très connectés (des « hubs » dans le graphe) produiraient des activations bien plus grandes que les nœuds peu connectés, créant un déséquilibre numérique lors de l’entraînement. La normalisation D̃⁻¹/² Ã D̃⁻¹/² garantit que les caractéristiques agrégées restent dans une échelle comparable.
  • H⁽ˡ⁾ est la matrice d’activations à la couche l. À la couche d’entrée (l = 0), c’est simplement la matrice des caractéristiques des nœuds X. À chaque couche suivante, H⁽ˡ⁾ contient les représentations enrichies des nœuds, ayant déjà intégré l’information de voisins de plus en plus éloignés.
  • W⁽ˡ⁾ est la matrice de poids apprenable de la couche l. C’est le seul paramètre que le réseau va optimiser pendant l’entraînement. Malgré sa simplicité apparente, cette matrice linéaire est suffisante pour apprendre des transformations puissantes des caractéristiques agrégées.
  • σ est une fonction d’activation non linéaire, généralement ReLU (Rectified Linear Unit). C’est cette non-linéarité qui permet au GCN d’apprendre des fonctions complexes et de capturer des motifs subtils dans la structure du graphe.

Le Champ de Vision : k Sauts sur k Couches

Un point essentiel à comprendre est que chaque couche GCN agrège l’information des voisins directs (1 saut). Empiler k couches signifie que, à la couche finale, l’information de chaque nœud a été mélangée avec celle de tous les nœuds situés à au plus k sauts de distance dans le graphe. C’est ce qu’on appelle le champ de vision du nœud.

En pratique, les GCN utilisent généralement 2 ou 3 couches. Au-delà, deux problèmes surgissent :

  1. Le sur-lissage (over-smoothing) : après trop de couches d’agrégation, les représentations de nœuds éloignés — même de classes différentes — convergent vers des valeurs similaires, rendant la classification impossible. C’est l’équivalent, sur graphe, du problème de vanishing gradients dans les réseaux profonds classiques.
  2. Le coût computationnel : le champ de vision croît exponentiellement avec le nombre de couches (sur un graphe dont le degré moyen est d). Pour un graphe social moyen de degré 50, 3 couches impliquent un champ de vision théorique de 50³ = 125 000 nœuds.

Fonction de Perte Semi-Supervisée

La fonction de perte du GCN semi-supervisé combine deux composantes. La première est une entropie croisée standard, mais calculée uniquement sur les nœuds d’entraînement (ceux qui possèdent une étiquette connue) :

L = -Σ{l∈train} Σ{f=1}^{F} Y_{lf} · ln(Ŷ_{lf}) + λ · Ω

Où :

  • Y_{lf} est le label encodé en one-hot pour le nœud l et la classe f. Si le nœud l appartient à la classe f, Y_{lf} = 1, sinon Y_{lf} = 0.
  • Ŷ_{lf} est la probabilité prédite par le modèle pour le nœud l et la classe f, obtenue après application d’un softmax sur les sorties de la dernière couche GCN.
  • L’entropie croisée pénalise le modèle lorsque ses prédictions diffèrent des labels réels, mais uniquement pour les nœuds dont on connaît l’étiquette (l ∈ train).
  • λ · Ω est un terme de régularisation sur les poids W de toutes les couches (typiquement une régularisation L2 : Ω = ||W||²). Ce terme contrôle la complexité du modèle et évite le surapprentissage (overfitting), particulièrement critique en apprentissage semi-supervisé où le nombre de labels disponibles est très faible.

La magie du GCN semi-supervisé réside dans le fait que la minimisation de cette perte, calculée sur un petit sous-ensemble de nœuds, entraîne une optimisation des poids W qui profite à tous les nœuds du graphe — même ceux sans étiquette. C’est parce que les représentations H⁽ᴸ⁾ de tous les nœuds sont calculées via les mêmes opérations d’agrégation : si le modèle apprend à bien classer les nœuds étiquetés, il a nécessairement appris des représentations utiles pour leurs voisins.

Intuition : La Conférence aux Badges Perdus

Imaginez une conférence où seules quelques personnes portent des badges avec leur nom et leur rôle. Les autres n’ont pas de badge. Mais en observant qui parle avec qui, et en sachant que les data scientists parlent surtout avec d’autres data scientists, les ingénieurs avec d’autres ingénieurs, et les designers avec d’autres designers, on peut progressivement deviner le rôle des personnes sans badge. C’est exactement ce que fait le GCN semi-supervisé.

Décomposons l’analogie :

  • Le graphe : chaque participant de la conférence est un nœud. Chaque conversation observée est une arête reliant deux nœuds.
  • Les caractéristiques (features) : pour chaque participant, on connaît certains attributs — le vocabulaire qu’il utilise, les mots-clés de sa conversation, son apparence vestimentaire. Ce sont les features du nœud.
  • Les labels connus : quelques participants portent un badge. On sait avec certitude que « Marie est une data scientist » et que « Pierre est un designer ». Ce sont les nœuds labellisés du jeu d’entraînement.
  • La propagation : on remarque que Thomas discute intensément avec Marie (connue comme data scientist). Il utilise également un vocabulaire technique proche de celui de Marie. Très probablement, Thomas est aussi un data scientist. Puis on remarque que Sophie discute avec Thomas — elle pourrait bien l’être aussi. C’est la propagation de l’information à travers les couches GCN.
  • La régularisation locale : l’hypothèse implicite est que des personnes connectées (qui se parlent) ont de bonnes chances de partager le même rôle. C’est l’hypothèse de smoothness (lissage) sur le graphe : les nœuds proches dans la topologie du graphe devraient avoir des étiquettes similaires. Le GCN encode mécaniquement cette hypothèse dans sa couche d’agrégation.

Cette intuition capture l’essence du GCN semi-supervisé : un petit nombre de vérités connues, combiné à une structure relationnelle riche, suffit à déduire l’inconnu par propagation itérative.

Implémentation Python avec PyTorch Geometric

Installation et Préparation

import torch
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import numpy as np

torch.manual_seed(42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Chargement du Jeu de Données Cora

Le dataset Cora est le banc d’essai canonique pour les GCN. Il contient 2 708 publications scientifiques reliées par 5 429 citations, classées en 7 catégories thématiques. Chaque publication est représentée par un vecteur de 1 433 mots (bag-of-words binaire).

dataset = Planetoid(root='/tmp/Cora', name='Cora')
data = dataset[0]

print(f"Nombre de nœuds : {data.num_nodes}")
print(f"Nombre d'arêtes : {data.num_edges}")
print(f"Nombre de features : {data.num_features}")
print(f"Nombre de classes : {dataset.num_classes}")

Création du Masque Semi-Supervisé

En configuration standard semi-supervisée, on ne conserve que 20 nœuds par classe pour l’entraînement, soit 140 nœuds au total sur 2 708 (à peine 5 %). Les 500 nœuds suivants servent de validation, et le reste de test.

def create_semi_supervised_mask(data, num_per_class=20, num_val=500):
    """Crée des masques d'entraînement semi-supervisé avec 20 labels par classe."""
    num_classes = dataset.num_classes
    train_mask = torch.zeros(data.num_nodes, dtype=torch.bool)

    # Sélection aléatoire de num_per_class nœuds par classe
    for c in range(num_classes):
        indices = torch.where(data.y == c)[0]
        selected = indices[torch.randperm(len(indices))[:num_per_class]]
        train_mask[selected] = True

    # Masque de validation et test
    remaining = ~train_mask
    remaining_indices = torch.where(remaining)[0]
    val_mask = torch.zeros(data.num_nodes, dtype=torch.bool)
    val_indices = remaining_indices[torch.randperm(len(remaining_indices))[:num_val]]
    val_mask[val_indices] = True

    test_mask = ~train_mask & ~val_mask

    return train_mask, val_mask, test_mask

train_mask, val_mask, test_mask = create_semi_supervised_mask(data)
print(f"Nœuds d'entraînement : {train_mask.sum().item()}")
print(f"Nœuds de validation : {val_mask.sum().item()}")
print(f"Nœuds de test : {test_mask.sum().item()}")

Définition du Modèle GCN

class SemiSupervisedGCN(torch.nn.Module):
    def __init__(self, num_features, num_classes, hidden_channels=128, dropout=0.5):
        super().__init__()
        self.conv1 = GCNConv(num_features, hidden_channels, cached=True)
        self.conv2 = GCNConv(hidden_channels, num_classes, cached=True)
        self.dropout = dropout

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, p=self.dropout, training=self.training)
        x = self.conv2(x, edge_index)
        return x

model = SemiSupervisedGCN(
    num_features=dataset.num_features,
    num_classes=dataset.num_classes,
    hidden_channels=128,
    dropout=0.5
).to(device)

data = data.to(device)
print(f"Modèle : {model}")
print(f"Paramètres entraînables : {sum(p.numel() for p in model.parameters())}")

Boucle d’Entraînement

optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.01,
    weight_decay=5e-4
)

def train_epoch(epoch, data, train_mask):
    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = F.cross_entropy(out[train_mask], data.y[train_mask])
    loss.backward()
    optimizer.step()

    pred = out[train_mask].argmax(dim=1)
    acc = (pred == data.y[train_mask]).sum().item() / train_mask.sum().item()

    if epoch % 20 == 0:
        print(f"Époque {epoch:03d} | Perte : {loss.item():.4f} | Précision train : {acc*100:.1f}%")

    return loss.item(), acc

def test(data, mask, description="test"):
    model.eval()
    out = model(data.x, data.edge_index)
    pred = out[mask].argmax(dim=1)
    correct = (pred == data.y[mask]).sum().item()
    acc = correct / mask.sum().item()
    print(f"  {description} : {acc*100:.1f}% ({correct}/{mask.sum().item()})")
    return acc

# Entraînement sur 300 époques
best_val_acc = 0
best_train_acc = 0

for epoch in range(301):
    _, train_acc = train_epoch(epoch, data, train_mask)

    # Évaluation périodique
    if epoch % 50 == 0 or epoch == 300:
        with torch.no_grad():
            test(data, val_mask, "  Validation")
            test(data, test_mask, "  Test")

    # Sauvegarder le meilleur modèle
    if train_acc > best_train_acc:
        best_train_acc = train_acc
        best_state = {k: v.clone() for k, v in model.state_dict().items()}

Visualisation des Embeddings avec t-SNE

Après l’entraînement, on peut visualiser les représentations apprises par la dernière couche cachée (avant la classification) pour vérifier visuellement la qualité de la séparation des classes.

def visualize_embeddings(data, save_path='gcn_tsne_cora.png'):
    """Visualise les embeddings de la dernière couche cachée avec t-SNE."""
    model.eval()

    # Obtenir les embeddings de la couche cachée
    model.eval()
    with torch.no_grad():
        x = model.conv1(data.x, data.edge_index)
        x = F.relu(x)
        embeddings = x.cpu().numpy()

    # Réduction de dimension avec t-SNE
    tsne = TSNE(n_components=2, random_state=42, perplexity=30)
    embeddings_2d = tsne.fit_transform(embeddings)

    noms_classes = [
        'Case-Based', 'Genetic Algorithms',
        'Neural Networks', 'Probabilistic Methods',
        'Reinforcement Learning', 'Rule Learning',
        'Theory'
    ]

    cmap = plt.get_cmap('tab10')
    plt.figure(figsize=(12, 8))

    for c in range(dataset.num_classes):
        mask = data.y.numpy() == c
        plt.scatter(
            embeddings_2d[mask, 0], embeddings_2d[mask, 1],
            c=[cmap(c/7)], label=noms_classes,
            s=30, alpha=0.7, edgecolors='white', linewidth=0.5
        )

    plt.title(
        "GCN Semi-Supervisé — Visualisation t-SNE des Embeddings (Cora)",
        fontsize=14, fontweight='bold'
    )
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=9)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig(save_path, dpi=150, bbox_inches='tight')
    plt.close()
    print(f"Visualisation sauvegardée sous {save_path}")

visualize_embeddings(data)

Analyse de l’Impact du Nombre de Labels

Un aspect crucial du GCN semi-supervisé est l’effet du nombre de labels disponibles sur la performance. Voici une analyse systématique :

def analyse_impact_labels(data, labels_per_class_list=[5, 10, 15, 20, 40, 80]):
    """Compare la précision du GCN selon le nombre de labels par classe."""
    resultats = []

    for n_labels in labels_per_class_list:
        train_m, val_m, test_m = create_semi_supervised_mask(data, n_labels, 500)

        mdl = SemiSupervisedGCN(
            num_features=dataset.num_features,
            num_classes=dataset.num_classes,
            hidden_channels=128,
            dropout=0.5
        ).to(device)

        opt = torch.optim.Adam(
            mdl.parameters(), lr=0.01, weight_decay=5e-4
        )

        # Entraînement accéléré
        for ep in range(200):
            mdl.train()
            opt.zero_grad()
            out = mdl(data.x, data.edge_index)
            loss = F.cross_entropy(out[train_m], data.y[train_m])
            loss.backward()
            opt.step()

        # Évaluation
        mdl.eval()
        with torch.no_grad():
            out = mdl(data.x, data.edge_index)
            pred = out[test_m].argmax(dim=1)
            acc = (pred == data.y[test_m]).sum().item() / test_m.sum().item()

        resultats.append((n_labels, acc * 100))
        print(f"  {n_labels} labels/classe → {acc*100:.1f}% de précision test")

    # Tracé de la courbe
    labels = [r[0] for r in resultats]
    accs = [r[1] for r in resultats]

    plt.figure(figsize=(10, 5))
    plt.plot(labels, accs, 'o-', linewidth=2.5, markersize=8, color='#2196F3')
    plt.fill_between(labels, accs, alpha=0.1, color='#2196F3')
    plt.xlabel("Nombre de labels par classe", fontsize=12)
    plt.ylabel("Précision test (%)", fontsize=12)
    plt.title(
        "GCN Semi-Supervisé — Impact du Nombre de Labels sur Cora",
        fontsize=14, fontweight='bold'
    )
    plt.grid(True, alpha=0.3)
    plt.xticks(labels)
    plt.ylim(0, 100)

    for x, y in zip(labels, accs):
        plt.annotate(f"{y:.1f}%", (x, y), textcoords="offset points",
                     xytext=(0, 12), ha='center', fontsize=10)

    plt.tight_layout()
    plt.savefig('gcn_impact_labels.png', dpi=150)
    plt.close()
    print("\nCourbe d'impact sauvegardée sous gcn_impact_labels.png")
    return resultats

analyse_impact_labels(data)

Hyperparamètres du GCN Semi-Supervisé

Le choix des hyperparamètres est particulièrement sensible en régime semi-supervisé, car chaque paramètre influence directement la capacité du modèle à généraliser à partir de très peu d’exemples étiquetés.

hidden_channels

C’est la dimension de l’espace latent de la couche cachée. Des valeurs trop faibles (16-32) limitent la capacité expressive du modèle, tandis que des valeurs trop élevées (256-512) augmentent le risque de surapprentissage. Pour Cora, 128 channels est un excellent point de départ — suffisant pour capturer la diversité des 7 classes thématiques sans surparamétriser.

num_layers

Comme discuté, 2 couches sont quasi universelles pour les GCN semi-supervisés sur des graphes de taille modérée. La première couche agrège les voisins directs (1 saut), la deuxième agrège les voisins des voisins (2 sauts). Une troisième couche peut être utile sur des graphes très grands où l’information doit se propager plus loin, mais avec une régularisation accrue pour contrer le sur-lissage.

dropout

Le dropout est l’outil principal de régularisation du GCN. En régime semi-supervisé, il doit être élevé (0.4 à 0.6) car le nombre de labels disponibles ne suffit pas à contraindre les poids du réseau. Sans dropout suffisant, le modèle mémorise les quelques labels d’entraînement et généralise très mal. Le GAT utilise des coefficients d’attention, mais le GCN classique dépend fortement du dropout pour réussir en mode semi-supervisé.

learning_rate

Le taux d’apprentissage par défaut 0.01 avec l’optimiseur Adam fonctionne remarquablement bien pour les GCN sur Cora. Un taux plus élevé (0.05) peut aider pour des graphes plus grands, mais augmente le risque d’instabilité. Un taux plus bas (0.001) donne une convergence plus douce mais nécessite beaucoup plus d’époques — souvent inutile en semi-supervisé car le signal d’entraînement est déjà faible.

weight_decay

La régularisation L2 avec weight_decay=5e-4 (soit 0.0005) contrôle la norme des poids, évitant ainsi que le modèle ne s’appuie excessivement sur quelques poids dominants. Ce paramètre est souvent sous-estimé mais s’avère critique : sans weight_decay, la précision test chute généralement de 3 à 5 points de pourcentage sur Cora.

Avantages et Limites du GCN Semi-Supervisé

Avantages

  1. Extrême efficacité en labels : avec seulement 20 labels par classe (140 sur 2 708), le GCN atteint des précisions de l’ordre de 80 % sur Cora. C’est 10 à 50 fois moins de labels qu’un classifieur classique nécessiterait pour des résultats comparables.
  2. Propagation naturelle de l’information : la structure du graphe n’est pas un obstacle au modèle — c’est le modèle lui-même. Cela contraste avec les approches classiques où la structure relationnelle doit être « aplatie » en features supplémentaires.
  3. Simplicité architecturale : malgré sa puissance, un GCN semi-supervisé typique ne compte que quelques milliers de paramètres, contre des millions pour un réseau profond classique. Cela permet un entraînement rapide (quelques secondes sur GPU, quelques minutes sur CPU).
  4. Généralisation par régularisation de graphe : le GCN implémente implicitement une régularisation Laplacienne (la contrainte de lissage sur le graphe), ce qui lui confère une robustesse théorique en régime de faible supervision.

Limites

  1. Inductif vs transductif : le GCN classique est transductif — il nécessite de connaître l’ensemble du graphe (y compris les nœuds sans label) pendant l’entraînement. Pour classifier des nœuds qui n’existaient pas à l’entraînement (nouveaux nœuds arrivant dans un graphe dynamique), il faut des variantes inductives comme GraphSAGE ou GAT (voir les références en fin d’article).
  2. Sensibilité au sur-lissage : au-delà de 2-3 couches, les représentations des nœuds tendent à converger, détruisant la variabilité nécessaire à la classification. C’est une limite fondamentale de l’agrégation de voisinage itérative.
  3. Dépendance à la structure du graphe : si les arêtes sont bruyantes, incomplètes, ou incorrectes, la propagation de l’information devient inefficace voire nuisible. Le GCN suppose que des nœuds connectés sont sémantiquement proches — une hypothèse qui n’est pas toujours vérifiée.
  4. Scalabilité : le GCN original nécessite la matrice d’adjacence complète en mémoire. Pour des graphes de centaines de milliers de nœuds, des approches mini-batch (échantillonnage de voisinage) sont nécessaires.

4 Cas d’Usage Concrets

1. Classification d’Articles Scientifiques (Cora, Pubmed, Citeseer)

C’est l’application canonique. Les articles sont des nœuds, les citations sont des arêtes, et le contenu textuel (bag-of-words) sert de caractéristiques. En ne labellisant que 20 articles par catégorie thématique, le GCN classe l’ensemble de la bibliographie. En recherche, cela permet d’explorer rapidement des corpus de milliers d’articles sans annotation manuelle exhaustive.

2. Détection de Fraude dans les Réseaux de Transactions

Les comptes bancaires forment les nœuds d’un graphe, et les transactions forment les arêtes. En labellisant manuellement quelques dizaines de comptes frauduleux (identifiés par des enquêteurs), le GCN semi-supervisé propage cette information et identifie les comptes suspects connectés aux fraudeurs connus. Cette approche est déployée par plusieurs grandes banques pour détecter des réseaux frauduleux sophistiqués.

3. Annotation de Gènes dans les Réseaux de Protéines

En bio-informatique, les protéines sont connectées par des interactions physiques connues. La fonction biologique de certains gènes est bien documentée, mais beaucoup restent inconnus. Le GCN semi-supervisé utilise les interactions protéine-protéine comme structure du graphe et les caractéristiques biochimiques des protéines comme features, pour prédire la fonction des gènes non annotés. Cette approche a permis d’annoter des milliers de gènes avec une fiabilité comparable à l’expérience biologique directe.

4. Recommandation de Produits avec Connaissance A-priori

Dans un graphe de recommandation, les clients et les produits sont des nœuds, et les achats relient clients et produits. En connaissant les préférences de quelques clients (via un questionnaire, une étude de marché), le GCN semi-supervisé peut classifier les préférences des autres clients — non pas à partir d’historiques d’achats massifs, mais en exploitant la similarité de comportement à travers le réseau d’achats croisés.

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.