How to Use qml.GradientDescentOptimizer with AngleEmbedding Template

Hi , I am trying to use qml.GradientDescentOptimizer along with Templates as follows ,

from sklearn.datasets import load_iris

# import some data to play with

iris = datasets.load_iris()

X = iris.data[:, :]  # we only take the first two features.

Y = iris.target

n_qubits = 4

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

@qml.qnode(dev)

def qnode(X,weights):

    qml.templates.AngleEmbedding(X, wires=range(n_qubits))

    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))

    return [qml.expval(qml.PauliZ(i)) for i in range(n_qubits)]

weights = np.array([0.1,0.2,0.3,0.4,0.5,0.6,0.1,0.2,0.3,0.4,0.5,0.6]).reshape(1,4,3)

def cost(x,weights):

    return qnode(x,weights)

opt = qml.GradientDescentOptimizer(stepsize=0.1)

# set the number of steps

steps = 100

# set the initial parameter values

params = weights

for i in range(steps):

    # update the circuit parameters

    weights = opt.step(cost, weights)

    if (i + 1) % 5 == 0:

        print("Cost after step {:5d}: {: .7f}".format(i + 1, cost(params)))

print("Optimized rotation angles: {}".format(params))

The problem I have is that how should I be passing inputs and weights to the cost function ?

I was able to do the same with Keras layer since it allows me to pass weight_shapes and data was passed during model.fit(X,Y)

Is it expected that we follow steps given in https://github.com/HIPS/autograd/blob/master/docs/tutorial.md

Hi @Hemant_Gahankari,

You can find more details on using the Autograd interface here. You could also check out this tutorial.

It seems like the problem is in opt.step(). Here, one should pass a function with a single argument (the trainable weights):

opt.step(lambda weights: cost(X[i], weights), weights)

Also, note that the cost function should return a single number, so that opt.step() knows what to minimize.

Hope that helps!

Thanks for the suggestion , I made a few changes as suggested ,

n_qubits = 4

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

@qml.qnode(dev)

def qnode(X,weights):

    qml.templates.AngleEmbedding(X, wires=range(n_qubits))

    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))

    return [qml.expval(qml.PauliZ(i)) for i in range(n_qubits)]

weights = np.array([0.1,0.2,0.3,0.4,0.5,0.6,0.1,0.2,0.3,0.4,0.5,0.6]).reshape(1,4,3)

opt = qml.GradientDescentOptimizer(stepsize=0.1)

epochs = 10

for e in range(epochs):

  for i in X:

    weights = opt.step(lambda weights: qnode(X[i], weights), weights)

I get an error , IndexError: arrays used as indices must be of integer (or boolean) type.

Hi @Hemant_Gahankari,

A couple of thoughts here:

  1. The definition of the quantum function would require an update so that the non-differentiable parameter (X) has a default parameter: def qnode(weights, X=None). This is required when defining the QNode (included a bit more context in this answer).
  2. The quantum function that you’ve defined currently returns an array with 4 (n_qubits) scalars. Therefore optimization will happen as described for Vector-valued QNodes:
for e in range(epochs):
    for i in range(len(X)):
        weights = opt.step(lambda weights: cost(weights, X=X[i]), weights, grad_fn=lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X[i]))

As convenience, defined a cost function:

def cost(weights, X):
    res = qnode(weights, X=X)
    return res

Breaking this down a bit:

  • lambda weights: cost(weights, X=X[i]): this helps to pass the current batch of features (X[i]) to the QNode
  • lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X[i]): this has to be defined so that Autograd can compute the gradient of the cost function. argnum=0 is specified as the first argument is differentiable, and we need to pass X[i] as keyword argument.

  1. A small adjustment would be required for looping through the extracted features (for i in range(len(X)) loop definition):
for e in range(epochs):
    for i in range(len(X)):
        weights = opt.step(lambda weights: cost(weights, X=X[i]), weights, grad_fn=lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X[i]))
  1. A possible way of creating batches for each step is defining a batch_size and selecting a random batch for each epoch:
batch_size = 4
for e in range(epochs):
    batch_index = np.random.randint(0, len(X), (batch_size))
    X_batch = X[batch_index,0]
    weights = opt.step(lambda weights: cost(weights, X=X_batch), weights, grad_fn=lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X_batch))

Hopefully, this gives a bit of direction! :slight_smile:

(Note: edited this answer as on the first read didn’t consider the solution as a vector-valued QNode, but rather a QNode that has a tensor product observable as measurement and returns a single scalar. Let us know if specifying a tensor product observable would be desired here, and can provide further pointers!)

Thanks a lot for giving some clarity on this. The optimization loop is running fine now. I need your suggestion on writing a loss/cost function here. I am using iris dataset as follows and trying to predict the class.

The idea here is to adjust weights of the StronglyEntangledLayer , so that loss is minimised.

The catch is how to write a loss function for comparing 4 expected value returns and hot encoded label consisting of 3 bits. Like in Keras, when we compile we select loss function, is there any way to do so here as well ? If not how should we compare expectation values, 4 in this case and hot encoded label ?

from sklearn.datasets import load_iris

# import some data to play with

iris = datasets.load_iris()

X = iris.data[:, :]  # we only take the first two features.

Y = iris.target

trainX, testX, trainy, testy = train_test_split(X, Y, test_size=0.3, random_state=42)

trainy = tf.one_hot(trainy, depth=3)

testy = tf.one_hot(testy, depth=3)

n_qubits = 4

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

@qml.qnode(dev)

def qnode(weights,X=None):

    qml.templates.AngleEmbedding(X, wires=range(n_qubits))

    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))

    return [qml.expval(qml.PauliZ(i)) for i in range(n_qubits)]

