Batching in TorchLayer

Hi Ivana, thanks for the welcome. Sorry about the messy code, I’m not sure how best to format things for this forum. I’ve tidied it up a bit and removed some things but there isn’t a whole lot more that I can remove as the problem is quite specific:

“”"

Pennylane QCNN example on MNIST

Trying to compare performance vs qiskit (qiskit implementation in seperate script)

“”"

Necessary imports

import numpy as np
import matplotlib.pyplot as plt

from torch import Tensor
from torch.nn import Linear, CrossEntropyLoss, MSELoss
from torch.optim import LBFGS

import torch
from torchsummary import summary
from torch import cat, no_grad, manual_seed
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
import torch.optim as optim
from torch.nn import (
Module,
Conv2d,
Linear,
Dropout2d,
NLLLoss,
MaxPool2d,
Flatten,
Sequential,
ReLU,
)
import torch.nn.functional as F

import pennylane as qml
from pennylane.templates import AngleEmbedding, BasicEntanglerLayers

Train Dataset

-------------

manual_seed(42)

batch_size = 10
n_samples = 500

X_train = datasets.MNIST(
root=“./data”, train=True, download=True, transform=transforms.Compose([transforms.ToTensor()])
)

idx = np.append(
np.where(X_train.targets == 0)[0][:n_samples], np.where(X_train.targets == 1)[0][:n_samples]
)
X_train.data = X_train.data[idx]
X_train.targets = X_train.targets[idx]

train_loader = DataLoader(X_train, batch_size=batch_size, shuffle=True)

Test Dataset

-------------

n_samples = 250

X_test = datasets.MNIST(
root=“./data”, train=False, download=True, transform=transforms.Compose([transforms.ToTensor()])
)

idx = np.append(
np.where(X_test.targets == 0)[0][:n_samples], np.where(X_test.targets == 1)[0][:n_samples]
)
X_test.data = X_test.data[idx]
X_test.targets = X_test.targets[idx]

test_loader = DataLoader(X_test, batch_size=batch_size, shuffle=True)

n_qubits = 2
dev = qml.device(“default.qubit”, wires=n_qubits)

Simple parameterized circuit

@qml.qnode(dev)
def qnode(inputs, weights):
‘’‘for i in range(n_qubits):
‘’’‘’‘qml.RX(weights[i],wires=i)
‘’’‘’'qml.RX(weights[i+2],wires=i)
‘’'qml.CNOT(wires=[0, 1])
‘’'return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

n_layers = 1
n_params = 4
weight_shapes = {“weights”: (n_params,)}

qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)

Define torch NN module

class Net(Module):
def init(self):
super().init()
self.conv1 = Conv2d(1, 2, kernel_size=5)
self.conv2 = Conv2d(2, 16, kernel_size=5)
self.dropout = Dropout2d()
self.fc1 = Linear(256, 64)
self.fc2 = Linear(64, 2)
self.qlayer_1 = qml.qnn.TorchLayer(qnode, weight_shapes)

