Inputs dimension mix with batch dimension in qml.qnn.TorchLayer

Hi, I found that when using qml.qnn.TorchLayer, it recognizes batch dimension differently from version pennylane==0.30.0 against versions higher than pennylane==0.31.0. Suppose the shape of a single batch of data is x=(N,D), where N and D are the dimension of batch and data respectively. Consider forwarding x through a qml.qnn.TorchLayer L:

  • In pennylane==0.30.0
    It behaves correctly that L(x) is also in shape (N,\text{some output shape})
    . To be more precise, if the dimension of x is now (N_1,N_2,N_3,D), the output dimension of L(X) will be (N_1,N_2,N_3,\text{some output shape})

  • In pennylane>=0.31.0
    It seems to forwarding all data in a batch as inputs. When I print out the shape of inputs in the circuit, the shape is exactly the shape of x, i.e., (N,D)

Here is the code for reproducing the error (or maybe it was designed on purpose :thinking:):

import sys
import torch
import torch.nn as nn
import pennylane as qml
print(f"Python version: {sys.version}")
print(f"PyTorch version: {torch.__version__}")
print(f"Pennylane version: {qml.__version__}")

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

@qml.qnode(device)
def circuit(inputs):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        torch_layer = qml.qnn.TorchLayer(circuit, weight_shapes={})
        self.net = nn.Sequential(torch_layer)
    def forward(self, x):
        print(f"model x shape = {x.shape}")
        x = self.net(x)
        print(f"output shape = {x.shape}")
        return x

N, D = 5, 3
model = Model()
y = model(torch.rand(N, D))

Outputs in 0.30.0:
model x shape = torch.Size([3, 2])
circuit inputs shape = torch.Size([2])
circuit inputs shape = torch.Size([2])
circuit inputs shape = torch.Size([2])
output shape = torch.Size([3])

Outputs in 0.31.0:
model x shape = torch.Size([3, 2])
circuit inputs shape = torch.Size([3, 2])
RuntimeError: shape ‘[3]’ is invalid for input of size 1

Dependencies:

  • python 3.9.12
  • pip==23.3.1
  • torch==2.1.2
1 Like

Also an additional question, since usually a data might not be 1-dimensional (although we still can reshape it to 1-D than reshape back). I was wondering whether TorchLayer or quantum circuits can specify the dimension of batch and inputs, just like Conv2d. Thanks in advance :smile:

Hey @Yian_Chen,

I think your example is highlighting a non-issue that has a vague error message. Essentially the problem is down to your circuit not doing anything with inputs, but Torch sees inputs as having a batch dimension and is expecting your circuit output to have a batch dimension as well. But, if you run your circuit outside of Torch, you get this:

@qml.qnode(device)
def circuit(inputs):
    print(f"circuit inputs shape = {inputs.shape}")

    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

circuit(np.random.uniform(0, 1, size=(100))
circuit inputs shape = (100,)
tensor(0., requires_grad=True)

PennyLane doesn’t do any broadcasting because there’s no instructions for what to do when you aren’t using inputs :slight_smile:.

If I put in something that deals with inputs, then PennyLane will give you an output with a leading dimension that matches the inputs’ leading dimension:

@qml.qnode(device)
def circuit(inputs):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.AngleEmbedding(inputs, wires=range(2))
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

circuit(np.random.uniform(0, 1, size=(100, 2)))
circuit inputs shape = (100, 2)
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0.], requires_grad=True)

Putting this back together with your torch example:

@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.AngleEmbedding(inputs, wires=range(2))
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

torch_layer = qml.qnn.TorchLayer(circuit, weight_shapes = {"weights": (0)})
net = nn.Sequential(torch_layer)

N, D = 5, 2
net(torch.rand(N, D))
tensor([0., 0., 0., 0., 0.])

You just need to do something with inputs :slight_smile:

As for the PennyLane version discrepancy, this might be due to some bugs that were fixed or features that were added to our parameter broadcasting functionality (it’s a relatively new feature; about 1 year old). I’m using the latest version of PennyLane (v0.33.1) :slight_smile:

Hopefully this also answers your second post! If not, let me know :+1:

1 Like

