Intermédiaire Chapitre 7-8 / 4

Anatomie d'un classificateur variationnel (VQC) & Encodage Basis

Décomposer le VQC en ses trois blocs — feature map, ansatz, mesure — et encoder des données binaires dans la base computationnelle avec Qiskit.

Anatomie d’un classificateur variationnel

L’idée en une phrase

Un classificateur quantique variationnel (VQC) réalise la fonction F(x, θ) du chapitre 1 en enchaînant trois blocs : un feature map qui encode les données classiques x dans un état quantique, un ansatz paramétré par θ qui transforme cet état, et une mesure qui extrait une prédiction classique — le tout piloté par un optimiseur classique dans la boucle variationnelle hybride.

Analogie : Un VQC fonctionne comme une chaîne de production à trois postes. Le premier poste (feature map) reçoit la matière première — les données classiques — et la transforme en pièces calibrées — un état quantique. Le deuxième poste (ansatz) ajuste ces pièces selon des réglages θ que le contremaître (l’optimiseur classique) affine après chaque lot. Le troisième poste (mesure) inspecte le produit fini et émet un verdict — la classe prédite.

Points clés

  • Le feature map U(x) transforme un vecteur classique x en état quantique U(x)|0⟩. Ce bloc dépend des données d’entrée mais n’est pas entraînable. Le choix du feature map détermine la géométrie de l’espace de Hilbert exploité — un sujet central des chapitres 5 à 6.
  • L’ansatz W(θ) est un circuit paramétré composé de rotations et de portes d’intrication. Ses paramètres θ sont les variables d’optimisation. Un bon ansatz est suffisamment expressif pour séparer les classes mais suffisamment peu profond pour rester entraînable en ère NISQ — le compromis expressivité ↔ entraînabilité identifié au chapitre 3.
  • La mesure projette l’état quantique sur la base computationnelle. La probabilité d’observer un certain résultat (par exemple P(q₀ = 1)) sert de score de classification. L’espérance d’un observable via l’Estimator est une alternative, traitée au chapitre 7.
  • L’optimiseur classique (COBYLA, SPSA, L-BFGS-B) reçoit la fonction de coût et met à jour θ — exactement la descente de gradient du chapitre 2, adaptée au contexte quantique via le Parameter Shift Rule.
  • La structure complète est : prédiction = Mesure[W(θ) · U(x) |0⟩], et l’entraînement minimise la fonction de coût L(θ) sur un jeu étiqueté — le workflow supervisé du chapitre 2.

Exemple concret

On veut classifier des iris en deux catégories à partir de 2 features normalisées (longueur et largeur de sépale). Le VQC à 2 qubits procède ainsi : (1) le feature map encode les 2 features comme rotations Ry(x₀) et Ry(x₁) — chaque feature contrôle l’angle d’un qubit, (2) l’ansatz applique des rotations entraînables Ry(θ₀), Ry(θ₁) suivies d’un CNOT pour corréler les qubits, (3) la mesure du qubit 0 produit P(q₀ = 1), interprétée comme la probabilité de la classe 1, (4) l’optimiseur classique compare cette probabilité au label réel via la cross-entropy et ajuste les 2 paramètres θ. Le QPU n’intervient que dans les étapes (1) à (3) ; le reste est classique.

Les trois blocs du VQC

BlocRôleDépend deEntraînableExécuté sur
Feature map U(x)Encoder x dans l’espace de HilbertDonnées xNonQPU
Ansatz W(θ)Transformer l’état encodéParamètres θOuiQPU
MesureExtraire une prédiction classiqueNonQPU → CPU
OptimiseurMettre à jour θCoût L(θ)CPU

Code Python — les trois blocs d’un VQC

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