def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2)
x = self.dropout(x)
x = x.view(x.shape[0], -1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
quantum_output = self.qlayer_1(x)
concatenated_output = torch.cat((x, quantum_output), dim=1)
return concatenated_output

model4 = Net()

Define model, optimizer, and loss function

optimizer = optim.Adam(model4.parameters(), lr=0.001)
loss_func = CrossEntropyLoss()

Start training

epochs = 10
loss_list = [ ]
model4.train()

for epoch in range(epochs):
‘’‘total_loss = [ ]
‘’‘for batch_idx, (data, target) in enumerate(train_loader):
‘’’’‘‘optimizer.zero_grad(set_to_none=True)
‘’’’‘‘output = model4(data)
‘’’’‘‘loss = loss_func(output, target)
‘’’’‘‘loss.backward()
‘’’’‘‘optimizer.step()
‘’’’''total_loss.append(loss.item())
‘’'loss_list.append(sum(total_loss) / len(total_loss))
‘’'print(“Training [{:.0f}%]\tLoss: {:.4f}”.format(100.0 * (epoch + 1) / epochs, loss_list[-1]))

Hey @kieran_mcdowall , thanks!

There’s a lot going on here. :slight_smile: First, let me format this as code — you can press the image button right above the text editor when you’re writing a post to get into the right environment.

import numpy as np
import matplotlib.pyplot as plt

from torch import Tensor
from torch.nn import Linear, CrossEntropyLoss, MSELoss
from torch.optim import LBFGS

import torch
from torchsummary import summary
from torch import cat, no_grad, manual_seed
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
import torch.optim as optim
from torch.nn import (
Module,
Conv2d,
Linear,
Dropout2d,
NLLLoss,
MaxPool2d,
Flatten,
Sequential,
ReLU,
)
import torch.nn.functional as F

import pennylane as qml
from pennylane.templates import AngleEmbedding, BasicEntanglerLayers

manual_seed(42)

batch_size = 10
n_samples = 500

X_train = datasets.MNIST(
root='./data', train=True, download=True, transform=transforms.Compose([transforms.ToTensor()])
)

idx = np.append(
np.where(X_train.targets == 0)[0][:n_samples], np.where(X_train.targets == 1)[0][:n_samples]
)
X_train.data = X_train.data[idx]
X_train.targets = X_train.targets[idx]

train_loader = DataLoader(X_train, batch_size=batch_size, shuffle=True)

n_samples = 250

X_test = datasets.MNIST(
root='./data', train=False, download=True, transform=transforms.Compose([transforms.ToTensor()])
)

idx = np.append(
np.where(X_test.targets == 0)[0][:n_samples], np.where(X_test.targets == 1)[0][:n_samples]
)
X_test.data = X_test.data[idx]
X_test.targets = X_test.targets[idx]

test_loader = DataLoader(X_test, batch_size=batch_size, shuffle=True)

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

@qml.qnode(dev)
def qnode(inputs, weights):
    for i in range(n_qubits):
        qml.RX(weights[i],wires=i)
        qml.RX(weights[i+2],wires=i)
        qml.CNOT(wires=[0, 1])
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

n_layers = 1
n_params = 4
weight_shapes = {'weights': (n_params,)}

qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)

class Net(Module):
    def init(self):
        super().init()
        self.conv1 = Conv2d(1, 2, kernel_size=5)
        self.conv2 = Conv2d(2, 16, kernel_size=5)
        self.dropout = Dropout2d()
        self.fc1 = Linear(256, 64)
        self.fc2 = Linear(64, 2)
        self.qlayer_1 = qml.qnn.TorchLayer(qnode, weight_shapes)

def forward(self, x):
    x = F.relu(self.conv1(x))
    x = F.max_pool2d(x, 2)
    x = F.relu(self.conv2(x))
    x = F.max_pool2d(x, 2)
    x = self.dropout(x)
    x = x.view(x.shape[0], -1)
    x = F.relu(self.fc1(x))
    x = self.fc2(x)
    quantum_output = self.qlayer_1(x)
    concatenated_output = torch.cat((x, quantum_output), dim=1)
    return concatenated_output

model4 = Net()

optimizer = optim.Adam(model4.parameters(), lr=0.001)
loss_func = CrossEntropyLoss()

epochs = 10
loss_list = [ ]
model4.train()

for epoch in range(epochs):
    total_loss = [ ]
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad(set_to_none=True)
        output = model4(data)
        loss = loss_func(output, target)
        loss.backward()
        optimizer.step()
        total_loss.append(loss.item())
    loss_list.append(sum(total_loss) / len(total_loss))
    print('Training [{:.0f}%]\tLoss: {:.4f}'.format(100.0 * (epoch + 1) / epochs, loss_list[-1]))

I can’t seem to run your code as it is because model4.parameters() doesn’t seem to be defined right, so I can’t help you right away.
But if I understand your question a bit better now, you say you’re getting an error when you’re running the training? At first glance, I can’t parse the issue. Any chance you could check if there’s an error with the code transcription, and also include the output you get from qml.about? Thanks! :slight_smile:

Hi Ivana, yeh thank you there was a problem in the formatting. Here is the correct format:

"""

Pennylane QCNN example on MNIST

Trying to compare performance vs qiskit (qiskit implementation in seperate script)

"""

# Necessary imports
import numpy as np
import matplotlib.pyplot as plt

from torch import Tensor
from torch.nn import Linear, CrossEntropyLoss, MSELoss
from torch.optim import LBFGS

