0
$\begingroup$

I read this article on a Hybrid Quantum LSTM in Pennylane and I'm trying to replicate it in Qiskit. Nevertheless it doesn't seem to work very well. Here's my code

from typing import Optional, Union

import numpy as np

from qiskit import QuantumCircuit
from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes
from qiskit.providers import Backend, BaseBackend
from qiskit.utils import QuantumInstance
from qiskit.opflow import StateFn, PauliSumOp, ListOp, AerPauliExpectation

from qiskit_machine_learning.neural_networks import OpflowQNN

import torch
import torch.nn as nn
from torch import Tensor

class QLongShortTermMemory(nn.Module):
    def __init__(self,
                 input_size: int,
                 hidden_size: int,
                 n_layers: Optional[int] = 1,
                 n_qubits: Optional[int] = 4,
                 batch_first: Optional[bool] = True,
                 feature_map: QuantumCircuit = None,
                 ansatz: QuantumCircuit = None,
                 quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None
                 ):
        super(QLongShortTermMemory, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.n_layers = n_layers
        self.batch_first = batch_first
        self._qlayers = {}
        self._set_quantum_instance(quantum_instance)

        # layers preparation
        if feature_map:
            if feature_map.num_qubits == n_qubits:
                _feature_map = feature_map
            else:
                raise ValueError(f"Incompatible parameter n_qubits={n_qubits} with "
                                 f"feature_map of {feature_map.num_qubits} qubits")
        else:
            _feature_map = ZZFeatureMap(n_qubits)

        _ansatz = ansatz if ansatz else \
            RealAmplitudes(n_qubits, entanglement='linear', reps=n_layers)
        # quantum layers
        self._construct_quantum_layers(_feature_map, _ansatz)

        # classical layers
        self.clayer_in = nn.Linear(input_size + hidden_size, n_qubits)
        self.clayer_out = nn.Linear(n_qubits, hidden_size)

    def _construct_quantum_layers(self, feature_map, ansatz):
        for layer_name in ['forget', 'input', 'update', 'output']:
            # define the layer using OpflowQNN from qiskit ml
            n_inputs = feature_map.num_qubits
            qc = QuantumCircuit(n_inputs)
            qc.append(feature_map, range(n_inputs))
            qc.append(ansatz, range(n_inputs))

            readout_op = ListOp([
                                    ~StateFn(PauliSumOp.from_list([('Z' * self.hidden_size, 1)])) @ StateFn(qc)
                                ] * n_inputs)

            input_params = list(feature_map.parameters)
            weight_params = list(ansatz.parameters)

            layer = OpflowQNN(operator=readout_op,
                              input_params=input_params,
                              weight_params=weight_params,
                              exp_val=AerPauliExpectation(),
                              quantum_instance=self.quantum_instance
                              )

            initial_weights = Tensor(np.zeros(n_inputs * self.n_layers * 2))
            self._qlayers[layer_name] = TorchConnector(layer, initial_weights=initial_weights)

    def _set_quantum_instance(
            self,
            quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]]):
        if isinstance(quantum_instance, (BaseBackend, Backend)):
            quantum_instance = QuantumInstance(quantum_instance)

        self._quantum_instance = quantum_instance

    def forward(self,
                x: Tensor,
                input_data: Optional[Tensor] = None):
        if self.batch_first:
            batch, seq, _ = x.size()
        else:
            seq, batch, _ = x.size()

        hidden_seq = []
        if input_data is None:
            h_t = torch.zeros(batch, self.hidden_size)  # hidden state (output)
            c_t = torch.zeros(batch, self.hidden_size)  # cell state
        else:
            h_t, c_t = input_data.detach()

        for t in range(seq):
            # get features from the t-th element in seq, for all entries in the batch
            x_t = x[:, t, :]

            # concatenate input and hidden state
            v_t = torch.cat((h_t, x_t), dim=1)

            # match qubit dimension
            y_t = self.clayer_in(v_t)

            # for each time step `t` we compute the forget, input, update and output gates
            # using 3 sigmoid layers and a hyperbolic tangent layer.
            # forget
            f_t = torch.sigmoid(self.clayer_out(self._qlayers['forget'](y_t)))
            # input
            i_t = torch.sigmoid(self.clayer_out(self._qlayers['input'](y_t)))
            # update
            g_t = torch.tanh(self.clayer_out(self._qlayers['update'](y_t)))
            # output
            o_t = torch.sigmoid(self.clayer_out(self._qlayers['output'](y_t)))

            # eventually, the hidden state and the cell state are evaluated
            # (see RNN architecture)
            c_t = (f_t * c_t) + (i_t * g_t)
            h_t = o_t * torch.tanh(c_t)

            hidden_seq.append(h_t.unsqueeze(0))

        # update hidden seq
        hidden_seq = torch.cat(hidden_seq, dim=0)
        hidden_seq = hidden_seq.transpose(0, 1).contiguous()
        return hidden_seq, (h_t, c_t)

