Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing trainable-encoding layers #48

Open
wants to merge 3 commits into
base: diffrules
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/qiboml/interfaces/pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class QuantumModel(torch.nn.Module):

encoding: QuantumEncoding
circuit: Circuit
trainable_circuit: Circuit
decoding: QuantumDecoding
differentiation_rule: DifferentiationRule = None

Expand All @@ -24,11 +24,12 @@ def __post_init__(
):
super().__init__()

circuit = self.encoding.circuit
params = [p for param in self.circuit.get_parameters() for p in param]
circuit = self.encoding.circuit + self.trainable_circuit
params = [p for param in circuit.get_parameters() for p in param]
params = torch.as_tensor(self.backend.to_numpy(x=params)).ravel()
params.requires_grad = True
self.circuit_parameters = torch.nn.Parameter(params)
# all trainable parameters
self.trainable_parameters = torch.nn.Parameter(params)

def forward(self, x: torch.Tensor):
if (
Expand All @@ -39,15 +40,15 @@ def forward(self, x: torch.Tensor):
x = QuantumModelAutoGrad.apply(
x,
self.encoding,
self.circuit,
self.trainable_circuit,
self.decoding,
self.backend,
self.differentiation_rule,
*list(self.parameters())[0],
)
else:
self.circuit.set_parameters(list(self.parameters())[0])
x = self.encoding(x) + self.circuit
x = self.encoding(x) + self.trainable_circuit
x.set_parameters(list(self.parameters())[0])
x = self.decoding(x)
return x

Expand All @@ -67,6 +68,10 @@ def backend(
def output_shape(self):
return self.decoding.output_shape

def draw(self):
circuit = self.encoding.circuit + self.trainable_circuit
circuit.draw()


class QuantumModelAutoGrad(torch.autograd.Function):

Expand Down
32 changes: 23 additions & 9 deletions src/qiboml/models/ansatze.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import random
from typing import List, Optional

import numpy as np
from qibo import Circuit, gates


def ReuploadingCircuit(
nqubits: int, qubits: list[int] = None, nlayers: int = 1
) -> Circuit:
def entangling_circuit(nqubits: int, entangling_gate: gates.Gate = gates.CNOT):
"""Construct entangling layer."""
circuit = Circuit(nqubits)
for q in range(nqubits):
circuit.add(entangling_gate(q0=q % nqubits, q1=(q + 1) % nqubits))
return circuit


def layered_ansatz(
nqubits: int,
nlayers: int = 1,
qubits: list[int] = None,
gates_list: Optional[List[gates.Gate]] = [
gates.RY,
gates.RZ,
], # TODO: this has to be a circuit
Comment on lines +23 to +26
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but it seems like this entire file is a duplication of qibo.models.encodings.phase_encoder and qibo.models.encodings.entangling_layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is true! Those are just small and simple functions to make qiboml work now :)

But they are definitely temporary

entanglement: bool = True,
):
if qubits is None:
qubits = list(range(nqubits))

circuit = Circuit(nqubits)

for _ in range(nlayers):
for q in qubits:
circuit.add(gates.RY(q, theta=random.random() * np.pi, trainable=True))
circuit.add(gates.RZ(q, theta=random.random() * np.pi, trainable=True))
for i, q in enumerate(qubits[:-2]):
circuit.add(gates.CNOT(q0=q, q1=qubits[i + 1]))
circuit.add(gates.CNOT(q0=qubits[-1], q1=qubits[0]))
for gate in gates_list:
circuit.add(gate(q, theta=random.random(), trainable=True))
if entanglement:
circuit += entangling_circuit(nqubits, gates.CNOT)

return circuit
61 changes: 61 additions & 0 deletions src/qiboml/models/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from qibo.config import raise_error

from qiboml import ndarray
from qiboml.models.ansatze import layered_ansatz


@dataclass
Expand Down Expand Up @@ -66,3 +67,63 @@ def __call__(self, x: ndarray) -> Circuit:
for bit in ones:
circuit.add(gates.X(self.qubits[bit]))
return circuit


@dataclass
class ReuploadingEncoding(QuantumEncoding):
"""
Implementing reuploading scheme alternating encoding U and training V layers.
It follows the scheme V - U - V - U - V (in case of 2 layers), namely upload `nlayers` times x and
and each encoding layer is preceded and followed by a trainable layer V.
The chosen default V layer is a
`qiboml.models.ansatze.layered_ansatz(nqubits, 1, qubits, [RY, RZ], True)`.
"""

# big TODO: make this model more flexible enabling e.g. lambda function of data and params

# TODO: rm this and raise error when calling the class
data_shape: tuple = (1,)
nlayers: int = 1
trainable_ansatz: Circuit = None
encoding_gate: gates.Gate = gates.RX

def __post_init__(
self,
):

super().__post_init__()

data_dim = np.prod(self.data_shape, axis=0)

if int(data_dim) % len(self.qubits) != 0:
raise_error(
ValueError,
f"The data dimension has to be equal to the length of the chosen {self.qubits} subset of the {self.nqubits} system.",
)

# TODO: use deepcopy to repeat the call creating single elements
if self.trainable_ansatz is None:
self._circuit += layered_ansatz(nqubits=self.nqubits, qubits=self.qubits)
for _ in range(self.nlayers):
for q in self.qubits:
self._circuit.add(self.encoding_gate(q=q, theta=0.0, trainable=False))
if self.trainable_ansatz is None:
self._circuit += layered_ansatz(
nqubits=self.nqubits, qubits=self.qubits
)

def _set_phases(self, x: ndarray):
encoding_gates = [
g for g in self._circuit.parametrized_gates if g.trainable == False
]
data_length = len(x.ravel())
for l in range(self.nlayers):
for gate, phase in zip(
encoding_gates[data_length * l : data_length * l + data_length],
x.ravel(),
):
gate.parameters = phase

def __call__(self, x: ndarray) -> Circuit:
self._set_phases(x)
return self._circuit
Loading
Loading