Thank you @isaacdevlugt , the code above is reproducible and the explanations are really clear :smile:. I notice that you have used qml.AngleEmbedding , so that qml.qnn.TorchLayer somehow knows the batch dimension. However, due to the design of my circuit, there is no corresponding function I can use for embedding. Particularly, I need to use for loops to embed the inputs. I try to modify your code that can roughly demonstrate what I want to do:

  • Code from your example
# pennylane==0.33.1
@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.AngleEmbedding(inputs, wires=range(2))
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))
circuit inputs shape = torch.Size([5, 2])
tensor([0., 0., 0., 0., 0.])
  • What if I need to use for loop
# pennylane==0.33.1
@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    for i in range(2):
        qml.RX(inputs[i], wires=i)
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))
circuit inputs shape = torch.Size([5, 2])
RuntimeError: shape '[5]' is invalid for input of size 2

The only difference is that I use qml.RX explicitly to embed the inputs.


On the other hand, I notice the difference between 0.30.0 and 0.33.1 comes from how qml.qnn.TorchLayer feed in the inputs. So suppose you set

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

@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.AngleEmbedding(inputs, wires=range(2))
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

torch_layer = qml.qnn.TorchLayer(circuit, weight_shapes = {"weights": (0)})
net = nn.Sequential(torch_layer)

batch_size = (5,4)
inputs_shape = (3,2)
x = torch.rand(*batch_size, *inputs_shape)
print(f"net(x) shape = {net(x).shape}")
  • In Pennylane==0.30.0
circuit inputs shape = torch.Size([2])
net(x) shape = torch.Size([5, 4, 3])
  • In Pennylane==0.33.1, which
circuit inputs shape = torch.Size([60, 2])
net(x) shape = torch.Size([5, 4, 3])

So for both versions, they recognize [5,4,3] as the batch dimension (actually I just want [5,4]), but different inputs shape. Nonetheless, in old version (0.30.0), it is easy to solved with

@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    inputs = inputs.reshape(3,2)
    for i in range(len(inputs)):
        qml.AngleEmbedding(inputs[i], wires=range(2))
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

torch_layer = qml.qnn.TorchLayer(circuit, weight_shapes = {"weights": (0)})
net = nn.Sequential(torch_layer)

batch_size = (5,4)
inputs_shape = (3,2)
x = torch.rand(*batch_size, *inputs_shape)
x = torch.flatten(x, start_dim=-2, end_dim=-1)
print(f"net(x) shape = {net(x).shape}")
circuit inputs shape = torch.Size([6])
net(x) shape = torch.Size([5, 4, 1])

However, in newer versions, if the inputs is fed with whole batch (e.g. torch.Size([20, 6]) in the example above), I think I can’t use the for loop.

I notice that you have used qml.AngleEmbedding , so that qml.qnn.TorchLayer somehow knows the batch dimension. However, due to the design of my circuit, there is no corresponding function I can use for embedding.

I’m not using AngleEmbedding just so that torch can see a batch dimension, I’m using it because there must be something done to the inputs, and AngleEmbedding is one of the many ways to do something with inputs.

I’m a little confused about your use case and why something like this won’t suite your needs. Could you tell me what your data is and how do you want to embed it into a quantum layer?

1 Like

Hi @isaacdevlugt , thanks for your reply. I apologize for the confusion of my question, what I meant is that one can use pennylane’s embedding functions (e.g. AngleEmbedding , AmplitudeEmbedding, IQPEmbedding). But what if my embedding method is so weird that pennylane doesn’t provide a template? In such a situation, if I don’t use pennylane’s embedding functions, then I cannot do something with inputs properly.

For example, suppose one of the data in a single batch is x=(x_0, x_1) with 2 features, and my weird_embedding method looks like

def weird_embedding(x):
    # x is supposed to be a single data, not a batch of data
    for i in range(len(x)-1):
        qml.CRX(x[i], wires=[i, (i+1)])
        qml.T(wires=i)
        qml.CRY(x[i], wires=[i, (i+1)])
        qml.S(wires=(i+1))
        qml.CRZ(x[i], wires=[i, (i+1)])
    qml.Hadamard(wires=0)

