I’m constructing a VQC class using Pennylane. All’s working well save one exception – I’m finding that having the tensor of parameters as a property of the class is causing issues with automatic differentiation.
Main Question: Do tensors in autograd’s computational graph need to be in globals()
? If not, how can i explicitly pass the parameter tensor to the optimization function s.t. autograd is able to use it? Below is a stripped down version of my code showing how the references to the parameters (self.theta
), circuit, and qnode are being passed around:
class QuantumClassifier(object):
def __init__(self, model_config:dict={}):
self.n_layers = model_config["n_layers"]
self.m_qubits = model_config["m_qubits"]
# ...other properties defined here
self.theta_shape = (self.n_layers, self.m_qubits, 3)
self.theta = (np.random.randn(*self.theta_shape) * np.pi, 0.0)
def circuit (self, X:np.ndarray):
qml.templates.AmplitudeEmbedding(X, wires = self.qubits[self.input_qubit_register], pad_with = 1.0, normalize = True)
for i in range(self.n_layers):
self.layer(layer_index = i)
return [qml.expval(qml.PauliZ(qubit)) for qubit in self.qubits[self.ancilla_qubit_register]]
def cost_function (self, y:np.ndarray, yhat:np.ndarray):
return np.sum(np.subtract(y, yhat) ** 2) / y.shape[0]
def train(self, X:np.ndarray, y:np.ndarray, batch_size:int=10, epochs:int=1):
self.qnode = qml.QNode(self.circuit, device = self.device, diff_method = self.differentiation_method)
for epoch in range(epochs):
for i in range((X.shape[0] // batch_size) + 1):
batch_index = np.random.randint(0, X.shape[0], (batch_size,))
X_batch, y_batch = X[batch_index], y[batch_index]
yhat_batch = np.array([self.qnode(X_batch[j,:], shots = self.shots) for j in range(batch_size)], dtype = "float32")
self.theta = self.optimizer.step(lambda theta: self.cost_function(y_batch, yhat_batch), self.theta)
A Bit More Context: This issue seems to have arisen from a separate issue with the qnode
decorator. Specifically, if you apply this decorator to a class method and provide kwdargs
from the class instance (e.g. @qml.qnode(self.device)
), then the decorator doesn’t work (from my understanding, this is expected and can be viewed as a design constraint imposed by the decorator approach).
An obvious first attempt at a workaround is to initialize the qnode
within a class method that receives self
as the first parameter as I did above:
def train(self, X:np.ndarray, y:np.ndarray, batch_size:int=10, epochs:int=1):
self.qnode = qml.QNode(self.circuit, device = self.device, diff_method = self.diff_method)
# ...
From what I’ve read in the Pennylane documentation, it seems this isn’t the preferred method of instantiating a QNode, but for my use case it appears necessary given the challenge with decorators mentioned above.
I know that decorators are common to other JIT and ML libraries that consider specific hardware devices (numba, jax, tensorflow, pytorch, etc.). However, it seems to me that if the end-user wants to automate, say, many experiments with differentiation method as a variable, this library architecture inhibits their ability to build extensible classes on top of the library.
Any advice/guidance on how to handle this (or a response showing me the error of my ways!) would be very much appreciated.
Thanks,
Ben
P.S. When I output the tensor instantiated in the __init__
method, it does appear to be able to attach to autograd:
(
tensor(
[[[ 0.12316359, -0.03912076, -0.06289622],
[ 0.63608486, -0.26805952, -0.00448637],
[ 0.06343109, -0.25281549, -0.45259441],
[ 0.08460864, 0.56978016, 0.12710347],
[ 0.02829714, 0.02599053, -0.08118433],
[ 0.37180194, -0.15878937, -0.42624673]]],
requires_grad=True),
0.0
)