Hybrid Quantum-Classical Health Predictor

Build a quantum ML classifier that predicts cancer diagnosis from medical data.

Introduction

30 Second Summary

Every year, doctors analyze medical scans and lab results to catch cancer early. The patterns hiding in those measurements can be subtle, and machine learning helps surface them faster than any human could alone.

In this project, you will build a hybrid quantum-classical machine learning model that classifies breast tumors as malignant or benign using real medical data and a trainable quantum circuit.

What You'll Build

A variational quantum circuit that encodes real-world medical features into quantum states, trains gate parameters with PyTorch, and outputs cancer diagnosis predictions.

By the end of this project, you'll have:

  • A working hybrid quantum-classical classifier trained on the Wisconsin Breast Cancer dataset to predict malignant or benign diagnoses.
  • A 4-qubit quantum circuit with angle embedding and entangling layers that processes real medical features.
  • A side-by-side accuracy comparison between your quantum model and a classical logistic regression baseline.
  • Secret Mission: Swap in StronglyEntanglingLayers and tune hyperparameters to push accuracy above your classical baseline.

Are there any prerequisites?

You should have basic Python knowledge (variables, functions, loops) and some familiarity with basic linear algebra concepts like vectors and matrices. No prior quantum computing experience is needed.

Before We Start

Before We Start...

Set Up Your Environment

Quantum machine learning combines two powerful technologies: quantum computing's ability to explore complex data patterns and classical ML's optimization algorithms. To wire these together, you need a few specialized libraries.

In this step, you will install PennyLane (your quantum computing framework), PyTorch (for gradient-based optimization), scikit-learn (for data loading and preprocessing), and matplotlib (for visualizations). You will also create the project file and confirm everything runs correctly.

In this step, get ready to:
  • Create a virtual environment and install all dependencies.
  • Create the project file with initial imports.
  • Verify PennyLane's quantum simulator runs correctly.
Create a virtual environment and install dependencies

A virtual environment isolates your project's packages from other Python projects on your machine. This prevents version conflicts between libraries.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a project folder and move into it by running these commands:
mkdir quantum-health-predictor
cd quantum-health-predictor
  • Create the virtual environment by running:
python -m venv qml-env

What is a virtual environment?

A virtual environment is a self-contained folder that holds its own Python interpreter and installed packages. When activated, any pip install commands install packages inside this folder only, keeping your system Python clean.

  • Activate the virtual environment:

macOS

  • Activate the environment by running:
source qml-env/bin/activate

You should see (qml-env) appear at the beginning of your terminal prompt.

Windows

  • Activate the environment by running:
qml-env\Scripts\activate

You should see (qml-env) appear at the beginning of your terminal prompt.

  • Install PennyLane by running:
pip install pennylane
  • Install PyTorch (CPU version) by running:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
  • Install scikit-learn and matplotlib by running:
pip install scikit-learn matplotlib

Why the CPU version of PyTorch?

PyTorch ships with GPU (CUDA) support by default, which adds several gigabytes to the download. Since PennyLane's default.qubit simulator runs on CPU, you only need the lightweight CPU build.

The --index-url flag tells pip to fetch PyTorch wheels from a special repository that only contains CPU builds.

  • Verify PennyLane installed correctly by running:
python -c "import pennylane as qml; print(qml.version())"

You should see a version number printed, such as 0.45.1.

Seeing an error during installation?

If pip install pennylane fails with a NumPy compatibility error, make sure your virtual environment is activated (you should see (qml-env) in your prompt). PennyLane requires NumPy 2.0 or higher, which installs automatically in a fresh virtual environment.

If the PyTorch install is slow, this is normal. The CPU build is around 200-300 MB and may take a few minutes on slower connections.

help me debug my PennyLane or PyTorch installation error

Create the project file

With your packages installed, it is time to create the Python script that will hold your quantum classifier.

  • Create a new file called quantum_classifier.py by running:
touch quantum_classifier.py
  • Open quantum_classifier.py in VS Code by running:
code quantum_classifier.py
  • Add the initial imports by copying this code into quantum_classifier.py:
import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

What are these imports for?

  • pennylane provides the quantum circuit building blocks (gates, devices, measurements).
  • torch handles tensor operations and gradient-based optimization for training.
  • numpy provides numerical array operations for data manipulation.
  • sklearn loads the medical dataset, splits it, and provides preprocessing tools.
  • matplotlib creates the comparison charts at the end of the project.
  • Save quantum_classifier.py.
  • Verify the imports work by running:
python quantum_classifier.py

The script should run without errors and produce no output. If you see an error, double-check that your virtual environment is activated and all packages installed successfully.

Seeing a ModuleNotFoundError?

Make sure (qml-env) appears in your terminal prompt. If it does not, re-run the activation command for your platform.

