Expert Chapitre 21-22 / 12

Le Data Re-uploading & Du CNN au QCNN

Du data re-uploading (approximation universelle à 1 qubit) au QCNN — le CNN quantique qui esquive les plateaus de Barren — avec du code Qiskit exécutable sur simulateur.

Le Data Re-uploading

L’idée en une phrase

Le data re-uploading consiste à réencoder les données classiques x entre chaque couche d’ansatz paramétré, brisant la compositionnalité linéaire des unitaires et augmentant le degré de la série de Fourier accessible au circuit. Dans la boucle variationnelle hybride, chaque couche enchaîne un encodage S(x) et un ansatz U(θₗ) — le résultat de Pérez-Salinas et al. montre qu’un seul qubit avec suffisamment de couches de re-uploading est un approximateur universel (chapitre 19-20).

Analogie : Un potier travaille l’argile en plusieurs passes. À chaque tour de roue (couche), il réexamine le pot (données) avant d’ajuster la pression (ansatz). S’il ne regardait le pot qu’au premier tour et modelait ensuite à l’aveugle, la forme finale serait grossière. Le re-uploading, c’est réexaminer le pot à chaque tour : les données irriguent toutes les couches, pas seulement la première.

Points clés

  • Sans re-uploading, un circuit U(θ)·S(x)|0⟩ n’encode x qu’une fois. La sortie P(|1⟩) est une série de Fourier dont le degré maximal dépend du nombre de portes d’encodage — ajouter des couches d’ansatz sans réencoder ne change pas les fréquences accessibles.
  • Avec re-uploading sur L couches, le circuit devient [U(θ_L)·S(x)]·…·[U(θ_1)·S(x)]|0⟩. Les données sont injectées L fois, ce qui porte le degré de Fourier à L et étend la classe de fonctions représentables.
  • Théorème d’approximation universelle quantique (Pérez-Salinas et al.) : un seul qubit avec L couches de re-uploading peut approcher toute fonction continue sur un compact, à condition que L soit assez grand. C’est l’analogue quantique du théorème de Cybenko pour les réseaux de neurones classiques.
  • La perspective des quantum Fourier features formalise ce lien : la sortie d’un circuit paramétré est une série de Fourier en x, dont les fréquences sont fixées par la structure du circuit (portes d’encodage) et les coefficients par les paramètres entraînables. Le re-uploading multiplie les portes d’encodage, donc les fréquences accessibles.
  • Le coût : chaque couche supplémentaire ajoute 2 paramètres et augmente la profondeur du circuit. Sur matériel NISQ, cela dégrade le rapport signal/bruit et rapproche des plateaus de Barren (chapitre 23-24). Le re-uploading est un curseur sur l’axe expressivité ↔ entraînabilité.

Exemple concret

Un classificateur à 1 qubit avec L couches de re-uploading. L’entrée est un scalaire x ∈ [0, 2π]. Chaque couche applique Rx(x)·Rz(x) (encodage) puis Ry(θ_l)·Rz(φ_l) (ansatz). La sortie est P(|1⟩). Avec L = 1, la sortie est une sinusoïde simple de fréquence 1 — le modèle ne peut séparer qu’un seul intervalle. Avec L = 3, la sortie contient des composantes de fréquences 1, 2 et 3 — le modèle peut découper jusqu’à 3 intervalles distincts. Avec L = 10, le circuit approche n’importe quelle frontière de décision 1D, au prix de 20 paramètres et d’une profondeur de circuit qui défie les processeurs NISQ actuels.

Expressivité en fonction du nombre de couches

Couches (L)ParamètresDegré Fourier maxComplexité de la frontièreRisque NISQ
121Sinusoïde simpleFaible
363Oscillations multiplesModéré
102010Arbitrairement complexeÉlevé
L2LLCroissant avec LCroissant

Code Python — classificateur data re-uploading à 1 qubit

# Data re-uploading : réencoder x à chaque couche augmente l'expressivité.
# Comparaison L=1 vs L=3 : la fonction de sortie oscille davantage.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

def reuploading_circuit(x, params, n_layers):
    """Circuit de re-uploading à 1 qubit.
    Chaque couche : Rx(x)·Rz(x) (encodage) puis Ry(θ)·Rz(φ) (ansatz)."""
    qc = QuantumCircuit(1)
    for l in range(n_layers):
        # Re-upload des données à chaque couche
        qc.rx(x, 0)
        qc.rz(x, 0)
        # Ansatz paramétré (optimisé par l'optimiseur classique)
        qc.ry(params[l][0], 0)
        qc.rz(params[l][1], 0)
    return qc

