Batch Input Errors in Circuits Transpiled from Qiskit

Hello, I would like to implement a classical-quantum hybrid neural network using Qiskit to construct a quantum circuit that includes a feature map (ZZFeatureMap ) and a parameterized strongly entangling layer (StronglyEntanglingLayers ). I aim to integrate this quantum circuit into PyTorch via PennyLane. However, when I start training the network, it seems there is an issue with batch processing. I have tried using batch_input , but the error messages remain unchanged. I would greatly appreciate your help with this. Thank you very much!

import pennylane as qml
import pennylane_qiskit
from qiskit_aer import Aer
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import ZZFeatureMap
from qiskit_aer import AerSimulator
from collections import OrderedDict

def StronglyEntanglingLayers(n_qubits, reps=2, entangler="cx"):
    qc = QuantumCircuit(n_qubits)
    parameters = []
    for l in range(reps):
        layer_params = []
        for q in range(n_qubits):
            theta = Parameter(f'theta_{l}_{q}')
            phi = Parameter(f'phi_{l}_{q}')
            lam = Parameter(f'lambda_{l}_{q}')
            layer_params.append((theta, phi, lam))
        parameters.append(layer_params)

    for l in range(reps):
        for q in range(n_qubits):
            theta, phi, lam = parameters[l][q]
            qc.rx(theta, q)
            qc.ry(phi, q)
            qc.rz(lam, q)
        entangle_range = l + 1 
        for i in range(n_qubits):
            target = (i + entangle_range) % n_qubits
            if i != target:
                if entangler == "cx":
                    qc.cx(i, target)
                elif entangler == "cz":
                    qc.cz(i, target)
    return qc

def create_feature_map(n_qubits):
    feature_map = ZZFeatureMap(n_qubits)
    qc = QuantumCircuit(n_qubits)
    qc.compose(feature_map, inplace=True)
    simulator = AerSimulator(method='statevector')    
    compiled_qc = transpile(qc, simulator)
    return compiled_qc

def create_ansatz(n_qubits, num_layers):
    ansatz = StronglyEntanglingLayers(n_qubits, reps=num_layers)
    qc = QuantumCircuit(n_qubits)
    qc.compose(ansatz, inplace=True)
    simulator = AerSimulator(method='statevector')    
    compiled_qc = transpile(qc, simulator)
    return compiled_qc
    
feature_map = create_feature_map(4)
ansatz = create_ansatz(4, 2)
# feature_map.draw('mpl')
dev = qml.device("lightning.qubit", wires=4)
feature_map_pl = pennylane_qiskit.load(feature_map, measurements=None)
ansatz_pl = pennylane_qiskit.load(ansatz, measurements=None)


@qml.qnode(dev,interface="torch",diff_method="best") 
def qnode(inputs,params):
    x = inputs.clone().detach().requires_grad_(False)
    ansatz_params = OrderedDict({
        f"theta_{i//4}_{i%4}": params[i] for i in range(8)
    })
    ansatz_params.update({
        f"phi_{i//4}_{i%4}": params[8 + i] for i in range(8)
    })
    ansatz_params.update({
        f"lambda_{i//4}_{i%4}": params[16 + i] for i in range(8)
    })
    feature_map_pl(x , wires=range(4))  
    ansatz_pl(**ansatz_params, wires=range(4))  
    return [qml.expval(qml.PauliZ(i)) for i in range(4)]  

weight_shapes = {"params": 24}
qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset


class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(16, 4) 
        self.qdense = nn.Sequential(
            nn.Linear(4, 4),
            qlayer
        )
        self.fc3 = nn.Linear(4, 1)
        self.sigmoid = nn.Sigmoid()  
    def forward(self, x):
        x = self.fc1(x)
        x = torch.relu(x)
        x = self.qdense(x)
        x = torch.relu(x)
        x = self.fc3(x)
        x = self.sigmoid(x)
        return x

def generate_data(num_samples=1000):
    X = torch.randn(num_samples, 16)
    y = torch.randint(0, 2, (num_samples, 1), dtype=torch.float32)
    return X, y

X, y = generate_data(1000)
dataset = TensorDataset(X, y)
data_loader = DataLoader(dataset, batch_size=25, shuffle=True)

model = SimpleNet()

criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

def train(model, data_loader, criterion, optimizer, epochs=10):
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for batch_idx, (inputs, targets) in enumerate(data_loader):
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, targets)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        print(f"Epoch {epoch + 1}/{epochs}, Loss: {total_loss / len(data_loader):.4f}")

train(model, data_loader, criterion, optimizer)

If you want help with diagnosing an error, please put the full error message below:

{
	"name": "CircuitError",
	"message": "\"Parameter vector 'x' has length 4, but was assigned to 25 values.\"",
	"stack": "---------------------------------------------------------------------------
CircuitError                              Traceback (most recent call last)
Cell In[17], line 57
     54         print(f\"Epoch {epoch + 1}/{epochs}, Loss: {total_loss / len(data_loader):.4f}\")
     56 # 训练模型
---> 57 train(model, data_loader, criterion, optimizer)

Cell In[17], line 48, in train(model, data_loader, criterion, optimizer, epochs)
     46 for batch_idx, (inputs, targets) in enumerate(data_loader):
     47     optimizer.zero_grad()
---> 48     outputs = model(inputs)
     49     loss = criterion(outputs, targets)
     50     loss.backward()

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1736, in Module._wrapped_call_impl(self, *args, **kwargs)
   1734     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1735 else:
-> 1736     return self._call_impl(*args, **kwargs)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1747, in Module._call_impl(self, *args, **kwargs)
   1742 # If we don't have any hooks, we want to skip the rest of the logic in
   1743 # this function, and just call forward.
   1744 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1745         or _global_backward_pre_hooks or _global_backward_hooks
   1746         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1747     return forward_call(*args, **kwargs)
   1749 result = None
   1750 called_always_called_hooks = set()

Cell In[17], line 20, in SimpleNet.forward(self, x)
     18 x = self.fc1(x)
     19 x = torch.relu(x)
---> 20 x = self.qdense(x)
     21 x = torch.relu(x)
     22 x = self.fc3(x)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1736, in Module._wrapped_call_impl(self, *args, **kwargs)
   1734     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1735 else:
-> 1736     return self._call_impl(*args, **kwargs)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1747, in Module._call_impl(self, *args, **kwargs)
   1742 # If we don't have any hooks, we want to skip the rest of the logic in
   1743 # this function, and just call forward.
   1744 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1745         or _global_backward_pre_hooks or _global_backward_hooks
   1746         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1747     return forward_call(*args, **kwargs)
   1749 result = None
   1750 called_always_called_hooks = set()

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\container.py:250, in Sequential.forward(self, input)
    248 def forward(self, input):
    249     for module in self:
--> 250         input = module(input)
    251     return input

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1736, in Module._wrapped_call_impl(self, *args, **kwargs)
   1734     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1735 else:
-> 1736     return self._call_impl(*args, **kwargs)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\torch\
n\\modules\\module.py:1747, in Module._call_impl(self, *args, **kwargs)
   1742 # If we don't have any hooks, we want to skip the rest of the logic in
   1743 # this function, and just call forward.
   1744 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1745         or _global_backward_pre_hooks or _global_backward_hooks
   1746         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1747     return forward_call(*args, **kwargs)
   1749 result = None
   1750 called_always_called_hooks = set()

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\qnn\\torch.py:404, in TorchLayer.forward(self, inputs)
    401     inputs = torch.reshape(inputs, (-1, inputs.shape[-1]))
    403 # calculate the forward pass as usual
--> 404 results = self._evaluate_qnode(inputs)
    406 if isinstance(results, tuple):
    407     if has_batch_dim:

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\qnn\\torch.py:430, in TorchLayer._evaluate_qnode(self, x)
    418 \"\"\"Evaluates the QNode for a single input datapoint.
    419 
    420 Args:
   (...)
    424     tensor: output datapoint
    425 \"\"\"
    426 kwargs = {
    427     **{self.input_arg: x},
    428     **{arg: weight.to(x) for arg, weight in self.qnode_weights.items()},
    429 }
--> 430 res = self.qnode(**kwargs)
    432 if isinstance(res, torch.Tensor):
    433     return res.type(x.dtype)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\workflow\\qnode.py:1020, in QNode.__call__(self, *args, **kwargs)
   1018 if qml.capture.enabled():
   1019     return qml.capture.qnode_call(self, *args, **kwargs)
-> 1020 return self._impl_call(*args, **kwargs)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\workflow\\qnode.py:1002, in QNode._impl_call(self, *args, **kwargs)
    999     override_shots = kwargs[\"shots\"]
   1001 # construct the tape
-> 1002 self.construct(args, kwargs)
   1004 original_grad_fn = [self.gradient_fn, self.gradient_kwargs, self.device]
   1005 self._update_gradient_fn(shots=override_shots, tape=self._tape)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\logging\\decorators.py:61, in log_string_debug_func.<locals>.wrapper_entry(*args, **kwargs)
     54     s_caller = \"::L\".join(
     55         [str(i) for i in inspect.getouterframes(inspect.currentframe(), 2)[1][1:3]]
     56     )
     57     lgr.debug(
     58         f\"Calling {f_string} from {s_caller}\",
     59         **_debug_log_kwargs,
     60     )
---> 61 return func(*args, **kwargs)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane\\workflow\\qnode.py:851, in QNode.construct(self, args, kwargs)
    849 with pldb_device_manager(self.device):
    850     with qml.queuing.AnnotatedQueue() as q:
--> 851         self._qfunc_output = self.func(*args, **kwargs)
    853 self._tape = QuantumScript.from_queue(q, shots)
    855 params = self.tape.get_parameters(trainable_only=False)

Cell In[15], line 18, in qnode(inputs, params)
     12 ansatz_params.update({
     13     f\"phi_{i//4}_{i%4}\": params[8 + i] for i in range(8)
     14 })
     15 ansatz_params.update({
     16     f\"lambda_{i//4}_{i%4}\": params[16 + i] for i in range(8)
     17 })
---> 18 feature_map_pl(x , wires=range(4))  
     19 ansatz_pl(**ansatz_params, wires=range(4))  
     20 return [qml.expval(qml.PauliZ(i)) for i in range(4)]

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane_qiskit\\converter.py:484, in load.<locals>._function(params, wires, *args, **kwargs)
    482 params = _format_params_dict(quantum_circuit, params, *args, **kwargs)
    483 unbound_params = _extract_variable_refs(params)
--> 484 qc = _check_circuit_and_assign_parameters(quantum_circuit, params, unbound_params)
    486 # Wires from a qiskit circuit have unique IDs, so their hashes are unique too
    487 qc_wires = [hash(q) for q in qc.qubits]

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\pennylane_qiskit\\converter.py:312, in _check_circuit_and_assign_parameters(quantum_circuit, params, diff_params)
    309     del params[k]
    311 # Disabling \"strict\" assignment allows extra parameters to be ignored.
--> 312 return quantum_circuit.assign_parameters(params, strict=False)

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\qiskit\\circuit\\quantumcircuit.py:4334, in QuantumCircuit.assign_parameters(self, parameters, inplace, flat_input, strict)
   4331     target._name_update()
   4333 if isinstance(parameters, collections.abc.Mapping):
-> 4334     raw_mapping = parameters if flat_input else self._unroll_param_dict(parameters)
   4335     our_parameters = self._data.unsorted_parameters()
   4336     if strict and (extras := raw_mapping.keys() - our_parameters):

File c:\\Users\\huminjun\\anaconda3\\envs\\PennyLane\\Lib\\site-packages\\qiskit\\circuit\\quantumcircuit.py:4391, in QuantumCircuit._unroll_param_dict(self, parameter_binds)
   4389 if isinstance(parameter, ParameterVector):
   4390     if len(parameter) != len(value):
-> 4391         raise CircuitError(
   4392             f\"Parameter vector '{parameter.name}' has length {len(parameter)},\"
   4393             f\" but was assigned to {len(value)} values.\"
   4394         )
   4395     out.update(zip(parameter, value))
   4396 elif isinstance(parameter, str):

CircuitError: \"Parameter vector 'x' has length 4, but was assigned to 25 values.\""
}

qml.about().

Name: PennyLane
Version: 0.38.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: c:\Users\huminjun\anaconda3\envs\PennyLane\Lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, packaging, pennylane-lightning, requests, rustworkx, scipy, toml, typing-extensions
Required-by: PennyLane-qiskit, PennyLane_Lightning

Platform info:           Windows-10-10.0.19045-SP0
Python version:          3.12.4
Numpy version:           2.1.1
Scipy version:           1.14.0
Installed devices:
- default.clifford (PennyLane-0.38.0)
- default.gaussian (PennyLane-0.38.0)
- default.mixed (PennyLane-0.38.0)
- default.qubit (PennyLane-0.38.0)
- default.qubit.autograd (PennyLane-0.38.0)
- default.qubit.jax (PennyLane-0.38.0)
- default.qubit.legacy (PennyLane-0.38.0)
- default.qubit.tf (PennyLane-0.38.0)
- default.qubit.torch (PennyLane-0.38.0)
- default.qutrit (PennyLane-0.38.0)
- default.qutrit.mixed (PennyLane-0.38.0)
- default.tensor (PennyLane-0.38.0)
- null.qubit (PennyLane-0.38.0)
- lightning.qubit (PennyLane_Lightning-0.38.0)
- qiskit.aer (PennyLane-qiskit-0.39.0)
- qiskit.basicaer (PennyLane-qiskit-0.39.0)
- qiskit.basicsim (PennyLane-qiskit-0.39.0)
- qiskit.remote (PennyLane-qiskit-0.39.0)

Hi @gigajun , welcome to the Forum!

I’m not completely sure but I think the issue might be related to the issue shown here. The solution I proposed there would break the batch into each individual element before running it through the QNode.

I’ve created a very “draft” hybrid between the code I shared in that thread and your code. It doesn’t work perfect but it also doesn’t show any errors anymore so hopefully it can be a good starting point for you to keep iterating from there.

# imports
import torch
import numpy as np
import pennylane as qml
import pennylane_qiskit
from qiskit_aer import Aer
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import ZZFeatureMap
from qiskit_aer import AerSimulator
from collections import OrderedDict

import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset


# Quantum circuit
# I didn't modify anything here
def StronglyEntanglingLayers(n_qubits, reps=2, entangler="cx"):
    qc = QuantumCircuit(n_qubits)
    parameters = []
    for l in range(reps):
        layer_params = []
        for q in range(n_qubits):
            theta = Parameter(f'theta_{l}_{q}')
            phi = Parameter(f'phi_{l}_{q}')
            lam = Parameter(f'lambda_{l}_{q}')
            layer_params.append((theta, phi, lam))
        parameters.append(layer_params)

    for l in range(reps):
        for q in range(n_qubits):
            theta, phi, lam = parameters[l][q]
            qc.rx(theta, q)
            qc.ry(phi, q)
            qc.rz(lam, q)
        entangle_range = l + 1 
        for i in range(n_qubits):
            target = (i + entangle_range) % n_qubits
            if i != target:
                if entangler == "cx":
                    qc.cx(i, target)
                elif entangler == "cz":
                    qc.cz(i, target)
    return qc

def create_feature_map(n_qubits):
    feature_map = ZZFeatureMap(n_qubits)
    qc = QuantumCircuit(n_qubits)
    qc.compose(feature_map, inplace=True)
    simulator = AerSimulator(method='statevector')    
    compiled_qc = transpile(qc, simulator)
    return compiled_qc

def create_ansatz(n_qubits, num_layers):
    ansatz = StronglyEntanglingLayers(n_qubits, reps=num_layers)
    qc = QuantumCircuit(n_qubits)
    qc.compose(ansatz, inplace=True)
    simulator = AerSimulator(method='statevector')    
    compiled_qc = transpile(qc, simulator)
    return compiled_qc
    
feature_map = create_feature_map(4)
ansatz = create_ansatz(4, 2)
# feature_map.draw('mpl')
dev = qml.device("lightning.qubit", wires=4)
feature_map_pl = pennylane_qiskit.load(feature_map, measurements=None)
ansatz_pl = pennylane_qiskit.load(ansatz, measurements=None)

# QNode (slightly modified)
@qml.qnode(dev,interface="torch",diff_method="best") 
def qnode(inputs,params):
    x = inputs.clone().detach().requires_grad_(False)
    ansatz_params = OrderedDict({
        f"theta_{i//4}_{i%4}": params[i] for i in range(8)
    })
    ansatz_params.update({
        f"phi_{i//4}_{i%4}": params[8 + i] for i in range(8)
    })
    ansatz_params.update({
        f"lambda_{i//4}_{i%4}": params[16 + i] for i in range(8)
    })
    #feature_map_pl(x , wires=range(4))  # Commented this
    ansatz_pl(**ansatz_params, wires=range(4))  
    return [qml.expval(qml.PauliZ(i)) for i in range(4)]

# Data and batches (modified)
n_samples = 50 #1000
batch_size = 2 # 25
batches = n_samples // batch_size

def generate_data(num_samples=n_samples):
    X = torch.randn(num_samples, 4)
    y = torch.randint(0, 2, (num_samples, 1), dtype=torch.float32)
    return X, y

X, y = generate_data(n_samples)
dataset = TensorDataset(X, y)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# Model (completely changed)
n_qubits = 4
class HybridModel(nn.Module):

    def __init__(self):

        super().__init__()
        self.clayer_1 = nn.Linear(4, n_qubits)
        self.q_params = nn.Parameter(torch.randn(24))
        self.clayer_2 = nn.Linear(n_qubits, 2)
        self.softmax = nn.Softmax(dim=1)

    def forward(self, inputs):
        q_in = self.clayer_1(inputs)

        # Apply the quantum circuit to each element of the batch and append to q_out
        q_out = torch.Tensor(0, n_qubits)

        for elem in q_in:
            q_out_elem = torch.hstack(qnode(elem, self.q_params)).float().unsqueeze(0)
            q_out = torch.cat((q_out, q_out_elem))

        # return the two-dimensional prediction from the postprocessing layer
        x = self.clayer_2(q_out)
        return self.softmax(x)

model = HybridModel()

# Optimization (completely changed)
epochs = 2
loss = nn.L1Loss()
opt = torch.optim.SGD(model.parameters(), lr=0.2)

for epoch in range(epochs):

    running_loss = 0

    for xs, ys in data_loader:
        opt.zero_grad()

        loss_evaluated = loss(model(xs), ys)
        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))

y_pred = model(X)
predictions = torch.argmax(y_pred, axis=1).detach().numpy()

correct = [1 if p == p_true else 0 for p, p_true in zip(predictions, y)]
accuracy = sum(correct) / len(correct)
print(f"Accuracy: {accuracy * 100}%")

I hope this helps! Let me know if you have any further questions.