Closed mckunkel closed 1 year ago
Can you please clarify your question? You can create an instance of TorchConnector
backed by a QNN that is created on top of your custom circuit. See the tutorial here: https://qiskit.org/documentation/machine-learning/tutorials/05_torch_connector.html
Greetings,
Unless I am mistaken, the example you provided is not a custom circuit, rather it relies on built-in feature map and ansatz modules, i.e.
feature_map = ZZFeatureMap(2)
ansatz = RealAmplitudes(2, reps=1)
What I am referring in the terms of custom circuit is that I have my own circuit, with a custom state preparation and a custom ansatz that is returned as a whole circuit, for example
def MKCircuit(numQubits, numClassical, setup, inputAngles):
circuit = QuantumCircuit(numQubits + numClassical, numClassical)
# Encoding the input data into the quantum state
#the input data are the weights coming from the linear layer of a pytorch NN
encodingsetup(circuit, setup, numQubits, inputAngles)
...
...
...
#now measure the last qubit
circuit.measure(numQubits, 0)
return circuit
and I have custom forward and backward functions to train this, but I rather move toward a more modern TorchConnector.
This is what I am referring to as a custom circuit. I hope this clarifies my intention.
BR MK
I see the point, but what is preventing you from replacing a library circuit with your own? Like
feature_map = MKCircuit(....)
Greetings and apologies for my lateness in reply,
I have been trying a few different ways to do as you suggested with no avail. I believe it has something to do with my ansatz not working properly with TorchConnector.
Is there help on how to write custom feature_maps and custom ansatz to work with Torchconnector?
Is there help on how to write custom feature_maps and custom ansatz to work with Torchconnector?
The question is too broad. Basically, the tutorial covers the topic pretty well.
Here is a quick snippet with truly custom circuits.
import torch
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from torch.nn import MSELoss
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN
fm = QuantumCircuit(2)
input_parameters = ParameterVector("x", 2)
fm.ry(input_parameters[0], 0)
fm.ry(input_parameters[1], 1)
fm.cx(0, 1)
ansatz = QuantumCircuit(2)
weights = ParameterVector("w", 4)
ansatz.ry(weights[0], 0)
ansatz.rz(weights[1], 0)
ansatz.ry(weights[2], 1)
ansatz.rz(weights[3], 1)
ansatz.cx(0, 1)
qnn_qc = QuantumCircuit(2)
qnn_qc.compose(fm, inplace=True)
qnn_qc.compose(ansatz, inplace=True)
qnn = EstimatorQNN(circuit=qnn_qc, input_params=input_parameters, weight_params=weights)
model = TorchConnector(qnn)
features = torch.rand((20, 2))
labels = torch.randint(0, 2, (20, 1)) * 2. - 1
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
criterion = MSELoss()
for i in range(10):
optimizer.zero_grad()
loss = criterion(model(features), labels)
print(f"Iteration: {i}, loss: {loss.item()}")
loss.backward()
optimizer.step()
I would like to start with that I appreciate the time you are investing in my question.
However, my question was very specific in the sense the custom feature maps and ansatzs are well covered for how a circuit can be created in how the package wants circuits to be constructed, but not for circuits you coined "truly custom".
Below is my circuit, that is truly custom. Currently I see no means on how to integrate this with TorchConnector, SamplerQNN or EstimatorQNN. Notice that my final observable, of a N qubit circuit in my setup, is the result of a measurement of a single ancillary qubit with a data encoding of the input parameter of the NN being used as rations and conditional rotations.
This is what I am regarding as a custom circuit.
For context, this circuit setup was used in the old method of hybrid MNST training by attaching it to a linear layer. It worked great, but was not able to fully utilize gpu running and trained rather slow on more sophisticated NN setups and backbones, such as Transformers.
def MKCircuit(numQubits):
fm = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
input_parameters = ParameterVector("theta", 3*numQubits)
#performing the input data encoding as a RY and a RZ for
#2 bits of data per qubit
for i in range(numQubits):
fm.ry(input_parameters[i], i)
fm.rz(input_parameters[numQubits+i], i)
fm.barrier()
# the ansatz is that upon entangling each qubit to another input parameter
# the circuit will train like a bounded 5D clock
ansatz = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
for i in range(numQubits-1):
ansatz.crx(input_parameters[2*numQubits+i], i, i+1)
#dont forget to close the clock and entangle the last qubit to the first qubit
ansatz.crx(input_parameters[3*numQubits-1], numQubits-1,0)
# now if all works as calculated, the result of the
# 5D clock projected to this ancilla
# qubit should train to the binary classification of my label map
ansatz.cx(numQubits-1 , numQubits)
#Only measure the ancilla qubit
ansatz.measure(numQubits, 0)
weights = ParameterVector("w", 4)
qnn_qc = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
qnn_qc.compose(fm, inplace=True)
qnn_qc.compose(ansatz, inplace=True)
ansatz = QuantumCircuit
return qnn_qc, input_parameters, weights
circuit, _ , _ = MKCircuit(3)
circuit.draw()
It is still possible to wrap this circuit into TorchConnector
. But I noticed that you have one single ParameterVector
for both input data and trainable weights. I updated the code the way I thought it should look like. This is what I've got:
import torch
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit import ParameterVector
from torch.nn import MSELoss
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import EstimatorQNN, SamplerQNN
def MKCircuit(numQubits):
fm = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
input_parameters = ParameterVector("theta", 2*numQubits)
#performing the input data encoding as a RY and a RZ for
#2 bits of data per qubit
for i in range(numQubits):
fm.ry(input_parameters[i], i)
fm.rz(input_parameters[numQubits+i], i)
fm.barrier()
# the ansatz is that upon entangling each qubit to another input parameter
# the circuit will train like a bounded 5D clock
weights = ParameterVector("w", numQubits)
ansatz = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
for i in range(numQubits-1):
ansatz.crx(weights[i], i, i+1)
#dont forget to close the clock and entangle the last qubit to the first qubit
ansatz.crx(weights[-1], numQubits-1,0)
# now if all works as calculated, the result of the
# 5D clock projected to this ancilla
# qubit should train to the binary classification of my label map
ansatz.cx(numQubits-1 , numQubits)
#Only measure the ancilla qubit
ansatz.measure(numQubits, 0)
# weights = ParameterVector("w", 4)
qnn_qc = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
qnn_qc.compose(fm, inplace=True)
qnn_qc.compose(ansatz, inplace=True)
return qnn_qc, input_parameters, weights
def train_custom(qnn_qc, input_parameters, weights):
qnn = SamplerQNN(
circuit=qnn_qc,
input_params=input_parameters,
weight_params=weights,
output_shape=2, interpret=lambda x: x
)
model = TorchConnector(qnn)
features = torch.rand((20, 6))
labels = torch.randint(0, 2, (20, 2)) * 2. - 1
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
criterion = MSELoss()
for i in range(10):
optimizer.zero_grad()
output = model(features)
loss = criterion(output, labels)
print(f"Iteration: {i}, loss: {loss.item()}")
loss.backward()
optimizer.step()
if __name__ == '__main__':
circuit, input_params, weights = MKCircuit(3)
train_custom(circuit, input_params, weights)
Key points here is that you set output_shape=2
and interpret
to the identity function. Thus, you tell the QNN that you are expecting a vector of two numbers as an output of the network as you measure only one qubit. And the identity does nothing with the measured results, just passes them as is. In general, you may transform the output somehow else and then interpret
can be more sophisticated. Hope this helps.
Greetings,
This does indeed help in the construction of my circuit. I can now train the circuit you provided, but it does not seem to function as it did when it was implemented without torch connector. If you could clarify my interpretation then I think I would know how to proceed next.
I believe it is not training as it did before because now we are no longer utilizing the quantum parameter shifting technique on the inputs for gradient searching as I did before, but now it's using the weights in the ansatz to train the network and not the NN input.
For comparison, a simple training NN New:
class HybridNet(Module):
def __init__(self, dim, numQubits):
super().__init__()
self.fc2 = Linear(dim, 2*numQubits) # 2*numQubits-dimensional input to QNN
self.qnn = TorchConnector(create_qnn(numQubits)) # Apply torch connector, weights chosen
def forward(self, x):
x = self.fc2(x)
x = self.qnn(x) # apply QNN
return cat((x, 1 - x), -1)
old:
class HybridNet(Module):
def __init__(self, dim, numQubits):
super().__init__()
self.fc2 = Linear(dim, 2*numQubits) # 2*numQubits-dimensional input to QNN
self.hybrid = Hybrid(numQubits, numClassical, backend, shots, shift, setup) # <--this has a forward and backward function with quantum parameter shift for gradient calculation
def forward(self, x):
x = self.fc2(x)
x = self.hybrid(x) # apply QNN
return cat((x, 1 - x), -1)
On the same MNST data, with the same training code
# Define model, optimizer, and loss function
hybridModel = HybridNet(28*28,3)
optimizer = optim.Adam(hybridModel.parameters(), lr=0.001)
loss_func = NLLLoss()
# Start training
epochs = 20 # Set number of epochs
hybrid_loss_list = [] # Store loss history
hybridModel.train() # Set model to training mode
for epoch in range(epochs):
total_loss = []
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad(set_to_none=True) # Initialize gradient
# Flatten MNIST images into a 784 long vector
data = data.view(data.shape[0], -1)
output = hybridModel(data) # Forward pass
loss = loss_func(output, target) # Calculate loss
loss.backward() # Backward pass
optimizer.step() # Optimize weights
total_loss.append(loss.item()) # Store loss
hybrid_loss_list.append(sum(total_loss) / len(total_loss))
print("Training [{:.0f}%]\tLoss: {:.4f}".format(100.0 * (epoch + 1) / epochs, hybrid_loss_list[-1]))
Yields: New Methodology:
Training [5%] Loss: -0.4932
Training [10%] Loss: -0.4932
...
...
Training [95%] Loss: -0.4933
Training [100%] Loss: -0.4933
Old Methodology:
Training [5%] Loss: -0.8658
Training [10%] Loss: -0.9484
Training [15%] Loss: -0.9613
...
...
Training [95%] Loss: -0.9847
Training [100%] Loss: -0.9856
Would this be the logical reasoning in why this circuit no longer trains as before?
Again, Many Thanks for all the help.
I don't have the whole script, but I guess you have to enable gradients wrt input data, like here:
qnn = EstimatorQNN(
circuit=qc,
input_params=feature_map.parameters,
weight_params=ansatz.parameters,
input_gradients=True,
)
I have not enabled them in the previous snippets since I had only an instance of the connector (=one layer) without additional layers, but when you have a hybrid network, this parameter input_gradients=True
is essential to propagate gradients. This parameter must be set in hybrid case to get proper training.
Answering your questions - if you don't pass a gradient instance to QNN, and this is what I omitted in the snippets above, then parameter shift gradient is evaluated by default.
Greetings,
Many thanks for that information, unfortunately, I had that parameter enabled previously and the hybrid network did not converge as it had previously without TorchConnector.
If I train the below circuit without TorchConnector, i.e. with forward and backward functions in the NN module, then I get a convergence of the network. If I train the same circuit with SamplerQNN and TorchConnector, I see no convergence.
def MKCircuit(numQubits):
fm = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
input_parameters = ParameterVector("theta", 3*numQubits)
#performing the input data encoding as a RY and a RZ for
#3 bits of data per qubit
for i in range(numQubits):
fm.ry(input_parameters[i], i)
fm.rz(input_parameters[numQubits+i], i)
fm.barrier()
for i in range(numQubits-1):
fm.crx(input_parameters[2*numQubits+i], i, i+1)
fm.crx(input_parameters[3*numQubits-1], numQubits-1,0)
fm.barrier()
# the ansatz is that upon entangling each qubit to another input parameter
# the circuit will train like a bounded 5D clock
weights = ParameterVector("w", numQubits)
ansatz = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
for i in range(numQubits):
ansatz.ry(weights[i], i)
# now if all works as calculated, the result of the
# 5D clock projected to this ancilla
# qubit should train to the binary classification of my label map
#Only measure the ancilla qubit
ansatz.barrier()
ansatz.measure(numQubits, 0)
qnn_qc = QuantumCircuit(QuantumRegister(numQubits+1), ClassicalRegister(1))
qnn_qc.compose(fm, inplace=True)
qnn_qc.compose(ansatz, inplace=True)
return qnn_qc, input_parameters, weights
qnn_qc, input_parameters, weights = MKCircuit(4)
qnn_qc.draw()
Here is how I use SamplerQNN (EstimatorQNN does not function here)
# Define and create QNN
def create_qnn(numQubits):
circuit, input_params, weights = MKCircuit(numQubits)
# REMEMBER TO SET input_gradients=True FOR ENABLING HYBRID GRADIENT BACKPROP
qnn = SamplerQNN(
circuit=circuit,
input_params=input_params,
weight_params=weights,
input_gradients=True,
output_shape=2, interpret=lambda x: x
)
return qnn
Here's my very simple NN structure that converged to 0.99 without TorchConnector and 0.5 with TorchConnector and SamplerQNN
class HybridNet(Module):
def __init__(self, dim, numQubits):
super().__init__()
self.fc2 = Linear(dim, 3*numQubits) # 3-dimensional input to QNN
self.qnn = TorchConnector(create_qnn(numQubits)) # Apply torch connector, weights chosen
def forward(self, x):
x = self.fc2(x)
x = self.qnn(x) # apply QNN
return cat((x, 1 - x), -1)
and here is the training script that derivative of already working examples
# Define model, optimizer, and loss function
hybridModel = HybridNet(28*28,12)
optimizer = optim.Adam(hybridModel.parameters(), lr=0.001)
loss_func = NLLLoss()
# Start training
epochs = 20 # Set number of epochs
hybrid_loss_list = [] # Store loss history
hybridModel.train() # Set model to training mode
for epoch in range(epochs):
total_loss = []
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad(set_to_none=True) # Initialize gradient
# Flatten MNIST images into a 784 long vector
data = data.view(data.shape[0], -1)
output = hybridModel(data) # Forward pass
loss = loss_func(output, target) # Calculate loss
loss.backward() # Backward pass
optimizer.step() # Optimize weights
total_loss.append(loss.item()) # Store loss
hybrid_loss_list.append(sum(total_loss) / len(total_loss))
print("Training [{:.0f}%]\tLoss: {:.4f}".format(100.0 * (epoch + 1) / epochs, hybrid_loss_list[-1]))
My previous method was a derivation of the hybrid network MSNT example found in the qiskit textbook. Any ideas why the network will not converge when I migrated over to TorchConnector and SamplerQNN? My only possible speculation is that "weights" are included in the circuit that I am using with TorchConnector where as on my previous method of training, I only used input parameters and back-propagated using quantum parameter shift.
Here is the data loader in case you wish to try it out
# Train Dataset
# -------------
# Set train shuffle seed (for reproducibility)
manual_seed(42)
batch_size = 1
n_samples = 100 # We will concentrate on the first 100 samples
# Use pre-defined torchvision function to load MNIST train data
X_train = datasets.MNIST(
root="./data", train=True, download=True, transform=transforms.Compose([transforms.ToTensor()])
)
# Filter out labels (originally 0-9), leaving only labels 0 and 1
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]
# Define torch dataloader with filtered data
train_loader = DataLoader(X_train, batch_size=batch_size, shuffle=True)
I guess something wrong with the circuit here. The measured qubit is not connected to other qubits.
@mckunkel do need more help in this issue?
@adekusar-drl I had thought I sent a comment back giving you accolades for all the wonderful insights and helping me get to an end state, but alas I think I hit enter and didn't notice it didn't go through.
Yes, this issue has been resolved and I thank you very much.
BR MK
Thanks, appreciated!
What should we add?
I do not see how I can create a custom circuit and use TorchConnector. Would be great if this availability was created, and if it already exists, documentation on it.