Error reloading circuit from qasm string

Hello,

I have a hybrid quantum neuronal network build with pennylane and TensorFlow. For my application I need to train it, save it to disk and later reload it into another application. Unfortunately, using the standard TensorFlow model.safe function on the entire model does not work, as TensorFlow can not process the pennylane information correctly.
(Depending on the format used for saving, the error is raised either during saving or loading. When saving the weights into an ‘.h5’ file, they can be loaded without raising an error, but the resulting model is not equal to the model that has been saved.)

With some help I came to the conclusion to split the hybrid network into classical and quantum layers and save both of them separately. For this, I have the following function:

   def save_hybrid_model( model , c_layers_path , q_layer_path ):

        # 1) split off
        quantum_layer = model.layers[-1] # in my case the quantum layer is always the last layer
        model.pop() # pops off last layer (i.e.e quantum layer

        # 2) safe classical part
        model.save_weights(c_layers_path)

        # 3) safe quantum part
        qasm_string = quantum_layer.qnode.qtape.to_openqasm()
        with open(q_layer_path, "w") as f:
            f.write(qasm_string)

this is then to be loaded with the following function

    def load_hybrid_model(c_layers_path, q_layer_path, hybrid_network_shapes):
        # 1) Classical Layers
        # 1.1) create classical layers
        model = build_classical_layers(hybrid_network_shapes)

        # 1.2) load classical weights
        model.load_weights(c_layers_path)

        # 2) Quantum layer
        # 2.1) Load the PennyLane QNode from the QASM string
        with open(q_layer_path, "r") as f:
            qasm_string = f.read()
        quantum_circuit = qml.from_qasm(qasm_string)

        # 2.2) circuit to quantum layer
        quantum_layer = build_quantum_layer(quantum_circuit, hybrid_network_shapes)

        # 2.3) add to other layers
        model.add(quantum_layer)

However, this leads to an error,

qiskit.qasm2.exceptions.QASM2ParseError: "<input>:5,3: 'tf' is not a parameter or custom instruction defined in this scope"

A look at the ‘qasm_string’ shows that it is something like

'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[3];\ncreg c[3];\nrx(tf.Tensor(\n[-0.2504485  -0.16435128  0.10509843 -0.22871123 -0.00794225 -0.50951445\n -0.5434188   0.00815093 -0.4871847  -0.1050231 ], shape=(10,), dtype=float32)) q[0];\nrx(tf.Tensor(\n[ 0.6483429  -0.14002065  0.48616743 -0.10395715  0.23674686  0.3900744\n  0.70458996  0.50224775  0.34395373  0.34140524], shape=(10,), dtype=float32)) q[1];\nrx(tf.Tensor(\n[0.6155473  0.6074002  0.371422   0.5381343  0.69457984 0.75203544\n 0.73663884 0.31011117 0.67915124 0.35447973], shape=(10,), dtype=float32)) q[2];\nrx(tf.Tensor(-0.5243009, shape=(), dtype=float32)) q[0];\nrx(tf.Tensor(0.9711641, shape=(), dtype=float32)) q[1];\nrx(tf.Tensor(-0.03388357, shape=(), dtype=float32)) q[2];\ncx q[0],q[1];\ncx q[1],q[2];\ncx q[2],q[0];\nmeasure q[0] -> c[0];\nmeasure q[1] -> c[1];\nmeasure q[2] -> c[2];\n'

Therefore, the problem seems to be that the to_openqasm() method converts the circuit into a string that includes the TensorFlow Tensors and which from_qasm() can not read.
I guess this is a bug?
Would anyone know a workaround for this?

I already tried to use sting processing to convert the loaded qasm_string into something from_qasm() can read, but I do not have sufficient knowledge OPENQASM about how to do so.

For reference: I am using pennylane version 0.33.1 and TensorFlow version 2.13.0
And here some further code to reproduce the issue (together with the two functions shown above)

import os
import tensorflow as tf
import pennylane as qml
from pennylane import numpy as np

# ---- functions to build hybrid model ---- #
def build_classical_layers(hybrid_network_shapes):

    neuron_numbers = hybrid_network_shapes['neuron_numbers']
    num_qubits = hybrid_network_shapes['num_qubits']

    # Create model
    classical_layers = tf.keras.models.Sequential()

    # add dense layers
    for nn in neuron_numbers:
        classical_layers.add(tf.keras.layers.Dense( nn , activation=tf.nn.tanh ))

    # one more dense layer to connect to qantum circuit
    classical_layers.add(tf.keras.layers.Dense(num_qubits, activation=tf.nn.tanh))

    return classical_layers