I'm using the same NLP example proposed in the article and taken from Pytorch documentation on LSTM.

# see tutorial: https://pytorch.org/tutorials/beginner/nlp/sequence_models_tutorial.html


import numpy as np
import qiskit
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim


from matplotlib import pyplot as plt

tag_to_ix = {"DET": 0, "NN": 1, "V": 2}  # Assign each tag with a unique index
ix_to_tag = {i: k for k, i in tag_to_ix.items()}


def prepare_sequence(seq, to_ix):
    idxs = [to_ix[w] for w in seq]
    return torch.tensor(idxs, dtype=torch.long)


training_data = [
    # Tags are: DET - determiner; NN - noun; V - verb
    # For example, the word "The" is a determiner
    ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),
    ("Everybody read that book".split(), ["NN", "V", "DET", "NN"])
]
word_to_ix = {}

# For each words-list (sentence) and tags-list in each tuple of training_data
for sent, tags in training_data:
    for word in sent:
        if word not in word_to_ix:  # word has not been assigned an index yet
            word_to_ix[word] = len(word_to_ix)  # Assign each word with a unique index

print(f"Vocabulary: {word_to_ix}")
print(f"Entities: {ix_to_tag}")


class LSTMTagger(nn.Module):

    def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size, n_qubits=0, backend='default.qubit'):
        super(LSTMTagger, self).__init__()
        self.hidden_dim = hidden_dim

        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)

        # The LSTM takes word embeddings as inputs, and outputs hidden states
        # with dimensionality hidden_dim.
        if n_qubits > 0:
            seed = 71
            np.random.seed = seed
            from qiskit.utils import QuantumInstance, algorithm_globals
            algorithm_globals.random_seed = seed

            
            
            quantum_instance = QuantumInstance(
                backend=qiskit.Aer.get_backend("aer_simulator_statevector"), seed_transpiler=seed, seed_simulator=seed,
                backend_options={"device": 'CPU', "max_parallel_experiments": 0}
            )
            print(f"Tagger will use Quantum LSTM running on backend {backend}")
            self.lstm = QLongShortTermMemory(embedding_dim, hidden_dim, n_qubits=n_qubits, quantum_instance=quantum_instance)
        else:
            print("Tagger will use Classical LSTM")
            self.lstm = nn.LSTM(embedding_dim, hidden_dim)

        # The linear layer that maps from hidden state space to tag space
        self.hidden2tag = nn.Linear(hidden_dim, tagset_size)

    def forward(self, sentence):
        embeds = self.word_embeddings(sentence)
        lstm_out, _ = self.lstm(embeds.view(len(sentence), 1, -1))
        tag_logits = self.hidden2tag(lstm_out.view(len(sentence), -1))
        tag_scores = F.log_softmax(tag_logits, dim=1)
        return tag_scores