Instead of using pennylane’s embedding functions, I want to use my own designed embedding functions. Now another problem shows up, in 0.30.0 the inputs is default to be feed one by one, so I just need to write

device = qml.device("default.qubit", wires=2)
@qml.qnode(device)
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    weird_embedding(inputs)
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

torch_layer = qml.qnn.TorchLayer(circuit, weight_shapes = {"weights": (0)})
net = nn.Sequential(torch_layer)

batch_size = (5,4,3)
inputs_shape = (2)
dataset = torch.rand(*batch_size, inputs_shape)
print(f"net(dataset) shape = {net(dataset).shape}")

Note the shape of inputs is

# version 0.30.0
circuit inputs shape = torch.Size([2])

On the other hand, in 0.33.1, I understand I need to do something with inputs, e.g.

# this is fine
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    qml.AmplitudeEmbedding(inputs, wires=range(2), pad_with=0, normalize=True)
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

# but this is not fine
def circuit(inputs, weights):
    print(f"circuit inputs shape = {inputs.shape}")
    weird_embedding(inputs)
    qml.Hadamard(wires=0)
    qml.CNOT(wires=(0,1))
    return qml.expval(qml.PauliX(0))

Also notice that the shape of inputs is now (not feed one by one but whole batch of data)

# version 0.33.1
circuit inputs shape = torch.Size([60, 2])

So if I want to use my own weird_embedding, then it will break down since now the inputs is the whole batch, not one by one. So my question is how can I do such that my wried_embedding can handle the batch data just like pennylane’s embedding functions? Thank you :smile:

Ah! Thank you. This clears things up a bit :+1:

There are a couple of different ways that you can make a custom embedding procedure and have it work with broadcasting. You can assume that the input has a leading (batch) dimension like this:

def weird_embedding(x):
    qml.CRX(x[:, 0], wires=[0, 1])
    qml.T(wires=0)
    qml.CRY(x[:, 1], wires=[0, 1])
    qml.S(wires=1)
    qml.CRZ(x[:, 2], wires=[0, 1])

You can then call it like this:

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

@qml.qnode(dev)
def circuit(x):
    weird_embedding(x)
    return [qml.expval(qml.PauliZ(i)) for i in range(2)]

x = np.random.uniform(0, np.pi, size=(50,3))

circuit(x)
[tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1.], requires_grad=True),
 tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1.], requires_grad=True)]

But, in the case that x is just one data point (not a batch), then this way doesn’t generalize like it would with three separate variables:

def weird_embedding(a, b, c):
    # x is supposed to be a single data, not a batch of data
    qml.CRX(a, wires=[0, 1])
    qml.T(wires=0)
    qml.CRY(b, wires=[0, 1])
    qml.S(wires=1)
    qml.CRZ(c, wires=[0, 1])

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

@qml.qnode(dev)
def circuit(a, b, c):
    weird_embedding(a, b, c)
    return [qml.expval(qml.PauliZ(i)) for i in range(2)]

a = np.random.uniform(0, np.pi, size=(50,))
b = np.random.uniform(0, np.pi, size=(50,))
c = np.random.uniform(0, np.pi, size=(50,))

circuit(a, b, c)

When this is in the context of a torch layer, you’re stuck with just having inputs — you can’t split it up into input1, input2, etc., for example. So you’ll have to write some logic at the start of your quantum functions to discern if `inputs is a batch or not. E.g.,

def weird_embedding(x):
    if len(x.shape) > 1:
        # is a batch of data
        a, b, c = x[:,0], x[:, 1], x[:, 2]
    else:
        # is a single data point
        a, b, c = x[0], x[1], x[2]

    qml.CRX(a, wires=[0, 1])
    qml.T(wires=0)
    qml.CRY(b, wires=[0, 1])
    qml.S(wires=1)
    qml.CRZ(c, wires=[0, 1])

You can find more info here: Templates — PennyLane 0.33.0 documentation

Let me know if this helps!

1 Like

Hi @isaacdevlugt , thanks for your solution! It works fine and I now know how to modify my codes for 0.33.1. Merry Christmas!

1 Like

Awesome! Glad to hear that we finally figured it out :smiley:

Happy holidays!