def build_circuit(hybrid_network_shapes):

    num_qubits = hybrid_network_shapes['num_qubits']
    output_dim = hybrid_network_shapes['output_dim']

    def circuit(inputs, weights):
        qml.templates.AngleEmbedding(inputs, wires=range(len(inputs)) )
        qml.templates.BasicEntanglerLayers(weights, wires=range(num_qubits) )
        return [qml.expval(qml.PauliZ(wires=i)) for i in range(output_dim)]

    return circuit

def build_quantum_layer(circuit, hybrid_network_shapes):

    num_qubits = hybrid_network_shapes['num_qubits']
    output_dim = hybrid_network_shapes['output_dim']

    # get device
    device = qml.device('default.qubit.tf', wires=num_qubits)

    # quantum circuit to Qnode
    qnode = qml.QNode(circuit, device=device, interface='tf')

    # Qnode to keras layer
    weight_shapes = {"weights": ( 1, num_qubits )}
    quantum_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=output_dim)

    return quantum_layer

# ---- create randomised data for training ---- #
def create_random_training_data(hybrid_network_shapes, num_examples=100):

    input_dim  = hybrid_network_shapes['neuron_numbers'][0]
    output_dim = hybrid_network_shapes['output_dim']


    # Generate random input data
    input_data = np.random.rand(num_examples, input_dim, )

    # Generate random output data (assuming a binary classification task)
    output_data = np.random.randint(2, size=(num_examples, output_dim))

    return input_data, output_data


# ---- puting it all together ---- #

# 0) definitions
c_layers_path = os.path.join('test_models','c_layers')
q_layer_path  = os.path.join('test_models','q_layer')

hybrid_network_shapes = {   'neuron_numbers' : [ 21 , 9 , 6 ],
                            'num_qubits' : 3,
                            'output_dim' : 1 }


# 1) Build hybrid model
# 1.1) build classical layers
model = build_classical_layers(hybrid_network_shapes)

# 1.2) build quantum ciruit
circuit = build_circuit(hybrid_network_shapes)

# 1.3) add as pennylane quantum layer
model.add(build_quantum_layer(circuit, hybrid_network_shapes))

# 1.4) compile model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])


# 2) Train model
# 2.1) create example training data
input_data, output_data = create_random_training_data(hybrid_network_shapes)

# 2.2) train for one epoch
model.fit(input_data, output_data, epochs=1, batch_size=10)

# 3) save model
save_hybrid_model( model , c_layers_path , q_layer_path )

# 4) load model (will raise the described error)
load_hybrid_model(c_layers_path , q_layer_path , hybrid_network_shapes)

Hey @PhilippHS! Welcome back :grin:

I’m not sure if the above behaviour is intentional or not (I think that what you’re trying to do shouldn’t be possible… but I’ll check with our team to be sure there isn’t a bug). But I think you’re over-complicating the process by getting qasm involved. You should be able to save / load a hybrid model with model.save_weights (see Usage Details in the docs for KerasLayer: qml.qnn.KerasLayer — PennyLane 0.33.0 documentation):

# saving weights
clayer = tf.keras.layers.Dense(2, input_shape=(2,))
qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
model = tf.keras.Sequential([clayer, qlayer])
model.save_weights(SAVE_PATH)

# loading weights
clayer = tf.keras.layers.Dense(2, input_shape=(2,))
qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
model = tf.keras.Sequential([clayer, qlayer])
model.load_weights(SAVE_PATH)

# save the whole model
clayer = tf.keras.layers.Dense(2, input_shape=(2,))
qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
model = tf.keras.Sequential([clayer, qlayer])
model.save(SAVE_PATH)

# load the whole model
model = tf.keras.models.load_model(SAVE_PATH)

Let me know if this helps!

Small update:

Spoke with one of our devs and it appears that your observed error isn’t intentional — it’s a bug :bug:. The interface information should be removed before exporting to QASM), so we’ll make a fix for that. However, even once that is fixed, batching isn’t supported right now in importing to and from QASM, and we support exporting measurements, but not importing them again. You would need both of those things to be supported for what you are doing to work, so currently its not feasible.

