Theory provides the foundation, but implementing quantum feature maps helps solidify understanding and prepares you for building actual Quantum Machine Learning models. In the previous sections, we explored the mathematical structure and properties of various encoding strategies like higher-order polynomial maps and data re-uploading. Now, we'll translate these concepts into executable code using a quantum computing library. We'll use Pennylane for its seamless integration with classical machine learning tools, but the principles apply equally to other frameworks like Qiskit.Our objective is to create reusable functions or templates that take classical data vectors as input and generate the corresponding quantum states $|\phi(x)\rangle$. This hands-on experience will allow you to experiment with different circuit structures and understand their practical implementation details.Setting Up the EnvironmentFirst, ensure you have Pennylane and NumPy installed. We'll start with the necessary imports:import pennylane as qml from pennylane import numpy as np import matplotlib.pyplot as plt # Set a default device for our examples # Use 'default.qubit' for simulation. You might choose others later. num_qubits = 2 # Default number of qubits for examples dev = qml.device('default.qubit', wires=num_qubits) print(f"Using PennyLane version: {qml.__version__}") print(f"Default device set to: {dev.name} with {num_qubits} qubits.")We define a default simulator device default.qubit. The number of qubits (wires in Pennylane) required often depends on the dimension of the input data and the chosen encoding strategy.Basic Angle Encoding Feature MapThe simplest encoding strategy involves mapping classical data features directly to the rotation angles of single-qubit gates. Let's implement a feature map where each feature $x_i$ controls an $RY$ rotation on a corresponding qubit.def simple_angle_encoding_map(x): """Encodes N data features into N qubits using RY rotations.""" if len(x) > dev.num_wires: raise ValueError(f"Input vector dimension ({len(x)}) exceeds number of qubits ({dev.num_wires}).") # Apply RY rotation controlled by input features for i in range(len(x)): qml.RY(x[i], wires=i) # Example Usage: Define a QNode that uses this feature map @qml.qnode(dev) def simple_angle_circuit(x): simple_angle_encoding_map(x) # Typically followed by measurements or further processing # For demonstration, let's return the state vector return qml.state() # Try encoding a sample data point sample_data_point = np.array([0.5, -1.2]) # Example for 2 qubits encoded_state = simple_angle_circuit(sample_data_point) print("Example usage of Simple Angle Encoding:") print(f"Input data: {sample_data_point}") # print(f"Encoded state vector (showing first 4 components): \n{encoded_state[:4]}") # State can be large qml.draw_mpl(simple_angle_circuit, style='pennylane')(sample_data_point) plt.show()This function simple_angle_encoding_map takes a classical vector x and applies rotations. The @qml.qnode decorator turns a Python function describing a quantum circuit into an executable quantum node linked to our device dev. This basic encoding is often insufficient for complex tasks as it doesn't introduce entanglement or higher-order correlations between features.Implementing a ZZ Feature MapTo capture relationships between input features, we need entangling gates. The ZZ Feature Map, often discussed in the context of second-order polynomial kernels, uses controlled-phase ($CZ$) or equivalent $CNOT$-based gates after data encoding rotations. It can approximate a kernel function with terms like $x_i x_j$.Let's implement a version using $RX$ encoding followed by $CZ$ entanglers.def zz_feature_map(x, num_layers=1): """Implements a ZZ-type feature map. Args: x (np.ndarray): Input data vector. Dimension must match num_qubits. num_layers (int): Number of times to repeat the encoding-entanglement block. """ num_qubits_map = len(x) if num_qubits_map != dev.num_wires: raise ValueError(f"Input vector dimension ({num_qubits_map}) must match number of qubits ({dev.num_wires}).") for _ in range(num_layers): # Data encoding layer for i in range(num_qubits_map): qml.RX(x[i], wires=i) # Using RX encoding here # Entangling layer (example: linear entanglement) for i in range(num_qubits_map - 1): qml.CZ(wires=[i, i+1]) # Optional: Add entanglement between the last and first qubit for cyclic boundary # if num_qubits_map > 1: # qml.CZ(wires=[num_qubits_map - 1, 0]) @qml.qnode(dev) def zz_map_circuit(x, num_layers=1): # Often start with Hadamard gates for superposition for i in range(dev.num_wires): qml.Hadamard(wires=i) zz_feature_map(x, num_layers=num_layers) # Return state for analysis or add measurements for kernel estimation return qml.state() # Example Usage sample_data_point_zz = np.array([np.pi/4, np.pi/2]) # Example for 2 qubits num_encoding_layers = 2 encoded_state_zz = zz_map_circuit(sample_data_point_zz, num_layers=num_encoding_layers) print("\nExample usage of ZZ Feature Map:") print(f"Input data: {sample_data_point_zz}") print(f"Number of layers: {num_encoding_layers}") # print(f"Encoded state vector (showing first 4 components): \n{encoded_state_zz[:4]}") qml.draw_mpl(zz_map_circuit, style='pennylane')(sample_data_point_zz, num_layers=num_encoding_layers) plt.show()In this example, zz_feature_map first applies single-qubit rotations encoding the data features $x_i$ using $RX$ gates. Then, it applies $CZ$ gates between adjacent qubits to introduce entanglement. The initial Hadamard layer creates a superposition basis, which is common in many feature map constructions. Repeating the encoding and entangling layers (num_layers > 1) can increase the complexity and expressivity of the map.Implementing a Data Re-uploading Feature MapAs discussed earlier, data re-uploading involves repeatedly encoding the data within a parameterized quantum circuit. This structure can significantly increase the model's ability to approximate complex functions. The circuit typically alternates between data encoding layers and layers of parameterized (or fixed) variational gates.Let's implement a simple data re-uploading circuit where data is encoded using $RZ$ rotations, followed by fixed $CX$ entanglement and parameterized $RY$ rotations.def data_reuploading_map(x, weights, num_layers=1): """Implements a data re-uploading feature map. Args: x (np.ndarray): Input data vector. Should have dimension <= num_qubits. weights (np.ndarray): Variational parameters for the circuit. Shape should match expectations based on layers and qubits. num_layers (int): Number of re-uploading layers. """ num_qubits_map = dev.num_wires # Assuming weights is a flat array, reshape based on layers and qubits # Example: Each layer needs num_qubits RY parameters expected_weights_per_layer = num_qubits_map if len(weights) != num_layers * expected_weights_per_layer: raise ValueError("Incorrect number of weights provided.") weights_reshaped = weights.reshape((num_layers, num_qubits_map)) feature_dim = len(x) for layer in range(num_layers): # Data encoding layer (example: RZ rotations, cycle through features if dim < qubits) for i in range(num_qubits_map): qml.RZ(x[i % feature_dim], wires=i) # Use modulo for feature reuse # Variational layer (example: RY rotations) for i in range(num_qubits_map): qml.RY(weights_reshaped[layer, i], wires=i) # Entangling layer (example: fixed CNOT ladder) for i in range(num_qubits_map - 1): qml.CNOT(wires=[i, i+1]) # Optional: Add entanglement between the last and first qubit # if num_qubits_map > 1: # qml.CNOT(wires=[num_qubits_map - 1, 0]) @qml.qnode(dev) def data_reuploading_circuit(x, weights, num_layers=1): # No initial Hadamards needed if encoding starts from |0> state data_reuploading_map(x, weights, num_layers=num_layers) # Return state or expectation values return qml.expval(qml.PauliZ(0)) # Example measurement # Example Usage num_reupload_layers = 3 num_qubits_reupload = 2 # Let's stick to 2 qubits dev.num_wires = num_qubits_reupload # Update device setting if needed # Weights for the variational layers (3 layers * 2 qubits = 6 weights) sample_weights = np.random.uniform(0, 2 * np.pi, num_reupload_layers * num_qubits_reupload) sample_data_point_reupload = np.array([0.2, 0.8]) # Example 2D data # Execute the circuit output_expval = data_reuploading_circuit(sample_data_point_reupload, sample_weights, num_layers=num_reupload_layers) print("\nExample usage of Data Re-uploading Feature Map:") print(f"Input data: {sample_data_point_reupload}") print(f"Sample weights (first 4): {sample_weights[:4]}...") print(f"Number of layers: {num_reupload_layers}") print(f"Output expectation value <Z_0>: {output_expval:.4f}") qml.draw_mpl(data_reuploading_circuit, style='pennylane')(sample_data_point_reupload, sample_weights, num_layers=num_reupload_layers) plt.show() # Restore default qubit count if changed dev.num_wires = num_qubits In this implementation, data_reuploading_map takes both the data vector x and variational weights. Inside the loop defining the layers, we first apply data-dependent $RZ$ rotations. Notice the use of x[i % feature_dim] which allows encoding data vectors smaller than the number of qubits by reusing features. This is followed by a layer of trainable $RY$ rotations (controlled by weights) and a fixed entangling structure ($CNOT$ gates). The parameters weights are typically optimized during the training of a larger QML model (like a VQC).Customizing and ExperimentingThe examples above provide starting points. The real power lies in designing feature maps tailored to specific problems or exploring variations. Here are some ideas for customization:Encoding Gates: Replace $RX$, $RY$, $RZ$ with other single-qubit rotations or combinations. You could encode data into phase gates ($qml.PhaseShift$) or use multi-parameter gates like $qml.U3$.Entanglement Strategy: Change the pattern of entangling gates. Instead of linear $CZ$ or $CNOT$ chains, try all-to-all entanglement, cyclic patterns, or hardware-specific efficient structures. Use different two-qubit gates like $qml.IsingXX$, $qml.IsingYY$, or $qml.IsingZZ$.Layer Structure: Vary the number of layers in the ZZ map or data re-uploading map. Experiment with different orderings of encoding, variational, and entangling blocks.Hybrid Approaches: Combine elements from different strategies. For instance, use angle encoding on some qubits and a ZZ-style interaction on others.Input Preprocessing: Apply classical preprocessing to x before encoding. This might involve scaling, normalization, or dimensionality reduction techniques like PCA, which could influence the effectiveness of the quantum feature map.Here is a basic template structure you can adapt:def my_custom_feature_map(x, params=None): """Template for a custom quantum feature map.""" num_qubits_map = dev.num_wires # Check input dimension consistency if len(x) > num_qubits_map: print(f"Warning: Input dimension {len(x)} > qubits {num_qubits_map}. Using first {num_qubits_map} features.") x_eff = x[:num_qubits_map] else: x_eff = x # Optional: Pad x with zeros or reuse features if len(x) < num_qubits_map # --- Start Custom Circuit Definition --- # Example: Layer 1 - Initial rotation based on data for i in range(len(x_eff)): qml.RX(x_eff[i] * np.pi, wires=i) # Scale input for rotation range # Example: Layer 2 - Fixed Entanglement if num_qubits_map > 1: qml.broadcast(qml.CNOT, wires=range(num_qubits_map), pattern="ring") # Example: Layer 3 - Data-dependent controlled rotations (requires params if trainable) # if params is not None: ... for i in range(num_qubits_map - 1): # Example: Controlled rotation based on product of features angle = x_eff[i] * x_eff[i+1] qml.CRZ(angle, wires=[i, i+1]) # --- End Custom Circuit Definition --- @qml.qnode(dev) def custom_circuit(x, params=None): my_custom_feature_map(x, params) # Return state or measure observables return qml.probs(wires=range(dev.num_wires)) # Example usage would follow...Analyzing Feature Map Outputs: Quantum KernelsOne way to analyze the effect of a feature map is to compute the quantum kernel matrix. The kernel element between two data points $x_i$ and $x_j$ is given by the squared overlap of their corresponding quantum states: $K(x_i, x_j) = |\langle \phi(x_j) | \phi(x_i) \rangle|^2$. This value reflects the similarity between the data points in the quantum feature space.We can compute this using the implemented feature maps. Pennylane provides convenient ways to calculate state overlaps or fidelity.# Use the ZZ Map circuit as an example @qml.qnode(dev) def zz_map_state_vector(x, num_layers=1): # Prepare initial state |0...0> # Apply Hadamard layer for i in range(dev.num_wires): qml.Hadamard(wires=i) # Apply ZZ Feature Map zz_feature_map(x, num_layers=num_layers) return qml.state() # Define two sample data points (must match num_qubits) data_point_1 = np.array([0.1, 0.2]) data_point_2 = np.array([0.3, 0.4]) num_layers_kernel = 1 # Generate the state vectors state_1 = zz_map_state_vector(data_point_1, num_layers=num_layers_kernel) state_2 = zz_map_state_vector(data_point_2, num_layers=num_layers_kernel) # Compute the overlap |<state_2 | state_1>|^2 # Note: state vectors can be complex, use conjugate transpose overlap = np.abs(np.vdot(state_2, state_1))**2 print(f"\nQuantum Kernel Element Example (ZZ Map, {num_layers_kernel} layer):") print(f"Data point 1: {data_point_1}") print(f"Data point 2: {data_point_2}") print(f"Kernel value K(x1, x2) = |<phi(x2)|phi(x1)>|^2 = {overlap:.6f}") # Using Pennylane's qml.kernels module (more efficient for full matrix) kernel_func = qml.kernels.EmbeddingKernel(zz_feature_map, dev) # Pass the map *function* kernel_value = kernel_func(data_point_1, data_point_2, num_layers=num_layers_kernel) # Note: EmbeddingKernel uses fidelity, which is sqrt of what we calculated above if states are pure. # Check documentation for precise definition used by qml.kernels # For pure states |psi>, |phi>, Fidelity F = |<psi|phi>|. Kernel often uses F^2. print(f"Kernel value using qml.kernels (may differ definitionally): {kernel_value:.6f}") Computing and analyzing the kernel matrix for a dataset can reveal how the feature map transforms the data geometry, potentially making linearly inseparable data separable in the higher-dimensional Hilbert space. This is fundamental to Quantum Kernel Methods like QSVM, which we will explore in the next chapter.Integration and Next StepsThe feature map implementations created here are building blocks. They can be directly integrated into:Quantum Kernel Estimation: Use the state overlap calculation (or specialized kernel functions) to compute kernel matrices for algorithms like QSVM.Variational Quantum Classifiers (VQCs): Combine a feature map with a subsequent parameterized variational circuit. The feature map prepares the input state, and the variational circuit learns to classify it. Data re-uploading circuits often blend these two aspects.This practical session aimed to equip you with the ability to implement various quantum feature maps. Experimenting with different structures, encoding gates, and entanglement patterns is significant for developing effective QML models. Remember that the choice of feature map can dramatically impact performance, and understanding how to build and modify them is a valuable skill in QML development.