def build_vqc(n_qubits: int = 2):
    """Construire un VQC minimal : feature map + ansatz + mesure."""
    x = ParameterVector('x', n_qubits)      # paramètres d'entrée (données)
    theta = ParameterVector('θ', n_qubits)   # paramètres entraînables

    qc = QuantumCircuit(n_qubits)

    # Bloc 1 — Feature map : encoder chaque donnée comme rotation Ry
    for i in range(n_qubits):
        qc.ry(x[i], i)

    # Bloc 2 — Ansatz : rotations paramétrées + intrication
    for i in range(n_qubits):
        qc.ry(theta[i], i)
    qc.cx(0, 1)

    return qc, x, theta

qc, x, theta = build_vqc()

# Évaluer avec des données concrètes
data = [np.pi / 4, np.pi / 3]     # 2 features normalisées
weights = [0.5, 0.8]               # paramètres entraînables

bound = qc.assign_parameters({
    x[0]: data[0], x[1]: data[1],
    theta[0]: weights[0], theta[1]: weights[1]
})
sv = Statevector.from_instruction(bound)
probs = sv.probabilities()

# Bloc 3 — Mesure : P(q₀ = 1) comme score de classification
p_class_1 = sum(probs[j] for j in range(len(probs)) if j % 2 == 1)
print(f"P(classe 1) = {p_class_1:.4f}")
# L'optimiseur ajustera weights pour rapprocher cette probabilité du label réel

Piège courant : « Le VQC apprend en modifiant le circuit quantique » est inexact. La structure du circuit (portes, connectivité) est fixée à la conception. Ce qui change pendant l’entraînement, ce sont les valeurs numériques de θ dans l’ansatz. Le QPU évalue le même circuit avec des θ différents à chaque itération — c’est l’optimiseur classique qui « apprend ».


Encodage Basis

L’idée en une phrase

L’encodage basis est la méthode la plus directe pour transférer des données classiques binaires vers un registre quantique : chaque bit bᵢ contrôle l’état du qubit i via une porte X, produisant l’état |bₙ₋₁…b₁b₀⟩ — un point de départ minimal où toute l’expressivité de la boucle variationnelle repose sur l’ansatz.

Analogie : L’encodage basis est comparable au rangement de livres dans une étagère numérotée : le livre n° 0 va dans la case 0, le livre n° 1 dans la case 1 — un rangement direct, sans compression ni réarrangement. Les encodages continus (angle, amplitude) sont comme un bibliothécaire qui empile plusieurs livres par case ou code leur position en fonction de leur poids — plus dense, mais plus complexe à mettre en œuvre.

Points clés

  • L’encodage basis mappe un vecteur binaire b = [b₀, b₁, …, bₙ₋₁] vers l’état computationnel |bₙ₋₁…b₁b₀⟩. Pour chaque bᵢ = 1, une porte X est appliquée au qubit i (car X|0⟩ = |1⟩).
  • Le coût en qubits est linéaire : n qubits pour n bits. Il n’y a aucune compression — contrairement à l’encodage amplitude qui stocke 2ⁿ valeurs dans n qubits.
  • Les états encodés sont mutuellement orthogonaux : pour deux entrées distinctes b ≠ b’, le produit scalaire est nul. Cette propriété garantit une séparabilité maximale dans l’espace de Hilbert.
  • L’encodage basis ne contient aucun paramètre continu : le circuit est composé uniquement de portes X. Toute l’expressivité du VQC repose donc sur l’ansatz qui suit.
  • La limitation principale est le domaine : seules des données binaires peuvent être encodées. Pour des données continues, il faut les discrétiser (perte d’information). Les encodages angle et amplitude (chapitre 5) lèvent cette restriction.

Exemple concret

On dispose d’un jeu de données médical binaire à 3 features : x₁ = [1, 0, 1] (symptômes A et C présents) et x₂ = [0, 1, 0] (symptôme B présent). L’encodage basis produit |101⟩ pour x₁ et |010⟩ pour x₂ — deux états orthogonaux. Un ansatz de 3 qubits avec rotations Ry et CNOT transforme ces états, et la mesure du qubit 0 donne P(q₀ = 1) comme probabilité de diagnostic positif. L’orthogonalité garantit que le VQC peut distinguer parfaitement x₁ et x₂ si l’ansatz est suffisamment expressif.

