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 quantiqueU(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ûtL(θ)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
| Bloc | Rôle | Dépend de | Entraînable | Exécuté sur |
|---|---|---|---|---|
Feature map U(x) | Encoder x dans l’espace de Hilbert | Données x | Non | QPU |
Ansatz W(θ) | Transformer l’état encodé | Paramètres θ | Oui | QPU |
| Mesure | Extraire une prédiction classique | — | Non | QPU → CPU |
| Optimiseur | Mettre à 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 chaquebᵢ = 1, une porte X est appliquée au qubiti(carX|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ère | Basis | Angle (ch. 5) | Amplitude (ch. 5) |
|---|---|---|---|
| Type de données | Binaires (0/1) | Continues | Continues |
| Qubits requis | n (= nb de features) | n (= nb de features) | log₂(N) |
| Portes utilisées | X uniquement | Ry, Rz | Initialisation d’état |
| Orthogonalité garantie | Oui | Non en général | Non en général |
| Expressivité de l’encodage | Minimale | Modérée | Maximale |
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.
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.