import torch
from torchsummary import summary
from torch import cat, no_grad, manual_seed
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
import torch.optim as optim
from torch.nn import (
    Module,
    Conv2d,
    Linear,
    Dropout2d,
    NLLLoss,
    MaxPool2d,
    Flatten,
    Sequential,
    ReLU,
)
import torch.nn.functional as F

import pennylane as qml
from pennylane.templates import AngleEmbedding, BasicEntanglerLayers

# Train Dataset
# -------------
manual_seed(42)

batch_size = 10
n_samples = 500  

X_train = datasets.MNIST(
    root="./data", train=True, download=True, transform=transforms.Compose([transforms.ToTensor()])
)


idx = np.append(
    np.where(X_train.targets == 0)[0][:n_samples], np.where(X_train.targets == 1)[0][:n_samples]
)
X_train.data = X_train.data[idx]
X_train.targets = X_train.targets[idx]

train_loader = DataLoader(X_train, batch_size=batch_size, shuffle=True)


# Test Dataset
# -------------
n_samples = 250 

X_test = datasets.MNIST(
    root="./data", train=False, download=True, transform=transforms.Compose([transforms.ToTensor()])
)

idx = np.append(
    np.where(X_test.targets == 0)[0][:n_samples], np.where(X_test.targets == 1)[0][:n_samples]
)
X_test.data = X_test.data[idx]
X_test.targets = X_test.targets[idx]

test_loader = DataLoader(X_test, batch_size=batch_size, shuffle=True)


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

# Simple parameterized circuit
@qml.qnode(dev)
def qnode(inputs, weights):
    for i in range(n_qubits):
        qml.RX(weights[i],wires=i)
        qml.RX(weights[i+2],wires=i)
    qml.CNOT(wires=[0, 1])
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

n_layers = 1 
n_params = 4
weight_shapes = {"weights": (n_params,)} 

qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)



# Define torch NN module
class Net(Module):
    def __init__(self):
        super().__init__()
        self.conv1 = Conv2d(1, 2, kernel_size=5)
        self.conv2 = Conv2d(2, 16, kernel_size=5)
        self.dropout = Dropout2d()
        self.fc1 = Linear(256, 64)
        self.fc2 = Linear(64, 2)  
        self.qlayer_1 = qml.qnn.TorchLayer(qnode, weight_shapes)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = self.dropout(x)
        x = x.view(x.shape[0], -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)

        quantum_output = self.qlayer_1(x)

        concatenated_output = torch.cat((x, quantum_output), dim=1)
        
        return concatenated_output

model4 = Net()


# Define model, optimizer, and loss function
optimizer = optim.Adam(model4.parameters(), lr=0.001)
loss_func = CrossEntropyLoss()

# Start training
epochs = 10  
loss_list = []  
model4.train()  

for epoch in range(epochs):
    total_loss = []
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad(set_to_none=True)  
        output = model4(data) 
        loss = loss_func(output, target) 
        loss.backward()  
        optimizer.step() 
        total_loss.append(loss.item())  
    loss_list.append(sum(total_loss) / len(total_loss))
    print("Training [{:.0f}%]\tLoss: {:.4f}".format(100.0 * (epoch + 1) / epochs, loss_list[-1]))

And yes the error occurs when running training. This all works when the batch_size =1 but when increasing this I get this error:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[15], line 14
     12 for batch_idx, (data, target) in enumerate(train_loader):
     13     optimizer.zero_grad(set_to_none=True)  
---> 14     output = model4(data) 
     15     loss = loss_func(output, target) 
     16     loss.backward()  

File ~/.local/lib/python3.8/site-packages/torch/nn/modules/module.py:1501, in Module._call_impl(self, *args, **kwargs)
   1496 # If we don't have any hooks, we want to skip the rest of the logic in
   1497 # this function, and just call forward.
   1498 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1499         or _global_backward_pre_hooks or _global_backward_hooks
   1500         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1501     return forward_call(*args, **kwargs)
   1502 # Do not call functions when jit is used
   1503 full_backward_hooks, non_full_backward_hooks = [], []

Cell In[14], line 22, in Net.forward(self, x)
     19 x = F.relu(self.fc1(x))
     20 x = self.fc2(x)
---> 22 quantum_output = self.qlayer_1(x)
     24 concatenated_output = torch.cat((x, quantum_output), dim=1)
     26 return concatenated_output

