Siamese Network : Guide Complet — Réseaux Siamois pour la Similarité

Siamese Network : Guide Complet — Réseaux Siamois pour la Similarité

Siamese Network

Le Siamese Network (réseau siamois) est une architecture de deep learning conçue pour apprendre à comparer deux entrées en mesurant leur degré de similarité. Contrairement aux réseaux de neurones classiques qui classifient une seule entrée, le réseau siamois évalue la relation entre deux échantillons. Cette approche révolutionnaire, introduite par Bromley et al. en 1993 pour la vérification de signatures manuscrites, est aujourd’hui au cœur de la reconnaissance faciale, de la biométrie et des systèmes de recommandation.

Résumé

Un Siamese Network se compose de deux sous-réseaux identiques partageant exactement les mêmes poids. Chaque sous-réseau transforme une entrée en une représentation compacte appelée embedding. La distance entre ces deux embeddings détermine si les entrées sont similaires ou différentes. L’apprentissage repose sur des fonctions de coût spécialisées — notamment la contrastive loss et la triplet loss — qui pénalisent les mauvaises distances et récompensent les bonnes associations. Cette architecture permet de généraliser à des classes jamais vues pendant l’entraînement, ce qui la rend particulièrement adaptée aux situations où certaines catégories ne disposent que de quelques exemples.

Principe Mathématique

Architecture à Branches Jumelles

Le cœur du Siamese Network repose sur une idée élégante : deux sous-réseaux strictement identiques, notés f(x₁) et f(x₂), partagent l’ensemble de leurs paramètres (poids et biais). Ces deux branches traitent chacune une entrée différente — par exemple deux images — et produisent deux vecteurs d’embedding de dimension fixée.

On note :

  • f(x₁) = embedding de la première entrée, obtenu après propagation avant à travers le réseau partagé
  • f(x₂) = embedding de la deuxième entrée, calculé avec exactement les mêmes poids
  • D = ||f(x₁) − f(x₂)|| = distance euclidienne entre les deux embeddings
  • y = étiquette binaire : 1 si les deux entrées appartiennent à la même classe (paire positive), 0 sinon (paire négative)

Le principe fondamental est que le réseau apprend à rapprocher les embeddings des paires positives (y = 1) et à éloigner les embeddings des paires négatives (y = 0), jusqu’à une marge prédéfinie. Cette stratégie contraste avec la classification traditionnelle où le réseau apprend des frontières de décision entre classes fixes.

Contrastive Loss

La fonction de coût contrastive (contrastive loss), introduite par Hadsell, Chopra et LeCun en 2006, est la fonction d’objectif historique pour entraîner les réseaux siamois :

L = y · D² + (1 − y) · max(0, m − D)²

Où :

  • y = 1 (paire positive) : le terme actif est . Le réseau minimise la distance entre les embeddings similaires. L’objectif idéal est D = 0.
  • y = 0 (paire négative) : le terme actif est max(0, m − D)². Si la distance D est inférieure à la marge m, le réseau subit une pénalité proportionnelle au carré de l’écart. Dès que D ≥ m, la pénalité s’annule.
  • m (margin) est un hyperparamètre qui contrôle la distance minimale souhaitée entre les paires négatives.

Intuitivement, la contrastive loss dit au réseau : « Rapproche les échantillons similaires le plus possible, mais éloigne les échantillons différents d’au moins m. » C’est un mécanisme d’apprentissage par distance métrique plutôt que par classification classique.

Triplet Loss

La triplet loss, popularisée par Google FaceNet en 2015, constitue une évolution puissante de la contrastive loss. Au lieu de travailler sur des paires, elle utilise des triplets de trois échantillons :

  • a (ancre) : un exemple de référence
  • p (positif) : un autre exemple de la même classe que l’ancre
  • n (négatif) : un exemple d’une classe différente

La fonction de coût s’écrit :

L = max(0, ||f(a) − f(p)||² − ||f(a) − f(n)||² + marge)

Cette formulation impose que la distance entre l’ancre et le positif soit strictement inférieure à la distance entre l’ancre et le négatif, avec un écart minimum égal à la marge. Autrement dit, non seulement les deux paires positives doivent être plus proches que les paires négatives, mais il faut un « espace de sécurité » entre les deux distances.

La triplet loss présente deux avantages majeurs par rapport à la contrastive loss. Premièrement, elle compare directement des distances positives et négatives au sein d’un même terme de coût, ce qui produit des gradients plus informatifs à chaque étape d’apprentissage. Deuxièmement, le paramètre de marge joue un rôle plus naturel : il ne s’agit pas de repousser les négatifs à une distance absolue, mais de garantir un écart relatif entre positif et négatif.

