Quantum NNs with feature gradients

I want to give a quantum NN a set of point locations and optimize an ansantz such that du/dx = 1 with u(x=0) = 0, 0 <= x <= 1. The solution here is a simple, u(x) = x. My question is (1) is the code i wrote doing what I think I it is, and (2) if so, I’ve noticed that it gives back pretty close to linear solutions but with a slope /= 1, even though I am asking it to enforce this. Could this because the quantum solution is only proportional to the answer?
Thanks everyone!


import pennylane as qml
from pennylane import numpy as np

import autograd as ag

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

nlayers = 3
nqbits = 2
max_iterations = 2000 # Maximum number of calls to the optimizer 

#shape = qml.StronglyEntanglingLayers.shape(n_layers=nlayers, n_wires=nqbits)
#params = np.random.random(size=shape)

nparams = 3 * nlayers
params = np.random.random([nparams], requires_grad=True)
print("initial params: ",params)

#x = np.array([[0.1, 0.2], [0.3, 0.4], [1.1, 1.2], [1.3, 1.4]], requires_grad=True)
x = np.array([[0.0], [0.3], [0.5], [0.8]], requires_grad=True)
opt = qml.GradientDescentOptimizer(0.01)


@qml.qnode(dev)
def circuit(params, x):
    #qml.RX(x[:, 0], wires=0)
    #qml.RY(x[:, 1], wires=1)
    #qml.RZ(x[:, 0], wires=2)
    qml.templates.AngleEmbedding(x, wires=range(nqbits))

    #qml.templates.StronglyEntanglingLayers(params, wires=range(nqbits))

    for i in range(0,nlayers):
        qml.RX(params[i*(nlayers-1) + 0], wires=[0])
        qml.RY(params[i*(nlayers-1) + 1], wires=[1])
        qml.CNOT(wires=[0,1])
        qml.RY(params[i*(nlayers-1) + 2], wires=[0])

    #qml.RX(params[0], wires=0)
    #qml.RY(params[1], wires=1)
    #qml.RZ(params[2], wires=2)

    return qml.expval(qml.PauliZ(0))


def loss(params, x):
    grad_circ = ag.elementwise_grad(circuit)
    #print(grad_circ(params, x)[0]) # I think* [0] = x, [1] = 1, [2] = params
    #print(grad_circ(params, x)[1])
    #print(grad_circ(params, x)[2])
    u_x = grad_circ(params, x)[0]

    #grad2_circ = ag.elementwise_grad(grad_circ)
    #u_xx = grad2_circ(params, x)[0]

    #print("u_x: ",u_x)
    #print("u_xx: ",u_xx)

    print("circuit at x=0: ",circuit(params,x[0]))
    print("u_x: ",u_x);

    cost = np.abs(u_x - 1) + np.abs(circuit(params,x[0]))
    
    return cost

cost = [loss(params,x)] # Store the values of the cost function
for n in range(max_iterations):
    (params,  _), prev_cost = opt.step_and_cost(loss,params,x)
    cost.append(loss(params,x))
    print(f"Step = {n},  Cost function = {cost[-1]:.8f} ")

print("\n" f"Final value of the cost function = {cost[-1]:.8f} ")

plt.semilogy(range(len(cost)), cost)
plt.ylabel("loss")
plt.xlabel("iteration")

Here is an example of the list few lines of output:

Step = 1997, Cost function = 0.04233369
circuit at x=0: Autograd ArrayBox with value 0.0027276330820018013
u_x: Autograd ArrayBox with value 1.039606057608671
circuit at x=0: -0.013977220553959335
u_x: 0.9783536868220091
Step = 1998, Cost function = 0.03562353
circuit at x=0: Autograd ArrayBox with value -0.013977220553959335
u_x: Autograd ArrayBox with value 0.9783536868220091
circuit at x=0: 0.003179064896253525
u_x: 1.0395193985245172
Step = 1999, Cost function = 0.04269846

Final value of the cost function = 0.04269846

I have also attached a plot of the analytic and qml solutions.

Hey @Corey,

Interesting! I’m not sure :thinking: let’s see if anyone else has any insight!