That said, I still think you don’t need to use qasm, but if that’s not the case then let me know!

Hi @isaacdevlugt

thank you so much for your response. Sad that there is no easy fix for this. I think batching would not be that essential, as I could also switch to batch sizes of one. But that the measurements is problematic. Would it work to add the measurements after loading?

I am asking this because the official solution: using model.save_weights, does unfortunately not work:

Yes it is possible to reload the hybrid model weights with the load_weoghts function without raising an error. However, the resulting model is not identical to the model that was saved.
I modified to code above to illustrate this

import os
import tensorflow as tf
import pennylane as qml
from pennylane import numpy as np

def load_hybrid_model(model_path, hybrid_network_shapes):

    # 1) Classical Layers
    # 1.1) create classical layers
    model = build_classical_layers(hybrid_network_shapes)

    # 1.2) build quantum ciruit
    circuit = build_circuit(hybrid_network_shapes)

    # 1.3) add as pennylane quantum layer
    model.add(build_quantum_layer(circuit, hybrid_network_shapes))

    # 1.2) load classical weights
    model.load_weights(model_path)

    return model


# Function to build hybrid model
def build_classical_layers(hybrid_network_shapes):

    neuron_numbers = hybrid_network_shapes['neuron_numbers']
    num_qubits = hybrid_network_shapes['num_qubits']

    # Create model
    classical_layers = tf.keras.models.Sequential()

    # add dense layers
    for nn in neuron_numbers:
        classical_layers.add(tf.keras.layers.Dense( nn , activation=tf.nn.tanh ))

    # one more dense layer to connect to qantum circuit
    classical_layers.add(tf.keras.layers.Dense(num_qubits, activation=tf.nn.tanh))

    return classical_layers


def build_circuit(hybrid_network_shapes):

    num_qubits = hybrid_network_shapes['num_qubits']
    output_dim = hybrid_network_shapes['output_dim']

    def circuit(inputs, weights):
        qml.templates.AngleEmbedding(inputs, wires=range(len(inputs)) )
        qml.templates.BasicEntanglerLayers(weights, wires=range(num_qubits) )
        return [qml.expval(qml.PauliZ(wires=i)) for i in range(output_dim)]

    return circuit

def build_quantum_layer(circuit, hybrid_network_shapes):

    num_qubits = hybrid_network_shapes['num_qubits']
    output_dim = hybrid_network_shapes['output_dim']

    # get device
    device = qml.device('default.qubit.tf', wires=num_qubits)

    # quantum circuit to Qnode
    qnode = qml.QNode(circuit, device=device, interface='tf')

    # Qnode to keras layer
    weight_shapes = {"weights": ( 1, num_qubits )}
    quantum_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=output_dim)

    return quantum_layer

def create_radom_raining_data(hybrid_network_shapes, num_examples=100):

    input_dim  = hybrid_network_shapes['neuron_numbers'][0]
    output_dim = hybrid_network_shapes['output_dim']


    # Generate random input data
    input_data = np.random.rand(num_examples, input_dim, )

    # Generate random output data (assuming a binary classification task)
    output_data = np.random.randint(2, size=(num_examples, output_dim))

    return input_data, output_data


#puting it all together
# 0) definitions
model_path  = os.path.join('test_models','hybid_model_weights')

hybrid_network_shapes = {   'neuron_numbers' : [ 21 , 9 , 6 ],
                            'num_qubits' : 3,
                            'output_dim' : 1 }


# 1) Build hybrid model
# 1.1) build classical layers
model = build_classical_layers(hybrid_network_shapes)

# 1.2) build quantum ciruit
circuit = build_circuit(hybrid_network_shapes)

# 1.3) add as pennylane quantum layer
model.add(build_quantum_layer(circuit, hybrid_network_shapes))

# 1.4) compile model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])


# 2) Train model
# 2.1) create example training data
input_data, output_data = create_radom_raining_data(hybrid_network_shapes)

# 2.2) train for one epoch
model.fit(input_data, output_data, epochs=1, batch_size=10)


# 3) save model
model.save_weights( model_path )

# 4) load model (will raise the described error)
loaded_model = load_hybrid_model( model_path,  hybrid_network_shapes)