File ~/.local/lib/python3.8/site-packages/torch/nn/modules/module.py:1501, in Module._call_impl(self, *args, **kwargs)
   1496 # If we don't have any hooks, we want to skip the rest of the logic in
   1497 # this function, and just call forward.
   1498 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1499         or _global_backward_pre_hooks or _global_backward_hooks
   1500         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1501     return forward_call(*args, **kwargs)
   1502 # Do not call functions when jit is used
   1503 full_backward_hooks, non_full_backward_hooks = [], []

File ~/.local/lib/python3.8/site-packages/pennylane/qnn/torch.py:408, in TorchLayer.forward(self, inputs)
    405     results = torch.stack(reconstructor)
    406 else:
    407     # calculate the forward pass as usual
--> 408     results = self._evaluate_qnode(inputs)
    410 # reshape to the correct number of batch dims
    411 if has_batch_dim:

File ~/.local/lib/python3.8/site-packages/pennylane/qnn/torch.py:435, in TorchLayer._evaluate_qnode(self, x)
    432     return res.type(x.dtype)
    434 if len(x.shape) > 1:
--> 435     res = [torch.reshape(r, (x.shape[0], -1)) for r in res]
    437 return torch.hstack(res).type(x.dtype)

File ~/.local/lib/python3.8/site-packages/pennylane/qnn/torch.py:435, in <listcomp>(.0)
    432     return res.type(x.dtype)
    434 if len(x.shape) > 1:
--> 435     res = [torch.reshape(r, (x.shape[0], -1)) for r in res]
    437 return torch.hstack(res).type(x.dtype)

RuntimeError: shape '[10, -1]' is invalid for input of size 1

Hey @kieran_mcdowall, I think I’ve figured out your problem. You’re implementing parameter broadcasting (see here) and this is how you define the QNode:

@qml.qnode(dev)
def qnode(inputs, weights):
    for i in range(n_qubits):
        qml.RX(weights[i],wires=i)
        qml.RX(weights[i],wires=i)
    qml.CNOT(wires=[0, 1])
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

But you end up not actually using the inputs parameter in the definition.
So you end up telling the QNode to batch inputs/results, but you don’t actually use the parameter that’s supposed to do the batching for you.

To see what I mean, you could reproduce the same error if you use the example code from qml.qnn.TorchLayer — PennyLane 0.32.0 documentation and comment out the lines that use inputs:

import numpy as np
import pennylane as qml
import torch
import sklearn.datasets

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

@qml.qnode(dev)
def qnode(inputs, weights):
#    qml.templates.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))

weight_shapes = {"weights": (3, n_qubits, 3)}

qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)
clayer1 = torch.nn.Linear(2, 2)
clayer2 = torch.nn.Linear(2, 2)
softmax = torch.nn.Softmax(dim=1)
model = torch.nn.Sequential(clayer1, qlayer, clayer2, softmax)

samples = 100
x, y = sklearn.datasets.make_moons(samples)
y_hot = np.zeros((samples, 2))
y_hot[np.arange(samples), y] = 1

X = torch.tensor(x).float()
Y = torch.tensor(y_hot).float()

opt = torch.optim.SGD(model.parameters(), lr=0.5)
loss = torch.nn.L1Loss()

epochs = 8
batch_size = 5
batches = samples // batch_size

data_loader = torch.utils.data.DataLoader(list(zip(X, Y)), batch_size=batch_size,
                                          shuffle=True, drop_last=True)

for epoch in range(epochs):

    running_loss = 0

    for x, y in data_loader:
        opt.zero_grad()

        loss_evaluated = loss(model(x), y)
        loss_evaluated.backward()

        opt.step()

        running_loss += loss_evaluated

    avg_loss = running_loss / batches
    print("Average loss over epoch {}: {:.4f}".format(epoch + 1, avg_loss))

I’m not sure what you’re trying to do with your code, but this doesn’t seem to be the ‘expected’ way of using this functionality. I think it might be worth it if you tried to map our your algorithm sybmolically and see where the gaps might be. :slight_smile:

And we’d love to hear from you about how you are using PennyLane. We have a very short survey for PennyLane v0.32, if you could take a minute to give back and help our team keep improving PennyLane. Thank you! :slight_smile:

I’ve noticed a fair number of individuals are leveraging the Transfer Learning example as a basis for their projects. While it’s great to see enthusiasm for combining quantum and classical techniques, there are some nuances that should not be overlooked.