if __name__ == '__main__':
    ###############################
    # Change manually this params
    ###############################
    embedding_dim = 8
    hidden_dim = 4
    n_qubits = 2
    n_epochs = 300

    print(f"Embedding dim:    {embedding_dim}")
    print(f"LSTM output size: {hidden_dim}")
    print(f"Number of qubits: {n_qubits}")
    print(f"Training epochs:  {n_epochs}")

    model = LSTMTagger(embedding_dim,
                       hidden_dim,
                       vocab_size=len(word_to_ix),
                       tagset_size=len(tag_to_ix),
                       n_qubits=n_qubits,
                       backend='foo')

    loss_function = nn.NLLLoss()
    optimizer = optim.SGD(model.parameters(), lr=0.1)

    history = {
        'loss': [],
        'acc': []
    }
    for epoch in range(n_epochs):
        losses = []
        preds = []
        targets = []
        for sentence, tags in training_data:
            # Step 1. Remember that Pytorch accumulates gradients.
            # We need to clear them out before each instance
            model.zero_grad()

            # Step 2. Get our inputs ready for the network, that is, turn them into
            # Tensors of word indices.
            sentence_in = prepare_sequence(sentence, word_to_ix)
            labels = prepare_sequence(tags, tag_to_ix)

            # Step 3. Run our forward pass.
            tag_scores = model(sentence_in)

            # Step 4. Compute the loss, gradients, and update the parameters by
            #  calling optimizer.step()
            loss = loss_function(tag_scores, labels)
            loss.backward()
            optimizer.step()
            losses.append(float(loss))

            probs = torch.softmax(tag_scores, dim=-1)
            preds.append(probs.argmax(dim=-1))
            targets.append(labels)
        avg_loss = np.mean(losses)
        history['loss'].append(avg_loss)

        # print("preds", preds)
        preds = torch.cat(preds)
        targets = torch.cat(targets)
        corrects = (preds == targets)
        accuracy = corrects.sum().float() / float(targets.size(0))
        history['acc'].append(accuracy)

        print(f"Epoch {epoch + 1} / {n_epochs}: Loss = {avg_loss:.3f} Acc = {accuracy:.2f}")

    # See what the scores are after training
    with torch.no_grad():
        input_sentence = training_data[0][0]
        labels = training_data[0][1]
        inputs = prepare_sequence(input_sentence, word_to_ix)
        tag_scores = model(inputs)

        tag_ids = torch.argmax(tag_scores, dim=1).numpy()
        tag_labels = [ix_to_tag[k] for k in tag_ids]
        print(f"Sentence:  {input_sentence}")
        print(f"Labels:    {labels}")
        print(f"Predicted: {tag_labels}")

What happens is that the loss doesn't actually decrease as in the version with PennyLane and the accuracy never reaches 1. Also, it's way slower, but I suspect this is qiskit backend's fault.

I'm a CS Student who recently approached Quantum, then my apologies if my mistake is something very obvious. Thanks in advance=)

EDIT:

to make it easier to help me out, I show here the only parts of the code related to qiskit/pennylane which are very likely where the problem is

Pennylane version:

import pennylane as qml

def _circuit(inputs, weights):
    qml.templates.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.templates.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return [qml.expval(qml.PauliZ(wires=i)) for i in range(n_qubits)]

Qiskit version:

have look at the method _construct_quantum_layers, mainly this part


            n_inputs = feature_map.num_qubits
            qc = QuantumCircuit(n_inputs)
            qc.append(feature_map, range(n_inputs))
            qc.append(ansatz, range(n_inputs))

            readout_op = ListOp([
                                    ~StateFn(PauliSumOp.from_list([('Z' * self.hidden_size, 1)])) @ StateFn(qc)
                                ] * n_inputs)

            input_params = list(feature_map.parameters)
            weight_params = list(ansatz.parameters)

            layer = OpflowQNN(operator=readout_op,
                              input_params=input_params,
                              weight_params=weight_params,
                              exp_val=AerPauliExpectation(),
                              quantum_instance=self.quantum_instance
                              )
$\endgroup$
5
  • $\begingroup$ I think that Qiskit is very slow in general. Maybe using another ansatz like efficient SU(2) or adding more layers would help. Also you could define manually the torchconnector part, but still this will be slow. StateVector simulators would also make your simulation faster. There exist also Torchruntime, but I don't know if it works on a local computer. $\endgroup$ May 4 at 18:54
  • $\begingroup$ Ok, the problem is that you should measure ZIII, IZII, IIZI, IIIZ... but you are doing ZZZZ,ZZZZ,ZZZZ, ZZZZ then your final nn would be like a 1--> hidden dim nn, thats why it never converges. I'm working on this too, if you want to collaborate send me a message $\endgroup$ May 13 at 20:19
  • $\begingroup$ If you just fix this, does my code work for you? $\endgroup$
    – Elle
    May 16 at 22:17
  • $\begingroup$ it converges extremely slow, and the code runs slow too, but it does. Maybe you need to fix the part of AerExpectation and the quantum instance to not be in trouble too $\endgroup$ May 17 at 4:49
  • $\begingroup$ I used CircuitQNN and it runs like x20 faster, I'd highly recomend using this and then do the mapping of the probabilities to expectation values by hand $\endgroup$ May 17 at 21:04

1 Answer 1

0
$\begingroup$

Ok, so my recomendation is to use CircuitQNN and use the probs as output. That will solve the issue of speed and convergence at the same time.

I used embedding_dim = 8, hidden_dim = 6 and 3 qubits

enter image description here

$\endgroup$
1
  • $\begingroup$ Could you share the entire code so that I can fully see what you changed wrt mine ? thanks a lot for your help! $\endgroup$
    – Elle
    May 18 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.