If you see an error mentioning a specific package, re-install it with pip install <package-name> and try again.

help me fix a ModuleNotFoundError in my quantum classifier project

Verify the quantum simulator works

PennyLane includes a built-in quantum simulator called default.qubit that runs on your CPU. Before building the full classifier, confirm the simulator can execute a simple quantum circuit.

  • Test the quantum simulator by running this command in your terminal:
python -c "
import pennylane as qml
import numpy as np
dev = qml.device('default.qubit', wires=1)
@qml.qnode(dev)
def test_circuit():
    qml.PauliX(0)
    return qml.expval(qml.PauliZ(0))
result = test_circuit()
print(f'Expectation value: {result}')
"

You should see Expectation value: -1.0 printed.

What does this test do?

This mini-circuit creates a 1-qubit device, applies a PauliX gate (which flips the qubit from state 0 to state 1), and measures the expectation value of PauliZ.

A qubit in state 1 has a PauliZ expectation of -1.0, confirming the simulator executed the gate correctly. Your full classifier will use 4 qubits with many more gates, but the principle is the same.

Not seeing the expected output?

If you see a syntax error, make sure you copied the entire command including the opening and closing quotes around the multi-line Python code.

If you see an import error for PennyLane, confirm your virtual environment is still activated by checking for (qml-env) in your prompt.

help me troubleshoot PennyLane's default.qubit simulator

✔️ Awesome, I've got everything!

Great. Make sure you have saved quantum_classifier.py with all the imports.

ⓧ I'd like to double check the full code

Here is the complete quantum_classifier.py file at this point:

import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

Your environment is ready and the quantum simulator is running. Next up, you will load real medical data and prepare it for your quantum circuit.

Load and Prepare the Data

Your environment is ready. Now you need data for your quantum circuit to classify.

A quantum circuit with 4 qubits can only accept 4 input values at a time. The Wisconsin Breast Cancer dataset has 30 features per sample. In this step, you will load the dataset, compress those 30 features down to 4 using PCA, and prepare the data as PyTorch tensors ready for quantum processing.

In this step, get ready to:
  • Load the Wisconsin Breast Cancer dataset and inspect its structure.
  • Reduce 30 features to 4 using PCA and scale them for quantum angle encoding.
  • Split the data into training and test sets and convert to PyTorch tensors.
Load the dataset and explore it

The Wisconsin Breast Cancer dataset contains 569 tumor samples, each described by 30 numeric measurements (radius, texture, perimeter, etc.). Each sample is labeled as malignant (0) or benign (1).

  • In quantum_classifier.py, add the following code below your imports to load and inspect the dataset:
# --- Step 2: Load and Prepare Data ---

# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
print(f"Classes: {list(data.target_names)}")
print(f"Class distribution: {np.bincount(y)}")

What does this code do?

load_breast_cancer() returns a scikit-learn Bunch object containing the feature matrix and labels. You unpack the features into X (569 rows x 30 columns) and the target labels into y (0 for malignant, 1 for benign).

The print statements confirm the dataset's shape and class balance before you start transforming it.

  • Save quantum_classifier.py.
  • Run the script by entering this command in your terminal:
python quantum_classifier.py
  • You should see output showing 569 samples, 30 features, the class names ['malignant', 'benign'], and the class distribution [212 357].

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active. Your terminal prompt should show (qml-env) at the beginning. If it does not, activate it again before running the script.

Help me fix a ModuleNotFoundError when running my quantum classifier script.

Preprocess with PCA

Your quantum circuit has 4 qubits, so it can only accept 4 input values per sample. PCA compresses the 30 original features into 4 components that capture the most important patterns in the data.

After PCA, you also need to scale the values into the range [0, pi]. Quantum angle encoding maps numbers to rotation angles on qubits, and rotations repeat after 2pi, so keeping values in [0, pi] ensures each data point maps to a unique quantum state.

  • Add this code below the dataset loading section in quantum_classifier.py:
# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Reduce to 4 features with PCA
n_qubits = 4
pca = PCA(n_components=n_qubits)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance: {pca.explained_variance_ratio_.sum():.2%}")

# Scale to [0, pi] for angle encoding
X_min = X_pca.min(axis=0)
X_max = X_pca.max(axis=0)
X_encoded = np.pi * (X_pca - X_min) / (X_max - X_min)

What does this code do?

StandardScaler() normalizes each feature to have zero mean and unit variance. This prevents features with large values (like area) from dominating features with small values (like smoothness).

PCA(n_components=4) finds the 4 directions in the data that capture the most variance. The n_qubits = 4 variable ties this directly to your quantum circuit size.

The final scaling step maps every value into [0, pi] using min-max normalization multiplied by pi. This gives each data point a unique rotation angle for quantum encoding.

Why reduce to exactly 4 features?

Each qubit in your circuit encodes exactly one feature as a rotation angle. With 4 qubits, you need exactly 4 input values. PCA is a natural fit because it ranks features by importance and lets you keep the top N.

  • Save quantum_classifier.py and run it again.
  • You should see a new line showing PCA explained variance: followed by a percentage (around 44%). This tells you how much of the original information those 4 components retain.

Seeing a NameError for StandardScaler or PCA?

Check that your imports at the top of the file include from sklearn.preprocessing import StandardScaler and from sklearn.decomposition import PCA. These were added in Step 1.

Help me fix a NameError in my quantum classifier preprocessing code.

Split into train and test sets

Machine learning models need separate data for training and evaluation. You will split the dataset 80/20 and then convert everything to PyTorch tensors, which is the format your quantum circuit expects.

  • Add this code below the PCA section in quantum_classifier.py:
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=42
)

# Convert to PyTorch tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32)

What does this code do?

train_test_split randomly divides the data so 80% goes to training and 20% to testing. The random_state=42 ensures you get the same split every time you run the script.

PennyLane's PyTorch interface requires tensors as input. Converting with dtype=torch.float32 ensures the data type matches what the quantum circuit's gradient computation expects.

  • Save quantum_classifier.py and run it again.
  • The script should complete without errors. You will see the same dataset and PCA output as before. The split and tensor conversion happen silently.

Seeing a TypeError about tensor conversion?

Make sure you are passing X_encoded (the scaled PCA output) to train_test_split, not X_pca or X_scaled. The tensor conversion expects numpy arrays, which is what train_test_split returns.

Help me fix a TypeError when converting my data to PyTorch tensors.

✔️ Awesome, I've got everything!

Great. Double check you have saved quantum_classifier.py and your script runs without errors.

ⓧ I'd like to double check the full code

Here is the complete quantum_classifier.py file at this point:

import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# --- Step 2: Load and Prepare Data ---

# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
print(f"Classes: {list(data.target_names)}")
print(f"Class distribution: {np.bincount(y)}")

# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Reduce to 4 features with PCA
n_qubits = 4
pca = PCA(n_components=n_qubits)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance: {pca.explained_variance_ratio_.sum():.2%}")

# Scale to [0, pi] for angle encoding
X_min = X_pca.min(axis=0)
X_max = X_pca.max(axis=0)
X_encoded = np.pi * (X_pca - X_min) / (X_max - X_min)

# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=42
)

# Convert to PyTorch tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32)

Your data pipeline is complete. You have 455 training samples and 114 test samples, each compressed to 4 features and scaled for quantum encoding. Next up, you will build the quantum circuit that processes these 4 values.

Build the Quantum Circuit

Your data is ready. You have 4 PCA features per sample, scaled to the range [0, pi] and stored as PyTorch tensors.

Now you need a model that can process this data. In this step, you will define a variational quantum circuit. This circuit encodes each data point into quantum states using rotation gates, then applies trainable parameters that get optimized during training to produce a classification prediction.

In this step, get ready to:
  • Define a 4-qubit quantum device and variational circuit.
  • Visualize the circuit's gate structure.
  • Test the circuit with a sample data point.
Create the quantum device and circuit

A QNode in PennyLane is the bridge between a quantum circuit and classical code. It wraps a quantum function so that PyTorch can differentiate through it and optimize gate parameters during training.

  • In quantum_classifier.py, scroll to the bottom of your file (after the PyTorch tensor conversions).
  • Add the quantum circuit definition by copying this code at the end of the file:
# --- Step 3: Build the Quantum Circuit ---

n_layers = 2
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))

# Initialize weights
weight_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_qubits)
weights = torch.randn(weight_shape, requires_grad=True)

What does this code do?

qml.device("default.qubit", wires=n_qubits) creates a simulated quantum computer with 4 qubits. The default.qubit device is PennyLane's built-in statevector simulator that runs on your CPU.

The @qml.qnode decorator turns quantum_circuit into a differentiable quantum function. Setting interface="torch" tells PennyLane to track gradients through PyTorch so you can train the circuit parameters.

Inside the circuit, two things happen:

  • qml.AngleEmbedding encodes your 4 PCA features as rotation angles on 4 qubits. Each feature becomes the angle of an RX gate on one qubit.
  • qml.BasicEntanglerLayers applies 2 layers of trainable RX rotations followed by a ring of CNOT gates that entangle neighboring qubits. These are the parameters that get tuned during training.
  • qml.expval(qml.PauliZ(0)) measures the first qubit, producing a value between -1 and 1 that becomes your classification signal.

The weight shape (2, 4) means 2 layers of 4 rotation angles. These start as random values and get optimized during training.

  • Save quantum_classifier.py.

Seeing an import error?

If you see an error about PennyLane or PyTorch not being found, make sure your qml-env virtual environment is still activated. You should see (qml-env) at the start of your terminal prompt.

help me fix a PennyLane import error in my quantum classifier

Visualize the circuit

Before training, it helps to see exactly what gates your circuit applies. PennyLane can print a text diagram of your circuit structure.

  • Add this visualization code below the weights initialization line:
# Visualize the circuit
print("\nQuantum Circuit:")
sample_input = X_train_t[0]
print(qml.draw(quantum_circuit)(sample_input, weights))

# Test with sample data
sample_output = quantum_circuit(sample_input, weights)
print(f"\nSample circuit output: {sample_output.item():.4f}")

What does this code do?

qml.draw(quantum_circuit) prints a text-based diagram of your circuit, showing the gate sequence across all 4 wires. You will see the angle embedding rotations followed by the entangling layers.

The test line passes one training sample through the circuit and prints the raw output. This verifies the circuit runs correctly and produces a value in the [-1, 1] range (from the PauliZ measurement).

  • Save quantum_classifier.py.
  • Run the script in your terminal:
python quantum_classifier.py
  • You should see the dataset information from Step 2, followed by a text circuit diagram showing gates on 4 wires, and a sample output value between -1 and 1.

Circuit not printing or seeing an error?

Make sure you added the new code after the existing PyTorch tensor lines, not inside them. The # --- Step 3 comment should come after the y_test_t line.

If the output shows a very large or very small number (not between -1 and 1), double check that you are returning qml.expval(qml.PauliZ(0)) in your circuit function.

help me debug my PennyLane quantum circuit that is not printing correctly

Test the circuit with sample data

The line Sample circuit output: in your terminal confirms the circuit is working. The value is a single number between -1 and 1.

Why does the output range matter?

The PauliZ measurement always returns a value between -1 and 1. In the next step, you will rescale this to a probability between 0 and 1 using the formula (output + 1) / 2. Values close to 1 mean "benign" and values close to 0 mean "malignant."

Right now the weights are random, so the output is meaningless. Training will adjust these weights so the circuit produces predictions that match the actual labels.

✔️ Awesome, I've got everything!

Great. Double check you have saved quantum_classifier.py and your circuit is printing correctly.

ⓧ I'd like to double check the full code

Here is the full quantum_classifier.py file up to this point:

import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# --- Step 2: Load and Prepare Data ---

data = load_breast_cancer()
X, y = data.data, data.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
print(f"Classes: {list(data.target_names)}")
print(f"Class distribution: {np.bincount(y)}")

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

n_qubits = 4
pca = PCA(n_components=n_qubits)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance: {pca.explained_variance_ratio_.sum():.2%}")

X_min = X_pca.min(axis=0)
X_max = X_pca.max(axis=0)
X_encoded = np.pi * (X_pca - X_min) / (X_max - X_min)

X_train, X_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=42
)

X_train_t = torch.tensor(X_train, dtype=torch.float32)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32)

# --- Step 3: Build the Quantum Circuit ---

n_layers = 2
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))

# Initialize weights
weight_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_qubits)
weights = torch.randn(weight_shape, requires_grad=True)

# Visualize the circuit
print("\nQuantum Circuit:")
sample_input = X_train_t[0]
print(qml.draw(quantum_circuit)(sample_input, weights))

# Test with sample data
sample_output = quantum_circuit(sample_input, weights)
print(f"\nSample circuit output: {sample_output.item():.4f}")

Your quantum circuit is built and running. Next up, you will train those random weights so the circuit actually learns to classify tumors as malignant or benign.

Train the Quantum Model

Your quantum circuit can process data, but it hasn't learned anything yet. The variational parameters (weights) are still random. Training means adjusting those parameters so the circuit's output matches the correct diagnosis labels.

In this step, you'll wire up a standard PyTorch training loop around your quantum circuit. The optimizer will tweak the gate rotations epoch by epoch until the circuit can reliably distinguish malignant from benign samples.

In this step, get ready to:
  • Define the training loop with an optimizer and loss function.
  • Run training and track loss over 30 epochs.
  • Evaluate the trained model on unseen test data.
Define the training loop

The quantum circuit outputs a value between -1 and 1 (from the PauliZ expectation). To use a standard classification loss function, you need to map that output to a probability between 0 and 1.

  • In quantum_classifier.py, scroll to the bottom of the file (below the sample circuit output line).
  • Add the training loop code by copying the following:
# --- Step 4: Train the Quantum Model ---

# Re-initialize weights fresh for training
weights = torch.randn(weight_shape, requires_grad=True)
# Adam optimizer adjusts weights to minimize loss
optimizer = torch.optim.Adam([weights], lr=0.1)
# Binary cross-entropy loss for 0/1 classification
loss_fn = torch.nn.BCELoss()

# Track loss per epoch for plotting later
losses = []
n_epochs = 30

print("\nTraining quantum model...")
for epoch in range(n_epochs):
    optimizer.zero_grad()

    # Run each training sample through the circuit
    predictions = torch.stack([quantum_circuit(x, weights) for x in X_train_t])
    # Convert from [-1, 1] to [0, 1] probability
    probabilities = (predictions + 1) / 2

    loss = loss_fn(probabilities, y_train_t)
    loss.backward()
    optimizer.step()

    losses.append(loss.item())

    if (epoch + 1) % 5 == 0:
        pred_labels = (probabilities > 0.5).float()
        train_acc = (pred_labels == y_train_t).float().mean()
        print(f"Epoch {epoch+1}/{n_epochs} | Loss: {loss.item():.4f} | Train Acc: {train_acc:.4f}")

What does this code do?

The weights are re-initialized fresh so training starts from a clean slate, separate from the test output you saw earlier.

  • (predictions + 1) / 2 maps the circuit's raw output from [-1, 1] to [0, 1]. This makes it a probability that BCELoss can work with.
  • loss.backward() calculates gradients for each weight. PennyLane uses parameter-shift rules to compute quantum gradients that PyTorch can use.
  • Every 5 epochs, the loop prints the current loss and training accuracy so you can watch the model improve.
  • losses.append(loss.item()) stores each epoch's loss value for the comparison chart you'll build in the next step.
  • Save quantum_classifier.py.

Why re-initialize the weights?

The previous step already used weights to visualize the circuit and test a sample output. Those operations consumed the gradient graph. Re-initializing gives the optimizer a fresh tensor with a clean requires_grad=True state for proper gradient tracking.

Run training and track progress

Training a quantum circuit on CPU is slower than a classical model because each forward pass simulates quantum state evolution. With 455 training samples and 30 epochs, expect this to take approximately 3-5 minutes.

  • Run the script by entering this command in your terminal:
python quantum_classifier.py

You should see the data loading output and circuit diagram from previous steps, followed by training progress that looks like this:

Training quantum model...
Epoch 5/30 | Loss: 0.5832 | Train Acc: 0.7231
Epoch 10/30 | Loss: 0.4156 | Train Acc: 0.8352
Epoch 15/30 | Loss: 0.3204 | Train Acc: 0.8791
...

What should I see?

The loss value should decrease over time, and the training accuracy should increase. Your exact numbers will differ because the weights start from random values.

If the loss bounces around instead of decreasing steadily, that's normal for quantum gradient descent. The optimizer will still converge over 30 epochs.

Training seems stuck or errors out?

  • If you see a RuntimeError about gradients, make sure requires_grad=True is set on the weights line in the Step 4 section (not just the Step 3 one).
  • If training is extremely slow (over 10 minutes), confirm you installed the CPU-only version of PyTorch. The GPU version without a compatible GPU can cause overhead.
  • If you see NaN values in the loss, the learning rate may be too high for your random seed. This is rare. Try running the script again to get different random initial weights.

Still stuck? Help me debug my quantum model training loop

Evaluate on the test set

Training accuracy tells you how well the model fits data it has already seen. The real test is how it performs on data it has never seen before. This is what the test set measures.

  • In quantum_classifier.py, add the following code at the bottom of the file (below the training loop):
# Evaluate on test data (no gradient tracking needed)
with torch.no_grad():
    test_predictions = torch.stack([quantum_circuit(x, weights) for x in X_test_t])
    test_probabilities = (test_predictions + 1) / 2
    test_pred_labels = (test_probabilities > 0.5).float()
    quantum_accuracy = (test_pred_labels == y_test_t).float().mean().item()

print(f"\nQuantum Model Test Accuracy: {quantum_accuracy:.4f}")

What does this code do?

  • torch.no_grad() disables gradient computation. You don't need gradients during evaluation, and turning them off speeds things up.
  • The same conversion (predictions + 1) / 2 maps outputs to probabilities. Values above 0.5 are classified as benign (1), below as malignant (0).
  • quantum_accuracy is the fraction of test samples the model classifies correctly. You'll compare this against a classical model in the next step.
  • Save quantum_classifier.py and run the script again:
python quantum_classifier.py

After training completes, you should see a line reporting the quantum model's test accuracy (typically between 0.80 and 0.95 depending on random initialization).

Test accuracy looks very low?

  • If test accuracy is below 0.60, check that the evaluation code is placed AFTER the training loop, not inside it. The with torch.no_grad(): block should be at the same indentation level as the for epoch loop.
  • Make sure the evaluation uses the same weights variable that the training loop optimized. If you accidentally re-initialize weights between training and evaluation, the model loses everything it learned.

Need help interpreting your accuracy? Is my quantum model accuracy normal?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete quantum_classifier.py up to this point:

import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# --- Step 2: Load and Prepare Data ---

data = load_breast_cancer()
X, y = data.data, data.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
print(f"Classes: {list(data.target_names)}")
print(f"Class distribution: {np.bincount(y)}")

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

n_qubits = 4
pca = PCA(n_components=n_qubits)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance: {pca.explained_variance_ratio_.sum():.2%}")

X_min = X_pca.min(axis=0)
X_max = X_pca.max(axis=0)
X_encoded = np.pi * (X_pca - X_min) / (X_max - X_min)

X_train, X_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=42
)

X_train_t = torch.tensor(X_train, dtype=torch.float32)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32)

# --- Step 3: Build the Quantum Circuit ---

n_layers = 2
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))

weight_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_qubits)
weights = torch.randn(weight_shape, requires_grad=True)

print("\nQuantum Circuit:")
sample_input = X_train_t[0]
print(qml.draw(quantum_circuit)(sample_input, weights))

sample_output = quantum_circuit(sample_input, weights)
print(f"\nSample circuit output: {sample_output.item():.4f}")

# --- Step 4: Train the Quantum Model ---

weights = torch.randn(weight_shape, requires_grad=True)
optimizer = torch.optim.Adam([weights], lr=0.1)
loss_fn = torch.nn.BCELoss()

losses = []
n_epochs = 30

print("\nTraining quantum model...")
for epoch in range(n_epochs):
    optimizer.zero_grad()

    predictions = torch.stack([quantum_circuit(x, weights) for x in X_train_t])
    probabilities = (predictions + 1) / 2

    loss = loss_fn(probabilities, y_train_t)
    loss.backward()
    optimizer.step()

    losses.append(loss.item())

    if (epoch + 1) % 5 == 0:
        pred_labels = (probabilities > 0.5).float()
        train_acc = (pred_labels == y_train_t).float().mean()
        print(f"Epoch {epoch+1}/{n_epochs} | Loss: {loss.item():.4f} | Train Acc: {train_acc:.4f}")

with torch.no_grad():
    test_predictions = torch.stack([quantum_circuit(x, weights) for x in X_test_t])
    test_probabilities = (test_predictions + 1) / 2
    test_pred_labels = (test_probabilities > 0.5).float()
    quantum_accuracy = (test_pred_labels == y_test_t).float().mean().item()

print(f"\nQuantum Model Test Accuracy: {quantum_accuracy:.4f}")

Your quantum circuit is now a trained classifier. Next up, you'll pit it against a classical logistic regression model and visualize the showdown.

Compare Against a Classical Baseline

Your quantum circuit is trained and you have a test accuracy score. But a single number in isolation tells you nothing about how well your quantum approach actually performs.

In this step, you'll train a classical logistic regression model on the exact same PCA-reduced data. Then you'll visualize both results side by side to see how quantum stacks up against classical.

In this step, get ready to:
  • Train a classical logistic regression model on the same data.
  • Create a visualization comparing quantum and classical accuracy.
  • Interpret why the quantum model's performance matters in low-dimensional feature spaces.
Train the classical baseline

A fair comparison requires training a classical model under the same conditions. LogisticRegression from scikit-learn is a strong baseline for binary classification on small, low-dimensional datasets.

  • In quantum_classifier.py, scroll to the bottom of your file (after the quantum test accuracy print statement).
  • Add the classical baseline training and comparison summary by pasting this code:
# --- Step 5: Compare Against Classical Baseline ---

# Train classical model
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, y_train)
classical_predictions = clf.predict(X_test)
classical_accuracy = accuracy_score(y_test, classical_predictions)

print(f"Classical Model Test Accuracy: {classical_accuracy:.4f}")
print(f"\nComparison:")
print(f"  Quantum:   {quantum_accuracy:.4f}")
print(f"  Classical: {classical_accuracy:.4f}")

What does this code do?

LogisticRegression(max_iter=1000, random_state=42) creates a classical linear classifier. max_iter=1000 gives the optimizer enough iterations to converge, and random_state=42 makes results reproducible.

The model trains on the same X_train and y_train numpy arrays your quantum model used. Then it predicts on X_test and accuracy_score calculates the percentage of correct predictions.

  • Save quantum_classifier.py.
  • Run the script in your terminal by running:
python quantum_classifier.py

After training completes (3-5 minutes), you should see the classical test accuracy printed, followed by a comparison showing both quantum and classical scores side by side.

Not seeing the classical accuracy?

Make sure your new code is placed after the print(f"\nQuantum Model Test Accuracy: {quantum_accuracy:.4f}") line. The classical code depends on variables defined earlier in the script.

Check that LogisticRegression and accuracy_score are imported at the top of your file. They should already be there from Step 2.

Help me debug my classical baseline code

Visualize the comparison

Numbers are useful, but a chart makes the comparison immediately clear. You'll create a two-panel figure with a bar chart for accuracy and a line plot showing how your quantum model's loss decreased during training.

  • In quantum_classifier.py, add the visualization code below your comparison print statements:
# Visualize results
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Bar chart comparison
axes[0].bar(["Quantum\n(BasicEntanglerLayers)", "Classical\n(Logistic Regression)"],
            [quantum_accuracy, classical_accuracy],
            color=["#6366f1", "#10b981"])
axes[0].set_ylim(0.5, 1.0)
axes[0].set_ylabel("Test Accuracy")
axes[0].set_title("Quantum vs Classical Accuracy")

# Training loss curve
axes[1].plot(range(1, n_epochs + 1), losses, color="#6366f1", linewidth=2)
axes[1].set_xlabel("Epoch")
axes[1].set_ylabel("Loss")
axes[1].set_title("Quantum Model Training Loss")

plt.tight_layout()
plt.savefig("results.png", dpi=150)
plt.show()
print("\nResults saved to results.png")

What does this code do?

plt.subplots(1, 2) creates a figure with two side-by-side panels. The left panel shows a bar chart comparing test accuracy between your quantum and classical models.

The right panel plots your quantum model's training loss over 30 epochs. A downward curve means the model is learning. plt.savefig("results.png", dpi=150) saves the figure to disk so you can reference it later.

  • Save quantum_classifier.py and run the script again:
python quantum_classifier.py

After training completes, a plot window should appear showing both charts. You should also see results.png saved in your project folder.

  • Confirm results.png was created by running:
ls

You should see results.png alongside quantum_classifier.py in the output.

Plot window not appearing?

Some environments (remote servers, WSL) don't support GUI display. The plot still saves to results.png regardless. Open it from your file explorer to view the chart.

If you see a matplotlib backend error, the image file is still saved. Check your project folder for results.png.

Help me troubleshoot my matplotlib display

Interpret the results

Look at the comparison printed in your terminal. Your quantum model's accuracy should be in a similar range to the classical logistic regression.

Why is the quantum model competitive here?

Variational quantum circuits have high expressivity in low-dimensional feature spaces. With only 4 PCA features mapped onto 4 qubits, the quantum circuit can explore complex decision boundaries that emerge from entanglement and interference.

On this small dataset, that expressivity gives the quantum model a fair shot against classical approaches. On larger datasets with hundreds of features, classical models currently have a significant speed advantage.

What if my quantum accuracy is lower than classical?

That's expected sometimes. The quantum model only has 2 layers of trainable parameters and trains for 30 epochs. The Secret Mission addresses this by upgrading to a more expressive circuit architecture.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that results.png exists in your project folder.

ⓧ I'd like to double check the full code

Here's the complete quantum_classifier.py file. Compare it against yours to spot any differences.

import pennylane as qml
import torch
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# --- Step 2: Load and Prepare Data ---

# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
print(f"Classes: {list(data.target_names)}")
print(f"Class distribution: {np.bincount(y)}")

# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Reduce to 4 features with PCA
n_qubits = 4
pca = PCA(n_components=n_qubits)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA explained variance: {pca.explained_variance_ratio_.sum():.2%}")

# Scale to [0, pi] for angle encoding
X_min = X_pca.min(axis=0)
X_max = X_pca.max(axis=0)
X_encoded = np.pi * (X_pca - X_min) / (X_max - X_min)

# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=42
)

# Convert to PyTorch tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32)

# --- Step 3: Build the Quantum Circuit ---

n_layers = 2
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))

# Initialize weights
weight_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_qubits)
weights = torch.randn(weight_shape, requires_grad=True)

# Visualize the circuit
print("\nQuantum Circuit:")
sample_input = X_train_t[0]
print(qml.draw(quantum_circuit)(sample_input, weights))

# Test with sample data
sample_output = quantum_circuit(sample_input, weights)
print(f"\nSample circuit output: {sample_output.item():.4f}")

# --- Step 4: Train the Quantum Model ---

weights = torch.randn(weight_shape, requires_grad=True)
optimizer = torch.optim.Adam([weights], lr=0.1)
loss_fn = torch.nn.BCELoss()

losses = []
n_epochs = 30

print("\nTraining quantum model...")
for epoch in range(n_epochs):
    optimizer.zero_grad()

    # Forward pass over all training data
    predictions = torch.stack([quantum_circuit(x, weights) for x in X_train_t])
    # Convert from [-1, 1] to [0, 1]
    probabilities = (predictions + 1) / 2

    loss = loss_fn(probabilities, y_train_t)
    loss.backward()
    optimizer.step()

    losses.append(loss.item())

    if (epoch + 1) % 5 == 0:
        pred_labels = (probabilities > 0.5).float()
        train_acc = (pred_labels == y_train_t).float().mean()
        print(f"Epoch {epoch+1}/{n_epochs} | Loss: {loss.item():.4f} | Train Acc: {train_acc:.4f}")

# Evaluate on test set
with torch.no_grad():
    test_predictions = torch.stack([quantum_circuit(x, weights) for x in X_test_t])
    test_probabilities = (test_predictions + 1) / 2
    test_pred_labels = (test_probabilities > 0.5).float()
    quantum_accuracy = (test_pred_labels == y_test_t).float().mean().item()

print(f"\nQuantum Model Test Accuracy: {quantum_accuracy:.4f}")

# --- Step 5: Compare Against Classical Baseline ---

# Train classical model
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, y_train)
classical_predictions = clf.predict(X_test)
classical_accuracy = accuracy_score(y_test, classical_predictions)

print(f"Classical Model Test Accuracy: {classical_accuracy:.4f}")
print(f"\nComparison:")
print(f"  Quantum:   {quantum_accuracy:.4f}")
print(f"  Classical: {classical_accuracy:.4f}")

# Visualize results
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Bar chart comparison
axes[0].bar(["Quantum\n(BasicEntanglerLayers)", "Classical\n(Logistic Regression)"],
            [quantum_accuracy, classical_accuracy],
            color=["#6366f1", "#10b981"])
axes[0].set_ylim(0.5, 1.0)
axes[0].set_ylabel("Test Accuracy")
axes[0].set_title("Quantum vs Classical Accuracy")

# Training loss curve
axes[1].plot(range(1, n_epochs + 1), losses, color="#6366f1", linewidth=2)
axes[1].set_xlabel("Epoch")
axes[1].set_ylabel("Loss")
axes[1].set_title("Quantum Model Training Loss")

plt.tight_layout()
plt.savefig("results.png", dpi=150)
plt.show()
print("\nResults saved to results.png")

Secret mission

Supercharge Your Quantum Circuit

Your BasicEntanglerLayers circuit works, but it uses the simplest entangling template available. What if you gave the circuit more room to learn complex patterns in the data? In this secret mission, you will swap in a more expressive circuit architecture and tune your training hyperparameters to push accuracy higher.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally on your CPU using PennyLane's built-in simulator, so there are no ongoing costs.

Resources you used:

  • Virtual environment qml-env (contains PennyLane, PyTorch, scikit-learn, and matplotlib).
  • Project file quantum_classifier.py.
  • Output file results.png.

Keep everything running

No action needed. Choose this if you're still actively experimenting with quantum circuits or want to tweak hyperparameters further.

  • Your virtual environment stays activated and ready to use.
  • You can rerun quantum_classifier.py at any time to retrain the model.

Pause - I'll come back to this later

Shut down the virtual environment to free up your terminal session. Your files and installed packages remain on disk for when you return.

  • Deactivate your virtual environment by running:
deactivate
  • When you come back, reactivate with qml-env\Scripts\activate (Windows) or source qml-env/bin/activate (macOS/Linux).

Delete - I don't want to use this again

Remove all project files and the virtual environment to free disk space. This deletes all installed packages (PennyLane, PyTorch, scikit-learn, matplotlib) along with your script and output.

  • First, deactivate your virtual environment if it is still active:
deactivate
  • Delete the virtual environment and project files.

macOS

  • Remove the virtual environment folder and project files by running:
rm -rf qml-env
rm quantum_classifier.py results.png

Windows

  • Remove the virtual environment folder and project files by running:
rmdir /s /q qml-env
del quantum_classifier.py results.png
  • Confirm the files are gone by running:
ls
  • You should no longer see qml-env, quantum_classifier.py, or results.png in the output.

Nice Work!

Nice Work!

Nice work! You just built a hybrid quantum-classical machine learning model that classifies real medical data using a variational quantum circuit.

You've learned how to:

  • Built a 4-qubit variational quantum circuit with angle embedding and entangling layers that processes real medical features from the Wisconsin Breast Cancer dataset.
  • Trained quantum gate parameters using PyTorch's gradient descent optimizer, converting quantum measurement outputs into classification predictions.
  • Compared your quantum model's accuracy against a classical logistic regression baseline with a side-by-side visualization saved to results.png.
  • Secret Mission: Upgraded to StronglyEntanglingLayers with tuned hyperparameters for improved classification performance.

Ready to quiz yourself?