weights = np.array([0.1,0.2,0.3,0.4,0.5,0.6,0.1,0.2,0.3,0.4,0.5,0.6]).reshape(1,4,3)

def cost(weights, X):

    res = qnode(weights, X=X)

    return res

opt = qml.GradientDescentOptimizer(stepsize=0.1)

epochs = 10

for e in range(epochs):

    print(e)

    for i in range(len(X)):

        weights = opt.step(lambda weights: cost(weights, X=X[i]), weights, grad_fn=lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X[i]))

        print(weights)

Thanks for your time and help in advance.

I am trying to use something like below ,

from sklearn.metrics import log_loss

def cost1(weights, X , Y):

    pred = qnode(weights, X=X)

    return log_loss(Y,pred)


opt = qml.GradientDescentOptimizer(stepsize=0.1)

epochs = 10

for e in range(epochs):

    print(e)

    for i in range(len(X)):

        weights = opt.step(lambda weights: cost1(weights, X=trainX[i],Y=trainy[i]), weights, grad_fn=lambda weights: qml.jacobian(qnode, argnum=0)(weights, X=X[i]))

        print(weights)

Do you see any problem with this ? Also what is a good way to print cost1 ? like if I execute cost1 , it updates weights , so how to print cost1 ?

Hi @Hemant_Gahankari,

One challenge with the definition of cost1 is that its gradient needs to support automatic differentiation and this involves both classical processing (using sklearn) and quantum computation (QNode using qml.jacobian). As sklearn does not support automatic differentiation, his might not be feasible, unfortunately. For further points see this relevant discussion on using sklearn in a cost function. As suggested there, using a supported PennyLane interface (Torch/TF) or using layers from qml.qnn package with built-in optimizers & functions could be a feasible approach.

Printing cost1: one way to do this is to evaluate the cost function right before/after the update of weights.

Hope this gives an idea for moving forward! :slight_smile:


For further reference, could you help out with a couple of small adjustments when sharing code?

  • Adjusting formatting of the code (e.g., enclosing strings with "" and '' instead of ““, double-checking that imports are correct and the code snippet can be executed independently)
  • Placing code into a highlighted code blocks so that the code renders and indentation is correct

Overall this helps with quickly recreating the case that is shared and also helps other users easily get up to speed with parts of the solution.

Thank you so much! :slight_smile:

1 Like

Hi , I am trying to use tf optimizer and loss function as follows , but getting an error , TypeError: zip argument #2 must support iteration. What do you think could be the issue here ?

import pennylane as qml
import tensorflow as tf
from sklearn import datasets
from pennylane import numpy as np
from sklearn.model_selection import train_test_split

from sklearn.datasets import load_iris

# import some data to play with

iris = datasets.load_iris()

X = iris.data[:, :]  # we only take the first two features.

Y = iris.target

trainX, testX, trainy, testy = train_test_split(X, Y, test_size=0.3, random_state=42)

trainy = tf.one_hot(trainy, depth=4)

testy = tf.one_hot(testy, depth=4)

n_qubits = 4

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

@qml.qnode(dev, interface='tf')

def qnode(weights,X=None):

    qml.templates.AngleEmbedding(X, wires=range(n_qubits))

    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))

    return [qml.expval(qml.PauliZ(i)) for i in range(n_qubits)]


weights = np.array([0.1,0.2,0.3,0.4,0.5,0.6,0.1,0.2,0.3,0.4,0.5,0.6]).reshape(1,4,3)

weights = tf.Variable(weights, dtype=tf.float32 ,trainable=True)

opt = tf.keras.optimizers.SGD(learning_rate=0.1)

for i in range(len(trainX)):
    pred = qnode(weights, X=trainX[i])
    with tf.GradientTape() as tape:        
        loss = tf.keras.losses.categorical_crossentropy(trainy[i],pred)
        print(loss)
    gradients = tape.gradient(loss,weights)
    opt.apply_gradients(zip(gradients,weights))

I also tried making a change to the above optimizer calling code as follows but even that is giving an error , TypeError: ‘NoneType’ object is not callable. Can you suggest a right way to do this ?

opt = tf.keras.optimizers.SGD(learning_rate=0.1)

def cost(weights,X,Y):

  pred = qnode(weights, X=X)

  loss = tf.keras.losses.categorical_crossentropy(Y,pred)

for i in range(len(trainX)):

  weights = opt.minimize(cost(weights, X=trainX[i],Y=trainy[i]), weights)

Hi @Hemant_Gahankari,

I managed to get it working using

weights = tf.Variable(weights, dtype=tf.float64 ,trainable=True)

opt = tf.keras.optimizers.SGD(learning_rate=0.1)

for i in range(len(trainX)):
    with tf.GradientTape() as tape:        
        pred = qnode(weights, X=trainX[i])
        loss = tf.keras.losses.categorical_crossentropy(trainy[i],pred)

    gradients = tape.gradient(loss, [weights])

    opt.apply_gradients(zip(gradients,[weights]))

Things to note:

  • When directly using a QNode rather than a KerasLayer, the output tensor will be of type tf.float64. Hence, the weights also have to be cast to the same type.
  • The pred = qnode(weights, X=trainX[i]) line must be included within the tape context to keep track of the gradient, otherwise the gradient will be None.
  • weights is nested as [weights] to get things to work.

An alternative to the above could be:

weights = tf.Variable(weights, dtype=tf.float64 ,trainable=True)

opt = tf.keras.optimizers.SGD(learning_rate=0.1)

def loss(weights, x, y):
    pred = qnode(weights, X=x)
    return tf.keras.losses.categorical_crossentropy(y,pred)

for x, y in zip(trainX, trainy):
    opt.minimize(lambda: loss(weights, x, y), [weights])
1 Like

Thanks a lot for your help , it is working now.

1 Like