Le principal inconvénient de la triplet loss est la difficulté de sélection des triplets efficaces. Un triplet « facile », où l’ancre est déjà beaucoup plus proche du positif que du négatif, ne produit aucun gradient (la fonction max sature à zéro). En pratique, on utilise des stratégies de mining (extraction) de triplets, notamment les semi-hard triplets, où le négatif est plus proche de l’ancre que le positif mais la marge n’est pas encore atteinte. Ces triplets produisent les gradients les plus utiles.

Intuition

Pour comprendre vraiment le Siamese Network, il faut changer de perspective. Un classifieur classique apprend la réponse à la question : « À quelle catégorie appartient cette image ? » Le réseau siamois, lui, répond à une question différente : « Ces deux images appartiennent-elles à la même catégorie ? »

C’est une distinction fondamentale. Imaginez que vous deviez reconnaître le visage de votre ami dans une foule. La reconnaissance individuelle exige d’avoir mémorisé chaque visage que vous connaissez. Mais déterminer si deux photos montrent la même personne est un problème différent : vous comparez les traits, les proportions, les caractéristiques, sans nécessairement savoir qui est cette personne.

Le réseau siamois apprend exactement cette capacité de comparaison plutôt que d’identification. Il extrait des caractéristiques discriminantes (la forme des yeux, la structure osseuse, les proportions du visage) et les encode dans un espace vectoriel où la distance géométrique reflète la similarité réelle. Deux visages du même individu donneront deux embeddings proches dans cet espace, indépendamment de l’éclairage, de l’angle ou de l’expression.

Cette approche confère un avantage décisif : le réseau peut comparer des identités jamais rencontrées pendant l’entraînement. Il suffit de passer les nouvelles images à travers les deux branches partagées et de mesurer la distance entre leurs embeddings. Aucun ré-entraînement n’est nécessaire pour ajouter de nouveaux individus au système. C’est ce qu’on appelle l’apprentissage one-shot ou few-shot — reconnaître une nouvelle classe avec un seul ou très peu d’exemples.

Implémentation Python avec Keras

Voici une implémentation complète d’un Siamese Network pour la reconnaissance faciale, construit avec Keras et TensorFlow.

Modèle de Base Partagé

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models, optimizers
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.layers import (
    Conv2D, MaxPooling2D, Dense, Dropout,
    Flatten, Input, Lambda, BatchNormalization
)
from tensorflow.keras.optimizers import Adam
import matplotlib.pyplot as plt
import os

def create_base_network(input_shape):
    """
    Crée le sous-réseau partagé (le « réseau jumeau »).
    Deux instances de ce réseau partagent les mêmes poids.
    Utilise une architecture CNN classique avec normalisation par lots.
    """
    model = Sequential([
        Conv2D(64, (3, 3), input_shape=input_shape,
               activation='relu', padding='same'),
        BatchNormalization(),
        MaxPooling2D((2, 2)),

        Conv2D(128, (3, 3), activation='relu', padding='same'),
        BatchNormalization(),
        MaxPooling2D((2, 2)),

        Conv2D(256, (3, 3), activation='relu', padding='same'),
        BatchNormalization(),
        MaxPooling2D((2, 2)),

        Flatten(),
        Dense(512, activation='relu'),
        Dropout(0.5),
        Dense(256, activation='relu'),
        Dropout(0.4),
        Dense(128, activation='relu'),
        # L'embedding final : 128 dimensions
    ])

    return model

Construction du Modèle Siamois

def create_siamese_network(input_shape=(105, 105, 1)):
    """
    Construit l'architecture siamoise complète :
    deux branches partageant les mêmes poids,
    plus une couche de distance euclidienne.
    """
    base_network = create_base_network(input_shape)

    # Deux entrées (paire d'images)
    input_a = Input(shape=input_shape, name='entree_gauche')
    input_b = Input(shape=input_shape, name='entree_droite')

    # Les deux entrées passent par le même réseau (poids partagés)
    embedding_a = base_network(input_a)
    embedding_b = base_network(input_b)

    # Couche de distance euclidienne personnalisée
    def euclidean_distance(vects):
        x, y = vects
        return tf.sqrt(tf.reduce_sum(tf.square(x - y), axis=1, keepdims=True))

    def euclidean_dist_output_shape(shapes):
        shape1, shape2 = shapes
        return (shape1[0], 1)

    distance = Lambda(euclidean_distance,
                      output_shape=euclidean_dist_output_shape,
                      name='distance_euclidienne')([embedding_a, embedding_b])

    model = Model(inputs=[input_a, input_b], outputs=distance)
    return model

