Source code for lambeq.training.pennylane_model

# Copyright 2021-2024 Cambridge Quantum Computing Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
PennyLaneModel
==============
Module implementing quantum and quantum/classical hybrid lambeq models,
based on a PennyLane and PyTorch backend.

"""
from __future__ import annotations

import copy
from typing import Any, TYPE_CHECKING

from sympy import default_sort_key, Symbol
import torch

from lambeq.backend.quantum import Diagram as Circuit
from lambeq.backend.tensor import Diagram
from lambeq.training.checkpoint import Checkpoint
from lambeq.training.model import Model

if TYPE_CHECKING:
    from lambeq.backend.pennylane import PennyLaneCircuit


[docs]class PennyLaneModel(Model, torch.nn.Module): """ A lambeq model for the quantum and hybrid quantum/classical pipeline using PennyLane circuits. It uses PyTorch as a backend for all tensor operations. """ weights: torch.nn.ParameterList symbols: list[Symbol]
[docs] def __init__(self, probabilities: bool = True, normalize: bool = True, diff_method: str = 'best', backend_config: dict[str, Any] | None = None) -> None: """Initialise a :py:class:`PennyLaneModel` instance with an empty `circuit_map` dictionary. Parameters ---------- probabilities : bool, default: True Whether to use probabilities or states for the output. backend_config : dict, optional Configuration for hardware or simulator to be used. Defaults to using the `default.qubit` PennyLane simulator analytically, with normalized probability outputs. Keys that can be used include 'backend', 'device', 'probabilities', 'normalize', 'shots', and 'noise_model'. """ Model.__init__(self) torch.nn.Module.__init__(self) self.circuit_map: dict[Diagram, PennyLaneCircuit] = {} self.symbol_weight_map: dict[Symbol, torch.FloatTensor] = {} self._probabilities = probabilities self._normalize = normalize self._diff_method = diff_method self._backend_config = backend_config
def _load_checkpoint(self, checkpoint: Checkpoint) -> None: """Load the model weights and symbols from a lambeq :py:class:`.Checkpoint`. Parameters ---------- checkpoint : :py:class:`.Checkpoint` Checkpoint containing the model weights, symbols and additional information. """ self.symbols = checkpoint['model_symbols'] self.weights = checkpoint['model_weights'] self._probabilities = checkpoint['model_probabilities'] self._normalize = checkpoint['model_normalize'] self._diff_method = checkpoint['model_diff_method'] self._backend_config = checkpoint['model_backend_config'] self.circuit_map = checkpoint['model_circuits'] self.load_state_dict(checkpoint['model_state_dict']) self.symbol_weight_map = dict(zip(self.symbols, self.weights)) for p_circ in self.circuit_map.values(): p_circ.initialise_device_and_circuit() def _make_checkpoint(self) -> Checkpoint: """Create checkpoint that contains the model weights and symbols. Returns ------- :py:class:`.Checkpoint` Checkpoint containing the model weights, symbols and additional information. """ checkpoint = Checkpoint() circuit_map = {k: copy.copy(v) for k, v in self.circuit_map.items()} for c in circuit_map.values(): c._device = None c._circuit = None checkpoint.add_many({'model_weights': self.weights, 'model_symbols': self.symbols, 'model_probabilities': self._probabilities, 'model_normalize': self._normalize, 'model_diff_method': self._diff_method, 'model_backend_config': self._backend_config, 'model_circuits': circuit_map, 'model_state_dict': self.state_dict()}) return checkpoint def _reinitialise_modules(self) -> None: """Reinitialise all modules in the model.""" for module in self.modules(): try: module.reset_parameters() except (AttributeError, TypeError): pass
[docs] def initialise_weights(self) -> None: """Initialise the weights of the model. Raises ------ ValueError If `model.symbols` are not initialised. """ self._reinitialise_modules() if not self.symbols: raise ValueError('Symbols not initialised. Instantiate through ' '`PennyLaneModel.from_diagrams()`.') self.weights = torch.nn.ParameterList( [torch.nn.Parameter(torch.rand(1).squeeze()) for _ in self.symbols] ) self.symbol_weight_map = dict(zip(self.symbols, self.weights))
[docs] def get_diagram_output(self, diagrams: list[Diagram]) -> torch.Tensor: """Evaluate outputs of circuits using PennyLane. Parameters ---------- diagrams : list of :py:class:`~lambeq.backend.quantum.Diagram` The :py:class:`Diagrams <lambeq.backend.quantum.Diagram>` to be evaluated. Raises ------ ValueError If `model.weights` or `model.symbols` are not initialised. Returns ------- torch.Tensor Resulting tensor. """ circuit_evals = [] for d in diagrams: p_circ = self.circuit_map[d] p_circ.initialise_concrete_params(self.symbol_weight_map) circuit_evals.append(p_circ.eval()) if self._normalize: if self._probabilities: circuit_evals = [c / torch.sum(c) for c in circuit_evals] else: circuit_evals = [c / torch.sum(torch.square(torch.abs(c))) for c in circuit_evals] stacked = torch.stack(circuit_evals) stacked = stacked.squeeze(-1) if self._probabilities: return stacked.to(self.weights[0].dtype) else: return stacked
[docs] def forward(self, x: list[Diagram]) -> torch.Tensor: """Perform default forward pass by running circuits. In case of a different datapoint (e.g. list of tuple) or additional computational steps, please override this method. Parameters ---------- x : list of :py:class:`~lambeq.backend.quantum.Diagram` The :py:class:`Circuits <lambeq.backend.quantum.Diagram>` to be evaluated. Returns ------- torch.Tensor Tensor containing model's prediction. """ return self.get_diagram_output(x)
[docs] @classmethod def from_diagrams(cls, diagrams: list[Diagram], probabilities: bool = True, normalize: bool = True, diff_method: str = 'best', backend_config: dict[str, Any] | None = None, **kwargs: Any) -> PennyLaneModel: """Build model from a list of :py:class:`Circuits <lambeq.backend.quantum.Diagram>`. Parameters ---------- diagrams : list of :py:class:`~lambeq.backend.quantum.Diagram` The circuit diagrams to be evaluated. backend_config : dict, optional Configuration for hardware or simulator to be used. Defaults to using the `default.qubit` PennyLane simulator analytically, with normalized probability outputs. Keys that can be used include 'backend', 'device', 'probabilities', 'normalize', 'shots', and 'noise_model'. """ model = cls(probabilities=probabilities, normalize=normalize, diff_method=diff_method, backend_config=backend_config, **kwargs) model.symbols = sorted( {sym for circ in diagrams for sym in circ.free_symbols}, key=default_sort_key) for circ in diagrams: assert isinstance(circ, Circuit) p_circ = circ.to_pennylane(probabilities=model._probabilities, diff_method=model._diff_method, backend_config=model._backend_config) model.circuit_map[circ] = p_circ return model