# Paramètres fixés (en pratique, optimisés par descente de gradient)
params_1 = [[1.0, 0.5]]
params_3 = [[0.5, 1.2], [1.0, -0.3], [0.8, 0.5]]

x_vals = np.linspace(0, 2 * np.pi, 13)
print("x       | L=1    | L=3")
print("--------|--------|-------")
for x in x_vals:
    p1 = Statevector(reuploading_circuit(x, params_1, 1)).probabilities()[1]
    p3 = Statevector(reuploading_circuit(x, params_3, 3)).probabilities()[1]
    print(f"{x:7.3f} | {p1:.4f} | {p3:.4f}")

Piège courant : « Le re-uploading est juste un circuit plus profond » est une confusion fréquente. Un circuit profond sans re-uploading (U_L·…·U_1·S(x)|0⟩) n’augmente pas le degré de Fourier de la sortie par rapport à x — seul le nombre de portes d’encodage compte. Le re-uploading réinjecte les données dans le circuit, ce qui multiplie les fréquences accessibles. Dix couches d’ansatz sans re-uploading ont le même pouvoir expressif (vis-à-vis de x) qu’une seule couche d’encodage.


Du CNN au QCNN

L’idée en une phrase

Le Quantum Convolutional Neural Network (QCNN) transpose au quantique les deux mécanismes du CNN classique : les couches convolutives (même filtre local appliqué partout) deviennent des unitaires à 2 qubits identiques sur chaque paire voisine, et le pooling (réduction de dimensionnalité) se réalise par des opérations contrôlées suivies du traçage de qubits. Dans la boucle variationnelle hybride, le QCNN structure l’ansatz en un entonnoir pyramidal dont le partage de poids réduit le nombre de paramètres et atténue les plateaus de Barren.

Analogie : Un CNN classique est une chaîne de tamis de plus en plus grossiers : le premier détecte les contours (motifs locaux), le deuxième les formes (combinaisons de contours), le dernier les objets. À chaque niveau, on jette la poussière (pooling) pour ne garder que l’essentiel. Le QCNN fonctionne de même, mais les tamis sont des unitaires quantiques et la poussière est de l’information quantique tracée. La forme en entonnoir — 8 qubits → 4 → 2 → 1 — est identique.

Points clés

  • Le CNN classique empile : couche convolutive (filtre glissant avec partage de poids) → activation non linéaire → pooling (max ou moyenne, divise la taille par 2). La force du CNN tient à l’invariance par translation et à la hiérarchie locale → globale.
  • Le QCNN (Cong, Choi, Lukin, 2019) applique le même unitaire paramétré U_conv(θ) à chaque paire de qubits voisins (convolution), puis effectue des rotations contrôlées d’un qubit source vers un qubit cible, suivies du traçage du qubit source (pooling). La moitié des qubits disparaît à chaque étage.
  • La structure pyramidale réduit les qubits actifs par un facteur 2 à chaque étage : n → n/2 → … → 1. Le qubit final est mesuré pour la classification. La profondeur totale est O(log n).
  • L’avantage décisif : le partage de poids limite les paramètres indépendants à O(log n) au lieu de O(n·L) pour un ansatz dense. Cela maintient des gradients non nuls et atténue les plateaus de Barren — un problème critique pour les circuits variationnels profonds (chapitre 23-24).
  • Le QCNN est particulièrement adapté aux tâches avec une structure spatiale ou une symétrie : classification de phases quantiques (l’application originale de Cong et al.), détection de codes correcteurs d’erreurs, reconnaissance de motifs dans des chaînes de spins. Pour des données classiques, il structure l’ansatz mais nécessite un encodage préalable.

Exemple concret

Un QCNN sur 8 qubits pour classifier deux phases quantiques. Étage 1 : 4 blocs convolutifs identiques sur les paires (0,1), (2,3), (4,5), (6,7), puis pooling → 4 qubits actifs. Étage 2 : 2 blocs convolutifs sur (1,3) et (5,7), puis pooling → 2 qubits actifs. Étage 3 : 1 bloc convolutif sur (3,7), puis pooling → 1 qubit (le 7). Le nombre total de paramètres indépendants est 3 jeux de paramètres convolutifs + 3 jeux de paramètres de pooling — un total constant, indépendant de n, pour une architecture à profondeur fixe.

CNN classique vs QCNN

AspectCNN classiqueQCNN
EntréeTenseur (image, séquence)Registre de n qubits
ConvolutionFiltre glissant, produit scalaireUnitaire U(θ) à 2 qubits
Non-linéaritéReLU, sigmoïdeMesure / traçage
PoolingMax-pool, average-poolRotation contrôlée + traçage
Partage de poidsOui (translation)Oui (même U(θ) par étage)
Réduction par étageTaille spatiale ÷ 2Qubits actifs ÷ 2
ParamètresO(k²·C·L)O(log n)

Code Python — QCNN à 4 qubits

# QCNN simplifié : conv → pool → conv → pool → classification.
# Structure pyramidale : 4 qubits actifs → 2 → 1.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

def conv_block(qc, q0, q1, params):
    """Bloc convolutif paramétré sur 2 qubits.
    Même bloc appliqué à chaque paire = partage de poids."""
    qc.rz(params[0], q0)
    qc.rz(params[1], q1)
    qc.cx(q0, q1)
    qc.ry(params[2], q0)
    qc.ry(params[3], q1)
    qc.cx(q1, q0)

def pool_block(qc, source, target, param):
    """Pooling : rotation contrôlée de source vers target.
    Le qubit source est ensuite considéré comme tracé."""
    qc.crz(param, source, target)

qc = QuantumCircuit(4)

# Encodage de données classiques (angle encoding)
for i, x in enumerate([0.5, 1.0, 1.5, 2.0]):
    qc.ry(x, i)

# Étage 1 — Convolution : même U(θ) sur paires (0,1) et (2,3)
conv_p1 = [0.3, 0.5, 0.7, 0.1]
conv_block(qc, 0, 1, conv_p1)
conv_block(qc, 2, 3, conv_p1)  # partage de poids

# Pooling 1 : transfert 0→1 et 2→3, puis traçage de 0 et 2
pool_block(qc, 0, 1, 0.4)
pool_block(qc, 2, 3, 0.4)

# Étage 2 — Convolution sur qubits actifs (1, 3)
conv_p2 = [0.2, 0.8, 0.6, 0.4]
conv_block(qc, 1, 3, conv_p2)

# Pooling 2 : transfert 1→3, qubit 3 = sortie finale
pool_block(qc, 1, 3, 0.5)

# Classification : P(qubit 3 = |1⟩)
sv = Statevector(qc)
probs = sv.probabilities()
# Qubit 3 = bit de poids 2³ en convention Qiskit (little-endian)
p_class1 = sum(probs[8:])
print(f"P(classe 1) = {p_class1:.4f}")
print(f"Paramètres : {4 + 4 + 1 + 1 + 1} = 11 (vs ansatz dense)")

Piège courant : « Le QCNN n’est utile que pour des données quantiques (classification de phases) » est trop restrictif. Le QCNN fonctionne aussi avec des données classiques encodées par angle encoding ou re-uploading — il structure simplement l’ansatz de manière hiérarchique. Son avantage principal n’est pas le type de données, mais la réduction du nombre de paramètres et la résistance aux plateaus de Barren grâce au partage de poids. Combiné au re-uploading dans les couches convolutives, le QCNN offre un compromis expressivité/entraînabilité parmi les plus favorables en QML NISQ.


Fil rouge — la frontière quantique/classique

Le data re-uploading et le QCNN illustrent deux stratégies complémentaires pour doser l’hybridation quantique-classique. Le re-uploading augmente l’expressivité du QPU en réinjectant les données classiques à chaque couche — le circuit devient un approximateur universel, mais au prix d’une profondeur croissante. Le QCNN apporte la réponse architecturale : en structurant le circuit comme un entonnoir à poids partagés, il réduit les paramètres à O(log n) et maintient des gradients exploitables par l’optimiseur classique. Sur l’axe expressivité ↔ entraînabilité, le re-uploading pousse vers l’expressivité (plus de couches = plus de fréquences de Fourier), tandis que le QCNN restaure l’entraînabilité (moins de paramètres indépendants = gradients non nuls). L’état de l’art combine les deux : des couches convolutives QCNN avec re-uploading interne.


Quiz — teste tes connaissances
Expert 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Remonte relire la fiche memo en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.

Kata Qiskit — classificateur data re-uploading

Objectif : implémenter un classificateur data re-uploading à 1 qubit, vérifier que la sortie P(|1⟩) est dans [0, 1] pour toute entrée, et constater que la classification binaire fonctionne avec des poids bien choisis.

Squelette :

# kata_day_21_22.py
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