Contrastive Loss Personnalisée

def contrastive_loss(y_true, y_pred, margin=1.0):
    """
    Contrastive Loss avec marge.

    y_true : 1 si similaire, 0 si différent
    y_pred : distance euclidienne prédite
    margin : distance minimale pour les paires négatives
    """
    square_pred = tf.square(y_pred)
    margin_square = tf.square(tf.maximum(margin - y_pred, 0.0))

    loss = tf.reduce_mean(
        y_true * square_pred + (1.0 - y_true) * margin_square
    )
    return loss


def accuracy_siamese(y_true, y_pred, threshold=0.5):
    """
    Métrique de similarité binaire.
    Prédit 1 si la distance < seuil, 0 sinon.
    """
    return tf.reduce_mean(
        tf.cast(
            tf.equal(y_true, tf.cast(y_pred < threshold, tf.float32)),
            tf.float32
        )
    )

Génération de Paires pour l’Entraînement

def generer_paires(X, y):
    """
    Génère des paires positives et négatives à partir
    d'un jeu de données d'images et d'étiquettes.

    Pour chaque exemple, on crée :
    - 1 paire positive : même classe, image différente
    - 1 paire négative : classe différente, image aléatoire
    """
    indices_par_classe = {}
    for idx, label in enumerate(y):
        indices_par_classe.setdefault(label, []).append(idx)

    paires_gauche = []
    paires_droite = []
    etiquettes = []

    classes_disponibles = list(indices_par_classe.keys())

    for i in range(len(X)):
        classe_i = y[i]
        indices_meme_classe = indices_par_classe[classe_i]

        # 1. Paire positive : même classe, image différente
        if len(indices_meme_classe) > 1:
            idx_pos = np.random.choice([j for j in indices_meme_classe if j != i])
            paires_gauche.append(X[i])
            paires_droite.append(X[idx_pos])
            etiquettes.append(1)

        # 2. Paire négative : classe différente
        autres_classes = 
        classe_diff = np.random.choice(autres_classes)
        idx_neg = np.random.choice(indices_par_classe[classe_diff])
        paires_gauche.append(X[i])
        paires_droite.append(X[idx_neg])
        etiquettes.append(0)

    return (np.array(paires_gauche), np.array(paires_droite),
            np.array(etiquettes, dtype=np.float32))

Entraînement et Évaluation

def entrainer_modele_siamese(X_train, y_train, X_test, y_test,
                              epochs=20, batch_size=128, margin=1.0):
    """
    Pipeline complet d'entraînement d'un Siamese Network
    pour la reconnaissance faciale ou la vérification.
    """
    input_shape = X_train.shape[1:]

    # Création du modèle
    siamese = create_siamese_network(input_shape)

    # Compilation avec contrastive loss
    siamese.compile(
        optimizer=Adam(learning_rate=0.001),
        loss=lambda y_t, y_p: contrastive_loss(y_t, y_p, margin),
        metrics=[accuracy_siamese]
    )

    # Résumé de l'architecture
    siamese.summary()

    # Génération des paires d'entraînement
    X_left_train, X_right_train, labels_train = generer_paires(X_train, y_train)
    X_left_test, X_right_test, labels_test = generer_paires(X_test, y_test)

    print(f"Paires d'entraînement : {len(X_left_train)}")
    print(f"Paires de test : {len(X_left_test)}")

    # Entraînement
    historique = siamese.fit(
        [X_left_train, X_right_train], labels_train,
        validation_data=(
            [X_left_test, X_right_test], labels_test
        ),
        batch_size=batch_size,
        epochs=epochs,
        verbose=1
    )

    # Tracer les courbes d'apprentissage
    plt.figure(figsize=(12, 4))

    plt.subplot(1, 2, 1)
    plt.plot(historique.history['loss'], label='Entraînement')
    plt.plot(historique.history['val_loss'], label='Validation')
    plt.title('Contrastive Loss')
    plt.xlabel('Époques')
    plt.ylabel('Perte')
    plt.legend()

    plt.subplot(1, 2, 2)
    plt.plot(historique.history['accuracy_siamese'], label='Entraînement')
    plt.plot(historique.history['val_accuracy_siamese'], label='Validation')
    plt.title('Précision de Similarité')
    plt.xlabel('Époques')
    plt.ylabel('Précision')
    plt.legend()

    plt.tight_layout()
    plt.savefig('historique_siamese.png', dpi=150)
    plt.show()

    return siamese, historique


# --- Exemple d'utilisation ---
# Application : reconnaissance faciale sur dataset de visages
# (omniglot et LFW sont des benchmarks classiques)

