├── qnn ├── __init__.py ├── ansatz │ ├── __init__.py │ ├── null_ansatz.py │ ├── sim_circ_13_half.py │ ├── sim_circ_14_half.py │ ├── rzx_gate.py │ ├── sim_circ_19.py │ ├── variational_ansatz.py │ ├── abbas.py │ ├── alternating_layer_tdcnot_ansatz.py │ ├── sim_circ_15.py │ ├── farhi_ansatz.py │ ├── sim_circ_13.py │ ├── sim_circ_14.py │ └── variational_ansatz_factory.py ├── input │ ├── __init__.py │ ├── vector_data_handler.py │ ├── havlicek_data_handler.py │ ├── data_handler_factory.py │ ├── neqr_sv_data_handler.py │ ├── data_handler.py │ ├── frqi_bennett_data_handler.py │ └── neqr_bennett_data_handler.py ├── activation_function │ ├── __init__.py │ ├── activation_function.py │ ├── null_activation_function.py │ ├── activation_function_factory.py │ └── partial_meas_activation_function.py ├── gradient_calculator.py ├── config.py ├── qnet.py └── quantum_network_circuit.py ├── test ├── __init__.py ├── test_quantum_network_circuit.py └── test_gradient_calculator.py ├── environment.yml ├── test_utils.py ├── setup.py ├── LICENSE ├── run_simple_network.py ├── .gitignore ├── README.md └── mnist_examples ├── keras.py ├── pytorch_with_fc_comparison.py └── pytorch_with_qnet.py /qnn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /qnn/ansatz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /qnn/input/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /qnn/activation_function/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: QNNenv 2 | dependencies: 3 | - python>=3.5 4 | - pip>=19 5 | - matplotlib 6 | - rise 7 | - opencv 8 | - pytorch 9 | - torchvision 10 | - pip: 11 | - qiskit 12 | - pylatexenc 13 | - pillow 14 | - watchlogs -------------------------------------------------------------------------------- /qnn/activation_function/activation_function.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class ActivationFunction(ABC): 5 | 6 | def __init__(self): 7 | self.qr = None 8 | self.cr = None 9 | self.qc = None 10 | 11 | @abstractmethod 12 | def get_quantum_circuit(self, n_qubits): 13 | """ 14 | Returns a quantum circuit for implementing the nonlinear action 15 | """ 16 | pass 17 | -------------------------------------------------------------------------------- /test_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | 6 | def create_random_bitstring(num_bits): # repeated code from random_data_test 7 | return np.random.choice([0, 1], size=num_bits) 8 | 9 | 10 | def create_random_bitstrings(num_strings, num_bits): # repeated code from random_data_test 11 | bitstrings = [] 12 | for i in range(num_strings): 13 | bitstrings.append(create_random_bitstring(num_bits)) 14 | 15 | logging.info("Random pixels created with values {}".format(bitstrings)) 16 | return bitstrings 17 | -------------------------------------------------------------------------------- /qnn/ansatz/null_ansatz.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class NullAnsatz(VariationalAnsatz): 7 | def add_rotations(self, n_data_qubits): 8 | pass 9 | 10 | def add_entangling_gates(self, n_data_qubits): 11 | pass 12 | 13 | def get_quantum_circuit(self, n_data_qubits): 14 | self.qr = QuantumRegister(n_data_qubits, name='qr') 15 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 16 | return self.qc 17 | -------------------------------------------------------------------------------- /qnn/input/vector_data_handler.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumRegister, QuantumCircuit 2 | from qiskit.circuit import Parameter 3 | 4 | from input.data_handler import DataHandler 5 | 6 | 7 | class VectorDataHandler(DataHandler): 8 | def get_quantum_circuit(self, input_data): 9 | self.qr = QuantumRegister(len(input_data)) 10 | self.qc = QuantumCircuit(self.qr) 11 | 12 | for index, _ in enumerate(input_data): 13 | param = Parameter("input{}".format(str(index))) 14 | self.qc.rx(param, index) 15 | 16 | return self.qc 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='quantum-neural-network', 5 | version='0.1.4', 6 | author='Benjamin Jaderberg', 7 | author_email='benjamin.jaderberg@physics.ox.ac.uk', 8 | packages=find_packages(), 9 | scripts=[], 10 | url='https://github.com/bjader/quantum-neural-network', 11 | license='LICENSE', 12 | description='For building quantum neural networks in Qiskit and integrating with PyTorch', 13 | long_description=open('README.md').read(), 14 | install_requires=[ 15 | "qiskit" 16 | ], 17 | ) -------------------------------------------------------------------------------- /qnn/activation_function/null_activation_function.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumRegister, QuantumCircuit 2 | 3 | from activation_function.activation_function import ActivationFunction 4 | 5 | 6 | class NullActivationFunction(ActivationFunction): 7 | """ 8 | Creates activation function circuit corresponding to identity operation i.e. no activation function 9 | """ 10 | 11 | def __init__(self): 12 | super().__init__() 13 | 14 | def get_quantum_circuit(self, n_qubits): 15 | self.qr = QuantumRegister(n_qubits, name='qr') 16 | self.qc = QuantumCircuit(self.qr) 17 | 18 | return self.qc 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ben Jaderberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /qnn/activation_function/activation_function_factory.py: -------------------------------------------------------------------------------- 1 | from activation_function.null_activation_function import NullActivationFunction 2 | from activation_function.partial_meas_activation_function import PartialMeasActivationFunction 3 | 4 | 5 | class ActivationFunctionFactory: 6 | 7 | def __init__(self, activation_function_type): 8 | self.activation_function_type = activation_function_type 9 | 10 | def get(self): 11 | """ 12 | Returns appropriate activation function object. 13 | """ 14 | 15 | if self.activation_function_type == 'null' or self.activation_function_type == None: 16 | return NullActivationFunction() 17 | elif self.activation_function_type == 'partial_measurement_half': 18 | return PartialMeasActivationFunction("half") 19 | 20 | elif self.activation_function_type[:len('partial_measurement_')] == 'partial_measurement_': 21 | n_measurements = int(self.activation_function_type[len('partial_measurement_'):]) 22 | return PartialMeasActivationFunction(n_measurements) 23 | 24 | else: 25 | raise ValueError("Invalid activation function type.") 26 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_13_half.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class SimCirc13Half(VariationalAnsatz): 7 | """ 8 | A variational circuit ansatz. Based on circuit 13 in arXiv:1905.10876 but modified to only use the first half of the 9 | circuit. Between each layer an activation function can be applied using appropriate nonlinear activation function. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def add_rotations(self, n_data_qubits): 16 | for i in range(0, n_data_qubits): 17 | param = Parameter("ansatz{}".format(str(self.param_counter))) 18 | self.qc.ry(param, self.qr[i]) 19 | self.param_counter += 1 20 | return self.qc 21 | 22 | def add_entangling_gates(self, n_data_qubits): 23 | for i in range(n_data_qubits): 24 | param = Parameter("ansatz{}".format(str(self.param_counter))) 25 | self.qc.crz(param, self.qr[i], self.qr[(i + 1) % n_data_qubits]) 26 | self.param_counter += 1 27 | return self.qc 28 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_14_half.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class SimCirc14Half(VariationalAnsatz): 7 | """ 8 | A variational circuit ansatz. Based on circuit 14 in arXiv:1905.10876 but modified to only use the first half of the 9 | circuit. Between each layer an activation function can be applied using appropriate nonlinear activation function. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def add_rotations(self, n_data_qubits): 16 | for i in range(0, n_data_qubits): 17 | param = Parameter("ansatz{}".format(str(self.param_counter))) 18 | self.qc.ry(param, self.qr[i]) 19 | self.param_counter += 1 20 | return self.qc 21 | 22 | def add_entangling_gates(self, n_data_qubits): 23 | for i in range(n_data_qubits): 24 | param = Parameter("ansatz{}".format(str(self.param_counter))) 25 | self.qc.crx(param, self.qr[i], self.qr[(i + 1) % n_data_qubits]) 26 | self.param_counter += 1 27 | return self.qc 28 | -------------------------------------------------------------------------------- /qnn/input/havlicek_data_handler.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumRegister, QuantumCircuit 3 | from qiskit.circuit import Parameter 4 | 5 | from input.data_handler import DataHandler 6 | 7 | 8 | class HavlicekDataHandler(DataHandler): 9 | """ 10 | Data encoding based on Havlicek et al. Nature 567, pp209–212 (2019). For quantum circuit diagram see Fig. 4 in 11 | arXiv:2011.00027. 12 | """ 13 | 14 | def __init__(self): 15 | super().__init__() 16 | 17 | def get_quantum_circuit(self, input_data): 18 | self.qr = QuantumRegister(len(input_data)) 19 | self.qc = QuantumCircuit(self.qr) 20 | num_qubits = len(input_data) 21 | param_list = [] 22 | for index in range(num_qubits): 23 | self.qc.h(self.qr[index]) 24 | 25 | param = Parameter("input{}".format(str(index))) 26 | param_list.append(param) 27 | 28 | self.qc.rz(param, self.qr[index]) 29 | 30 | for i in range(num_qubits - 1): 31 | for j in range(i + 1, num_qubits): 32 | param_i = param_list[i] 33 | param_j = param_list[j] 34 | self.qc.rzz((param_i - np.pi / 2) * (param_j - np.pi / 2), i, j) 35 | 36 | return self.qc 37 | -------------------------------------------------------------------------------- /run_simple_network.py: -------------------------------------------------------------------------------- 1 | """Single forward pass of quantum neural network.""" 2 | import logging 3 | import sys 4 | 5 | import numpy as np 6 | 7 | sys.path.append('qnn') 8 | 9 | from config import Config 10 | from quantum_network_circuit import QuantumNetworkCircuit 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | logging.getLogger('qiskit').setLevel(logging.WARN) 14 | 15 | 16 | def sum_vector_cost_func(vector): 17 | return sum(vector) 18 | 19 | 20 | n_input_data = 4 21 | 22 | input_data = np.random.random(n_input_data) 23 | config = Config(encoding='vector', ansatz_type='sim_circ_14', layers=1, sweeps_per_layer=1, 24 | activation_function_type='null', meas_method='all', backend_type='statevector_simulator') 25 | 26 | qnn = QuantumNetworkCircuit(config, n_input_data, input_data) 27 | parameter_values = np.random.random(len(qnn.qc.parameters)) 28 | 29 | print('Network with input from previous layer {}'.format(input_data)) 30 | print('Network initialised with weights {}'.format(parameter_values[n_input_data:])) 31 | 32 | measurement_result = qnn.get_vector_from_results(qnn.evaluate_circuit(parameter_values)) 33 | 34 | qnn.qc.draw(output='mpl').show() 35 | 36 | print('Cost function result = {}'.format(sum_vector_cost_func(measurement_result))) 37 | -------------------------------------------------------------------------------- /qnn/input/data_handler_factory.py: -------------------------------------------------------------------------------- 1 | from input.frqi_bennett_data_handler import FRQIBennettDataHandler 2 | from input.havlicek_data_handler import HavlicekDataHandler 3 | from input.neqr_bennett_data_handler import NEQRBennettDataHandler 4 | from input.neqr_sv_data_handler import NEQRSVDataHandler 5 | from input.vector_data_handler import VectorDataHandler 6 | 7 | 8 | class DataHandlerFactory: 9 | 10 | def __init__(self, encoding, method): 11 | self.encoding = encoding 12 | self.method = method 13 | 14 | def get(self): 15 | """ 16 | Returns the appropriate data handler 17 | """ 18 | if self.encoding == 'vector': 19 | return VectorDataHandler() 20 | if self.encoding == 'havlicek': 21 | return HavlicekDataHandler() 22 | if self.method is None: 23 | raise ValueError("This encoding requires a data handler method.") 24 | elif self.encoding == 'frqi': 25 | if self.method == 'bennett': 26 | return FRQIBennettDataHandler() 27 | elif self.encoding == 'neqr': 28 | if self.method == 'statevector': 29 | return NEQRSVDataHandler() 30 | elif self.method == 'bennett': 31 | return NEQRBennettDataHandler() 32 | else: 33 | raise ValueError("Invalid string used for encoding or data handler method.") 34 | -------------------------------------------------------------------------------- /qnn/ansatz/rzx_gate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumRegister, QuantumCircuit 3 | from qiskit.circuit import Gate 4 | from qiskit.extensions import CXGate, U3Gate, U1Gate, U2Gate 5 | 6 | 7 | class RZXGate(Gate): 8 | """Two-qubit XZ-rotation gate. Modified XX-rotation gate from Qiskit. 9 | 10 | This gate corresponds to the rotation U(θ) = exp(-1j * θ * Z⊗X / 2) 11 | """ 12 | 13 | def __init__(self, theta): 14 | """Create new rzx gate.""" 15 | super().__init__("rzx", 2, [theta]) 16 | 17 | def _define(self): 18 | """Calculate a subcircuit that implements this unitary.""" 19 | 20 | definition = [] 21 | q = QuantumRegister(2, "q") 22 | theta = self.params[0] 23 | rule = [ 24 | (U3Gate(np.pi / 2, theta, 0), [q[0]], []), 25 | (CXGate(), [q[0], q[1]], []), 26 | (U1Gate(-theta), [q[1]], []), 27 | (CXGate(), [q[0], q[1]], []), 28 | (U2Gate(-np.pi, np.pi - theta), [q[0]], []), 29 | ] 30 | for inst in rule: 31 | definition.append(inst) 32 | self.definition = definition 33 | 34 | def inverse(self): 35 | """Invert this gate.""" 36 | return RZXGate(-self.params[0]) 37 | 38 | 39 | def rzx(self, theta, qubit1, qubit2): 40 | """Apply RZX to circuit.""" 41 | return self.append(RZXGate(theta), [qubit1, qubit2], []) 42 | 43 | 44 | # Add to QuantumCircuit class 45 | QuantumCircuit.rzx = rzx 46 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_19.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class SimCirc19(VariationalAnsatz): 7 | """ 8 | A variational circuit ansatz. Prepares quantum circuit object for variational circuit 19 in arXiv:1905.10876. 9 | Between each layer an activation function can be applied using appropriate nonlinear activation function. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def add_rotations(self, n_data_qubits): 16 | for i in range(0, n_data_qubits): 17 | param = Parameter("ansatz{}".format(str(self.param_counter))) 18 | self.qc.rx(param, self.qr[i]) 19 | self.param_counter += 1 20 | 21 | for i in range(0, n_data_qubits): 22 | param = Parameter("ansatz{}".format(str(self.param_counter))) 23 | self.qc.rz(param, self.qr[i]) 24 | self.param_counter += 1 25 | 26 | return self.qc 27 | 28 | def add_entangling_gates(self, n_data_qubits): 29 | param = Parameter("ansatz{}".format(str(self.param_counter))) 30 | self.qc.crx(param, self.qr[n_data_qubits - 1], self.qr[0]) 31 | self.param_counter += 1 32 | for i in reversed(range(1, n_data_qubits)): 33 | param = Parameter("ansatz{}".format(str(self.param_counter))) 34 | self.qc.crx(param, self.qr[i - 1], self.qr[i]) 35 | self.param_counter += 1 36 | return self.qc 37 | -------------------------------------------------------------------------------- /qnn/ansatz/variational_ansatz.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from qiskit import QuantumRegister, QuantumCircuit 4 | 5 | 6 | class VariationalAnsatz(ABC): 7 | 8 | def __init__(self, layers, sweeps_per_layer, activation_function): 9 | self.layers = layers 10 | self.sweeps_per_layer = sweeps_per_layer 11 | self.qr = None 12 | self.qc = None 13 | self.n_parameters_required = None 14 | self.activation_function = activation_function 15 | 16 | self.param_counter = 0 17 | 18 | # Base logic for creating an ansatz, can be overwritten by children 19 | def get_quantum_circuit(self, n_data_qubits): 20 | self.qr = QuantumRegister(n_data_qubits, name='qr') 21 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 22 | 23 | for layer_no in range(self.layers): 24 | for sweep in range(0, self.sweeps_per_layer): 25 | self.add_rotations(n_data_qubits) 26 | self.add_entangling_gates(n_data_qubits) 27 | if layer_no < self.layers - 1: 28 | self.apply_activation_function(n_data_qubits) 29 | return self.qc 30 | 31 | @abstractmethod 32 | def add_rotations(self, n_data_qubits): 33 | pass 34 | 35 | @abstractmethod 36 | def add_entangling_gates(self, n_data_qubits): 37 | pass 38 | 39 | def apply_activation_function(self, n_data_qubits): 40 | activation_function_circuit = self.activation_function.get_quantum_circuit(n_data_qubits) 41 | self.qc.extend(activation_function_circuit) 42 | return self.qc 43 | -------------------------------------------------------------------------------- /qnn/input/neqr_sv_data_handler.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumCircuit 3 | 4 | from input.data_handler import DataHandler 5 | 6 | 7 | class NEQRSVDataHandler(DataHandler): 8 | """ 9 | A statevector data handler. This will represent the input data as a state vector and then ask Qiskit 10 | to find a circuit which initializes this state. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__() 15 | 16 | def get_quantum_circuit(self, input_data): 17 | 18 | encoded_data = self.encode_using_neqr(input_data) 19 | 20 | state = self.create_input_state(encoded_data) 21 | 22 | qc = QuantumCircuit((len(state) - 1).bit_length()) 23 | qc.initialize(state, qc.qregs) 24 | 25 | return qc 26 | 27 | def create_input_state(self, bitstrings): 28 | state = self.convert_bitstring_to_statevector(bitstrings[0]) 29 | 30 | for bitstring in bitstrings[1:]: 31 | state += self.convert_bitstring_to_statevector(bitstring) 32 | 33 | normalized_state = state / np.linalg.norm(state) 34 | 35 | return normalized_state 36 | 37 | def convert_bitstring_to_statevector(self, bitstring): 38 | state = np.array([1]) 39 | 40 | for b in bitstring: 41 | ket = self.convert_bit_to_bloch(b) 42 | state = np.kron(state, ket) 43 | 44 | return state 45 | 46 | def convert_bit_to_bloch(self, digit): 47 | if digit == 0: 48 | return np.array([1, 0]) 49 | 50 | elif digit == 1: 51 | return np.array([0, 1]) 52 | 53 | else: 54 | raise ValueError("Cannot convert non binary digit") 55 | -------------------------------------------------------------------------------- /qnn/ansatz/abbas.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from ansatz.variational_ansatz import VariationalAnsatz 5 | 6 | 7 | class Abbas(VariationalAnsatz): 8 | """ 9 | A variational circuit ansatz. Based on Figure 5 of arXiv:2011.00027 - which itself is based on circuit 15 in 10 | arXiv:1905.10876 but with all-to-all CNOT connectivity. 11 | """ 12 | 13 | def __init__(self, layers, sweeps_per_layer, activation_function): 14 | super().__init__(layers, sweeps_per_layer, activation_function) 15 | 16 | def get_quantum_circuit(self, n_data_qubits): 17 | self.qr = QuantumRegister(n_data_qubits, name='qr') 18 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 19 | 20 | for layer_no in range(self.layers): 21 | if layer_no == 0: 22 | self.add_rotations(n_data_qubits) 23 | for sweep in range(0, self.sweeps_per_layer): 24 | self.add_entangling_gates(n_data_qubits) 25 | self.add_rotations(n_data_qubits) 26 | if layer_no < self.layers - 1: 27 | self.apply_activation_function(n_data_qubits) 28 | return self.qc 29 | 30 | def add_rotations(self, n_data_qubits): 31 | for i in range(0, n_data_qubits): 32 | param = Parameter("ansatz{}".format(str(self.param_counter))) 33 | self.qc.ry(param, self.qr[i]) 34 | self.param_counter += 1 35 | return self.qc 36 | 37 | def add_entangling_gates(self, n_data_qubits): 38 | for i in range(n_data_qubits): 39 | for j in range(i + 1, n_data_qubits): 40 | self.qc.cx(self.qr[i], self.qr[j]) 41 | return self.qc 42 | -------------------------------------------------------------------------------- /qnn/activation_function/partial_meas_activation_function.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import floor 3 | 4 | from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit 5 | 6 | from activation_function.activation_function import ActivationFunction 7 | 8 | 9 | class PartialMeasActivationFunction(ActivationFunction): 10 | """ 11 | Creates activation function circuit in which a subset of qubits are measured and set to measurement value. Qubits to 12 | be to be measured will be equally spaced in circuit. 13 | """ 14 | 15 | def __init__(self, n_measurements): 16 | super().__init__() 17 | self.n_measurements = n_measurements 18 | 19 | def get_quantum_circuit(self, n_qubits): 20 | 21 | if self.n_measurements == 'half': 22 | self.n_measurements = floor(n_qubits / 2) 23 | 24 | if self.n_measurements > n_qubits: 25 | raise ValueError('Activation function was asked to measure more qubits than exist in the circuit.') 26 | 27 | if n_qubits % self.n_measurements != 0: 28 | logging.warning( 29 | f'In acivation function, number of qubits ({n_qubits}) is not multiple of number of measurements ' 30 | f'({self.n_measurements}), measurements will not be equally spaced in circuit.') 31 | 32 | self.qr = QuantumRegister(n_qubits, name='qr') 33 | self.cr = ClassicalRegister(self.n_measurements, name='activation_cr') 34 | self.qc = QuantumCircuit(self.qr, self.cr, name='Partial measurement') 35 | 36 | step = floor(n_qubits / self.n_measurements) 37 | 38 | for i, qubit in enumerate([step * j for j in range(0, self.n_measurements)]): 39 | self.qc.measure(self.qr[qubit], self.cr[i]) 40 | 41 | return self.qc 42 | -------------------------------------------------------------------------------- /qnn/ansatz/alternating_layer_tdcnot_ansatz.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class AlternatingLayerTDCnotAnsatz(VariationalAnsatz): 7 | """ 8 | A variational circuit ansatz. Prepares quantum circuit object for variational circuit consisting of thinly 9 | dressed C-NOT gates applied to pairs of qubits in an alternating layer pattern. A single sweep consists of two 10 | applications of thinly-dressed gates between nearest neighbour qubits, with alternating pairs between the two 11 | applications. The thinly dressed C-NOT gate is define in arXiv:2002.04612, we use single qubit Y rotations 12 | preceding and single qubit X rotations following the C-NOT gate. 13 | """ 14 | 15 | def __init__(self, layers, sweeps_per_layer, activation_function): 16 | super().__init__(layers, sweeps_per_layer, activation_function) 17 | 18 | def add_entangling_gates(self, n_data_qubits): 19 | 20 | for i in range(n_data_qubits - 1)[::2]: 21 | ctrl, tgt = i, ((i + 1) % self.qc.num_qubits) 22 | self.build_tdcnot(ctrl, tgt) 23 | 24 | for i in range(n_data_qubits)[1::2]: 25 | ctrl, tgt = i, ((i + 1) % self.qc.num_qubits) 26 | self.build_tdcnot(ctrl, tgt) 27 | 28 | return self.qc 29 | 30 | def build_tdcnot(self, ctrl, tgt): 31 | params = [Parameter("ansatz{}".format(str(self.param_counter + j))) for j in range(4)] 32 | self.qc.ry(params[0], self.qr[ctrl]) 33 | self.qc.ry(params[1], self.qr[tgt]) 34 | self.qc.cx(ctrl, tgt) 35 | self.qc.rz(params[2], self.qr[ctrl]) 36 | self.qc.rz(params[3], self.qr[tgt]) 37 | self.param_counter += 4 38 | 39 | def add_rotations(self, n_data_qubits): 40 | pass 41 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_15.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from ansatz.variational_ansatz import VariationalAnsatz 5 | 6 | 7 | class SimCirc15(VariationalAnsatz): 8 | """ 9 | A variational circuit ansatz. Based on circuit 15 in arXiv:1905.10876. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def get_quantum_circuit(self, n_data_qubits): 16 | self.qr = QuantumRegister(n_data_qubits, name='qr') 17 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 18 | 19 | for layer_no in range(self.layers): 20 | for sweep in range(0, self.sweeps_per_layer): 21 | self.add_rotations(n_data_qubits) 22 | self.add_entangling_gates(n_data_qubits, block=1) 23 | self.add_rotations(n_data_qubits) 24 | self.add_entangling_gates(n_data_qubits, block=2) 25 | if layer_no < self.layers - 1: 26 | self.apply_activation_function(n_data_qubits) 27 | return self.qc 28 | 29 | def add_rotations(self, n_data_qubits): 30 | for i in range(0, n_data_qubits): 31 | param = Parameter("ansatz{}".format(str(self.param_counter))) 32 | self.qc.ry(param, self.qr[i]) 33 | self.param_counter += 1 34 | return self.qc 35 | 36 | def add_entangling_gates(self, n_data_qubits, block=1): 37 | if block == 1: 38 | for i in reversed(range(n_data_qubits)): 39 | self.qc.cx(self.qr[i], self.qr[(i + 1) % n_data_qubits]) 40 | elif block == 2: 41 | for i in range(n_data_qubits): 42 | control_qubit = (i + n_data_qubits - 1) % n_data_qubits 43 | self.qc.cx(self.qr[control_qubit], self.qr[(control_qubit + 3) % n_data_qubits]) 44 | 45 | return self.qc 46 | -------------------------------------------------------------------------------- /qnn/input/data_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from itertools import product 3 | 4 | import numpy as np 5 | 6 | 7 | class DataHandler(ABC): 8 | 9 | def __init__(self): 10 | 11 | self.n_location_qubits = None 12 | self.n_ancilla_qubits = None 13 | self.n_colour_qubits = None 14 | 15 | self.qr = None 16 | self.qc = None 17 | 18 | self.location_register = None 19 | self.colour_register = None 20 | self.ancilla_register = None 21 | 22 | @abstractmethod 23 | def get_quantum_circuit(self, input_data): 24 | """ 25 | Returns a quantum circuit which initializes the qubits to the input data 26 | :return: 27 | """ 28 | pass 29 | 30 | def encode_using_neqr(self, input_data): 31 | """ 32 | Novel enhanced quantum representation (NEQR) encoding, where a pixel's location and value data is encoded in one 33 | computational basis state. To do this we append the location of pixels at the front of the bitstring. 34 | """ 35 | n_location_bits = (len(input_data) - 1).bit_length() 36 | 37 | location_strings = [i for i in product([0, 1], repeat=n_location_bits)] 38 | 39 | encoded_data = [] 40 | for index, location_string in enumerate(location_strings): 41 | encoded_data.append(np.concatenate([location_string, input_data[index]])) 42 | 43 | return encoded_data 44 | 45 | def encode_using_frqi(self, input_data): 46 | 47 | n_location_bits = (len(input_data) - 1).bit_length() 48 | 49 | location_strings = np.array([i for i in product([0, 1], repeat=n_location_bits)]) 50 | 51 | colour_ints = np.array([]) 52 | normalisation = 2 ** len(input_data[0]) 53 | for colour_bit_string in input_data: 54 | out = 0 55 | for bit in colour_bit_string: 56 | out = (out << 1) | bit 57 | colour_ints = np.append(colour_ints, out) 58 | angle_data = colour_ints / normalisation 59 | 60 | return location_strings, angle_data 61 | -------------------------------------------------------------------------------- /qnn/ansatz/farhi_ansatz.py: -------------------------------------------------------------------------------- 1 | from qiskit.circuit import Parameter 2 | 3 | from ansatz.variational_ansatz import VariationalAnsatz 4 | 5 | 6 | class FarhiAnsatz(VariationalAnsatz): 7 | """ 8 | A variational circuit ansatz. Prepares quantum circuit object for variational circuit consisting of X⊗X and Z⊗X 9 | rotations between n-1 qubits and single qubit described in arXiv:1802.06002. 10 | Between each layer (combined X⊗X and Z⊗X rotations), activation function can be applied using appropriate 11 | nonlinear activation function. 12 | """ 13 | 14 | def __init__(self, layers, sweeps_per_layer, activation_function): 15 | """ 16 | :param layers: Number of layers for ansatz. Between each layer the activation function is applied. 17 | :param sweeps_per_layer: Number parameterised gate sweeps in a single layer. No non-linearity will be applied 18 | between sweeps. 19 | :param activation_function: Type of activation function to be applied between each layer. Allowed values are 20 | 'partial_measurement' and 'null'. 21 | :param measurement_spacing: For 'partial measurement' activation function type, qubits to be measured will be 22 | uniformly spaced along quantum register by this amount. 23 | """ 24 | super().__init__(layers, sweeps_per_layer, activation_function) 25 | 26 | def add_entangling_gates(self, n_data_qubits): 27 | self.rxx_to_all(n_data_qubits) 28 | self.rzx_to_all(n_data_qubits) 29 | 30 | def add_rotations(self, n_data_qubits): 31 | pass 32 | 33 | def rxx_to_all(self, n_data_qubits): 34 | for i in range(n_data_qubits - 1): 35 | param = Parameter("ansatz{}".format(str(self.param_counter))) 36 | self.qc.rxx(param, self.qr[-1], self.qr[i]) 37 | self.param_counter += 1 38 | return self.qc 39 | 40 | def rzx_to_all(self, n_data_qubits): 41 | for i in range(n_data_qubits - 1): 42 | param = Parameter("ansatz{}".format(str(self.param_counter))) 43 | self.qc.rzx(param, self.qr[-1], self.qr[i]) 44 | self.param_counter += 1 45 | return self.qc 46 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_13.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from ansatz.variational_ansatz import VariationalAnsatz 5 | 6 | 7 | class SimCirc13(VariationalAnsatz): 8 | """ 9 | A variational circuit ansatz. Based on circuit 13 in arXiv:1905.10876. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def get_quantum_circuit(self, n_data_qubits): 16 | self.qr = QuantumRegister(n_data_qubits, name='qr') 17 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 18 | 19 | for layer_no in range(self.layers): 20 | for sweep in range(0, self.sweeps_per_layer): 21 | self.add_rotations(n_data_qubits) 22 | self.add_entangling_gates(n_data_qubits, block=1) 23 | self.add_rotations(n_data_qubits) 24 | self.add_entangling_gates(n_data_qubits, block=2) 25 | if layer_no < self.layers - 1: 26 | self.apply_activation_function(n_data_qubits) 27 | return self.qc 28 | 29 | def add_rotations(self, n_data_qubits): 30 | for i in range(0, n_data_qubits): 31 | param = Parameter("ansatz{}".format(str(self.param_counter))) 32 | self.qc.ry(param, self.qr[i]) 33 | self.param_counter += 1 34 | return self.qc 35 | 36 | def add_entangling_gates(self, n_data_qubits, block=1): 37 | if block == 1: 38 | for i in reversed(range(n_data_qubits)): 39 | param = Parameter("ansatz{}".format(str(self.param_counter))) 40 | self.qc.crz(param, self.qr[i], self.qr[(i + 1) % n_data_qubits]) 41 | self.param_counter += 1 42 | 43 | elif block == 2: 44 | for i in range(n_data_qubits): 45 | param = Parameter("ansatz{}".format(str(self.param_counter))) 46 | control_qubit = (i + n_data_qubits - 1) % n_data_qubits 47 | self.qc.crz(param, self.qr[control_qubit], self.qr[(control_qubit + 3) % n_data_qubits]) 48 | self.param_counter += 1 49 | return self.qc 50 | -------------------------------------------------------------------------------- /qnn/ansatz/sim_circ_14.py: -------------------------------------------------------------------------------- 1 | from qiskit import QuantumCircuit, QuantumRegister 2 | from qiskit.circuit import Parameter 3 | 4 | from ansatz.variational_ansatz import VariationalAnsatz 5 | 6 | 7 | class SimCirc14(VariationalAnsatz): 8 | """ 9 | A variational circuit ansatz. Based on circuit 14 in arXiv:1905.10876. 10 | """ 11 | 12 | def __init__(self, layers, sweeps_per_layer, activation_function): 13 | super().__init__(layers, sweeps_per_layer, activation_function) 14 | 15 | def get_quantum_circuit(self, n_data_qubits): 16 | self.qr = QuantumRegister(n_data_qubits, name='qr') 17 | self.qc = QuantumCircuit(self.qr, name='Shifted circ') 18 | 19 | for layer_no in range(self.layers): 20 | for sweep in range(0, self.sweeps_per_layer): 21 | self.add_rotations(n_data_qubits) 22 | self.add_entangling_gates(n_data_qubits, block=1) 23 | self.add_rotations(n_data_qubits) 24 | self.add_entangling_gates(n_data_qubits, block=2) 25 | if layer_no < self.layers - 1: 26 | self.apply_activation_function(n_data_qubits) 27 | return self.qc 28 | 29 | def add_rotations(self, n_data_qubits): 30 | for i in range(0, n_data_qubits): 31 | param = Parameter("ansatz{}".format(str(self.param_counter))) 32 | self.qc.ry(param, self.qr[i]) 33 | self.param_counter += 1 34 | return self.qc 35 | 36 | def add_entangling_gates(self, n_data_qubits, block=1): 37 | if block == 1: 38 | for i in reversed(range(n_data_qubits)): 39 | param = Parameter("ansatz{}".format(str(self.param_counter))) 40 | self.qc.crx(param, self.qr[i], self.qr[(i + 1) % n_data_qubits]) 41 | self.param_counter += 1 42 | 43 | elif block == 2: 44 | for i in range(n_data_qubits): 45 | param = Parameter("ansatz{}".format(str(self.param_counter))) 46 | control_qubit = (i + n_data_qubits - 1) % n_data_qubits 47 | self.qc.crx(param, self.qr[control_qubit], self.qr[(control_qubit + 3) % n_data_qubits]) 48 | self.param_counter += 1 49 | return self.qc 50 | -------------------------------------------------------------------------------- /qnn/ansatz/variational_ansatz_factory.py: -------------------------------------------------------------------------------- 1 | from ansatz.abbas import Abbas 2 | from ansatz.alternating_layer_tdcnot_ansatz import AlternatingLayerTDCnotAnsatz 3 | from ansatz.farhi_ansatz import FarhiAnsatz 4 | from ansatz.null_ansatz import NullAnsatz 5 | from ansatz.sim_circ_13 import SimCirc13 6 | from ansatz.sim_circ_13_half import SimCirc13Half 7 | from ansatz.sim_circ_14 import SimCirc14 8 | from ansatz.sim_circ_14_half import SimCirc14Half 9 | from ansatz.sim_circ_15 import SimCirc15 10 | from ansatz.sim_circ_19 import SimCirc19 11 | 12 | 13 | class VariationalAnsatzFactory: 14 | 15 | def __init__(self, ansatz_type, layers, sweeps_per_layer, activation_function): 16 | self.ansatz_type = ansatz_type 17 | self.layers = layers 18 | self.sweeps_per_layer = sweeps_per_layer 19 | self.activation_function = activation_function 20 | 21 | def get(self): 22 | """ 23 | Returns the appropriate ansatz circuit 24 | """ 25 | 26 | if self.ansatz_type == 'farhi': 27 | return FarhiAnsatz(self.layers, self.sweeps_per_layer, self.activation_function) 28 | 29 | elif self.ansatz_type == 'alternating_layer_tdcnot': 30 | return AlternatingLayerTDCnotAnsatz(self.layers, self.sweeps_per_layer, self.activation_function) 31 | 32 | elif self.ansatz_type == 'sim_circ_13_half': 33 | return SimCirc13Half(self.layers, self.sweeps_per_layer, self.activation_function) 34 | 35 | elif self.ansatz_type == 'sim_circ_13': 36 | return SimCirc13(self.layers, self.sweeps_per_layer, self.activation_function) 37 | 38 | elif self.ansatz_type == 'sim_circ_14_half': 39 | return SimCirc14Half(self.layers, self.sweeps_per_layer, self.activation_function) 40 | 41 | elif self.ansatz_type == 'sim_circ_14': 42 | return SimCirc14(self.layers, self.sweeps_per_layer, self.activation_function) 43 | 44 | elif self.ansatz_type == 'sim_circ_15': 45 | return SimCirc15(self.layers, self.sweeps_per_layer, self.activation_function) 46 | 47 | elif self.ansatz_type == 'sim_circ_19': 48 | return SimCirc19(self.layers, self.sweeps_per_layer, self.activation_function) 49 | 50 | elif self.ansatz_type == 'abbas': 51 | return Abbas(self.layers, self.sweeps_per_layer, self.activation_function) 52 | 53 | elif self.ansatz_type is None or 'null': 54 | return NullAnsatz(self.layers, self.sweeps_per_layer, self.activation_function) 55 | 56 | else: 57 | raise ValueError("Invalid ansatz type: {}".format(self.ansatz_type)) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,pycharm 3 | 4 | venv/ 5 | 6 | ### Linux ### 7 | *~ 8 | 9 | # temporary files which can be created if a process still has a handle open of a deleted file 10 | .fuse_hidden* 11 | 12 | # KDE directory preferences 13 | .directory 14 | 15 | # Linux trash folder which might appear on any partition or disk 16 | .Trash-* 17 | 18 | # .nfs files are created when an open file is removed but is still being accessed 19 | .nfs* 20 | 21 | ### PyCharm ### 22 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 23 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 24 | 25 | # User-specific stuff 26 | .idea/**/tasks.xml 27 | .idea/**/usage.statistics.xml 28 | .idea/**/dictionaries 29 | .idea/**/shelf 30 | 31 | # Generated files 32 | .idea/**/contentModel.xml 33 | 34 | # Sensitive or high-churn files 35 | .idea/**/dataSources/ 36 | .idea/**/dataSources.ids 37 | .idea/**/dataSources.local.xml 38 | .idea/**/sqlDataSources.xml 39 | .idea/**/dynamic.xml 40 | .idea/**/uiDesigner.xml 41 | .idea/**/dbnavigator.xml 42 | 43 | # Gradle 44 | .idea/**/gradle.xml 45 | .idea/**/libraries 46 | 47 | # Gradle and Maven with auto-import 48 | # When using Gradle or Maven with auto-import, you should exclude module files, 49 | # since they will be recreated, and may cause churn. Uncomment if using 50 | # auto-import. 51 | # .idea/modules.xml 52 | # .idea/*.iml 53 | # .idea/modules 54 | 55 | # CMake 56 | cmake-build-*/ 57 | 58 | # Mongo Explorer plugin 59 | .idea/**/mongoSettings.xml 60 | 61 | # File-based project format 62 | *.iws 63 | 64 | # IntelliJ 65 | out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Cursive Clojure plugin 74 | .idea/replstate.xml 75 | 76 | # Crashlytics plugin (for Android Studio and IntelliJ) 77 | com_crashlytics_export_strings.xml 78 | crashlytics.properties 79 | crashlytics-build.properties 80 | fabric.properties 81 | 82 | # Editor-based Rest Client 83 | .idea/httpRequests 84 | 85 | # Android studio 3.1+ serialized cache file 86 | .idea/caches/build_file_checksums.ser 87 | 88 | ### PyCharm Patch ### 89 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 90 | 91 | # *.iml 92 | # modules.xml 93 | # .idea/misc.xml 94 | # *.ipr 95 | 96 | # Sonarlint plugin 97 | .idea/sonarlint 98 | 99 | # Misc things still not covered 100 | .idea/ 101 | .ipynb_checkpoints/ 102 | __pycache__/ 103 | *.log 104 | *.DS_Store 105 | simclr_pytorch/runs/ 106 | simclr_pytorch/data 107 | mnist_examples/datasets 108 | /datasets 109 | *.txt 110 | results 111 | mnist_examples/results 112 | 113 | # End of https://www.gitignore.io/api/linux,pycharm 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quantum neural network 2 | 3 | This is a library for creating and training quantum neural networks with Qiskit. It has been used in the following works: 4 | 5 | >[Quantum Self-Supervised Learning](https://arxiv.org/abs/2103.14653) 6 | 7 | 8 | ## Training with entirely quantum networks 9 | 10 | Images can be loaded into a quantum circuit using the data handlers in `quantum-neural-network/input`. The parameters 11 | can then be trained by an external optimiser. 12 | 13 | A working example of a single forward pass for a random input vector can be run: 14 | 15 | ``` 16 | python run_simple_network.py 17 | ``` 18 | 19 | This uses a `vector_data_handler` in which the data is inputted as single qubit rotations on a product state. 20 | 21 | These networks can run by themselves or can be integrated as layers into a larger classical neural network. 22 | 23 | ## Embedding into classical neural networks 24 | 25 | For running on current quantum computers, it may be beneficial to embed a QNN within a classical network. Rather than 26 | inputting a whole image into the quantum circuit, the feature vector from the previous classical layer is passed in. 27 | 28 | A working example of how to integrate our `QNet` 29 | class with PyTorch can be found in `mnist_examples/pytorch_with_qnet.py`. The general structure is: 30 | 31 | ```python 32 | from qnet import QNet 33 | import torch.nn as nn 34 | 35 | 36 | class Net(nn.Module): 37 | def __init__(self): 38 | super(Net, self).__init__() 39 | 40 | # CLASSICAL PYTORCH LAYERS 41 | 42 | self.qnet = QNet(n_qubits=2, encoding='vector', ansatz_type='farhi', layers=1, 43 | activation_function_type='partial_measurement_1') 44 | 45 | def forward(self, x): 46 | # CLASSICAL PYTORCH LAYERS 47 | 48 | x = self.qnet(x) 49 | return x 50 | ``` 51 | 52 | where `QNet` returns a feature vector, the length of which is determiend by the input dimension and ansatz type. 53 | 54 | ## Config options 55 | 56 | ### Ansatzes 57 | 58 | - `abbas` (https://arxiv.org/abs/2011.00027) 59 | - `alternating_layer_tdcnot` (https://arxiv.org/abs/2002.04612) 60 | - `farhi` (https://arxiv.org/abs/1802.06002) 61 | - `sim_circ_13, sim_circ_13_half, sim_circ_14, sim_circ_14_half, sim_circ_15, sim_circ_19` (https://arxiv.org/abs/1905.10876) 62 | 63 | ### Quantum activation functions 64 | 65 | - `parial_meas_x` where x is the number of qubits to be measured between each layer. `x` can also be 'half'. 66 | 67 | ### Data handlers 68 | 69 | - `frqi` (https://link.springer.com/article/10.1007/s11128-010-0177-y) 70 | - `havlicek` (https://arxiv.org/abs/1804.11326) 71 | - `neqr` (https://link.springer.com/article/10.1007/s11128-013-0567-z) 72 | 73 | ## Usage and citation 74 | 75 | This repository was developed in conjunction with the following work, which we kindly ask any publication, whitepaper or 76 | project using this code to cite: 77 | 78 | ``` 79 | Jaderberg, B., Anderson, L.W., Xie, W., Albanie, S., Kiffner, M. and Jaksch, D., 2021. Quantum Self-Supervised Learning. arXiv preprint arXiv:2103.14653. 80 | ``` -------------------------------------------------------------------------------- /mnist_examples/keras.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from keras.datasets import mnist 5 | from keras.layers import Dense # Dense layers are "fully connected" layers 6 | from keras.models import Sequential # Documentation: https://keras.io/models/sequential/ 7 | 8 | # Load training and test data 9 | from keras.utils import to_categorical 10 | 11 | (x_train, y_train), (x_test, y_test) = mnist.load_data() 12 | 13 | # Remove all data which isn't labelled as a 3 or 6 14 | train_mask = np.isin(y_train, [3, 6]) 15 | test_mask = np.isin(y_test, [3, 6]) 16 | 17 | x_train, y_train = x_train[train_mask], y_train[train_mask] 18 | x_test, y_test = x_test[test_mask], y_test[test_mask] 19 | 20 | print("Training data shape: ", x_train.shape) 21 | print("Test data shape", x_test.shape) 22 | 23 | dsize = 4 24 | 25 | # Downsample image data 26 | x_train = np.array([cv2.resize(sample, dsize=(dsize, dsize), interpolation=cv2.INTER_CUBIC) for sample in x_train]) 27 | x_test = np.array([cv2.resize(sample, dsize=(dsize, dsize), interpolation=cv2.INTER_CUBIC) for sample in x_test]) 28 | 29 | # Flatten the images 30 | image_vector_size = dsize ** 2 31 | x_train = x_train.reshape(x_train.shape[0], image_vector_size) 32 | x_test = x_test.reshape(x_test.shape[0], image_vector_size) 33 | 34 | print("Training data shape: ", x_train.shape) # (12049, 784) -- 12049 images, each 784x1 vector 35 | print("Test data shape", x_test.shape) # (1968, 784) -- 1968 images, each 784x1 vector 36 | 37 | # Convert from greyscale to black and white binary 38 | _, x_train = cv2.threshold(x_train, 127, 255, cv2.THRESH_BINARY) 39 | _, x_test = cv2.threshold(x_test, 127, 255, cv2.THRESH_BINARY) 40 | 41 | # Map the labels "3" and "6" to labels starting from 0 e.g. ("0" and "1") 42 | y_train = np.array([0 if val == 3 else 1 for val in y_train]) 43 | y_test = np.array([0 if val == 3 else 1 for val in y_test]) 44 | 45 | # Convert to "one-hot" vectors using the to_categorical function 46 | y_train = to_categorical(y_train) 47 | y_test = to_categorical(y_test) 48 | 49 | # Create the network 50 | num_classes = 2 # two unique digits 51 | 52 | model = Sequential() 53 | # The input layer requires the special input_shape parameter which should match 54 | # the shape of our training data. 55 | model.add(Dense(units=3, activation='sigmoid', input_shape=(image_vector_size,))) 56 | model.add(Dense(units=num_classes, activation='softmax')) 57 | model.summary() 58 | 59 | model.compile(optimizer="sgd", loss='categorical_crossentropy', metrics=['accuracy']) 60 | 61 | epochs = 50 62 | history = model.fit(x_train, y_train, batch_size=128, epochs=epochs, verbose=False, validation_split=.1) 63 | loss, accuracy = model.evaluate(x_test, y_test, verbose=True) 64 | 65 | plt.plot(history.history['accuracy']) 66 | plt.plot(history.history['val_accuracy']) 67 | plt.title('model accuracy') 68 | plt.ylabel('accuracy') 69 | plt.ylim(0.5, 1.0) 70 | plt.xlabel('epoch') 71 | plt.legend(['training', 'validation'], loc='best', title=f'Test accuracy: {accuracy:.3}') 72 | plt.show() 73 | 74 | print(f'Test loss: {loss:.3}') 75 | print(f'Test accuracy: {accuracy:.3}') 76 | -------------------------------------------------------------------------------- /qnn/input/frqi_bennett_data_handler.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from qiskit import QuantumRegister, QuantumCircuit 3 | 4 | from input.data_handler import DataHandler 5 | 6 | 7 | class FRQIBennettDataHandler(DataHandler): 8 | """ 9 | A Bennett data handler for FRQI encoding. Prepares the input data using our hand-crafted scheme rather than Qiskit's initialize 10 | function. This has the benefit of being scalable since we don't need to store the statevector. 11 | 12 | The scheme is as follows: 13 | 1. Prepare the location qubits into all computational basis states (hadamard each qubit H^⊗n) 14 | 2. Prepare the pixels. This is achieved by: 15 | a. Pick a location basis. Apply X gates so that every qubit is in the |1> state. 16 | b. Apply N-control-Y-rotation gates to the colour qubits to rotate by angle encoding colour. 17 | The control should be across all location qubits. 18 | c. Apply X gates to reverse step a. such that the real location state is restored. 19 | d. Cycle to the next location basis and repeat. 20 | """ 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | def get_quantum_circuit(self, input_data): 26 | self.n_location_qubits = (len(input_data) - 1).bit_length() 27 | self.n_colour_qubits = 1 28 | self.n_ancilla_qubits = max(self.n_location_qubits - 2, 0) 29 | 30 | self.qr = QuantumRegister(self.n_location_qubits + self.n_ancilla_qubits + self.n_colour_qubits) 31 | self.qc = QuantumCircuit(self.qr, name='FRQI') 32 | 33 | self.location_register = self.qr[:self.n_location_qubits] 34 | self.colour_register = self.qr[self.n_location_qubits:self.n_location_qubits + self.n_colour_qubits] 35 | self.ancilla_register = self.qr[ 36 | self.n_location_qubits + self.n_colour_qubits:self.n_location_qubits + self.n_colour_qubits + self.n_ancilla_qubits:] 37 | 38 | location_strings, angle_data = self.encode_using_frqi(input_data) 39 | 40 | self.qc.h(self.location_register) 41 | 42 | self.prepare_colour_pixels(angle_data, location_strings) 43 | 44 | self.qc.barrier() 45 | 46 | return self.qc 47 | 48 | def prepare_colour_pixels(self, angle_data, location_strings): 49 | for i, location_string in enumerate(location_strings): 50 | theta = angle_data[i] * np.pi 51 | if theta != 0: 52 | location_qubits_flipped = self.make_location_qubits_ones(location_string) 53 | 54 | self.qc.mcry(theta, self.location_register, self.colour_register[0], self.ancilla_register, 55 | mode='basic') 56 | 57 | self.reverse_location_flips(location_qubits_flipped) 58 | 59 | def make_location_qubits_ones(self, location_string): # Repeated for FRQI and NEQR - move to DataHandler Class 60 | 61 | location_qubits_to_flip = np.where(location_string == 0)[0] 62 | 63 | for qubit in location_qubits_to_flip: 64 | self.qc.x(self.location_register[int(qubit)]) 65 | 66 | return location_qubits_to_flip 67 | 68 | def reverse_location_flips(self, qubits_to_flip): 69 | 70 | for qubit in qubits_to_flip: 71 | self.qc.x(qubit) 72 | -------------------------------------------------------------------------------- /test/test_quantum_network_circuit.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import sys 3 | 4 | sys.path.append('quantum-neural-network') 5 | from unittest import TestCase 6 | 7 | from qiskit import QuantumCircuit, Aer, execute 8 | 9 | from config import Config 10 | from quantum_network_circuit import QuantumNetworkCircuit 11 | 12 | default_config = Config('vector', ansatz_type='sim_circ_13_half', layers=1) 13 | 14 | 15 | class TestQuantumNetworkCircuit(TestCase): 16 | 17 | def test_when_create_qnn_then_happy_path(self): 18 | for ansatz in ['sim_circ_13', 'sim_circ_13_half', 'farhi', 'alternating_layer_tdcnot', 'sim_circ_15', 19 | 'sim_circ_19', 'abbas', 'null']: 20 | for af in ['null', None, 'partial_measurement_half', 'partial_measurement_2']: 21 | for encoding in ['vector', 'havlicek']: 22 | QuantumNetworkCircuit(Config(encoding, ansatz_type=ansatz, activation_function_type=af), 4, 23 | [1, 1, 1, 1]) 24 | 25 | def test_when_create_qnn_then_input_parameters_ordered(self): 26 | qnn = QuantumNetworkCircuit(copy.deepcopy(default_config), 12, input_data=[i for i in range(12)]) 27 | expected = ['input{}'.format(i) for i in range(12)] 28 | self.assertEqual(expected, [p.name for p in qnn.input_circuit_parameters]) 29 | 30 | def test_when_create_qnn_then_ansatz_parameters_ordered(self): 31 | qnn = QuantumNetworkCircuit(copy.deepcopy(default_config), input_qubits=6) 32 | expected = ['ansatz{}'.format(i) for i in range(12)] 33 | for i in range(len(qnn.ansatz_circuit_parameters)): 34 | self.assertEqual(expected[i], qnn.ansatz_circuit_parameters[i].name) 35 | 36 | def test_when_construct_network_then_not_empty(self): 37 | qnn = QuantumNetworkCircuit(copy.deepcopy(default_config), 2) 38 | 39 | qnn.construct_network([1, 1]) 40 | 41 | self.assertTrue(qnn.qc.depth() > 0) 42 | 43 | def test_when_bind_circuits_then_no_parameterised_gates_left(self): 44 | qnn = QuantumNetworkCircuit(copy.deepcopy(default_config), 2, [1, 1]) 45 | params = [1, 1, 2, 2, 2, 2] 46 | bound_qc = qnn.bind_circuit(params) 47 | 48 | self.assertEqual(bound_qc.num_parameters, 0) 49 | 50 | def test_evaluate_circuit(self): 51 | pass 52 | 53 | def test_given_qasm_simulator_when_hadamard_circuit_then_correct_values(self): 54 | qc = QuantumCircuit(5) 55 | qc.h(qc.qregs[0]) 56 | qc.measure_all() 57 | result = execute(qc, backend=Aer.get_backend('qasm_simulator'), shots=10000).result() 58 | 59 | vector = QuantumNetworkCircuit.get_vector_from_results(result) 60 | 61 | self.assertTrue(len(vector) == 5) 62 | for i in range(5): 63 | self.assertAlmostEqual(0.0, vector[i], delta=0.5) 64 | 65 | def test_given_statevector_simulator_when_hadamard_circuit_then_correct_values(self): 66 | qc = QuantumCircuit(5) 67 | qc.h(qc.qregs[0]) 68 | result = execute(qc, backend=Aer.get_backend('statevector_simulator'), shots=1).result() 69 | 70 | vector = QuantumNetworkCircuit.get_vector_from_results(result) 71 | 72 | self.assertTrue(len(vector) == 5) 73 | for i in range(5): 74 | self.assertAlmostEqual(0.0, vector[i], delta=1e-10) 75 | -------------------------------------------------------------------------------- /qnn/gradient_calculator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from qiskit import assemble, transpile 5 | 6 | 7 | def calculate_gradient_list(qnn, parameter_list, method='parameter shift', shots=100, eps=None): 8 | parameter_list = np.array(parameter_list, dtype=float) 9 | 10 | if method == 'parameter shift': 11 | r = 0.5 # for Farhi ansatz, e0 = -1, e1 = +1, a = 1 => r = 0.5 (Using notation in arXiv:1905.13311) 12 | 13 | qc_plus_list, qc_minus_list = get_parameter_shift_circuits(qnn, parameter_list, r) 14 | 15 | expectation_minus, expectation_plus = evaluate_gradient_jobs(qc_minus_list, qc_plus_list, qnn, shots) 16 | 17 | gradient_list = r * (expectation_plus - expectation_minus) 18 | 19 | elif method == 'finite difference': 20 | qc_plus_list, qc_minus_list = get_finite_difference_circuits(qnn, parameter_list, eps) 21 | 22 | expectation_minus, expectation_plus = evaluate_gradient_jobs(qc_minus_list, qc_plus_list, qnn, shots) 23 | 24 | gradient_list = (expectation_plus - expectation_minus) / (2 * eps) 25 | 26 | else: 27 | raise ValueError("Invalid gradient method") 28 | 29 | gradient_list = gradient_list.reshape([len(parameter_list), -1]) 30 | return gradient_list 31 | 32 | 33 | def evaluate_gradient_jobs(qc_minus_list, qc_plus_list, qnn, shots): 34 | qc_minus_list = [transpile(circ, basis_gates=['cx', 'u1', 'u2', 'u3']) for circ in qc_minus_list] 35 | qc_plus_list = [transpile(circ, basis_gates=['cx', 'u1', 'u2', 'u3']) for circ in qc_plus_list] 36 | job = assemble(qc_minus_list + qc_plus_list, backend=qnn.backend, shots=shots) 37 | results = qnn.backend.run(job).result() 38 | expectation_plus = [] 39 | expectation_minus = [] 40 | num_params = len(qc_plus_list) 41 | for i in range(num_params): 42 | expectation_minus.append(qnn.get_vector_from_results(results, i)) 43 | expectation_plus.append(qnn.get_vector_from_results(results, num_params + i)) 44 | logging.debug("Gradient calculated for {} out of {} parameters".format(i, num_params)) 45 | return np.array(expectation_minus), np.array(expectation_plus) 46 | 47 | 48 | def get_parameter_shift_circuits(qnn, parameter_list, r): 49 | qc_plus_list, qc_minus_list = [], [] 50 | for i in range(len(parameter_list)): 51 | shifted_params_plus = np.copy(parameter_list) 52 | shifted_params_plus[i] = shifted_params_plus[i] + np.pi / (4 * r) 53 | shifted_params_minus = np.copy(parameter_list) 54 | shifted_params_minus[i] = shifted_params_minus[i] - np.pi / (4 * r) 55 | 56 | qc_i_plus = qnn.bind_circuit(shifted_params_plus) 57 | qc_i_minus = qnn.bind_circuit(shifted_params_minus) 58 | qc_plus_list.append(qc_i_plus) 59 | qc_minus_list.append(qc_i_minus) 60 | 61 | return qc_plus_list, qc_minus_list 62 | 63 | 64 | def get_finite_difference_circuits(qnn, parameter_list, eps): 65 | if type(eps) == float: 66 | qc_plus_list, qc_minus_list = [], [] 67 | for i in range(len(parameter_list)): 68 | shifted_params_plus = np.copy(parameter_list) 69 | shifted_params_plus[i] = shifted_params_plus[i] + eps 70 | shifted_params_minus = np.copy(parameter_list) 71 | shifted_params_minus[i] = shifted_params_minus[i] - eps 72 | 73 | qc_i_plus = qnn.bind_circuit(shifted_params_plus) 74 | qc_i_minus = qnn.bind_circuit(shifted_params_minus) 75 | qc_plus_list.append(qc_i_plus) 76 | qc_minus_list.append(qc_i_minus) 77 | 78 | return qc_plus_list, qc_minus_list 79 | else: 80 | raise ValueError("eps for finite difference scheme must be float") 81 | -------------------------------------------------------------------------------- /qnn/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from qiskit import IBMQ, Aer 4 | from qiskit.providers import QiskitBackendNotFoundError 5 | from qiskit.providers.ibmq import IBMQAccountError 6 | from qiskit.providers.ibmq.api.exceptions import AuthenticationLicenseError 7 | 8 | from activation_function.activation_function import ActivationFunction 9 | from activation_function.activation_function_factory import ActivationFunctionFactory 10 | from ansatz.variational_ansatz import VariationalAnsatz 11 | from ansatz.variational_ansatz_factory import VariationalAnsatzFactory 12 | from input.data_handler import DataHandler 13 | from input.data_handler_factory import DataHandlerFactory 14 | 15 | 16 | class Config: 17 | def __init__(self, encoding, data_handler_method=None, ansatz_type=None, layers=3, sweeps_per_layer=1, 18 | activation_function_type=None, meas_method='all', grad_method='parameter shift', 19 | backend_type='qasm_simulator'): 20 | """ 21 | :param encoding: 22 | :param data_handler_method: 23 | :param ansatz_type: 24 | :param layers: Number of layers in variational ansatz 25 | :param sweeps_per_layer: Number of sweeps of parameterised gates within a single layer 26 | :param activation_function_type: Valid types are null, 'partial_measurement_half or 'partial_measurement_X' 27 | where X is an integer giving the number of measurements for the activation function. 28 | :param meas_method: Valid methods are 'all' or 'ancilla' 29 | :param grad_method: 30 | """ 31 | self.encoding = encoding 32 | self.data_handler_method = data_handler_method 33 | self.ansatz_type = ansatz_type 34 | self.layers = layers 35 | self.sweeps_per_layer = sweeps_per_layer 36 | self.activation_function_type = activation_function_type 37 | self.meas_method = meas_method 38 | self.grad_method = grad_method 39 | self.backend = self.get_backend(backend_type) 40 | self.data_handler: DataHandler = DataHandlerFactory(encoding, data_handler_method).get() 41 | self.activation_function: ActivationFunction = ActivationFunctionFactory(activation_function_type).get() 42 | self.ansatz: VariationalAnsatz = VariationalAnsatzFactory(ansatz_type, layers, sweeps_per_layer, 43 | self.activation_function).get() 44 | 45 | def log_info(self): 46 | logging.info(f"QNN configuration:\nencoding = {self.encoding}" + 47 | f"\ndata handler = {self.data_handler_method}" + 48 | f"\nansatz = {self.ansatz_type}" + 49 | f"\nnumber of layers = {self.layers}" + 50 | f"\nnumber of sweeps per layer = {self.sweeps_per_layer}" + 51 | f"\nactivation function = {self.activation_function_type}" + 52 | f"\noutput measurement = {self.meas_method}" + 53 | f"\ngradient method = {self.grad_method}" + 54 | f"\nsimulation backend = {self.backend}") 55 | 56 | def get_backend(self, backend_name): 57 | backend = None 58 | if backend_name not in ['qasm_simulator', 'statevector_simulator']: 59 | try: 60 | IBMQ.load_account() 61 | oxford_provider = IBMQ.get_provider(hub='ibm-q-oxford') 62 | backend = oxford_provider.get_backend(backend_name) 63 | except (IBMQAccountError, AuthenticationLicenseError): 64 | logging.warning("Unable to connect to IBMQ servers.") 65 | pass 66 | except QiskitBackendNotFoundError: 67 | logging.debug('{} is not a valid online backend, trying local simulators.'.format(backend_name)) 68 | pass 69 | if backend is None: 70 | backend = Aer.get_backend(backend_name) 71 | return backend 72 | -------------------------------------------------------------------------------- /qnn/input/neqr_bennett_data_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from qiskit import QuantumCircuit, QuantumRegister 5 | 6 | from input.data_handler import DataHandler 7 | 8 | 9 | class NEQRBennettDataHandler(DataHandler): 10 | """ 11 | A Bennett data handler. Prepares the input data using our hand-crafted scheme rather than Qiskit's initialize 12 | function. This has the benefit of being scalable since we don't need to store the statevector. 13 | 14 | The scheme is as follows: 15 | 1. Prepare the location qubits into all computational basis states (hadamard each qubit H^⊗n) 16 | 2. Prepare the colour qubits into a base colour who's bitstring has the most popular bit at each position. This 17 | ensures the minimum number of gates applied at step 3. 18 | 3. Prepare the pixels. This is achieved by: 19 | a. Pick a location basis. Apply X gates so that every qubit is in the |1> state. 20 | b. Apply N-control-X gates to the colour qubits that differ between the base colour and desired pixel colour. 21 | The control should be across all location qubits. 22 | c. Apply X gates to reverse step a. such that the real location state is restored. 23 | d. Cycle to the next location basis and repeat. 24 | """ 25 | 26 | def __init__(self): 27 | super().__init__() 28 | 29 | def get_quantum_circuit(self, input_data): 30 | self.n_location_qubits = (len(input_data) - 1).bit_length() 31 | self.n_ancilla_qubits = max(self.n_location_qubits - 2, 0) 32 | self.n_colour_qubits = len(input_data[0]) 33 | 34 | self.qr = QuantumRegister(self.n_location_qubits + self.n_ancilla_qubits + self.n_colour_qubits) 35 | self.qc = QuantumCircuit(self.qr, name='NEQR') 36 | 37 | self.colour_register = self.qr[self.n_location_qubits:self.n_location_qubits + self.n_colour_qubits] 38 | self.ancilla_register = self.qr[ 39 | self.n_location_qubits + self.n_colour_qubits:self.n_location_qubits + self.n_colour_qubits + self.n_ancilla_qubits:] 40 | 41 | encoded_data = self.encode_using_neqr(input_data) 42 | 43 | # Prepare the location qubits in all computational basis 44 | self.qc.h(self.location_register) 45 | 46 | base_colour = self.prepare_base_colour(encoded_data) 47 | 48 | self.prepare_colour_pixels(encoded_data, base_colour) 49 | 50 | self.qc.barrier() 51 | 52 | return self.qc 53 | 54 | def prepare_base_colour(self, encoded_data): 55 | 56 | bit_frequency = np.zeros(self.n_colour_qubits) 57 | 58 | for bit_string in encoded_data: 59 | bit_frequency += bit_string[-self.n_colour_qubits:] 60 | 61 | mode_bit_string = np.array( 62 | [int(np.floor(((2 * val) - 1) / len(encoded_data))) if val != 0 else 0 for val in bit_frequency]) 63 | 64 | logging.info( 65 | "Over all pixels, the modal bit at each position creates the base colour {}".format(mode_bit_string)) 66 | 67 | for colour_qubit_to_flip in np.where(mode_bit_string == 1)[0]: 68 | self.qc.x(self.colour_register[colour_qubit_to_flip]) 69 | 70 | return mode_bit_string 71 | 72 | def prepare_colour_pixels(self, encoded_data, base_colour): 73 | 74 | for bitstring in encoded_data: 75 | location_string = bitstring[:self.n_location_qubits] 76 | location_qubits_flipped = self.make_location_qubits_ones(location_string) 77 | 78 | target_colour = bitstring[self.n_location_qubits:] 79 | self.change_base_colour_to_pixel_colour(target_colour, base_colour) 80 | 81 | self.reverse_location_flips(location_qubits_flipped) 82 | 83 | def make_location_qubits_ones(self, location_string): 84 | 85 | location_qubits_to_flip = np.where(location_string == 0)[0] 86 | 87 | for qubit in location_qubits_to_flip: 88 | self.qc.x(self.location_register[qubit]) 89 | 90 | return location_qubits_to_flip 91 | 92 | def change_base_colour_to_pixel_colour(self, target_colour, base_colour): 93 | 94 | colour_qubits_to_flip = np.where(base_colour != target_colour)[0] 95 | 96 | for qubit in colour_qubits_to_flip: 97 | self.qc.mct(self.location_register, self.colour_register[qubit], 98 | self.ancilla_register, mode='basic') 99 | 100 | def reverse_location_flips(self, qubits_to_flip): 101 | 102 | for qubit in qubits_to_flip: 103 | self.qc.x(qubit) 104 | -------------------------------------------------------------------------------- /qnn/qnet.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | 4 | import numpy as np 5 | import torch 6 | import torch.nn as nn 7 | from torch import cuda 8 | from torch.autograd import Function 9 | 10 | from config import Config 11 | from gradient_calculator import calculate_gradient_list 12 | from input.vector_data_handler import VectorDataHandler 13 | from quantum_network_circuit import QuantumNetworkCircuit 14 | 15 | 16 | class QNetFunction(Function): 17 | 18 | @staticmethod 19 | def forward(ctx, input, weight, qnn: QuantumNetworkCircuit, shots, save_statevectors): 20 | weight_vector = torch.flatten(weight).tolist() 21 | batch_size = input.size()[0] 22 | 23 | if (input > np.pi).any() or (input < 0).any(): 24 | logging.info('Input data to quantum neural network is outside range {0,π}. Consider using a bounded \ 25 | activation function to prevent wrapping round of states within the Bloch sphere.') 26 | 27 | for i in range(batch_size): 28 | input_vector = torch.flatten(input[i, :]).tolist() 29 | 30 | if i == 0: 31 | logging.debug("First input vector of batch to QNN: {}".format(input_vector)) 32 | 33 | if type(qnn.config.data_handler is VectorDataHandler): 34 | if qnn.input_data is None: 35 | qnn.construct_network(input_vector) 36 | ctx.QNN = qnn 37 | else: 38 | single_input_qnn = copy.deepcopy(qnn) 39 | single_input_qnn.construct_network(input_vector) 40 | ctx.QNN = single_input_qnn 41 | 42 | parameter_list = np.concatenate((np.array(input_vector), weight_vector)) 43 | 44 | result = qnn.evaluate_circuit(parameter_list, shots=shots) 45 | vector = torch.tensor(qnn.get_vector_from_results(result)).unsqueeze(0).float() 46 | if save_statevectors and result.backend_name == 'statevector_simulator': 47 | state = result.get_statevector(0) 48 | qnn.statevectors.append(state) 49 | 50 | if i == 0: 51 | output = vector 52 | else: 53 | output = torch.cat((output, vector), 0) 54 | 55 | ctx.shots = shots 56 | 57 | if cuda.is_available(): 58 | device = torch.device("cuda") 59 | else: 60 | device = torch.device("cpu") 61 | ctx.save_for_backward(input, weight) 62 | ctx.device = device 63 | output = output.to(device) 64 | return output 65 | 66 | @staticmethod 67 | def backward(ctx, grad_output): 68 | input, weight = ctx.saved_tensors 69 | device = ctx.device 70 | weight_vector = torch.flatten(weight).tolist() 71 | batch_size = input.size()[0] 72 | 73 | for i in range(batch_size): 74 | input_vector = torch.flatten(input[i, :]).tolist() 75 | 76 | gradient = calculate_gradient_list(ctx.QNN, parameter_list=np.concatenate((input_vector, weight_vector)), 77 | method=ctx.QNN.config.grad_method, shots=ctx.shots) 78 | 79 | ctx.QNN.gradients.append(gradient.tolist()) 80 | 81 | single_vector_d_out_d_input = torch.tensor(gradient[:len(input_vector)]).double().to(device) 82 | single_vector_d_out_d_weight = torch.tensor(gradient[len(input_vector):]).double().to(device) 83 | 84 | if i == 0: 85 | batched_d_out_d_input = single_vector_d_out_d_input.unsqueeze(0) 86 | batched_d_out_d_weight = single_vector_d_out_d_weight.unsqueeze(0) 87 | else: 88 | batched_d_out_d_input = torch.cat((batched_d_out_d_input, single_vector_d_out_d_input.unsqueeze(0)), 0) 89 | batched_d_out_d_weight = torch.cat((batched_d_out_d_weight, single_vector_d_out_d_weight.unsqueeze(0)), 90 | 0) 91 | batched_d_loss_d_input = torch.bmm(batched_d_out_d_input, grad_output.unsqueeze(2).double()).squeeze() 92 | batched_d_loss_d_weight = torch.bmm(batched_d_out_d_weight, grad_output.unsqueeze(2).double()).squeeze() 93 | return batched_d_loss_d_input.to(device), batched_d_loss_d_weight.to(device), None, None, None 94 | 95 | 96 | class QNet(nn.Module): 97 | """ 98 | Custom PyTorch module implementing neural network layer consisting on a parameterised quantum circuit. Forward and 99 | backward passes allow this to be directly integrated into a PyTorch network. 100 | For a "vector" input encoding, inputs should be restricted to the range [0,π) so that there is no wrapping of input 101 | states round the bloch sphere and extreme value of the input correspond to states with the smallest overlap. If 102 | inputs are given outside this range during the forward pass, info level logging will occur. 103 | """ 104 | 105 | def __init__(self, n_qubits, encoding, ansatz_type, layers, sweeps_per_layer, activation_function_type, shots, 106 | backend_type='qasm_simulator', save_statevectors=False): 107 | super(QNet, self).__init__() 108 | 109 | config = Config(encoding=encoding, ansatz_type=ansatz_type, layers=layers, 110 | sweeps_per_layer=sweeps_per_layer, 111 | activation_function_type=activation_function_type, 112 | meas_method='all', backend_type=backend_type) 113 | self.qnn = QuantumNetworkCircuit(config, n_qubits) 114 | 115 | self.shots = shots 116 | 117 | num_weights = len(list(self.qnn.ansatz_circuit_parameters)) 118 | self.quantum_weight = nn.Parameter(torch.Tensor(num_weights)) 119 | 120 | self.quantum_weight.data.normal_(std=1. / np.sqrt(n_qubits)) 121 | 122 | self.save_statevectors = save_statevectors 123 | 124 | logging.debug("Quantum parameters initialised as {}".format(self.quantum_weight.data)) 125 | 126 | def forward(self, input_vector): 127 | return QNetFunction.apply(input_vector, self.quantum_weight, self.qnn, self.shots, self.save_statevectors) 128 | -------------------------------------------------------------------------------- /qnn/quantum_network_circuit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import log 3 | 4 | import numpy as np 5 | from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister, execute, transpile 6 | from qiskit.aqua.utils import get_subsystems_counts 7 | from qiskit.quantum_info import Pauli, Statevector 8 | 9 | from config import Config 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | logging.getLogger('qiskit').setLevel(logging.WARN) 13 | 14 | 15 | class QuantumNetworkCircuit: 16 | """ 17 | A quantum neural network. Combines state preparation circuit and variational ansatz to produce quantum neural 18 | network circuit. 19 | """ 20 | 21 | def __init__(self, config: Config, input_qubits, input_data=None): 22 | 23 | self.config = config 24 | self.input_qubits = input_qubits 25 | self.input_data = input_data 26 | self.input_circuit_parameters = None 27 | 28 | self.ansatz_circuit = self._create_ansatz_circuit(input_qubits) 29 | 30 | self.ansatz_circuit_parameters = sorted(list(self.ansatz_circuit.parameters), 31 | key=lambda p: int(''.join(filter(str.isdigit, p.name)))) 32 | 33 | self.qr = QuantumRegister(self.ansatz_circuit.num_qubits, name='qr') 34 | self.cr = ClassicalRegister(len(self.qr), name='cr') 35 | self.qc = QuantumCircuit(self.qr, self.cr) 36 | 37 | self.backend = config.backend 38 | 39 | if input_data is not None: 40 | self.construct_network(input_data) 41 | 42 | self.statevectors = [] 43 | self.gradients = [] 44 | self.transpiled = False 45 | 46 | def create_input_circuit(self): 47 | return self.config.data_handler.get_quantum_circuit(self.input_data) 48 | 49 | def _create_ansatz_circuit(self, input_qubits): 50 | return self.config.ansatz.get_quantum_circuit(input_qubits) 51 | 52 | def construct_network(self, input_data): 53 | self.input_data = input_data 54 | input_circuit = self.config.data_handler.get_quantum_circuit(input_data) 55 | self.input_circuit_parameters = sorted(list(input_circuit.parameters), 56 | key=lambda p: int(''.join(filter(str.isdigit, p.name)))) 57 | 58 | self.qc.append(input_circuit, self.qr[:input_circuit.num_qubits]) 59 | self.qc = self.qc.combine(self.ansatz_circuit) 60 | 61 | if self.backend.name() is not 'statevector_simulator': 62 | if self.config.meas_method == 'ancilla': 63 | self.qc.measure(self.qr[-1], self.cr[0]) 64 | 65 | elif self.config.meas_method == 'all': 66 | self.qc.measure(self.qr, self.cr) 67 | 68 | logging.info("QNN created with {} trainable parameters.".format(len(self.ansatz_circuit_parameters))) 69 | self.config.log_info() 70 | 71 | def bind_circuit(self, parameter_values): 72 | """ 73 | Assigns all parameterized gates to values 74 | :param parameter_values: List of parameter values for circuit. Input parameters should come before ansatz 75 | parameters. 76 | """ 77 | if self.input_circuit_parameters is None: 78 | raise NotImplementedError( 79 | "No input data was specified before binding. Please call construct_network() first.") 80 | combined_parameter_list = self.input_circuit_parameters + self.ansatz_circuit_parameters 81 | if len(parameter_values) != len(combined_parameter_list): 82 | raise ValueError('Parameter_values must be of length {}'.format(len(combined_parameter_list))) 83 | 84 | binding_dict = {} 85 | for i, value in enumerate(parameter_values): 86 | binding_dict[combined_parameter_list[i]] = value 87 | 88 | bound_qc = self.qc.bind_parameters(binding_dict) 89 | return bound_qc 90 | 91 | def evaluate_circuit(self, parameter_list, shots=100): 92 | # if self.transpiled is False: 93 | # self.qc = transpile(self.qc, optimization_level=0, basis_gates=['cx', 'u1', 'u2', 'u3']) 94 | # self.transpiled = True 95 | circuit = self.bind_circuit(parameter_list) 96 | job = execute(circuit, backend=self.backend, shots=shots) 97 | return job.result() 98 | 99 | @staticmethod 100 | def get_vector_from_results(results, circuit_id=0): 101 | """ 102 | Calculates the expectation value of individual qubits for a set of observed bitstrings. Assumes counts 103 | corresponding to classical register used for final measurement is final element in job counts array in 104 | order to exclude classical registers used for activation function measurements (if present). 105 | :param results: Qiskit results object. 106 | :param circuit_id: For results of multiple circuits, integer labelling which circuit result to use. 107 | :return: A vector, where the ith element is the expectation value of the ith qubit 108 | """ 109 | 110 | if results.backend_name == 'statevector_simulator': 111 | state = results.get_statevector(circuit_id) 112 | 113 | n = int(log(len(state), 2)) 114 | vector = [Statevector(state).expectation_value(Pauli.pauli_single(n, i, 'Z')).real for i in range(n)] 115 | return vector 116 | 117 | else: 118 | counts = results.get_counts(circuit_id) 119 | all_register_counts = get_subsystems_counts(counts) 120 | output_register_counts = all_register_counts[-1] 121 | num_measurements = len(next(iter(output_register_counts))) 122 | vector = np.zeros(num_measurements) 123 | 124 | for counts, frequency in output_register_counts.items(): 125 | for i in range(num_measurements): 126 | if counts[i] == '0': 127 | vector[i] += frequency 128 | elif counts[i] == '1': 129 | vector[i] -= frequency 130 | else: 131 | raise ValueError("Measurement returned unrecognised value") 132 | 133 | return vector / (sum(output_register_counts.values())) 134 | -------------------------------------------------------------------------------- /mnist_examples/pytorch_with_fc_comparison.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import logging 4 | import sys 5 | 6 | sys.path.append("../qnn") 7 | 8 | import argparse 9 | 10 | import numpy as np 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.functional as F 14 | import torch.optim as optim 15 | from torch.optim.lr_scheduler import StepLR 16 | from torchvision import datasets, transforms 17 | import torchsummary 18 | 19 | 20 | class Net(nn.Module): 21 | def __init__(self): 22 | super(Net, self).__init__() 23 | self.conv1 = nn.Conv2d(1, 8, 3, 1) 24 | self.fc1 = nn.Linear(8, 8) 25 | self.fc2 = nn.Linear(8, 2) 26 | logging.info('Network with conv, fully connected.') 27 | 28 | def forward(self, x): 29 | x = self.conv1(x) 30 | x = F.max_pool2d(x, 26) 31 | x = torch.flatten(x, 1) 32 | x = F.relu(x) 33 | x = self.fc1(x) 34 | x = F.relu(x) 35 | x = self.fc2(x) 36 | output = F.log_softmax(x, dim=1) 37 | return output 38 | 39 | 40 | def train(args, model, device, train_loader, optimizer, epoch): 41 | model.train() 42 | for batch_idx, (data, target) in enumerate(train_loader): 43 | data, target = data.to(device), target.to(device) 44 | optimizer.zero_grad() 45 | output = model(data) 46 | loss = F.nll_loss(output, target) 47 | loss.backward() 48 | optimizer.step() 49 | if batch_idx % args.log_interval == 0: 50 | logging.info('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( 51 | epoch, batch_idx * len(data), len(train_loader.dataset), 52 | 100. * batch_idx / len(train_loader), loss.item())) 53 | 54 | 55 | def test(model, device, test_loader): 56 | model.eval() 57 | test_loss = 0 58 | correct = 0 59 | with torch.no_grad(): 60 | for data, target in test_loader: 61 | data, target = data.to(device), target.to(device) 62 | output = model(data) 63 | test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss 64 | pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability 65 | correct += pred.eq(target.view_as(pred)).sum().item() 66 | 67 | test_loss /= len(test_loader.dataset) 68 | 69 | logging.info('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( 70 | test_loss, correct, len(test_loader.dataset), 71 | 100. * correct / len(test_loader.dataset))) 72 | 73 | 74 | def main(): 75 | # Training settings 76 | parser = argparse.ArgumentParser(description='PyTorch MNIST Example') 77 | parser.add_argument('--batch-size', type=int, default=64, metavar='N', 78 | help='input batch size for training (default: 64)') 79 | parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N', 80 | help='input batch size for testing (default: 1000)') 81 | parser.add_argument('--epochs', type=int, default=14, metavar='N', 82 | help='number of epochs to train (default: 14)') 83 | parser.add_argument('--lr', type=float, default=1.0, metavar='LR', 84 | help='learning rate (default: 1.0)') 85 | parser.add_argument('--gamma', type=float, default=0.7, metavar='M', 86 | help='Learning rate step gamma (default: 0.7)') 87 | parser.add_argument('--no-cuda', action='store_true', default=False, 88 | help='disables CUDA training') 89 | parser.add_argument('--seed', type=int, default=1, metavar='S', 90 | help='random seed (default: 1)') 91 | parser.add_argument('--log-interval', type=int, default=10, metavar='N', 92 | help='how many batches to wait before logging training status') 93 | 94 | parser.add_argument('--save-model', action='store_true', default=False, 95 | help='For Saving the current Model') 96 | 97 | args = parser.parse_args() 98 | use_cuda = not args.no_cuda and torch.cuda.is_available() 99 | 100 | torch.manual_seed(args.seed) 101 | 102 | device = torch.device("cuda" if use_cuda else "cpu") 103 | 104 | kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {} 105 | 106 | mnist_trainset = datasets.MNIST('./datasets', train=True, download=True, transform=transforms.Compose( 107 | [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) 108 | 109 | no_training_samples = 500 110 | 111 | train_labels = mnist_trainset.targets.numpy() 112 | train_idx = np.concatenate( 113 | (np.where(train_labels == 0)[0][0:no_training_samples], np.where(train_labels == 1)[0][0:no_training_samples])) 114 | mnist_trainset.targets = train_labels[train_idx] 115 | mnist_trainset.data = mnist_trainset.data[train_idx] 116 | 117 | train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=args.batch_size, shuffle=True, **kwargs) 118 | 119 | no_test_samples = 500 120 | 121 | mnist_testset = datasets.MNIST('./datasets', train=False, download=True, transform=transforms.Compose( 122 | [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) 123 | test_labels = mnist_testset.targets.numpy() 124 | idx = np.concatenate( 125 | (np.where(test_labels == 0)[0][0:no_test_samples], np.where(test_labels == 1)[0][0:no_test_samples])) 126 | mnist_testset.targets = test_labels[idx] 127 | mnist_testset.data = mnist_testset.data[idx] 128 | 129 | test_loader = torch.utils.data.DataLoader(mnist_testset, batch_size=args.test_batch_size, shuffle=True, **kwargs) 130 | 131 | model = Net().to(device) 132 | print(torchsummary.summary(model, (1, 28, 28))) 133 | 134 | optimizer = optim.Adadelta(model.parameters(), lr=args.lr) 135 | 136 | scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma) 137 | for epoch in range(1, args.epochs + 1): 138 | train(args, model, device, train_loader, optimizer, epoch) 139 | test(model, device, test_loader) 140 | scheduler.step() 141 | 142 | if args.save_model: 143 | torch.save(model.state_dict(), "mnist_cnn.pt") 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /test/test_gradient_calculator.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import TestCase 3 | 4 | import numpy as np 5 | 6 | sys.path.append('quantum-neural-network') 7 | 8 | from config import Config 9 | from gradient_calculator import calculate_gradient_list 10 | from quantum_network_circuit import QuantumNetworkCircuit 11 | 12 | 13 | class TestGradientCalculator(TestCase): 14 | def test_when_calculate_gradient_list_then_happy_path(self): 15 | config = Config('vector') 16 | qnn = QuantumNetworkCircuit(config, 2, [1, 1]) 17 | calculate_gradient_list(qnn, [1, 1]) 18 | 19 | def test_when_calculate_gradient_list_using_parameter_shifterence_then_happy_path(self): 20 | config = Config('vector') 21 | qnn = QuantumNetworkCircuit(config, 2, [1, 1]) 22 | calculate_gradient_list(qnn, [1, 1], method='parameter shift', eps=0.1) 23 | 24 | def test_when_calculate_gradient_list_then_non_zero_gradient(self): 25 | config = Config('vector', ansatz_type='sim_circ_13_half', layers=1, backend_type='statevector_simulator') 26 | qnn = QuantumNetworkCircuit(config, 4, [1, 1, 1, 1]) 27 | gradient_list = calculate_gradient_list(qnn, [1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 28 | self.assertTrue(any([abs(grad) > 1e-10 for grad in gradient_list.flatten()]), 29 | 'For a normal circuit, at least one gradient should not be zero') 30 | 31 | def test_when_calculate_gradient_list_with_parameter_shift_then_non_zero_gradient(self): 32 | config = Config('vector', ansatz_type='sim_circ_13_half', layers=1, backend_type='statevector_simulator') 33 | qnn = QuantumNetworkCircuit(config, 4, [1, 1, 1, 1]) 34 | gradient_list = calculate_gradient_list(qnn, [1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9], method='parameter shift', 35 | eps=0.01) 36 | self.assertTrue(any([abs(grad) > 1e-5 for grad in gradient_list.flatten()]), 37 | 'For a normal circuit, at least one gradient should not be zero') 38 | 39 | def test_given_input_zero_and_no_ansatz_when_calculate_gradient_then_zero_gradient(self): 40 | config = Config('vector', backend_type='statevector_simulator') 41 | input = [0, 0] 42 | qnn = QuantumNetworkCircuit(config, 2, input) 43 | gradient_list = calculate_gradient_list(qnn, input) 44 | self.assertTrue((np.zeros_like(gradient_list) == gradient_list).all(), 'Circuit should have zero gradient') 45 | 46 | def test_given_input_zero_and_no_ansatz_when_calculate_gradient_with_parameter_shift_then_zero_gradient(self): 47 | config = Config('vector', backend_type='statevector_simulator') 48 | input = [0, 0] 49 | qnn = QuantumNetworkCircuit(config, 2, input) 50 | gradient_list = calculate_gradient_list(qnn, input, method='parameter shift', eps=0.01) 51 | self.assertTrue((np.zeros_like(gradient_list) == gradient_list).all(), 'Circuit should have zero gradient') 52 | 53 | def test_given_input_pi_and_no_ansatz_when_calculate_gradient_then_zero_gradient(self): 54 | config = Config('vector', backend_type='statevector_simulator') 55 | input = [np.pi, np.pi] 56 | qnn = QuantumNetworkCircuit(config, 2, input) 57 | gradient_list = calculate_gradient_list(qnn, input) 58 | self.assertTrue((np.isclose(gradient_list, 0).all()), 'Circuit should have zero gradient') 59 | 60 | def test_given_non_zero_input_and_no_ansatz_when_calculate_gradient_then_expected_gradient(self): 61 | config = Config('vector', backend_type='statevector_simulator') 62 | input = [1] 63 | qnn = QuantumNetworkCircuit(config, 1, input) 64 | gradient_list = calculate_gradient_list(qnn, input) 65 | self.assertAlmostEqual(gradient_list[0][0], -np.sin(1), delta=1e-6) 66 | 67 | def test_given_non_zero_input_and_no_ansatz_when_calculate_gradient_with_qasm_simulator_then_expected_gradient( 68 | self): 69 | config = Config('vector', backend_type='qasm_simulator') 70 | input = [1] 71 | qnn = QuantumNetworkCircuit(config, 1, input) 72 | gradient_list = calculate_gradient_list(qnn, input, shots=10000) 73 | self.assertAlmostEqual(gradient_list[0][0], -np.sin(1), delta=1e-2) 74 | 75 | def test_given_non_zero_input_and_no_ansatz_when_calculate_gradient_with_parameter_shift_then_expected_gradient( 76 | self): 77 | config = Config('vector', backend_type='statevector_simulator') 78 | input = [1] 79 | qnn = QuantumNetworkCircuit(config, 1, input) 80 | gradient_list = calculate_gradient_list(qnn, input, method='parameter shift', eps=0.0001) 81 | self.assertAlmostEqual(gradient_list[0][0], -np.sin(1), delta=1e-6) 82 | 83 | def test_given_non_zero_input_and_no_ansatz_when_calculate_gradient_with_parameter_shift_and_qasm_simulator_then_expected_gradient( 84 | self): 85 | config = Config('vector', backend_type='qasm_simulator') 86 | input = [1] 87 | qnn = QuantumNetworkCircuit(config, 1, input) 88 | gradient_list = calculate_gradient_list(qnn, input, method='parameter shift', eps=0.0001, shots=10000) 89 | self.assertAlmostEqual(gradient_list[0][0], -np.sin(1), delta=1e-2) 90 | 91 | def test_given_product_circuit_when_calculate_gradient_then_offdiagonal_gradient_zero(self): 92 | config = Config('vector', backend_type='statevector_simulator') 93 | input = [1, 2, 3, 4] 94 | qnn = QuantumNetworkCircuit(config, 4, input) 95 | gradient_list = calculate_gradient_list(qnn, input) 96 | 97 | for i in range(len(gradient_list)): 98 | for j in range(len(gradient_list[0])): 99 | if i != j: 100 | self.assertAlmostEqual(gradient_list[i][j], 0, delta=1e-10, 101 | msg='For a circuit without entangling gates, each measured qubit should have' 102 | ' zero gradient with respect to rotations on a different qubit') 103 | 104 | def test_given_sim_circ_13_type_ansatz_when_calculate_gradient_then_final_crz_layer_has_zero_gradient(self): 105 | config = Config('vector', ansatz_type='sim_circ_13', layers=1, backend_type='statevector_simulator') 106 | qnn = QuantumNetworkCircuit(config, 4, [1, 1, 1, 1]) 107 | gradient_list = calculate_gradient_list(qnn, 108 | [1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) 109 | self.assertTrue(all([abs(grad) < 1e-10 for grad in gradient_list[-4:].flatten()]), 110 | 'For sim_cir_13, final crz layer should have zero gradient wrt measurement.') 111 | -------------------------------------------------------------------------------- /mnist_examples/pytorch_with_qnet.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import sys 7 | import time 8 | 9 | sys.path.append("../qnn") 10 | 11 | import argparse 12 | 13 | import numpy as np 14 | import torch 15 | import torch.nn as nn 16 | import torch.nn.functional as F 17 | import torch.optim as optim 18 | from torchvision import datasets, transforms 19 | import torchsummary 20 | from matplotlib import pyplot as plt 21 | 22 | from qnet import QNet 23 | 24 | test_accuracy_list = [] 25 | training_accuracy_list = [] 26 | batches_list = [] 27 | parameters_list = [] 28 | dhs_list = [] 29 | 30 | 31 | def create_output_model_path(args, version=0): 32 | if args.quantum: 33 | model_path = os.path.join('results', 34 | 'MNIST-quantum_{}-backend_{}-classes_{}-ansatz_{}-netwidth_{}-nlayers_{}-nsweeps_{}' 35 | '-activation_{}-shots_{}-samples_{}-bsize_{}-optimiser_{}-lr_{}-batchnorm_{}' 36 | '-tepochs_{}-loginterval_{}_{}'.format( 37 | args.quantum, args.q_backend, args.classes, args.q_ansatz, args.width, 38 | args.layers, args.q_sweeps, args.activation, args.shots, args.samples_per_class, 39 | args.batch_size, args.optimiser, args.lr, args.batchnorm, args.epochs, 40 | args.log_interval, version)) 41 | else: 42 | model_path = os.path.join('results', 43 | 'MNIST-quantum_{}-classes_{}-netwidth_{}-nlayers_{}-samples_{}-' 44 | 'bsize_{}-optimiser_{}-lr_{}-batchnorm_{}-tepochs_{}-loginterval_{}_{}'.format( 45 | args.quantum, args.classes, args.width, args.layers, 46 | args.samples_per_class, args.batch_size, args.optimiser, args.lr, args.batchnorm, 47 | args.epochs, 48 | args.log_interval, version)) 49 | 50 | if os.path.exists(model_path + ".npy"): 51 | return create_output_model_path(args, version=version + 1) 52 | else: 53 | return model_path 54 | 55 | 56 | class Net(nn.Module): 57 | def __init__(self, args): 58 | super(Net, self).__init__() 59 | self.args = args 60 | self.conv1 = nn.Conv2d(1, 32, 3, 1) 61 | self.conv2 = nn.Conv2d(32, 64, 3, 1) 62 | self.fc1 = nn.Linear(9216, args.width) 63 | self.bn1d = nn.BatchNorm1d(args.width) 64 | self.test_network = nn.ModuleList() 65 | 66 | if args.quantum: 67 | self.test_network.append(QNet(args.width, args.encoding, args.q_ansatz, args.layers, args.q_sweeps, 68 | args.activation, args.shots, args.q_backend, save_statevectors=True)) 69 | else: 70 | for i in range(args.layers): 71 | self.test_network.append(nn.Linear(args.width, args.width, bias=True)) 72 | 73 | self.fc2 = nn.Linear(args.width, args.classes) 74 | 75 | def forward(self, x): 76 | x = self.conv1(x) 77 | x = F.relu(x) 78 | x = self.conv2(x) 79 | x = F.relu(x) 80 | x = F.max_pool2d(x, 2) 81 | x = torch.flatten(x, 1) 82 | 83 | x = self.fc1(x) 84 | if self.args.batchnorm: 85 | x = self.bn1d(x) 86 | x = np.pi * torch.sigmoid(x) 87 | for f in self.test_network: 88 | x = f(x) 89 | 90 | x = self.fc2(x) 91 | output = F.log_softmax(x, dim=1) 92 | return output 93 | 94 | 95 | def train(args, model, device, train_loader, optimizer, epoch, test_loader, model_path): 96 | model.train() 97 | log_start_time = time.time() 98 | batches_per_epoch = len(train_loader) 99 | 100 | for batch_idx, (data, target) in enumerate(train_loader): 101 | model.test_network[0].qnn.statevectors = [] 102 | correct = 0 103 | data, target = data.to(device), target.to(device) 104 | optimizer.zero_grad() 105 | output = model(data) 106 | loss = F.nll_loss(output, target) 107 | loss.backward() 108 | optimizer.step() 109 | 110 | pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability 111 | correct += pred.eq(target.view_as(pred)).sum().item() 112 | 113 | if batch_idx % args.log_interval == 0: 114 | seen_images = ((batch_idx + 1) * train_loader.batch_size) 115 | logging.info('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f} \tTime: {:.3f}s'.format( 116 | epoch, seen_images, len(train_loader.dataset), 117 | 100. * (batch_idx + 1) / len(train_loader), loss.item(), 118 | time.time() - log_start_time)) 119 | 120 | # Report the training accuracy 121 | percentage_accuracy = 100. * correct / len(data) 122 | training_accuracy_list.append(percentage_accuracy) 123 | logging.info('Training set accuracy: {}/{} ({:.0f}%)\n'.format( 124 | correct, len(data), percentage_accuracy)) 125 | 126 | batches_list.append((epoch - 1) * batches_per_epoch + batch_idx) 127 | parameters_list.append(list(model.test_network[0].parameters())[0].detach().numpy().flatten().tolist()) 128 | 129 | if args.q_backend == 'statevector_simulator': 130 | statevectors = np.array(model.test_network[0].qnn.statevectors) 131 | 132 | labels = np.array(target) 133 | 134 | class_0_statevectors = statevectors[labels == 0] 135 | class_1_statevectors = statevectors[labels != 0] 136 | 137 | rho = np.mean([np.outer(vector, np.conj(vector)) for vector in class_0_statevectors], axis=0) 138 | sigma = np.mean([np.outer(vector, np.conj(vector)) for vector in class_1_statevectors], axis=0) 139 | 140 | dhs = np.trace(np.linalg.matrix_power((rho - sigma), 2)) 141 | dhs_list.append(dhs.real) 142 | 143 | test(model, device, test_loader) 144 | 145 | output = [batches_list, test_accuracy_list, parameters_list, training_accuracy_list, dhs_list] 146 | if args.quantum: 147 | gradients = model.test_network[0].qnn.gradients 148 | output.append(gradients) 149 | 150 | np.save(model_path, np.array(output)) 151 | log_start_time = time.time() 152 | 153 | 154 | def test(model, device, test_loader): 155 | model.eval() 156 | test_loss = 0 157 | correct = 0 158 | with torch.no_grad(): 159 | for data, target in test_loader: 160 | data, target = data.to(device), target.to(device) 161 | output = model(data) 162 | test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss 163 | pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability 164 | correct += pred.eq(target.view_as(pred)).sum().item() 165 | 166 | test_loss /= len(test_loader.dataset) 167 | 168 | percentage_accuracy = 100. * correct / len(test_loader.dataset) 169 | test_accuracy_list.append(percentage_accuracy) 170 | logging.info('Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( 171 | test_loss, correct, len(test_loader.dataset), percentage_accuracy)) 172 | print(test_accuracy_list) 173 | print(batches_list) 174 | 175 | 176 | def main(): 177 | # Training settings 178 | parser = argparse.ArgumentParser(description='PyTorch MNIST Example') 179 | parser.add_argument('--batch-size', type=int, default=64, metavar='N', 180 | help='input batch size for training (default: 64)') 181 | parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N', 182 | help='input batch size for testing (default: 1000)') 183 | parser.add_argument('--epochs', type=int, default=3, metavar='N', 184 | help='number of epochs to train (default: 3)') 185 | parser.add_argument('--samples-per-class', default=500, type=int, 186 | help='Number of training images per class in the training set (default: 500)') 187 | parser.add_argument('--optimiser', type=str, default='sgd', 188 | help='Optimiser to use (default: SGD)') 189 | parser.add_argument('--lr', type=float, default=0.1, metavar='LR', 190 | help='learning rate (default: 1.0)') 191 | parser.add_argument('--no-cuda', action='store_true', default=False, 192 | help='disables CUDA training') 193 | parser.add_argument('--seed', type=int, default=1, metavar='S', 194 | help='random seed (default: 1)') 195 | parser.add_argument('--log-interval', type=int, default=10, metavar='N', 196 | help='how many batches to wait before logging training status') 197 | parser.add_argument('--submission_time', type=str, default=datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), 198 | metavar='N', 199 | help='Timestamp at submission') 200 | 201 | parser.add_argument('--save-model', action='store_true', default=False, 202 | help='For Saving the current Model') 203 | 204 | parser.add_argument('--plot', action='store_true', default=False, 205 | help='Plot the results of the run') 206 | 207 | parser.add_argument('--batchnorm', action='store_true', default=False, 208 | help='If enabled, apply BatchNorm1d to the input of the pre-quantum Sigmoid.') 209 | 210 | parser.add_argument('-q', '--quantum', dest='quantum', action='store_true', 211 | help='If enabled, use a minimised version of ResNet-18 with QNet as the final layer') 212 | parser.add_argument('--q_backend', type=str, default='qasm_simulator', 213 | help='Type of backend simulator to run quantum circuits on (default: qasm_simulator)') 214 | 215 | parser.add_argument('-w', '--width', type=int, default=8, 216 | help='Width of the test network (default: 8). If quantum, this is the number of qubits.') 217 | parser.add_argument('--classes', type=int, default=8, 218 | help='Number of MNIST classses.') 219 | 220 | parser.add_argument('--encoding', type=str, default='vector', 221 | help='Data encoding method (default: vector)') 222 | parser.add_argument('--q_ansatz', type=str, default='abbas', 223 | help='Variational ansatz method (default: abbas)') 224 | parser.add_argument('--q_sweeps', type=int, default=1, 225 | help='Number of ansatz sweeeps.') 226 | parser.add_argument('--activation', type=str, default='null', 227 | help='Quantum layer activation function type (default: null)') 228 | parser.add_argument('--shots', type=int, default=100, 229 | help='Number of shots for quantum circuit evaulations.') 230 | parser.add_argument('--layers', type=int, default=1, 231 | help='Number of test network layers.') 232 | 233 | args = parser.parse_args() 234 | 235 | # Create the file where results will be saved 236 | model_path = create_output_model_path(args) 237 | np.save(model_path, []) 238 | 239 | use_cuda = not args.no_cuda and torch.cuda.is_available() 240 | 241 | torch.manual_seed(args.seed) 242 | 243 | device = torch.device("cuda" if use_cuda else "cpu") 244 | 245 | kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {} 246 | 247 | mnist_trainset = datasets.MNIST('./datasets', train=True, download=True, transform=transforms.Compose( 248 | [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) 249 | 250 | no_training_samples = args.samples_per_class 251 | num_classes = args.classes 252 | 253 | train_labels = mnist_trainset.targets.numpy() 254 | train_idx = np.concatenate( 255 | [np.where(train_labels == digit)[0][0:no_training_samples] for digit in range(num_classes)]) 256 | mnist_trainset.targets = train_labels[train_idx] 257 | mnist_trainset.data = mnist_trainset.data[train_idx] 258 | 259 | train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=args.batch_size, shuffle=True, **kwargs) 260 | 261 | no_test_samples = 500 262 | 263 | mnist_testset = datasets.MNIST('./datasets', train=False, download=True, transform=transforms.Compose( 264 | [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) 265 | test_labels = mnist_testset.targets.numpy() 266 | test_idx = np.concatenate([np.where(test_labels == digit)[0][0:no_test_samples] for digit in range(num_classes)]) 267 | mnist_testset.targets = test_labels[test_idx] 268 | mnist_testset.data = mnist_testset.data[test_idx] 269 | 270 | test_loader = torch.utils.data.DataLoader(mnist_testset, batch_size=args.test_batch_size, shuffle=True, **kwargs) 271 | 272 | model = Net(args).to(device) 273 | print(args) 274 | print(torchsummary.summary(model, (1, 28, 28))) 275 | 276 | if args.optimiser == 'adam': 277 | optimizer = optim.Adam(model.parameters(), lr=args.lr) 278 | elif args.optimiser == 'sgd': 279 | optimizer = optim.SGD(model.parameters(), lr=args.lr) 280 | elif args.optimiser == 'adadelta': 281 | optimizer = optim.Adadelta(model.parameters(), lr=args.lr) 282 | else: 283 | raise ValueError('Optimiser choice not implemented yet') 284 | 285 | for epoch in range(1, args.epochs + 1): 286 | train(args, model, device, train_loader, optimizer, epoch, test_loader, model_path) 287 | # test(model, device, test_loader) 288 | 289 | if args.save_model: 290 | torch.save(model.state_dict(), "mnist_cnn.pt") 291 | 292 | if args.plot: 293 | ax = plt.subplot(111) 294 | plt.plot(batches_list, test_accuracy_list, "--o") 295 | plt.xlabel('Training batches', fontsize=14) 296 | plt.ylabel('Accuracy (%)', fontsize=14) 297 | ax.tick_params(axis='both', which='major', labelsize=12) 298 | plt.show() 299 | 300 | 301 | if __name__ == '__main__': 302 | main() 303 | --------------------------------------------------------------------------------