The first misconception some might have is that the demo somehow proves that Quantum Neural Networks (QNNs) are superior to traditional deep learning models. This is not accurate. In reality, the heavy lifting in the demonstration is done by the classical neural network that has been pre-trained on the ImageNet dataset. This network acts as a feature extractor, a role it performs exceedingly well, given the breadth and depth of ImageNet. This method of feature extraction is a common and effective strategy in a variety of tasks, such as classification or unsupervised clustering, and much of the demo’s effectiveness is attributable to this.

The quantum circuit that follows this feature extraction is rather simplistic. The point to emphasise here is that you could quite easily replace this quantum circuit with another simple classical model (Logistic Regression), or even a different type of quantum circuit, and the results would likely remain largely unchanged. This reveals that the quantum component is not the primary driver of the model’s performance.

Additionally, if you were to swap out the pre-trained neural network for a more sophisticated model, say ResNext101, you would probably see an improvement in performance. This would be true irrespective of the quantum circuit you’ve attached to it, further underscoring the point that the classical neural network is the main contributor to the efficacy of the entire setup.

So, if you’re looking to demonstrate some form of “supremacy” of quantum computing in neural networks, you’ll need to do more than just tack a simple quantum circuit onto a classical neural network. A more compelling approach would be to utilise a fully quantum neural network for encoding the image using a quantum Conv2D, not the classical Conv2D.

There are plenty of academic papers that have explored these more advanced configurations, and it would be beneficial for tutorials to make this clear. Especially for those who are new to the field, understanding these nuances can make all the difference in how they approach their own projects.

1 Like

Thanks @Solomon! Interesting points and we appreciate your feedback :slightly_smiling_face:. A lot of things in QML are nuanced, so it’s good to be aware of this :+1:

Hi Ivana, thanks for pointing that out! This fixed the issue after changing my circuit to:

@qml.qnode(dev)
def qnode(inputs,weights): # 
    qml.RX(inputs[0],wires=0)
    qml.RX(inputs[0],wires=1)
    qml.RX(weights[0],wires=0)
    qml.RX(weights[1],wires=1)
    qml.CNOT(wires=[0, 1])
    
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

And adjusting the layer before my quantum layer to have output equal to the batch size ( self.fc2 = Linear(64, batch_size) ):

# Define torch NN module
class Net(Module):
    def __init__(self):
        super().__init__()
        self.conv1 = Conv2d(1, 2, kernel_size=5)
        self.conv2 = Conv2d(2, 16, kernel_size=5)
        self.dropout = Dropout2d()
        self.fc1 = Linear(256, 64)
        self.fc2 = Linear(64, batch_size) 
        self.qlayer_1 = qml.qnn.TorchLayer(qnode, weight_shapes)
        # No need for self.fc3 in this context

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = self.dropout(x)
        x = x.view(x.shape[0], -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)


        # print("Shape of x:", x.shape)  # Debugging line

        quantum_output = self.qlayer_1(x)

        # print("Shape of quantum_output:", quantum_output.shape)  # Debugging line
        
        # Concatenate quantum_output with the original x
        concatenated_output = torch.cat((x, quantum_output), dim=1) 
        
        return concatenated_output

2 Likes

That’s great to hear, glad we could find a workaround. :slight_smile:
Best of luck with your project!

1 Like

Hello, What has been the best QML/QiML results using both classical trainable parameters and quantum trainable parameters?

Hey @kevinkawchak!

I think by “classical / quantum trainable parameters” you mean parameters that are part of classical / quantum layers. There are no “classical / quantum trainable parameters” per se, since all parameters in a quantum / classical / hybrid ML model are updated classically — you can think of quantum neural networks as black boxes that behave very similarly to classical networks in terms of what goes in, what comes out, and how their parameters are updated at a high level :slight_smile:

For best results from QML models, that’s a tough one to answer :sweat_smile:. There’s a ton of literature and studies out there!

2 Likes

Hi PennyLane Team,