# Chargement des données (exemple avec un dataset fictif)
# X_train, y_train = charger_dataset_visages()
# X_train = X_train.astype('float32') / 255.0
# X_train = np.expand_dims(X_train, axis=-1)

# siamese, history = entrainer_modele_siamese(X_train, y_train,
# X_test, y_test,
# epochs=20,
# batch_size=128,
# margin=1.0)

Fonctions d’Inférence et Comparaison

def comparer_images(siamese_model, img1, img2, seuil=0.5):
    """
    Compare deux images et retourne la distance
    et une décision de similarité.
    """
    # Passage à travers les deux branches
    base_net = siamese_model.layers[2]  # Le réseau partagé

    emb1 = base_net(np.expand_dims(img1, axis=0))
    emb2 = base_net(np.expand_dims(img2, axis=0))

    # Calcul de la distance euclidienne
    dist = np.linalg.norm(emb1 - emb2)

    similaire = dist < seuil
    return dist, similaire


def verification_signature(siamese_model, signature_ref, signature_test,
                           seuil=0.3):
    """
    Vérification de signatures manuscrites :
    application historique des réseaux siamois.
    Compare une signature de référence avec
    une signature soumise pour détecter les falsifications.
    """
    distance, est_signee = comparer_images(
        siamese_model, signature_ref, signature_test, seuil
    )
    return {
        'distance': distance,
        'authentique': bool(est_signee),
        'confiance': 1.0 - distance
    }

Hyperparamètres du Siamese Network

La performance d’un Siamese Network dépend fortement du réglage de ses hyperparamètres. Voici les plus critiques :

Margin (Marge)

La marge contrôle la distance minimale entre paires négatives. C’est l’hyperparamètre le plus sensible.

  • Marge trop faible (0,2 – 0,3) : les paires négatives ne sont pas suffisamment séparées, ce qui crée des confusions entre classes différentes.
  • Marge optimale (0,8 – 1,5) : bon équilibre entre rapprochement des positifs et éloignement des négatifs. La valeur 1,0 est un excellent point de départ.
  • Marge trop élevée (> 2,0) : le réseau peut saturer, produisant des gradients nuls pour les paires négatives dès que la distance dépasse la marge, ce qui ralentit l’apprentissage.

Embedding Dimension (Dimension de l’Embedding)

La dimension du vecteur d’embedding détermine la richesse de la représentation :

  • Faible dimension (16 – 32) : rapide mais risque de sous-représentation. Deux images différentes peuvent accidentellement produire des embeddings proches (collision).
  • Dimension standard (64 – 256) : bon compromis pour la plupart des applications. 128 dimensions est un choix fréquent et efficace.
  • Haute dimension (512 – 1024) : nécessaire pour des tâches complexes comme la reconnaissance faciale haute précision (FaceNet utilise 128D, ArcFace utilise 512D). Mais augmente le risque de surapprentissage et le temps de calcul.

Architecture du Sous-Réseau

Le choix de l’architecture interne de la branche partagée détermine la qualité des caractéristiques extraites :

  • CNN simple (2-3 couches convolutives) : suffisant pour des images simples comme les chiffres MNIST ou les caractères Omniglot.
  • CNN profond (ResNet-18, VGG-16, EfficientNet) : nécessaire pour des visages ou des images complexes. Un transfert d’apprentissage depuis un réseau pré-entraîné sur ImageNet peut considérablement accélérer la convergence.
  • Attention / Transformers : pour des données non visuelles (texte, séquences), des architectures basées sur l’attention capturent mieux les relations longues distances.

Batch Size pour les Paires

Contrairement à un réseau classique, le Siamese Network traite des paires, ce qui signifie que la taille effective du batch dépend du nombre de paires générées :

  • Batch size = 64 à 256 paires est généralement approprié.
  • Les batchs plus grands offrent des gradients plus stables mais nécessitent plus de mémoire GPU.
  • Il est crucial de maintenir un ratio équilibré de 1:1 entre paires positives et négatives dans chaque batch pour éviter le biais vers un type de paire.

Avantages du Siamese Network

Le Siamese Network présente plusieurs avantages décisifs par rapport aux architectures de classification traditionnelles.

Le premier avantage est l’apprentissage one-shot et few-shot. Un réseau siamois peut reconnaître de nouvelles classes avec un seul exemple de référence, sans aucun ré-entraînement. C’est essentiel pour les applications biométriques où l’on ajoute fréquemment de nouveaux utilisateurs au système.