# 5) compare prediction results
orig_res = model.predict(input_data)
load_res = loaded_model.predict(input_data)

# if loading would work perfectly this would be zero, but it is for from it not
print( orig_res - load_res )

I actually did not know that this was supposed to work. I guess it is another bug then :confused:

Hey @PhilippHS,

Regarding the first issue of the interface information not being removed and that causing problems with going to / from QASM, our devs pushed a fix for this: Remove interface information in OpenQASM string by lillian542 · Pull Request #4849 · PennyLaneAI/pennylane · GitHub

You can access the update by installing PennyLane from source, or it should be live on v0.34 (scheduled for sometime in the new year).

Regarding your follow-up post, let me have a look at see what’s going on! Might be another bug… but let me see :slight_smile:

interesting… @PhilippHS there might indeed be a bug with save_weights and load_weights. I’m just trying to distill things a bit and here’s a clearer example. Here, I’m using model.save to just save the whole thing, rather than saving just the weights and then sticking them back into a newly-created model.

dev = qml.device("default.qubit")
n_qubits = 2

def create_model():
    @qml.qnode(dev)
    def circuit(inputs, weights):
        qml.AngleEmbedding(inputs, wires=range(n_qubits))
        qml.RX(weights[0], 0)
        qml.RX(weights[1], 1)
        return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))]

    weight_shapes = {"weights": (n_qubits,)}
    quantum_layer = qml.qnn.KerasLayer(circuit, weight_shapes, output_dim=2)

    model = tf.keras.models.Sequential()
    model.add(quantum_layer)
    model.add(tf.keras.layers.Dense(2, activation=tf.nn.softmax, input_shape=(2,)))

    model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
    return model

model = create_model()

num_points = 5
dummy_input_data = np.random.uniform(0, np.pi, size=(num_points, 2))
dummy_output_data = np.random.randint(2, size=(num_points, 2))

model.fit(dummy_input_data, dummy_output_data, epochs=1, batch_size=0)

model.save("model")

loaded_model = tf.keras.models.load_model("model")

print("saved weights:", model.weights)
print("Loaded weights:", loaded_model.weights)
print(model.predict(dummy_input_data) == loaded_model.predict(dummy_input_data))
saved weights: [<tf.Variable 'weights:0' shape=(2,) dtype=float32, numpy=array([0.6532126, 0.2114163], dtype=float32)>, <tf.Variable 'dense/kernel:0' shape=(2, 2) dtype=float32, numpy=
array([[ 0.7598482 , -1.0919516 ],
       [-0.02455088,  1.0425475 ]], dtype=float32)>, <tf.Variable 'dense/bias:0' shape=(2,) dtype=float32, numpy=array([ 0.00099999, -0.00099999], dtype=float32)>]
Loaded weights: [<tf.Variable 'dense/kernel:0' shape=(2, 2) dtype=float32, numpy=
array([[ 0.7598482 , -1.0919516 ],
       [-0.02455088,  1.0425475 ]], dtype=float32)>, <tf.Variable 'dense/bias:0' shape=(2,) dtype=float32, numpy=array([ 0.00099999, -0.00099999], dtype=float32)>]
[[ True  True]
 [ True  True]
 [ True  True]
 [ True  True]
 [ True  True]]

Using save_weights and load_weights doesn’t produce the same results, so I’ll have to get back to you on that. Also, it seems like the quanutm layer’s weights aren’t showing up after they’re loaded? :thinking:

Can you try running that example above to see if that works for you? If it does, then maybe you can try using model.save for now instead of save_weights and load_weights :slight_smile:

I made an issue on the repository if you’re interested in following :slight_smile:

Hi @isaacdevlugt ,
thank you very much for coming back to this. I was a bit surprised that using model.save does work as I tried it before but got errors when trying to save the model. Thanks to your code I now found that the problem was in my definition of the angle embedding:
Apparently I need to use

qml.templates.AngleEmbedding(inputs, wires=range(inputs.shape[1]) )

instead of

qml.templates.AngleEmbedding(inputs, wires=range(len(inputs)) )

(as used in my code above)

I don’t quite understand why the latter only raises a problem when the model is saved and not already during training, but I am happy that I now have a version that works :sweat_smile:
Thank you very much for your help :slight_smile:

Awesome! Glad I was able to help :slight_smile: