Expectation values and Tensors in QNN

I am writing some code for processing images in a QNN, where each image is being fed through a full quantum NN.
In almost every PennyLane QC example, the quantum circuit returns a tuple of expectation values like so:

exp_vals = [qml.expval(qml.PauliZ(position)) for position in range(n_qubits)]    
return tuple(exp_vals)                                                         

which are then processed in a NN like so: q_out_elem = self.pqc(elem, self.q_params).float().unsqueeze(0)

However, after debugging for hours trying to understand the source of the following error in my code:
AttributeError: 'tuple' object has no attribute 'float'
I came to the realisation that either one of these modifications are required:

Option 1:
return qml.expval(Tensor(*[qml.PauliZ(i) for i in range(n_qubits)])) e.g. adding a `Tensor’ type.

Option 2:
Inside the NN instead of q_out_elem = self.pqc(elem, self.q_params).float().unsqueeze(0) use q_out_elem = self.pqc(elem, self.q_params)[0].float().unsqueeze(0)

These are very subtle differences but I am trying to understand why I need them in my code while every other example does not. Also, what is the fastest way to get the float value of the expectation value?


[Python version]: 3.9.12 (main, Dec  2 2022, 15:48:07) 
[Clang 13.1.6 (clang-1316.]
[Deep Learning framework, Pytorch (Facebook) version]: 1.12.1
[Quantum Machine Learning framework (Pennylane) version]: 0.30.0

Hi @Solomon !

You can use the NN module from PennyLane for PyTorch check TorchLayer and for TF use KerasLayer. You can easily create quantum layers out of QNode, some post-processing like stacking multiple expectations values is applied automatically. qml.qnn.TorchLayer — PennyLane 0.30.0 documentation

Could you tell me wher you have found this example using? q_out_elem = self.pqc(elem, self.q_params).float().unsqueeze(0) it might be an outdate example/demo.

Let me know if you have more questions!

I found the culprit I think: Quantum transfer learning — PennyLane documentation

It was updated recently to q_out_elem = torch.hstack(quantum_net(elem, self.q_params)).float().unsqueeze(0), so you can apply this change. But I strongly recommend to use the TorchLayer that I linked in my previous message. We will update this demo soon in order to make the use of the TorchLayer module.

Hello Romain,
Thank you for your response. I was unable to find an example that specifically addresses my requirements of processing RGB images, encoding, and training the parameters of a PQC using a quantum circuit alone. Most existing examples tend to involve a classical feature extractor combined with a quantum FC layer, which is not what I am looking for.

I have written a self-contained example for binary classification using the bees/ants dataset, but facing an issue where the validation and training accuracy do not change. I believe this issue is not related to overfitting, but rather a problem in my code that I am unable to identify. I would appreciate it if you could take a look at my code and provide guidance on this and also how to address the related “float” tuple issue.

Here is the full code, all you have to do is extract the bees/ants dataset into ./datasets and run the code.

# Install necessary packages
# !pip install torch==1.12.1 torchvision==0.13.1 pennylane==0.30.0 efficientnet_pytorch

# Import required libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import pennylane as qml
from pennylane import numpy as np
import os
from tqdm import tqdm
import sys 
from pennylane.operation import Tensor
import psutil
import matplotlib.pyplot as plt

# from qblocks.qgates import *

print("[Python version]:", sys.version)
print("[Deep Learning framework, Pytorch (Facebook) version]:", torch.__version__)
print("[Quantum Machine Learning framework (Pennylane) version]:", qml.__version__)

# Set device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# Set parameters
patch_size = 3
img_size_single = 128
img_size_flat = img_size_single ** 2
RGB_C = 1  # Number of channels in the image (RGB)
batch_size = 32
num_epochs = 60
n_qubits = patch_size ** 2 * RGB_C

n_layers = 6
num_classes = None

# Note if you forget to wrap your circuit with dev: AttributeError: 'tuple' object has no attribute 'float' 
dev_train = qml.device('default.qubit', wires=n_qubits)   

# Define the quantum circuit
# @qml.qnode(dev_train, interface="torch")

def Q_Plot(cirq_0, q_b,q_d):
    # print("Plot Q:", q_b)
    fig, ax = qml.draw_mpl(cirq_0,expansion_strategy='device')(torch.zeros(q_b), torch.zeros(q_d))        
    # print (qml.draw(cirq_0,expansion_strategy='device')(torch.zeros(q_b), torch.zeros(q_d)))
    # plt.figure(figsize=(5,3))
    # from pylab import rcParams
    # rcParams['figure.figsize'] = 3, 6
    # fig.set_size_inches(12,6)

def Q_count_parameters(qnn):
    for name, param in qnn.named_parameters():
        # print (name, param.data)
    return sum(p.numel() for p in qnn.parameters() if p.requires_grad)

import random

def circuit(inputs, weights):
    # print('inputs / weights {}/{}'.format(inputs.shape, weights.shape))
    for qub in range(n_qubits):
        qml.RY(inputs[qub], wires=qub)

    for l in range(n_layers):
        qubit_indices = list(range(n_qubits))
        random.shuffle(qubit_indices)  # Shuffle the qubit indices to create random pairs

        for i in range(0, n_qubits, 2):
            control_qubit = qubit_indices[i]
            target_qubit = qubit_indices[(i + 1) % n_qubits]

            # Apply CRZ gate with conditional RY gate
            random_num = random.uniform(0, 1)
            qml.CRZ(weights[control_qubit], wires=[control_qubit, target_qubit])
            qml.RY(random_num * weights[control_qubit], wires=control_qubit)
            qml.CNOT(wires=[control_qubit, target_qubit])
            qml.CZ(wires=[control_qubit, (control_qubit + 2) % n_qubits])  # Additional CZ gate for entanglement

    return qml.expval(Tensor(*[qml.PauliZ(i) for i in range(n_qubits)]))

# Define the Quanvolutional Neural Network
class QuanvolutionalNeuralNetwork(nn.Module):
    def __init__(self, num_classes):
        self.fc1 = nn.Linear(n_qubits, num_classes)
        self.q_params = nn.Parameter(torch.Tensor(n_qubits, n_qubits))
        self.lr1 = nn.LeakyReLU(0.1)
        self.pqc = qml.QNode(circuit, dev_train, interface = 'torch')

    def extract_patches(self, x):
        patches = []
        bs, c, w, h = x.size()
        for i in range(w - patch_size + 1):
            for j in range(h - patch_size + 1):
                patch = x[:, :, i:i+patch_size, j:j+patch_size]
        patches = torch.stack(patches, dim=1).view(bs, -1, c * patch_size * patch_size)
        return patches

    def forward(self, x):
        assert len(x.shape) == 4  # (bs, c, w, h)
        bs = x.shape[0]  # batch_size = x.size(0)
        c = x.shape[1]  # RGB
        x = x.view(bs, c, img_size_single, img_size_single)
        q_in = self.extract_patches(x)
        q_in = q_in.to(device)
        # print (q_in.shape)
        # q_out = torch.Tensor(0, n_qubits)
        q_out = torch.Tensor(0, n_qubits)

        q_out = q_out.to(device)
        for elem in q_in:
            # print (elem.shape)
            # print (self.q_params.shape)
            q_out_elem = self.pqc(elem, self.q_params).float().unsqueeze(0)
            q_out = torch.cat((q_out, q_out_elem))        
        x = self.lr1(q_out.view(-1, n_qubits))
        x = self.fc1(x)

        return x

# Set the data directory and transformations
data_dir = 'datasets/hymenoptera/'
# import splitfolders as sf
# sf.ratio('datasets/mri/train', 'output', ratio=(0.65, 0.05, 0.3), seed=42)
data_transforms = {
    'train': transforms.Compose([
        # transforms.Resize(256),
        transforms.Grayscale() if RGB_C == 1 else transforms.Lambda(lambda x: x),
    'val': transforms.Compose([
        # transforms.Resize(256),
        transforms.Grayscale() if RGB_C == 1 else transforms.Lambda(lambda x: x),

# Load the image datasets
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes

dataloaders = {x: DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in ['train', 'val']}

# Initialize the Quanvolutional Neural Network
qnn = QuanvolutionalNeuralNetwork(num_classes=num_classes)
qnn = qnn.to(device)
# print ("Total trainable params:",Q_count_parameters(qnn))

# Set the loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(qnn.parameters(), lr=0.001)

# Train the model
for epoch in tqdm(range(num_epochs)):
    # print("Epoch {}/{}, Qubits:{}".format(epoch, num_epochs, n_qubits))
    cpu_percent = psutil.cpu_percent()
    mem_usage= psutil.virtual_memory().total / (1024 ** 3)
    used_ram_gb = psutil.virtual_memory().used / (1024 ** 3)    
    # print(f"Epoch{epoch+1}/{num_epochs}, Dataset:{data_dir,dataset_sizes}, Qubits:{n_qubits}, RGB:{RGB_C}, IMG:{img_size_single} Layers:{n_layers},QNN Params:{[sum(p.numel() for p in qnn.parameters())]},CPU:{cpu_percent},RAM(GB):{used_ram_gb}'/'{mem_usage}")
    print(f"Epoch:[{epoch+1}/{num_epochs}], Dataset:{data_dir,dataset_sizes}, Qubits:{n_qubits}, RGB:{RGB_C},IMG:{img_size_single} Layers:{n_layers},QNN Params:{[sum(p.numel() for p in qnn.parameters())]} CPU:{cpu_percent}, RAM(GB):{used_ram_gb}/{mem_usage}")
    running_loss = 0
    running_corrects = 0
    total_samples = 0

    for batch_idx, (data, target) in tqdm(enumerate(dataloaders['train'])):
        data = data.to(device)
        target = target.view(-1).to(device)
        batch_size = data.size(0)  # Get the actual batch size

        output = qnn(data)

        # Adjust the output tensor size if necessary
        if output.size(0) > batch_size:
            output = output[:batch_size]

        loss = criterion(output, target)

        running_loss += loss.item()
        _, predicted = torch.max(output, 1)
        running_corrects += torch.sum(predicted == target.data)
        total_samples += batch_size

    batch_loss = running_loss / len(dataloaders['train'])
    batch_acc = running_corrects / total_samples
    print(f'[{epoch + 1}] Training Loss: {batch_loss:.3f}, Training Accuracy: {batch_acc:.3f}')

    running_loss = 0.0
    running_corrects = 0
    total_samples = 0

    val_correct = 0
    val_total = 0
    with torch.no_grad():
        for val_data, val_target in dataloaders['val']:
            val_data = val_data.to(device)
            val_target = val_target.to(device)
            batch_size = val_data.size(0)  # Get the actual batch size

            val_output = qnn(val_data)
            _, val_predicted = torch.max(val_output.data, 1)

            # Adjust the output and target tensors if necessary
            if val_predicted.size(0) > batch_size:
                val_predicted = val_predicted.narrow(0, 0, batch_size)
                val_target = val_target.narrow(0, 0, batch_size)

            val_total += batch_size
            val_correct += (val_predicted == val_target).sum().item()

    val_accuracy = 100 * val_correct / val_total
    print(f"[{epoch + 1}] Validation Accuracy: {val_accuracy:.2f}%")


For your example, you remove the Tensor from the measurements and use

q_out_elem = torch.hstack(self.pqc(elem, self.q_params)).float().unsqueeze(0

Let me know if that helps :slight_smile:

Thank you for your input. I understand that the suggestion you provided is similar to what I have already tried and does not alter the functionality. However, it does not address the larger and more critical issue that I have reported. I would greatly appreciate further assistance in resolving the main problem I am facing.

Thanks for your feedback, could you create a minimal example that is not training and post your issue on GitHub? Issues · PennyLaneAI/pennylane · GitHub


Here you go:

Thanks for posting the issue there @Solomon!