Le deuxième avantage est la généralisation aux classes non vues. Le modèle apprend une fonction de distance universelle, pas des frontières de classes spécifiques. Il est capable de comparer des paires d’individus qu’il n’a jamais observés pendant l’entraînement.

Le troisième avantage est l’efficacité des données. En générant des paires positives et négatives à partir des données disponibles, on multiplie artificiellement la taille du jeu d’entraînement sans collecter de nouvelles données. N images peuvent produire jusqu’à N² paires.

Le quatrième avantage est l’indépendance vis-à-vis du nombre de classes. Un classifieur classique doit modifier sa couche de sortie pour chaque nouvelle classe. Le Siamese Network n’a aucune couche de sortie dépendante des classes — sa sortie est toujours une distance scalaire.

Le cinquième avantage est la robustesse aux déséquilibres. Même quand certaines classes ont très peu d’échantillons, les paires négatives peuvent être générées en abondance, ce qui maintient un signal d’apprentissage stable.

Limites et Défis

Malgré ses avantages, le Siamese Network n’est pas une solution universelle et présente plusieurs limitations importantes.

La génération de paires constitue le principal défi. Pour un dataset de N images, le nombre de paires possibles est approximativement N²/2. La plupart de ces paires sont négatives et donc « faciles » — elles ne produisent pas de gradients utiles. Un échantillonnage intelligent est nécessaire pour sélectionner les paires les plus informatives.

Le choix du seuil de décision est souvent arbitraire. Contrairement à un classifieur qui produit directement une probabilité par classe, le Siamese Network renvoie une distance. Déterminer le seuil optimal pour distinguer similaire de non-similaire nécessite une calibration sur un jeu de validation, et ce seuil peut varier selon les classes et les conditions d’acquisition.

Le risque d’effondrement (collapse) est un problème sérieux : le réseau peut apprendre à produire le même embedding pour toutes les entrées, rendant la distance toujours nulle. Ce phénomène est plus fréquent avec la contrastive loss qu’avec la triplet loss. Des techniques comme la normalisation des embeddings ou l’augmentation de données sont essentielles pour l’éviter.

La dépendance à la qualité des données est forte. Des images mal alignées, des variations d’éclairage non compensées, ou du bruit dans les étiquettes de paires peuvent dégrader significativement la performance. Le prétraitement des données est souvent aussi important que l’architecture elle-même.

Enfin, le temps d’entraînement peut être important. Contrairement à un réseau standard qui traite une image à la fois, le Siamese Network doit faire passer deux images à chaque étape, doublant le coût computationnel de la propagation avant.

4 Cas d’Usage Concrets

1. Vérification de Signatures Manuscrites (Application Historique)

L’application originale du Siamese Network, développée par Bromley et al. pour AT&T en 1993, consistait à vérifier l’authenticité de signatures manuscrites sur des chèques bancaires. Le réseau compare une signature de référence (celle du titulaire du compte) avec une signature soumise et détermine s’il s’agit de la même personne ou d’un faussaire. Cette application reste d’actualité dans les systèmes de vérification bancaire et juridique numériques.

2. Reconnaissance et Vérification Faciale

C’est l’application la plus connue du grand public. FaceNet de Google utilise un Siamese Network avec triplet loss pour encoder des visages dans un espace de 128 dimensions. La distance entre deux embeddings détermine s’il s’agit du même individu. Cette technologie équipe les systèmes de déverrouillage facial des smartphones, les contrôles frontaliers automatisés, et les systèmes de surveillance intelligents. Apple FaceID utilise une architecture similaire avec des capteurs de profondeur 3D.

3. Recherche de Produits par Similarité Visuelle

Les plateformes de commerce en ligne utilisent des réseaux siamois pour permettre aux clients de rechercher des produits similaires à partir d’une image. L’utilisateur photographie un vêtement qu’il a vu dans la rue, et le système retrouve des articles visuellement semblables dans le catalogue. Le réseau siamois encode chaque produit dans un espace de similarité où la distance euclidienne reflète la ressemblance visuelle. Cette approche est massivement utilisée par Amazon, Pinterest et Alibaba.

4. Détection de Plagiat et Similarité de Documents

Dans le traitement du langage naturel, des variantes du Siamese Network sont employées pour comparer des paragraphes, des articles ou des copies d’étudiants. En encodant des textes en embeddings (via des transformers comme BERT dans les branches du réseau siamois), on mesure la distance sémantique entre documents. Cette approche permet de détecter le plagiat même quand le texte a été reformulé, car la similarité sémantique est préservée même si les mots exacts diffèrent. Des outils comme Turnitin et Compilatio utilisent des principes similaires pour leurs analyses de similarité.

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.