Encodage Basis vs encodages continus (aperçu)

CritèreBasisAngle (ch. 5)Amplitude (ch. 5)
Type de donnéesBinaires (0/1)ContinuesContinues
Qubits requisn (= nb de features)n (= nb de features)log₂(N)
Portes utiliséesX uniquementRy, RzInitialisation d’état
Orthogonalité garantieOuiNon en généralNon en général
Expressivité de l’encodageMinimaleModéréeMaximale

Code Python — encoder des données binaires dans la base computationnelle

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

def basis_encode(data: list[int]) -> QuantumCircuit:
    """Encoder un vecteur binaire dans la base computationnelle.
    data[i] = 1 → porte X sur le qubit i.
    """
    n = len(data)
    qc = QuantumCircuit(n)
    for i, bit in enumerate(data):
        if bit == 1:
            qc.x(i)
    return qc

# Encoder [1, 0, 1]
qc = basis_encode([1, 0, 1])
sv = Statevector.from_instruction(qc)
print("Probabilités :", sv.probabilities_dict())
# {'101': 1.0} — un seul état avec probabilité 1

# Vérifier l'orthogonalité de deux encodages distincts
sv1 = Statevector.from_instruction(basis_encode([1, 0, 1]))
sv2 = Statevector.from_instruction(basis_encode([0, 1, 0]))
overlap = abs(sv1.inner(sv2)) ** 2
print(f"Recouvrement |⟨ψ₁|ψ₂⟩|² = {overlap:.1f}")
# 0.0 — orthogonalité parfaite

Piège courant : « L’encodage basis est inutile car il n’exploite pas la superposition » est une simplification. L’encodage lui-même ne crée pas de superposition, mais l’ansatz qui suit en créera. L’orthogonalité des états encodés est une propriété forte que les encodages continus ne garantissent pas. L’encodage basis reste la brique de référence pour comprendre les encodages plus sophistiqués.


Fil rouge — la frontière quantique/classique

Dans un VQC, la frontière est nettement tracée : le QPU exécute le feature map et l’ansatz (préparation et transformation de l’état), tandis que le CPU calcule le coût et met à jour θ. L’encodage basis sollicite le QPU de façon minimale — quelques portes X sans paramètre continu — et confie l’intégralité de l’expressivité à l’ansatz. Sur l’axe expressivité ↔ entraînabilité, un feature map basis impose une contrainte : l’ansatz doit compenser seul la simplicité de l’encodage, ce qui peut nécessiter davantage de couches et donc aggraver le compromis avec le bruit NISQ.


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 — classificateur variationnel avec encodage basis

Objectif : implémenter l’encodage basis et un pipeline VQC complet (encodage → ansatz → mesure) pour classifier des données binaires.

Squelette :

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

def basis_encode(data: list[int]) -> QuantumCircuit:
    """Encoder un vecteur binaire dans la base computationnelle.
    Pour chaque bit à 1, appliquer une porte X sur le qubit correspondant.
    """
    n = len(data)
    qc = QuantumCircuit(n)
    # TODO 1 : parcourir data et appliquer X sur le qubit i si data[i] == 1
    return qc

def vqc_predict(data: list[int], theta_values: list[float]) -> float:
    """Pipeline VQC : basis encoding → ansatz (Ry + CNOT chaîne) → P(q₀ = 1).

    L'ansatz applique Ry(θᵢ) sur chaque qubit i, puis une chaîne de CNOT :
    CNOT(0,1), CNOT(1,2), …, CNOT(n-2, n-1).
    Retourne la probabilité que le qubit 0 soit dans l'état |1⟩.
    """
    n = len(data)
    theta = ParameterVector('θ', n)
    # TODO 2 : créer le circuit avec basis_encode(data)
    # TODO 3 : ajouter l'ansatz — Ry(theta[i]) sur chaque qubit i
    # TODO 4 : ajouter la chaîne de CNOT : cx(i, i+1) pour i de 0 à n-2
    # TODO 5 : assigner theta_values aux paramètres, calculer le Statevector,
    #          retourner P(q₀ = 1) = somme des probabilités aux indices impairs
    return 0.0

Auto-correction :

# test_kata_day_7_8.py
import numpy as np
from kata_day_7_8 import basis_encode, vqc_predict
from qiskit.quantum_info import Statevector

# --- Tests basis_encode ---
# [0, 0] → |00⟩
sv = Statevector.from_instruction(basis_encode([0, 0]))
assert sv.probabilities_dict() == {'00': 1.0}, "basis_encode([0,0]) doit produire |00⟩"

# [1, 0] → |01⟩ en convention Qiskit (q₁q₀)
sv = Statevector.from_instruction(basis_encode([1, 0]))
probs = sv.probabilities_dict()
assert '01' in probs and abs(probs['01'] - 1.0) < 1e-8, \
    "basis_encode([1,0]) doit produire |01⟩"

# [1, 1, 0] → |011⟩
sv = Statevector.from_instruction(basis_encode([1, 1, 0]))
probs = sv.probabilities_dict()
assert '011' in probs and abs(probs['011'] - 1.0) < 1e-8, \
    "basis_encode([1,1,0]) doit produire |011⟩"

# --- Tests vqc_predict ---
# data=[0,0], θ=[0,0] → aucune rotation, P(q₀=1) = 0
p = vqc_predict([0, 0], [0.0, 0.0])
assert abs(p) < 1e-8, f"vqc_predict([0,0], [0,0]) = {p}, attendu 0"

# data=[0,0], θ=[π,0] → Ry(π)|0⟩ = |1⟩, CNOT propage, P(q₀=1) = 1
p = vqc_predict([0, 0], [np.pi, 0.0])
assert abs(p - 1.0) < 1e-8, f"vqc_predict([0,0], [π,0]) = {p}, attendu 1"

# data=[1,1], θ=[π,0] → l'ansatz défait l'encodage, P(q₀=1) = 0
p = vqc_predict([1, 1], [np.pi, 0.0])
assert abs(p) < 1e-8, f"vqc_predict([1,1], [π,0]) = {p}, attendu 0"

# data=[0,0], θ=[π/2,0] → superposition, P(q₀=1) = 0.5
p = vqc_predict([0, 0], [np.pi / 2, 0.0])
assert abs(p - 0.5) < 1e-8, f"vqc_predict([0,0], [π/2,0]) = {p}, attendu 0.5"

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

def basis_encode(data: list[int]) -> QuantumCircuit:
    n = len(data)
    qc = QuantumCircuit(n)
    for i, bit in enumerate(data):
        if bit == 1:
            qc.x(i)
    return qc

def vqc_predict(data: list[int], theta_values: list[float]) -> float:
    n = len(data)
    theta = ParameterVector('θ', n)
    qc = basis_encode(data)
    for i in range(n):
        qc.ry(theta[i], i)
    for i in range(n - 1):
        qc.cx(i, i + 1)
    bound = qc.assign_parameters({theta[i]: theta_values[i] for i in range(n)})
    sv = Statevector.from_instruction(bound)
    probs = sv.probabilities()
    return float(sum(probs[j] for j in range(len(probs)) if j % 2 == 1))

Pourquoi : le kata combine les deux thèmes du chapitre. basis_encode illustre l’encodage le plus simple : chaque bit classique contrôle directement un qubit via X. vqc_predict assemble les trois blocs du VQC — feature map (basis), ansatz (Ry + CNOT), mesure (P(q₀ = 1)) — en un pipeline fonctionnel. Le test avec data=[1,1], θ=[π,0] démontre que l’ansatz peut « défaire » l’encodage : Ry(π)|1⟩ = -|0⟩, un piège important lors de l’entraînement réel.