I have also problems when using batch input together with a TorchLayer. As you can see in my example code below, I create a simple quantum circuit, which accepts inputs and params. The shape of params should be [2, 2] and for a single data input, the shape of inputs is [2,].
Now, when I want to input a batch of data I would assume (according to the broadcasting explanation here qml.QNode — PennyLane 0.34.0 documentation) that the shape of the data input should be [2, batch_size].
However, if I execute the torch module with data = torch.rand(2, 3) (where the batch dimension is 3) I get the following error: RuntimeError: shape '[2, -1]' is invalid for input of size 3.
When I use data = torch.rand(3, 2) I get the error: RuntimeError: shape '[3, -1]' is invalid for input of size 2.
The code only works with data = torch.rand(3, 3), but in this case there has to be data which is not used.

Do you have any idea, how to input a batch of data? Thanks in advance!

My code:

import pennylane as qml

import torch
import matplotlib.pyplot as plt

torch.manual_seed(42)


class QuantumNN(torch.nn.Module):

    def __init__(self):
        super().__init__()
        weight_shape = {'params': (2, 2)}
        q_node = create_quantum_node()
        self.q_layer = qml.qnn.TorchLayer(q_node, weight_shape)

    def forward(self, xx):
        print(xx.shape)
        xx = self.q_layer(xx)
        return xx


def create_quantum_node():

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

    @qml.qnode(device)
    def circuit(inputs, params):
        qml.RY(inputs[0], wires=0)
        qml.RY(inputs[0], wires=1)
        qml.RX(inputs[1], wires=0)
        qml.RX(inputs[1], wires=1)

        qml.RY(params[0, 0], wires=0)
        qml.RX(params[0, 1], wires=0)
        qml.RY(params[1, 0], wires=0)
        qml.RX(params[1, 1], wires=0)
        qml.CNOT(wires=[0, 1])

        expvals = [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))]

        return expvals

    return circuit


def main():
    data = torch.rand(3, 2)

    qnn = QuantumNN()

    output = qnn(data)
    print(output)


if __name__ == '__main__':
    main()

Hey @nilserik, welcome to the forum :sunglasses:

Whenever you’re accessing individual elements of arrays/tensors in a QNode that you then want to be broadcast-friendly over those same parameters, you have to be careful.

Here’s an example that illustrates the problem more clearly.

import pennylane as qml
from pennylane import numpy as np

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit1(param):
    qml.RX(param, wires=0)
    return qml.expval(qml.PauliZ(0))

print(circuit1(np.random.uniform(0, 1, size=(10,))))
[0.98214979 0.9591634  0.9928843  0.84283224 0.92926838 0.99586592
 0.56027449 0.99994443 0.94710582 0.97803213]

Properly broadcasts! Here’s an example with multiple inputs that I want to broadcast over:

@qml.qnode(dev)
def circuit2(param1, param2):
    qml.RX(param1, wires=0)
    qml.RX(param2, wires=1)
    return [qml.expval(qml.PauliZ(i)) for i in range(2)]

print(
    circuit2(
        np.random.uniform(0, 1, size=(10,)), 
        np.random.uniform(0, 1, size=(10,))
    )
)
[tensor([0.98657916, 0.80634743, 0.91880379, 0.84127853, 0.9543103 ,
        0.89960924, 0.73327519, 0.93879418, 0.68850169, 0.79644582], requires_grad=True), tensor([0.99999716, 0.68590668, 0.77318251, 0.97157214, 0.96139906,
        0.86519501, 0.63647971, 0.98960799, 0.99764961, 0.87623469], requires_grad=True)]

Nice! But, what happens when I smush those two arguments into one argument and expect the same behaviour?

@qml.qnode(dev)
def circuit3(params):
    qml.RX(params[0], wires=0)
    qml.RX(params[1], wires=1)
    return [qml.expval(qml.PauliZ(i)) for i in range(2)]

print(circuit3(np.random.uniform(0, 1, size=(20,))))
[tensor(0.88176114, requires_grad=True), tensor(0.62645585, requires_grad=True)]

Ah! Not what I expected. I should be more careful about how I index things:

@qml.qnode(dev)
def circuit3(params):
    qml.RX(params[:10], wires=0)
    qml.RX(params[10:], wires=1)
    return [qml.expval(qml.PauliZ(i)) for i in range(2)]

print(circuit3(np.random.uniform(0, 1, size=(20,))))
[tensor([0.99782233, 0.97420952, 0.99957296, 0.99993142, 0.77111774,
        0.95343317, 0.77673955, 0.60145206, 0.78548762, 0.98895393], requires_grad=True), tensor([0.87447186, 0.8885168 , 0.80220563, 0.90393826, 0.8022565 ,
        0.73854892, 0.93283186, 0.96285274, 0.83873279, 0.602139  ], requires_grad=True)]

