Intermédiaire Chapitre 11-12 / 7

Feature maps d'ordre supérieur (ZZ Feature Map) & Concevoir l'ansatz

Enrichir l'encodage avec des corrélations entre features via le ZZ Feature Map, puis concevoir l'ansatz paramétré qui complète la boucle variationnelle — avec du code Qiskit exécutable.

Feature maps d’ordre supérieur et ZZ Feature Map

L’idée en une phrase

Un feature map d’ordre supérieur enrichit l’encodage des données en ajoutant des termes d’interaction entre paires de features — typiquement un produit croisé φ(xᵢ, xⱼ) — via des portes d’intrication, augmentant l’expressivité de la représentation quantique au sein de la boucle variationnelle hybride (encodage → ansatz → mesure → optimisation classique).

Analogie : Une carte topographique qui ne noterait que l’altitude de chaque point donnerait un semis de cotes isolées. En reliant ces points par des courbes de niveau — c’est-à-dire en capturant les pentes entre paires de points —, la carte révèle vallées, cols et crêtes invisibles autrement. Le ZZ Feature Map procède de même dans l’espace de Hilbert : les termes Z individuels encodent chaque feature, les termes ZZ croisés capturent les corrélations entre paires de features.

Points clés

  • Le ZZFeatureMap de Qiskit combine des termes de Pauli Z (single-qubit) et ZZ (two-qubit). Chaque répétition applique : H sur tous les qubits, P(2xᵢ) sur chaque qubit i, puis pour chaque paire (i, j) une interaction CNOT(i,j) → P(2·φ(xᵢ,xⱼ)) → CNOT(i,j).
  • La fonction de données par défaut est φ(xᵢ, xⱼ) = (π − xᵢ)(π − xⱼ). Ce produit croisé crée une dépendance non linéaire entre features dans l’espace de Hilbert, inaccessible aux encodages purement single-qubit (angle, dense angle du chapitre 5).
  • Augmenter le nombre de répétitions (reps) enrichit l’espace de features mais accroît la profondeur du circuit. Sur un processeur NISQ, chaque couche supplémentaire accumule du bruit — le compromis expressivité ↔ entraînabilité se manifeste directement ici.
  • Le ZZ Feature Map est un cas particulier du PauliFeatureMap (termes ['Z', 'ZZ']). On peut construire des feature maps d’ordre supérieur avec des termes ZZZ ou d’autres combinaisons de Pauli, au prix d’une profondeur croissante.
  • En pratique, la plupart des benchmarks QML utilisent 1 à 3 répétitions. Au-delà, le bruit NISQ annule le gain en expressivité.

Exemple concret

On encode 2 features x = [0.8, 1.2] dans un ZZ Feature Map à 2 qubits avec 1 répétition. La première couche applique H sur les 2 qubits, les plaçant en superposition uniforme. Puis P(2 × 0.8) = P(1.6) sur le qubit 0 et P(2 × 1.2) = P(2.4) sur le qubit 1 ajoutent des phases proportionnelles aux features individuelles. Le terme ZZ calcule φ(0.8, 1.2) = (π − 0.8)(π − 1.2) ≈ 2.34 × 1.94 ≈ 4.54 et applique CNOT(0,1) → P(9.08) → CNOT(0,1). Ce bloc crée de l’intrication conditionnelle aux données : l’état final n’est plus un produit tensoriel. Avec un encodage angle seul (chapitre 5), les qubits resteraient indépendants — aucune corrélation entre x₀ et x₁ ne serait capturée par le feature map.

Comparaison des feature maps

Feature mapTermesQubits (n features)Profondeur par repIntrication
Angle (ch. 5)n1Aucune
Dense Angle (ch. 5)⌈n/2⌉2Aucune
ZFeatureMapZnO(1)Aucune
ZZFeatureMapZ + ZZnO(n²)Oui (paires)

Code Python — ZZ Feature Map depuis la bibliothèque Qiskit

from qiskit.circuit.library import ZZFeatureMap
from qiskit.quantum_info import Statevector

# ZZ Feature Map pour 2 features, 1 répétition
zz_fm = ZZFeatureMap(feature_dimension=2, reps=1)
print(zz_fm.decompose().draw())
print(f"Paramètres : {sorted(zz_fm.parameters, key=lambda p: p.name)}")

# Encoder des données concrètes
x = [0.8, 1.2]
bound = zz_fm.assign_parameters(x)
sv = Statevector.from_instruction(bound)
print("Probabilités :", {k: round(v, 4) for k, v in sv.probabilities_dict().items()})

Code Python — construction manuelle du ZZ Feature Map

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.quantum_info import Statevector
import numpy as np

def manual_zz_feature_map(n_qubits: int = 2) -> QuantumCircuit:
    """Construire un ZZ Feature Map à la main pour comprendre chaque porte."""
    x = ParameterVector('x', n_qubits)
    qc = QuantumCircuit(n_qubits)

    # Couche Hadamard — préparer la superposition uniforme
    for i in range(n_qubits):
        qc.h(i)

    # Termes single-qubit Z : phase proportionnelle à chaque feature
    for i in range(n_qubits):
        qc.p(2 * x[i], i)

    # Terme ZZ : interaction entre la paire (0, 1)
    # Implémente exp(i · φ(x₀,x₁) · Z₀Z₁) via CNOT + P + CNOT
    qc.cx(0, 1)
    qc.p(2 * (np.pi - x[0]) * (np.pi - x[1]), 1)
    qc.cx(0, 1)

    return qc

qc = manual_zz_feature_map()
print(qc.draw())
# Vérifier que le résultat coïncide avec la version bibliothèque
bound_manual = qc.assign_parameters([0.8, 1.2])
sv = Statevector.from_instruction(bound_manual)
print("Probabilités (manuel) :", {k: round(v, 4) for k, v in sv.probabilities_dict().items()})

Piège courant : « Le ZZ Feature Map encode les corrélations entre features, donc il est toujours supérieur à l’encodage angle » est une simplification. La profondeur supplémentaire introduite par les termes ZZ accumule du bruit sur un processeur NISQ. Pour des données où les features sont largement indépendantes, un encodage angle de profondeur 1 (chapitre 5) peut donner de meilleurs résultats qu’un ZZ Feature Map bruité.


Concevoir l’ansatz

L’idée en une phrase

L’ansatz est le circuit paramétré qui suit le feature map dans la boucle variationnelle hybride : il définit l’espace des hypothèses que l’optimiseur classique explore en ajustant les paramètres θ — choisir un ansatz, c’est fixer le compromis entre expressivité (capacité à représenter la solution) et entraînabilité (facilité d’optimisation).

Analogie : Une table de mixage audio dispose, sur chaque canal, de boutons de volume et d’égalisation (les rotations paramétrées sur chaque qubit) et de commandes de cross-fading entre canaux (les portes d’intrication). Avec trop peu de boutons, tous les morceaux se ressemblent ; avec trop de boutons, le moindre ajustement produit des effets imprévisibles. L’art du mixage consiste à disposer de juste assez de contrôles pour le genre musical visé — et l’art de concevoir un ansatz, à doser paramètres et profondeur pour le problème QML ciblé.

Points clés

  • L’ansatz est composé de couches alternant des rotations paramétrées (Ry, Rz, Rx) et des portes d’intrication (CNOT, CZ). Le nombre de couches (reps) contrôle la profondeur et le nombre de paramètres.
  • Un ansatz hardware-efficient (comme EfficientSU2) est conçu pour respecter la topologie de connexion du processeur quantique cible. Il minimise la profondeur après transpilation mais ne garantit pas que l’espace des hypothèses est adapté au problème.
  • RealAmplitudes est un cas particulier utilisant uniquement des rotations Ry et des CNOT. Il ne produit que des amplitudes réelles, ce qui le rend plus simple à optimiser mais moins expressif qu’EfficientSU2 (qui ajoute Rz pour manipuler les phases).
  • L’expressibilité d’un ansatz mesure sa capacité à couvrir l’espace de Hilbert uniformément. Un ansatz trop expressif avec peu de données mène au surapprentissage ; un ansatz trop restreint ne peut pas capturer la structure des données.
  • Le nombre de paramètres d’un ansatz TwoLocal à n qubits avec g rotations par qubit par couche est n × g × (reps + 1). Par exemple, EfficientSU2(3, reps=2) a 3 × 2 × 3 = 18 paramètres.

Exemple concret

On construit un classificateur variationnel pour 2 features sur 2 qubits. Le feature map est un ZZ Feature Map (2 paramètres de données). L’ansatz est un RealAmplitudes(2, reps=1) : la première couche applique Ry(θ₀) et Ry(θ₁) puis CNOT(0,1) ; la couche finale applique Ry(θ₂) et Ry(θ₃). Le circuit complet a 6 paramètres : 2 features (non entraînables) + 4 θ (entraînables). L’optimiseur classique (COBYLA, SPSA) ajuste les 4 θ pour minimiser la loss. Avec EfficientSU2(2, reps=1), l’ansatz aurait 8 paramètres (Ry + Rz par qubit par couche), offrant plus de flexibilité au prix d’un paysage d’optimisation plus complexe.

Comparaison des ansatzes courants

AnsatzRotationsParams (n=2, reps=1)ExpressivitéCas d’usage typique
RealAmplitudesRy4ModéréeClassification binaire simple
EfficientSU2Ry + Rz8ÉlevéeProblèmes nécessitant des phases
TwoLocalConfigurableVariableVariablePersonnalisation fine

Code Python — comparer des ansatzes Qiskit

from qiskit.circuit.library import EfficientSU2, RealAmplitudes

# RealAmplitudes : rotations Ry uniquement, amplitudes réelles
ra = RealAmplitudes(num_qubits=2, reps=1, entanglement='linear')
print(f"RealAmplitudes — {len(ra.parameters)} paramètres, profondeur {ra.decompose().depth()}")
print(ra.decompose().draw())

# EfficientSU2 : rotations Ry + Rz, couverture plus complète de l'espace de Hilbert
esu2 = EfficientSU2(num_qubits=2, reps=1, entanglement='linear')
print(f"\nEfficientSU2 — {len(esu2.parameters)} paramètres, profondeur {esu2.decompose().depth()}")
print(esu2.decompose().draw())

Code Python — assembler un circuit variationnel complet

from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes

# Feature map : encode les données (paramètres non entraînables)
fm = ZZFeatureMap(feature_dimension=2, reps=1)
# Ansatz : espace des hypothèses (paramètres entraînables)
ansatz = RealAmplitudes(num_qubits=2, reps=1)

# Composer le circuit complet : feature map suivi de l'ansatz
vqc = fm.compose(ansatz)
print(f"Circuit complet : {len(vqc.parameters)} paramètres")
print(f"  Features (données)    : {fm.num_parameters}")
print(f"  Ansatz (entraînables) : {ansatz.num_parameters}")
print(vqc.decompose().draw())

Piège courant : « Un ansatz plus profond donne toujours de meilleurs résultats » est inexact. Au-delà d’une certaine profondeur, les gradients s’aplatissent exponentiellement — c’est le phénomène des plateaus de Barren (chapitre 12). Sur un processeur NISQ, le bruit dégrade en plus la fidélité. En pratique, 1 à 3 couches suffisent pour la plupart des problèmes de classification QML.


Fil rouge — la frontière quantique/classique

Ce chapitre couvre les deux briques qui composent le cœur quantique de la boucle variationnelle : le feature map et l’ansatz. Le QPU exécute le circuit complet (feature map + ansatz), mesure les qubits et renvoie des statistiques au processeur classique. L’optimiseur classique n’intervient que sur les paramètres θ de l’ansatz — les paramètres du feature map sont les données d’entrée, fixées à chaque itération. Le compromis expressivité ↔ entraînabilité se joue sur deux fronts : un ZZ Feature Map enrichit la représentation des données mais ajoute de la profondeur, et un ansatz expressif augmente la capacité du modèle mais rend l’optimisation plus difficile. L’art du QML NISQ consiste à doser ces deux sources de profondeur pour rester dans le budget de portes tolérable par le matériel.


Quiz — teste tes connaissances
Intermédiaire 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 — construire un circuit variationnel ZZ + ansatz

Objectif : construire manuellement un ZZ Feature Map et un ansatz hardware-efficient, puis les assembler en un circuit variationnel complet.

Squelette :

# kata_day_11_12.py
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
import numpy as np

def build_zz_feature_map(n_qubits: int = 2) -> QuantumCircuit:
    """Construire un ZZ Feature Map pour 2 qubits (1 répétition).
    Structure : H⊗n → P(2·xᵢ) sur chaque qubit → CNOT(0,1) → P(2·(π-x₀)(π-x₁)) → CNOT(0,1)
    """
    x = ParameterVector('x', n_qubits)
    qc = QuantumCircuit(n_qubits)
    # TODO 1 : appliquer une porte Hadamard sur chaque qubit
    # TODO 2 : appliquer P(2 * x[i]) sur chaque qubit i
    # TODO 3 : appliquer le terme ZZ — CNOT(0,1), P(2*(π - x[0])*(π - x[1])) sur qubit 1, CNOT(0,1)
    return qc

def build_hardware_ansatz(n_qubits: int = 2, reps: int = 1) -> QuantumCircuit:
    """Construire un ansatz hardware-efficient.
    Structure par couche : Ry(θ) sur chaque qubit → CNOT(0,1).
    Couche finale : Ry(θ) sur chaque qubit (sans CNOT).
    """
    n_params = n_qubits * (reps + 1)
    theta = ParameterVector('θ', n_params)
    qc = QuantumCircuit(n_qubits)
    idx = 0
    # TODO 4 : pour chaque répétition, appliquer Ry(theta[idx]) sur chaque qubit puis CNOT(0,1)
    # TODO 5 : couche finale — Ry(theta[idx]) sur chaque qubit, sans CNOT
    return qc

def assemble_vqc(feature_map: QuantumCircuit, ansatz: QuantumCircuit) -> QuantumCircuit:
    """Assembler le feature map et l'ansatz en un seul circuit variationnel."""
    # TODO 6 : composer feature_map suivi de ansatz
    return QuantumCircuit(1)  # placeholder

Auto-correction :

# test_kata_day_11_12.py
import numpy as np
from kata_day_11_12 import build_zz_feature_map, build_hardware_ansatz, assemble_vqc

# --- Test du ZZ Feature Map ---
fm = build_zz_feature_map(2)
assert fm.num_qubits == 2, "Le feature map doit avoir 2 qubits"
assert len(fm.parameters) == 2, f"Le feature map doit avoir 2 paramètres, trouvé {len(fm.parameters)}"
ops = [instr.operation.name for instr in fm.data]
assert 'h' in ops, "Il manque les portes Hadamard"
assert 'p' in ops, "Il manque les portes Phase (P)"
assert 'cx' in ops, "Il manque les CNOT pour l'interaction ZZ"

# --- Test de l'ansatz ---
ansatz = build_hardware_ansatz(2, reps=1)
assert ansatz.num_qubits == 2, "L'ansatz doit avoir 2 qubits"
assert len(ansatz.parameters) == 4, f"L'ansatz doit avoir 4 paramètres, trouvé {len(ansatz.parameters)}"
ops_a = [instr.operation.name for instr in ansatz.data]
assert 'ry' in ops_a, "L'ansatz doit contenir des rotations Ry"
assert 'cx' in ops_a, "L'ansatz doit contenir au moins un CNOT"

# --- Test du circuit assemblé ---
vqc = assemble_vqc(fm, ansatz)
assert vqc.num_qubits == 2, "Le VQC doit avoir 2 qubits"
assert len(vqc.parameters) == 6, f"Le VQC doit avoir 6 paramètres (2 + 4), trouvé {len(vqc.parameters)}"

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

def build_zz_feature_map(n_qubits: int = 2) -> QuantumCircuit:
    x = ParameterVector('x', n_qubits)
    qc = QuantumCircuit(n_qubits)
    for i in range(n_qubits):
        qc.h(i)
    for i in range(n_qubits):
        qc.p(2 * x[i], i)
    qc.cx(0, 1)
    qc.p(2 * (np.pi - x[0]) * (np.pi - x[1]), 1)
    qc.cx(0, 1)
    return qc

def build_hardware_ansatz(n_qubits: int = 2, reps: int = 1) -> QuantumCircuit:
    n_params = n_qubits * (reps + 1)
    theta = ParameterVector('θ', n_params)
    qc = QuantumCircuit(n_qubits)
    idx = 0
    for _ in range(reps):
        for i in range(n_qubits):
            qc.ry(theta[idx], i)
            idx += 1
        qc.cx(0, 1)
    for i in range(n_qubits):
        qc.ry(theta[idx], i)
        idx += 1
    return qc

def assemble_vqc(feature_map: QuantumCircuit, ansatz: QuantumCircuit) -> QuantumCircuit:
    return feature_map.compose(ansatz)

Pourquoi : le kata reconstruit les deux briques centrales de la boucle variationnelle. Le ZZ Feature Map ajoute des corrélations entre features via le terme (π − x₀)(π − x₁), implémenté par le triplet CNOT-P-CNOT. L’ansatz hardware-efficient alterne rotations Ry et CNOT pour créer un espace d’hypothèses entraînable. La composition via compose produit le circuit complet que le QPU exécutera — les 2 paramètres du feature map sont les données, les 4 de l’ansatz sont les variables d’optimisation.