Let's put the theory of Variational Quantum Algorithms into practice. In this section, we will implement a Variational Quantum Classifier (VQC) using Python and the Pennylane library. This hands-on exercise synthesizes the concepts discussed earlier in this chapter: designing a Parameterized Quantum Circuit (PQC), defining a suitable cost function based on measurement outcomes, and utilizing gradient-based optimization techniques to train the circuit's parameters for a classification task.Our goal is to train a quantum circuit to classify points belonging to two distinct classes in a 2D feature space. We will leverage the variational principle by iteratively adjusting the PQC parameters to minimize a cost function that quantifies the classification error.1. Setting Up the Environment and DataFirst, ensure you have the necessary libraries installed. You'll primarily need Pennylane for the quantum computations and NumPy for numerical operations. Scikit-learn is useful for generating sample data and potentially for data preprocessing.# Required imports import pennylane as qml from pennylane import numpy as np # Use PennyLane's wrapped NumPy import matplotlib.pyplot as plt from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # Generate a synthetic dataset X, y = make_moons(n_samples=100, noise=0.15, random_state=42) # Scale features for potentially better performance scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Convert labels from {0, 1} to {-1, 1} for convenience with some cost functions y_signed = 2 * y - 1 # Split into training and testing sets X_train, X_test, y_train, y_test = train_test_split( X_scaled, y_signed, test_size=0.3, random_state=42 ) print(f"Number of training samples: {len(X_train)}") print(f"Number of testing samples: {len(X_test)}") print(f"Data shape: {X_train.shape}") # Should be (n_samples, 2) for 2 featuresWe use make_moons to generate a dataset that is not linearly separable, providing a moderately challenging task suitable for demonstrating the potential of VQCs. The data is scaled, and labels are converted to $y \in {-1, 1}$, which simplifies aligning measurement outcomes (often in the range [-1, 1]) with target labels.{ "data": [ { "x": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95, 1.05], "y": [0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0, 0.85, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25, 0.15, 0.05, -0.05], "mode": "markers", "type": "scatter", "name": "Class -1", "marker": {"color": "#4263eb", "size": 8} }, { "x": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95], "y": [-0.05, 0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8], "mode": "markers", "type": "scatter", "name": "Class +1", "marker": {"color": "#f76707", "size": 8} } ], "layout": { "title": {"text": "Scaled Synthetic Moons Dataset"}, "xaxis": {"title": {"text": "Feature 1 (Scaled)"}}, "yaxis": {"title": {"text": "Feature 2 (Scaled)"}}, "width": 600, "height": 400, "plot_bgcolor": "#e9ecef" } }The synthetic 'moons' dataset after scaling features. The two classes are clearly non-linearly separable.2. Quantum Data EncodingWe need to encode our classical data points $x = (x_1, x_2)$ into quantum states. As discussed in Chapter 2, several strategies exist. Here, we'll use Angle Encoding, mapping each feature to the rotation angle of a specific gate. Since we have 2 features, we'll use 2 qubits.num_qubits = 2 def data_encoding_circuit(features): """Encodes 2 features into the state of 2 qubits.""" qml.AngleEmbedding(features, wires=range(num_qubits), rotation='Y') # You could add entanglement here if desired, e.g., qml.CNOT(wires=[0, 1]) # but let's keep the encoding simple for now.This function uses qml.AngleEmbedding to apply $R_Y(\phi)$ rotations on each qubit, where $\phi$ is the corresponding scaled feature value. This is a common and relatively simple encoding method.3. Parameterized Quantum Circuit (Ansatz) DesignNow, we design the core PQC or ansatz, whose parameters $\theta$ we will optimize. We'll use a structure with layers of single-qubit rotations and entangling CNOT gates. This follows the PQC design strategies discussed earlier (Section 4.2).num_layers = 3 # Number of layers in the ansatz def ansatz(params): """The parameterized quantum circuit (ansatz).""" for l in range(num_layers): # Layer of single-qubit rotations (parameterized) for i in range(num_qubits): qml.RX(params[l, i, 0], wires=i) qml.RZ(params[l, i, 1], wires=i) # Layer of entangling gates for i in range(num_qubits - 1): qml.CNOT(wires=[i, i+1]) # Add a CNOT between last and first qubit for more entanglement if desired if num_qubits > 1: qml.CNOT(wires=[num_qubits-1, 0])This ansatz consists of num_layers blocks. Each block applies parameterized $RX$ and $RZ$ rotations to all qubits, followed by a chain of CNOT gates to introduce entanglement. The shape of the params array will depend on num_layers, num_qubits, and the number of parameters per gate in each layer (here, 2 per qubit per layer).4. Defining the Quantum Node and Cost FunctionWe combine the data encoding and the ansatz into a full quantum circuit. The measurement outcome will be the expectation value of the Pauli-Z operator on the first qubit, $\langle \sigma_z^{(0)} \rangle$. This value lies between -1 and 1, naturally aligning with our labels $y \in {-1, 1}$.We define a quantum device (here, a simulator) and create a QNode, which encapsulates the quantum circuit logic.# Choose a quantum device (simulator) dev = qml.device("default.qubit", wires=num_qubits) @qml.qnode(dev) def quantum_classifier_circuit(params, features): """Full quantum circuit: encoding + ansatz + measurement.""" data_encoding_circuit(features) ansatz(params) # Measure the expectation value of Pauli Z on the first qubit return qml.expval(qml.PauliZ(0))Next, we define the cost function (Section 4.3). A common choice for binary classification with labels ${-1, 1}$ and predictions $p \in [-1, 1]$ is the squared loss: $L(\theta) = \frac{1}{N} \sum_{i=1}^{N} (y_i - p_i(\theta))^2$, where $p_i(\theta)$ is the output of the quantum circuit for input $x_i$ with parameters $\theta$.def square_loss(labels, predictions): """Calculates the mean squared loss.""" return np.mean((labels - predictions)**2) def cost_function(params, features_batch, labels_batch): """Cost function to be minimized.""" predictions = [quantum_classifier_circuit(params, f) for f in features_batch] return square_loss(labels_batch, predictions)This cost function takes a batch of features and labels, computes the predictions using the quantum_classifier_circuit for each feature vector with the current params, and then calculates the mean squared error.5. Optimization ProcessWe need an optimizer to update the circuit parameters $\theta$ based on the gradients of the cost function. Pennylane integrates with optimizers like Adam (Section 4.5) and automatically calculates gradients using methods like the parameter-shift rule (Section 4.4) by default for compatible gates.# Initialize parameters randomly param_shape = (num_layers, num_qubits, 2) # As defined by our ansatz structure initial_params = np.random.uniform(low=0, high=2 * np.pi, size=param_shape) # Select an optimizer optimizer = qml.AdamOptimizer(stepsize=0.05) # Training parameters epochs = 30 batch_size = 10 # Training loop params = initial_params cost_history = [] print("Starting training...") for epoch in range(epochs): # Create batches permutation = np.random.permutation(len(X_train)) X_train_perm = X_train[permutation] y_train_perm = y_train[permutation] batch_costs = [] for i in range(0, len(X_train), batch_size): X_batch = X_train_perm[i : i + batch_size] y_batch = y_train_perm[i : i + batch_size] # Gradient descent step params, current_cost = optimizer.step_and_cost( lambda p: cost_function(p, X_batch, y_batch), params ) batch_costs.append(current_cost) epoch_cost = np.mean(batch_costs) cost_history.append(epoch_cost) # Optional: Calculate accuracy on training set during training predictions_train = [np.sign(quantum_classifier_circuit(params, f)) for f in X_train] accuracy_train = np.mean(predictions_train == y_train) print(f"Epoch {epoch+1}/{epochs} - Cost: {epoch_cost:.4f} - Train Accuracy: {accuracy_train:.4f}") print("Training finished.")In this loop, we iterate through epochs, shuffle the data, process it in batches, and use the optimizer's step_and_cost method. This method implicitly calculates the cost and its gradient with respect to params (using the parameter-shift rule behind the scenes) and updates params according to the Adam optimization algorithm. We track the cost per epoch.6. EvaluationAfter training, we evaluate the performance of our VQC on the unseen test set. We use the final trained parameters (params) to predict labels for the test data and calculate the accuracy.# Predict on the test set predictions_test = [np.sign(quantum_classifier_circuit(params, f)) for f in X_test] # Calculate test accuracy accuracy_test = np.mean(predictions_test == y_test) print(f"\nTest Set Accuracy: {accuracy_test:.4f}") # Optional: Plot the cost history plt.figure(figsize=(8, 4)) plt.plot(range(1, epochs + 1), cost_history) plt.xlabel("Epoch") plt.ylabel("Cost (Mean Squared Loss)") plt.title("VQC Training Cost History") plt.grid(True, linestyle='--', alpha=0.6) plt.show(){ "data": [ { "y": [0.8, 0.6, 0.45, 0.3, 0.2, 0.15, 0.1, 0.08, 0.06, 0.05, 0.04, 0.035, 0.03, 0.028, 0.025, 0.023, 0.021, 0.02, 0.019, 0.018], "type": "scatter", "mode": "lines+markers", "name": "Cost", "marker": {"color": "#1c7ed6"} } ], "layout": { "title": {"text": "VQC Training Cost History"}, "xaxis": {"title": {"text": "Epoch"}}, "yaxis": {"title": {"text": "Cost (Mean Squared Loss)"}}, "width": 600, "height": 400, "plot_bgcolor": "#e9ecef" } }Example training cost curve, showing the decrease in mean squared loss over epochs.We can also visualize the decision boundary learned by the classifier. This involves creating a grid of points covering the feature space, predicting the class for each point using the trained VQC, and plotting the result.# Create a mesh grid for plotting decision boundary x_min, x_max = X_scaled[:, 0].min() - 0.5, X_scaled[:, 0].max() + 0.5 y_min, y_max = X_scaled[:, 1].min() - 0.5, X_scaled[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.05), np.arange(y_min, y_max, 0.05)) # Predict class for each point in the mesh grid grid_points = np.c_[xx.ravel(), yy.ravel()] Z = np.array([quantum_classifier_circuit(params, p) for p in grid_points]) Z = Z.reshape(xx.shape) # Prediction values (-1 to 1) # Plot decision boundary and data points plt.figure(figsize=(8, 6)) contour = plt.contourf(xx, yy, Z, levels=np.linspace(-1, 1, 3), cmap='coolwarm', alpha=0.8) plt.colorbar(contour, ticks=[-1, 0, 1]) # Plot training data plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap='coolwarm', edgecolors='k', marker='o', s=50, label='Train Data') # Plot test data plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap='coolwarm', edgecolors='k', marker='^', s=60, label='Test Data') plt.xlabel("Feature 1 (Scaled)") plt.ylabel("Feature 2 (Scaled)") plt.title("VQC Decision Boundary and Data") plt.legend() plt.grid(True, linestyle='--', alpha=0.3) plt.show()The resulting plot shows how the VQC partitions the feature space based on the learned parameters.7. DiscussionThis practical exercise demonstrates the end-to-end process of building and training a Variational Quantum Classifier. We successfully implemented data encoding, designed a PQC ansatz, defined a cost function based on quantum measurements, and used gradient descent with the parameter-shift rule for optimization.The accuracy achieved depends heavily on the dataset complexity, the expressivity of the chosen ansatz (number of layers, qubit connectivity), the encoding strategy, the optimizer settings, and the number of training epochs.Potential Next Steps:Experiment with Different Ansätze: Try deeper circuits, alternative gate sequences, or structures known for higher expressivity (like qml.StronglyEntanglingLayers). Be mindful of the potential for barren plateaus (Section 4.7) with deeper circuits.Explore Other Optimizers: Test optimizers like qml.QNGOptimizer (Quantum Natural Gradient, Section 4.6), although its computation can be more intensive. Compare convergence speed and final accuracy.Modify Data Encoding: Use different encoding methods (e.g., amplitude encoding, higher-order feature maps from Chapter 2) and observe the impact on performance.Noise Simulation: Run the simulation on a noisy backend (using Pennylane's noise simulation capabilities) to understand the impact of hardware noise, preparing for concepts in Chapter 7.Apply Error Mitigation: Implement basic error mitigation techniques (like Zero-Noise Extrapolation, discussed in Chapter 7) on noisy simulation results.This implementation provides a concrete foundation for understanding VQAs. By modifying components like the ansatz, optimizer, or cost function, you can explore the design space of these powerful hybrid algorithms.