You need to be explicit about how you want the broadcasting to happen when you have circuits like this.

Hope this helps!

Hi @isaacdevlugt, thanks für your reply! :grinning:

I am not sure if your answer helps me for inputting a batch to my quantum torch layer. Maybe I reformulate my question:

Considering a classical neural network which takes in 20 inputs (shape [20,]) and the output shape is [10,]. In this case I can just add an extra dimension, which is the batch dimension and the output shape will be altered conveniently. I.e. if the input has shape [5, 20], the output will have shape [5, 10]. Is there an equivalent for the qnodes?
Below I show some code, how I would handle batch input within a hybrid quantum-classical neural network. Here it it tedious to reshape the input before the qnode and after it. Is there a better way to handle batches with a qnode?

import pennylane as qml

import torch

torch.manual_seed(42)


class QuantumNN(torch.nn.Module):

    def __init__(self):
        super().__init__()
        weight_shape = {'params': (2,)}
        q_node = create_quantum_node()
        self.q_layer = qml.qnn.TorchLayer(q_node, weight_shape)
        self.linear = torch.nn.Linear(2, 1)

    def forward(self, xx):
        xx = xx.T.flatten()
        xx = self.q_layer(xx)
        xx = torch.reshape(xx, (int(xx.shape[0] / 2), 2))
        xx = self.linear(xx)
        return xx


def create_quantum_node():

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

    @qml.qnode(device)
    def circuit3(inputs, params):
        batch_size = int(inputs.shape[0] / 2)
        qml.RX(inputs[:batch_size], wires=0)
        qml.RX(inputs[batch_size:], wires=1)

        qml.RY(params[0], wires=0)
        qml.RY(params[1], wires=1)
        return [qml.expval(qml.PauliZ(ii)) for ii in range(2)]

    return circuit3


def main():
    # shape of data: [batch_size, 2]
    data = torch.rand(3, 2)

    qnn = QuantumNN()

    output = qnn(data)
    print(output)


if __name__ == '__main__':
    main()

1 Like

Ah, sorry if my last reply was off the mark :sweat_smile:.

… I.e. if the input has shape [5, 20], the output will have shape [5, 10]. …

Totally! Parameter broadcasting is natively supported:

import pennylane as qml
import torch

@qml.qnode(dev)
def circuit(inputs, weights):
    qml.AmplitudeEmbedding(inputs, wires=range(5), normalize=True)
    qml.AngleEmbedding(weights, wires=range(5))
    return [qml.expval(qml.PauliZ(i)) for i in range(5)]

weight_shapes = {"weights": (5,)}
qlayer = qml.qnn.TorchLayer(circuit, weight_shapes)

clayer = torch.nn.Linear(5, 2)
model = torch.nn.Sequential(qlayer, clayer)

batch_size = 4
inputs = torch.rand((batch_size, 2**5))

model(inputs)
tensor([[ 0.1587, -0.1878],
        [ 0.1613, -0.3218],
        [ 0.1628, -0.4035],
        [ 0.2724, -0.2711]], grad_fn=<AddmmBackward0>)

Thank’s for your clarification, but I have a further question (I am sorry of probably missing your point :grimacing:).

I slightly adapted your code:

import pennylane as qml
import torch

wires_num = 2
device = qml.device('default.qubit', wires=wires_num)

@qml.qnode(device)
def circuit(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(2), rotation='X')
    qml.AngleEmbedding(weights, wires=range(wires_num), rotation='Y')
    return [qml.expval(qml.PauliZ(i)) for i in range(wires_num)]

weight_shapes = {"weights": (wires_num,)}
qlayer = qml.qnn.TorchLayer(circuit, weight_shapes)
clayer = torch.nn.Linear(wires_num, 2)
model = torch.nn.Sequential(qlayer, clayer)

inputs = torch.tensor([[0.4], [0.32], [0.1], [0.7]])
inputs = torch.repeat_interleave(inputs, 2, dim=1)
weights = torch.tensor([0.5, 0.4])

print('\nInputs:')
print(inputs)

result_circuit = circuit(inputs, weights)

print('\nOutputs:')
print(result_circuit)

results = model(inputs)
print('\nOutput of QCNN:')
print(results)

which gives:

Inputs:
tensor([[0.4000, 0.4000],
        [0.3200, 0.3200],
        [0.1000, 0.1000],
        [0.7000, 0.7000]])

Outputs:
[tensor([0.8083, 0.8330, 0.8732, 0.6712], dtype=torch.float64), tensor([0.8484, 0.8743, 0.9165, 0.7045], dtype=torch.float64)]

Output of QCNN:
tensor([[ 0.0580,  0.4230],
        [ 0.0703,  0.4232],
        [ 0.0903,  0.4236],
        [-0.0102,  0.4217]], grad_fn=<AddmmBackward0>

Now, I exchange the AngleEmbedding with the same angle encoding inplemented by hand:

@qml.qnode(device)
def circuit(inputs, weights):
    qml.RX(inputs, wires=0)
    qml.RX(inputs, wires=1)
    qml.AngleEmbedding(weights, wires=range(wires_num), rotation='Y')
    return [qml.expval(qml.PauliZ(i)) for i in range(wires_num)]

qlayer = qml.qnn.TorchLayer(circuit, weight_shapes)

model = torch.nn.Sequential(qlayer, clayer)

inputs = torch.tensor([0.4, 0.32, 0.1, 0.7])
weights = torch.tensor([0.5, 0.4])

print('\nInputs:')
print(inputs)

result_circuit = circuit(inputs, weights)

print('\nOutputs:')
print(result_circuit)

results = model(inputs)
print('\nOutput of QCNN:')
print(results)

This circuit gives me the exact same output as the previous one, but now it does no longer work with the PyTorch quantum-classical neural network:

Inputs:
tensor([0.4000, 0.3200, 0.1000, 0.7000])

Outputs:
[tensor([0.8083, 0.8330, 0.8732, 0.6712], dtype=torch.float64), tensor([0.8484, 0.8743, 0.9165, 0.7045], dtype=torch.float64)]
Traceback (most recent call last):
  File "/mnt/c/Users/schu_ns/Nextcloud/dlr/phd/code/circuit_quantum/misc/xanadu_question_3.py", line 59, in <module>
    results = model(inputs)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1518, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1527, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/container.py", line 215, in forward
    input = module(input)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1518, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1527, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/nils-erik/Documents/virtual_environments/qml/lib/python3.10/site-packages/torch/nn/modules/linear.py", line 114, in forward
    return F.linear(input, self.weight, self.bias)
RuntimeError: mat1 and mat2 shapes cannot be multiplied (1x8 and 2x2)

What am I doing wrong here?

Ohhh okay I see what the issue is. Here’s a minimal example:

import pennylane as qml
from pennylane import numpy as np

import torch

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit(inputs, weights):
    qml.RX(inputs, wires=0)
    #return [qml.expval(qml.PauliZ(i)) for i in range(2)]
    return qml.probs(wires=0)

weight_shapes = {"weights": (2,)}

qlayer = qml.qnn.TorchLayer(circuit, weight_shapes)
clayer = torch.nn.Linear(2, 2)
model = torch.nn.Sequential(qlayer, clayer)

inputs = torch.tensor([0.4, 0.32, 0.1, 0.7])
weights = torch.tensor([0.5, 0.4])
print(circuit(inputs, weights))
print(model(inputs))

This works when the return is qml.probs(wires=0), for example, because the output of the QNode is shaped “properly”:

tensor([[0.9605, 0.0395],
        [0.9746, 0.0254],
        [0.9975, 0.0025],
        [0.8824, 0.1176]])
tensor([[ 0.8731, -0.3194],
        [ 0.8906, -0.3178],
        [ 0.9191, -0.3153],
        [ 0.7760, -0.3282]], grad_fn=<AddmmBackward0>)

With the expval over both wires substituted for probs, we get:

[tensor([0.9211, 0.9492, 0.9950, 0.7648], dtype=torch.float64), tensor([1.0000, 1.0000, 1.0000, 1.0000], dtype=torch.float64)]
RuntimeError: mat1 and mat2 shapes cannot be multiplied (1x8 and 2x2)

I don’t think this is a bug, per se… but it’s definitely annoying :thinking:. I’ll get back to you to see if there’s an obvious workaround!