def reuploading_circuit(x, weights, n_layers):
    """Construire un circuit de data re-uploading à 1 qubit.
    
    Chaque couche l (0 à n_layers-1) :
      1. Encoder x via Rx(x) et Rz(x)
      2. Appliquer l'ansatz Ry(weights[l][0]) et Rz(weights[l][1])
    
    Args:
        x: donnée à encoder (scalaire)
        weights: liste de n_layers paires [θ, φ]
        n_layers: nombre de couches
    Returns:
        QuantumCircuit à 1 qubit
    """
    qc = QuantumCircuit(1)
    for l in range(n_layers):
        # TODO 1 : encoder x avec Rx et Rz sur le qubit 0
        # TODO 2 : appliquer l'ansatz Ry(weights[l][0]) et Rz(weights[l][1])
        pass
    return qc

def predict(x, weights, n_layers):
    """Renvoyer P(|1⟩) pour le circuit de re-uploading.
    
    Args:
        x: donnée d'entrée (scalaire)
        weights: paramètres de l'ansatz
        n_layers: nombre de couches
    Returns:
        float : probabilité P(|1⟩)
    """
    # TODO 3 : construire le circuit, obtenir le Statevector, renvoyer P(|1⟩)
    return 0.0

def classify(x, weights, n_layers, threshold=0.5):
    """Classifier x en 0 ou 1 via le circuit de re-uploading."""
    # TODO 4 : renvoyer 1 si predict(...) > threshold, sinon 0
    return 0

Auto-correction :

# test_kata_day_21_22.py
import numpy as np
from kata_day_21_22 import reuploading_circuit, predict, classify

# Test 1 : une couche = 4 portes (rx, rz, ry, rz)
qc = reuploading_circuit(1.0, [[0.5, 0.3]], 1)
assert qc.num_qubits == 1, "Le circuit doit avoir 1 qubit"
ops = [instr.operation.name for instr in qc.data]
assert len(ops) == 4, f"1 couche = 4 portes, trouvé {len(ops)}"

# Test 2 : trois couches = 12 portes
qc3 = reuploading_circuit(1.0, [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], 3)
ops3 = [instr.operation.name for instr in qc3.data]
assert len(ops3) == 12, f"3 couches = 12 portes, trouvé {len(ops3)}"

# Test 3 : cas trivial — tout à zéro donne P(|1⟩) = 0
p0 = predict(0.0, [[0.0, 0.0]], 1)
assert abs(p0) < 1e-9, f"predict(0, zeros, 1) devrait être ~0, trouvé {p0}"

# Test 4 : predict renvoie une valeur dans [0, 1]
for x in np.linspace(0, 2 * np.pi, 20):
    p = predict(x, [[1.0, 0.5], [0.3, 0.7]], 2)
    assert -1e-9 <= p <= 1 + 1e-9, f"P(|1⟩) hors de [0,1] : {p}"

# Test 5 : classify fonctionne correctement
assert classify(0.0, [[np.pi, 0.0]], 1) == 1, "Ry(π)|0⟩ → P(|1⟩)=1 → classe 1"
assert classify(0.0, [[0.0, 0.0]], 1) == 0, "Identité → P(|1⟩)=0 → classe 0"

print("Kata validé !")
Solution et explication
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

def reuploading_circuit(x, weights, n_layers):
    qc = QuantumCircuit(1)
    for l in range(n_layers):
        qc.rx(x, 0)                  # TODO 1 : encodage Rx
        qc.rz(x, 0)                  # TODO 1 : encodage Rz
        qc.ry(weights[l][0], 0)      # TODO 2 : ansatz Ry
        qc.rz(weights[l][1], 0)      # TODO 2 : ansatz Rz
    return qc

def predict(x, weights, n_layers):
    qc = reuploading_circuit(x, weights, n_layers)  # TODO 3
    sv = Statevector(qc)
    return sv.probabilities()[1]     # P(|1⟩)

def classify(x, weights, n_layers, threshold=0.5):
    return 1 if predict(x, weights, n_layers) > threshold else 0  # TODO 4

Pourquoi : le circuit réencode x à chaque couche via Rx(x)·Rz(x), ce qui augmente le degré de la série de Fourier de la sortie. L’ansatz Ry(θ)·Rz(φ) ajuste les coefficients de Fourier — l’optimiseur classique cherche les valeurs de θ et φ qui minimisent la fonction de coût. La mesure finale (implicite dans Statevector.probabilities()) fournit la non-linéarité P(|1⟩) = |⟨1|ψ⟩|². C’est la boucle variationnelle hybride en action : le QPU évalue le circuit, la mesure produit la sortie non linéaire, l’optimiseur classique ajuste les paramètres.