<jupyter_start><jupyter_text>Quantum Convolutional Neural Networksfor High-Energy Physics Analysis at the LHC    ![version](https://img.shields.io/badge/version-1.0.0-blue)[![License: MIT](https://img.shields.io/badge/License-MIT-red.svg)](https://opensource.org/licenses/MIT)[![Open Source Love](https://firstcontributions.github.io/open-source-badges/badges/open-source-v2/open-source.svg)](https://github.com/firstcontributions/open-source-badges)A Google Summer of Code 2021 [Project](https://summerofcode.withgoogle.com/projects/5612096894533632).This project aims to demonstrate quantum machine learning's potential, specifically Quantum Convolutional Neural Network (QCNN), in HEP events classification from particle image data. The code used in the research is wrapped as an open-source package to ease future research in this field.        Note:This is a tutorial/example on how to use the package. For more detail information about the package itself and about notions like data re-uploading PQC, quantum classifier, quantum convolution, etc., please refer to the [Docs](https://colab.research.google.com/drive/1ojCEutBoHu-L6Q3PyWSYI51MNIr3OF_k?usp=sharing).We limit the tutorial to just binary classification tasks. But this package can easily be used for multi-class classification. For a more in-depth theoretical equation, design details of the data re-uploading circuit, and how to construct a multi-class classification circuit, it is highly recommended to read the [original paper](https://quantum-journal.org/papers/q-2020-02-06-226/). The paper is very well written (clear, concise, easy to understand).  0. Import the Packages Check GPU availability<jupyter_code>gpu_info = !nvidia-smi
gpu_info = '\n'.join(gpu_info)
if gpu_info.find('failed') >= 0:
  print('Select the Runtime > "Change runtime type" menu to enable a GPU accelerator, ')
  print('and then re-execute this cell.')
else:
  print(gpu_info)<jupyter_output>Sat Aug 28 15:01:31 2021       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.57.02    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:04.0 Off |                    0 |
| N/A   41C    P8     9W /  70W |      0MiB / 15109MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-------[...]<jupyter_text>Check RAM availability<jupyter_code>from psutil import virtual_memory
ram_gb = virtual_memory().total / 1e9
print('Your runtime has {:.1f} gigabytes of available RAM\n'.format(ram_gb))

if ram_gb < 20:
  print('To enable a high-RAM runtime, select the Runtime > "Change runtime type"')
  print('menu, and then select High-RAM in the Runtime shape dropdown. Then, ')
  print('re-execute this cell.')
else:
  print('You are using a high-RAM runtime!')<jupyter_output>Your runtime has 13.6 gigabytes of available RAM

To enable a high-RAM runtime, select the Runtime > "Change runtime type"
menu, and then select High-RAM in the Runtime shape dropdown. Then, 
re-execute this cell.<jupyter_text>Mount Google Drive to Google Colab (not needed if you run the Notebook locally)<jupyter_code>from google.colab import drive
drive.mount('/content/drive')<jupyter_output>Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).<jupyter_text>CD to where you want to clone the repository, and clone it<jupyter_code>%cd '/content/drive/My Drive/Projects/GSoC 2021/tutorials'
!git clone https://github.com/eraraya-ricardo/qcnn-hep.git

from IPython.display import clear_output
clear_output()<jupyter_output><empty_output><jupyter_text>After cloning the repository, cd to the `qcnn-hep` folder.Then, install the dependencies in the `requirements.txt` file, you only need to do this once.Once you have all the requirements, run the `!python setup.py` after cd-ing to the `qcnn-hep` folder everytime you want to load the package.<jupyter_code>%cd '/content/drive/My Drive/Projects/GSoC 2021/tutorials/qcnn-hep'
!python -m pip install -r requirements.txt
!python setup.py

from IPython.display import clear_output
clear_output()<jupyter_output><empty_output><jupyter_text>Now, let's import all the packages.Note: the `main_dir` is the directory where the models will be saved.<jupyter_code>main_dir = '/content/drive/My Drive/Projects/GSoC 2021/tutorials'

import os
import sys

# Append the path to the folder ../qcnn-hep so we can import the package
sys.path.append('/content/drive/My Drive/Projects/GSoC 2021/tutorials/qcnn-hep')

import numpy as np
from matplotlib import pyplot as plt
import tensorflow as tf

import cirq
from cirq.contrib.svg import SVGCircuit

from qcnn_drc.data_reuploading import ReUploadingPQC
from qcnn_drc.quantum_convolution import QConv2D_DRC<jupyter_output><empty_output><jupyter_text>1. Load MNIST Dataset For all examples, we will train the models to classify [MNIST](http://yann.lecun.com/exdb/mnist/) dataset. This dataset is open-source and relatively easy to obtain by everyone.<jupyter_code># Load the dataset
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# Scale it by 255 so the pixel's value is in [0, 1]
x_train_flatten = x_train/255.0
x_test_flatten = x_test/255.0

# Separate the dataset into classes
x_train_0 = x_train_flatten[y_train == 0]
x_train_1 = x_train_flatten[y_train == 1]
x_train_2 = x_train_flatten[y_train == 2]
x_train_3 = x_train_flatten[y_train == 3]
x_train_4 = x_train_flatten[y_train == 4]
x_train_5 = x_train_flatten[y_train == 5]
x_train_6 = x_train_flatten[y_train == 6]
x_train_7 = x_train_flatten[y_train == 7]
x_train_8 = x_train_flatten[y_train == 8]
x_train_9 = x_train_flatten[y_train == 9]

x_train_list = [x_train_0, x_train_1, x_train_2, x_train_3, x_train_4, x_train_5, x_train_6, x_train_7, x_train_8, x_train_9]

print(x_train_0.shape)
print(x_train_1.shape)
print(x_train_2.shape)
print(x_train_3.shape)
print(x_train_4.shape)
print(x_train_5.shape)
print(x_train_6.shape)
print(x_train_7.shape)
print(x_train_8.shape)
print(x_train_9.shape)

x_test_0 = x_test_flatten[y_test == 0]
x_test_1 = x_test_flatten[y_test == 1]
x_test_2 = x_test_flatten[y_test == 2]
x_test_3 = x_test_flatten[y_test == 3]
x_test_4 = x_test_flatten[y_test == 4]
x_test_5 = x_test_flatten[y_test == 5]
x_test_6 = x_test_flatten[y_test == 6]
x_test_7 = x_test_flatten[y_test == 7]
x_test_8 = x_test_flatten[y_test == 8]
x_test_9 = x_test_flatten[y_test == 9]

x_test_list = [x_test_0, x_test_1, x_test_2, x_test_3, x_test_4, x_test_5, x_test_6, x_test_7, x_test_8, x_test_9]

print(x_test_0.shape)
print(x_test_1.shape)
print(x_test_2.shape)
print(x_test_3.shape)
print(x_test_4.shape)
print(x_test_5.shape)
print(x_test_6.shape)
print(x_test_7.shape)
print(x_test_8.shape)
print(x_test_9.shape)
# Choose 2 classes (2 numbers)
class_set = [0, 1]

training_sample_per_class = 200  # number of training samples per class
X_train = np.concatenate((x_train_list[class_set[0]][:training_sample_per_class, :], x_train_list[class_set[1]][:training_sample_per_class, :]), axis=0)
Y_train = np.zeros((X_train.shape[0],), dtype=int)
Y_train[training_sample_per_class:] += 1

print("Train Set Shape:", X_train.shape, Y_train.shape)

testing_sample_per_class = 100  # number of testing samples per class
X_test = np.concatenate((x_test_list[class_set[0]][:testing_sample_per_class, :], x_test_list[class_set[1]][:testing_sample_per_class, :]), axis=0)
Y_test = np.zeros((X_test.shape[0],), dtype=int)
Y_test[testing_sample_per_class:] += 1

print("Test Set Shape:", X_test.shape, Y_test.shape)
# Examples from train set
plt.imshow(np.concatenate((X_train[0], X_train[-1]), axis=1), cmap='gray')
# Examples from test set
plt.imshow(np.concatenate((X_test[0], X_test[-1]), axis=1), cmap='gray')<jupyter_output><empty_output><jupyter_text>2. Utils  2.1 Learning Rate Scheduler This is an example of [Learning Rate Scheduler](https://keras.io/api/callbacks/learning_rate_scheduler/) function for training a model with Keras API. It will be used later in an example of training a quantum model.<jupyter_code>def lr_schedule(epoch, lr_init=1e-3):
    """Learning Rate Schedule

    Learning rate is scheduled to be reduced after 80, 120, 160, 180 epochs.
    Called automatically every epoch as part of callbacks during training.

    # Arguments
        epoch (int): The number of epochs

    # Returns
        lr (float32): learning rate
        lr_init (float32): initial learning rate, default: 1e-3
    """
    lr = lr_init
    if epoch > 180:
        lr *= 0.5e-3
    elif epoch > 160:
        lr *= 1e-3
    elif epoch > 120:
        lr *= 1e-2
    elif epoch > 80:
        lr *= 1e-1
    print('Learning rate: ', lr)
    return lr

lr_scheduler = tf.keras.callbacks.LearningRateScheduler(lr_schedule)

lr_reducer = tf.keras.callbacks.ReduceLROnPlateau(factor=np.sqrt(0.1),
                               cooldown=0,
                               patience=5,
                               min_lr=0.5e-6)<jupyter_output><empty_output><jupyter_text>2.2 Callbacks This is a [custom Keras Callback class](https://keras.io/guides/writing_your_own_callbacks/) to save the quantum model and its metrics during the training. It will be used later in an example of training a quantum model.<jupyter_code># Callback for saving model's weights, optimizer's states, and evaluation metrics
class CustomCallback(tf.keras.callbacks.Callback):

    def __init__(self, checkpoint_dir):
        super(CustomCallback, self).__init__()
        self.checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
        self.checkpoint = tf.train.Checkpoint(optimizer=opt_adam,
                                 model=model)

        try:
            self.best_metric_list = list(np.loadtxt(checkpoint_dir + '/history_best_metric_list.txt'))
        except:
            # acc, val acc, auc, val auc
            self.best_metric_list = [0, 0, 0, 0]

        def load_history(name):
            try:
                data = np.loadtxt(checkpoint_dir + '/history_' + name + '.txt')
                return list(data)
                
            except:
                return []

        self.accuracy_list = load_history('accuracy')
        self.val_accuracy_list = load_history('val_accuracy')
        self.auc_list = load_history('auc')
        self.val_auc_list = load_history('val_auc')
        self.loss_list = load_history('loss')
        self.val_loss_list = load_history('val_loss')

    def on_epoch_end(self, epoch, logs=None):
        self.checkpoint.save(file_prefix = self.checkpoint_prefix)
        print("Saved custom checkpoint for epoch {}.".format(epoch+1))

        def save_list_to_txt(metric, metric_list, name):
            metric_list += [metric]
            np.savetxt(checkpoint_dir + '/history_' + name + '.txt', np.array(metric_list))
            return metric_list

        self.accuracy_list = save_list_to_txt(logs["accuracy"], self.accuracy_list, 'accuracy')
        self.val_accuracy_list = save_list_to_txt(logs["val_accuracy"], self.val_accuracy_list, 'val_accuracy')
        self.auc_list = save_list_to_txt(logs["auc"], self.auc_list, 'auc')
        self.val_auc_list = save_list_to_txt(logs["val_auc"], self.val_auc_list, 'val_auc')
        self.loss_list = save_list_to_txt(logs["loss"], self.loss_list, 'loss')
        self.val_loss_list = save_list_to_txt(logs["val_loss"], self.val_loss_list, 'val_loss')
        
        print_space = 0

        if logs["accuracy"] > self.best_metric_list[0]:
            self.best_metric_list[0] = logs["accuracy"]
        
        if logs["val_accuracy"] > self.best_metric_list[1]:
            self.best_metric_list[1] = logs["val_accuracy"]

        if logs["auc"] > self.best_metric_list[2]:
            self.best_metric_list[2] = logs["auc"]

        if logs["val_auc"] > self.best_metric_list[3]:
            self.best_metric_list[3] = logs["val_auc"]

        np.savetxt(checkpoint_dir + '/history_best_metric_list.txt', np.array(self.best_metric_list))
        print("Best acc, val acc, auc, val auc:", self.best_metric_list)
        print('')

# Callbacks for saving weights only
def checkpoint_save_weights_only(checkpoint_dir):
    return tf.keras.callbacks.ModelCheckpoint(checkpoint_dir + '/{epoch:02d}epoch.h5',
                                              monitor='val_loss', verbose=1, save_best_only=False,
                                              save_weights_only=True, mode='auto', save_freq='epoch')<jupyter_output><empty_output><jupyter_text>3. Quantum Classifier In this third section, we will show how to build a quantum classifier with data re-uploading PQC. In classical fully-connected neural networks, to produce an output vector with more than one prediction value, we can simply put more than one fully-connected nodes at the output layer.With data re-uploading PQC, we can either use a single data re-uploading PQC with multiple qubits and measure each qubit with an observable, or we can use multiple data re-uploading PQCs and measure each PQC with an observable. These are not equivalent as entangling layers might be involved. There is no "absolutely right" design choice as it is case-by-case basis, similar to there is no "the best" neural nets architecture.  3.1 Data Preprocessing Since a quantum classifier with data re-uploading PQC is similar to the fully-connected layer of classical neural networks, the input vector must be 1D dataset. Since MNIST is an image dataset, we have to flatten the image into 1D vector. The total number of features will now be $28\times28 = 784$, this is too much for the current quantum computing simulator. Hence, we use PCA to reduce the dimension to just, let's say, $6$ features.<jupyter_code>from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train.reshape(-1, 28*28))
X_train_scaled = scaler.transform(X_train.reshape(-1, 28*28))
X_test_scaled = scaler.transform(X_test.reshape(-1, 28*28))

n_pca = 6
pca = PCA(n_components=n_pca)
pca.fit(X_train_scaled)

X_train_transform = pca.transform(X_train_scaled)
X_test_transform = pca.transform(X_test_scaled)

print(X_train_transform.shape, Y_train.shape)
print(X_test_transform.shape, Y_test.shape)<jupyter_output>(400, 6) (400,)
(200, 6) (200,)<jupyter_text>3.2 Example 1: A classifier consists of only one Data Re-uploading PQC (can be either single or multi-qubit circuit)  3.2.1 Single-Qubit Case (One Prediction Output / One Output Node)  Build the Model Building the Keras model with data re-uploading PQC is just like building a usual Keras model. We pass the input tensor to the data re-uploading PQC layer and construct the Keras model with the input tensor as its input and the measurement of data re-uploading PQC as its output. We use $|1⟩⟨1|$ observable because we want the PQC to give us a number between 0 and 1 (binary classifier). After the training, ideally, the model should map images of the first class to $|0⟩$ state and the second class to $|1⟩$ state.<jupyter_code>n_qubits = 1

# Construct the observable
qubits = cirq.GridQubit.rect(1, n_qubits)
Z = cirq.PauliString(cirq.Z(qubits[-1]))
I = cirq.PauliString(cirq.I(qubits[-1]))
observables = [-0.5*Z + 0.5*I]  # equivalent to |1⟩⟨1|

# Initialize the PQC
PQC = ReUploadingPQC(n_qubits, 1, n_pca, use_entanglement=True, use_terminal_entanglement=True, observables=observables)

# Construct the model
input_tensor = tf.keras.Input(shape=(n_pca,), dtype=tf.dtypes.float32, name='input')
quantum_classifier = PQC(input_tensor)
model = tf.keras.Model(inputs=[input_tensor], outputs=quantum_classifier)

# Optimizer
opt_adam = tf.keras.optimizers.Adam(lr=0.1)

# Compile the model with MSE loss
model.compile(loss='mse',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])

SVGCircuit(PQC.circuit)
model.summary()<jupyter_output>Model: "model_6"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           [(None, 6)]               0         
_________________________________________________________________
re-uploading_PQC (ReUploadin (None, 1)                 12        
=================================================================
Total params: 12
Trainable params: 12
Non-trainable params: 0
_________________________________________________________________<jupyter_text>Train the Model The constructed model can then be trained with the usual Keras training API.<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train_transform, Y_train, initial_epoch=last_epoch,
              batch_size=32,
              epochs=20,
              validation_data=(X_test_transform, Y_test),
              shuffle=True)

# Print and plot the best metrics obtained during the training
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/20
13/13 [==============================] - 1s 34ms/step - loss: 0.2580 - accuracy: 0.6215 - auc: 0.6569 - val_loss: 0.1709 - val_accuracy: 0.7750 - val_auc: 0.8523
Epoch 2/20
13/13 [==============================] - 0s 14ms/step - loss: 0.1725 - accuracy: 0.7459 - auc: 0.8726 - val_loss: 0.1489 - val_accuracy: 0.7900 - val_auc: 0.8946
Epoch 3/20
13/13 [==============================] - 0s 14ms/step - loss: 0.1203 - accuracy: 0.8397 - auc: 0.9383 - val_loss: 0.0261 - val_accuracy: 0.9900 - val_auc: 1.0000
Epoch 4/20
13/13 [==============================] - 0s 13ms/step - loss: 0.0337 - accuracy: 0.9616 - auc: 0.9983 - val_loss: 0.0527 - val_accuracy: 0.9400 - val_auc: 1.0000
Epoch 5/20
13/13 [==============================] - 0s 14ms/step - loss: 0.0377 - accuracy: 0.9555 - auc: 0.9943 - val_loss: 0.0260 - val_accuracy: 0.9800 - val_auc: 1.0000
Epoch 6/20
13/13 [==============================] - 0s 13ms/step - loss: 0.0242 - accuracy: 0.9845 - auc: 0.9991 - val_loss: 0.0382 - v[...]<jupyter_text>3.2.2 Multi-Qubit Case (One Prediction Output / One Output Node)  Build the Model<jupyter_code>n_qubits = 2

# Construct the observable
qubits = cirq.GridQubit.rect(1, n_qubits)
Z = cirq.PauliString(cirq.Z(qubits[-1]))
I = cirq.PauliString(cirq.I(qubits[-1]))
observables = [-0.5*Z + 0.5*I]  # equivalent to |1⟩⟨1| on the second qubit

# Initialize the PQC
PQC = ReUploadingPQC(n_qubits, 2, n_pca, use_entanglement=True, use_terminal_entanglement=True, observables=observables)

# Construct the model
input_tensor = tf.keras.Input(shape=(n_pca,), dtype=tf.dtypes.float32, name='input')
quantum_classifier = PQC(input_tensor)
model = tf.keras.Model(inputs=[input_tensor], outputs=quantum_classifier)

# Optimizer
opt_adam = tf.keras.optimizers.Adam(lr=0.1)

# Compile the model with MSE loss
model.compile(loss='mse',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])

SVGCircuit(PQC.circuit)
model.summary()<jupyter_output>Model: "model_13"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           [(None, 6)]               0         
_________________________________________________________________
re-uploading_PQC (ReUploadin (None, 1)                 48        
=================================================================
Total params: 48
Trainable params: 48
Non-trainable params: 0
_________________________________________________________________<jupyter_text>Train the Model<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train_transform, Y_train, initial_epoch=last_epoch,
              batch_size=32,
              epochs=20,
              validation_data=(X_test_transform, Y_test),
              shuffle=True)

# Print and plot the best metrics obtained during the training
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/20
13/13 [==============================] - 1s 55ms/step - loss: 0.2144 - accuracy: 0.6828 - auc: 0.7494 - val_loss: 0.1114 - val_accuracy: 0.8700 - val_auc: 0.9615
Epoch 2/20
13/13 [==============================] - 0s 34ms/step - loss: 0.1234 - accuracy: 0.8409 - auc: 0.9437 - val_loss: 0.1009 - val_accuracy: 0.8700 - val_auc: 0.9675
Epoch 3/20
13/13 [==============================] - 0s 34ms/step - loss: 0.1041 - accuracy: 0.8644 - auc: 0.9714 - val_loss: 0.0913 - val_accuracy: 0.8950 - val_auc: 0.9756
Epoch 4/20
13/13 [==============================] - 0s 33ms/step - loss: 0.1186 - accuracy: 0.8601 - auc: 0.9539 - val_loss: 0.1111 - val_accuracy: 0.9100 - val_auc: 0.9702
Epoch 5/20
13/13 [==============================] - 0s 35ms/step - loss: 0.1042 - accuracy: 0.8720 - auc: 0.9560 - val_loss: 0.0423 - val_accuracy: 0.9850 - val_auc: 0.9998
Epoch 6/20
13/13 [==============================] - 0s 34ms/step - loss: 0.0407 - accuracy: 0.9680 - auc: 0.9969 - val_loss: 0.0282 - v[...]<jupyter_text>3.2.3 Multi-Qubit Case (Two Prediction Outputs / Two Output Nodes) For a binary classification task, we can also make the model to produce two prediction outputs, $[1, 0]$ output for the first class and $[0, 1]$ output for the second class. For a quantum classifier with data re-uploading PQC, this can be realized by either:1. Use a single data re-uploading PQC but measure it with two different observables, let's say, with $|0⟩⟨0|$ on one qubit and $|1⟩⟨1|$ on the other. The example directly below is the implementation of this method.2. Use two data re-uploading PQC and measure each PQC with a single observable, let's say, measure the first PQC with $|0⟩⟨0|$ on one of its qubit and measure the second PQC with $|1⟩⟨1|$ on one of its qubit. See section **3.3.1** for the implementation.  Build the Model<jupyter_code>n_qubits = 2

# Construct the observable
qubits = cirq.GridQubit.rect(1, n_qubits)
Z_0 = cirq.PauliString(cirq.Z(qubits[0]))
I_0 = cirq.PauliString(cirq.I(qubits[0]))
Z_1 = cirq.PauliString(cirq.Z(qubits[1]))
I_1 = cirq.PauliString(cirq.I(qubits[1]))
observables = [0.5*Z_0 + 0.5*I_0, -0.5*Z_1 + 0.5*I_1]  # equivalent to |0⟩⟨0|⊗|1⟩⟨1|, or, |0⟩⟨0| for the first qubit and |1⟩⟨1| for the second qubit

# Initialize the PQC
PQC = ReUploadingPQC(n_qubits, 2, n_pca, use_entanglement=True, use_terminal_entanglement=True, observables=observables)

# Construct the model
input_tensor = tf.keras.Input(shape=(n_pca,), dtype=tf.dtypes.float32, name='input')
quantum_classifier = PQC(input_tensor)
model = tf.keras.Model(inputs=[input_tensor], outputs=quantum_classifier)

# Optimizer
opt_adam = tf.keras.optimizers.Adam(lr=0.1)

# Compile the model with MSE loss
model.compile(loss='mse',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])

SVGCircuit(PQC.circuit)
model.summary()<jupyter_output>Model: "model_16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           [(None, 6)]               0         
_________________________________________________________________
re-uploading_PQC (ReUploadin (None, 2)                 48        
=================================================================
Total params: 48
Trainable params: 48
Non-trainable params: 0
_________________________________________________________________<jupyter_text>Train the Model<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train_transform, tf.keras.utils.to_categorical(Y_train), initial_epoch=last_epoch,
              batch_size=32,
              epochs=20,
              validation_data=(X_test_transform, tf.keras.utils.to_categorical(Y_test)),
              shuffle=True)

# Compile the model with MSE loss
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/20
13/13 [==============================] - 1s 56ms/step - loss: 0.2953 - accuracy: 0.5535 - auc: 0.5456 - val_loss: 0.1855 - val_accuracy: 0.8100 - val_auc: 0.8008
Epoch 2/20
13/13 [==============================] - 0s 33ms/step - loss: 0.1740 - accuracy: 0.8009 - auc: 0.8198 - val_loss: 0.1013 - val_accuracy: 0.9150 - val_auc: 0.9388
Epoch 3/20
13/13 [==============================] - 0s 35ms/step - loss: 0.0857 - accuracy: 0.9264 - auc: 0.9625 - val_loss: 0.0875 - val_accuracy: 0.9650 - val_auc: 0.9513
Epoch 4/20
13/13 [==============================] - 0s 33ms/step - loss: 0.0621 - accuracy: 0.9586 - auc: 0.9804 - val_loss: 0.0425 - val_accuracy: 0.9700 - val_auc: 0.9950
Epoch 5/20
13/13 [==============================] - 0s 34ms/step - loss: 0.0494 - accuracy: 0.9654 - auc: 0.9850 - val_loss: 0.0506 - val_accuracy: 0.9700 - val_auc: 0.9890
Epoch 6/20
13/13 [==============================] - 0s 35ms/step - loss: 0.0458 - accuracy: 0.9672 - auc: 0.9888 - val_loss: 0.0294 - v[...]<jupyter_text>3.3 Example 2: A classifier consists of more than one Data Re-uploading PQC (use more than one qubit since a PQC needs at least one qubit) It doesn't really make sense to use more than one PQC to produce one prediction value as every PQC will at least produce a measurement result of an observable. So in this example, only a model with two prediction outputs will be shown.  3.3.1 Two Prediction Values / Two Output Nodes  Build the Model<jupyter_code># Construct the observable
n_qubits = 1
qubits = cirq.GridQubit.rect(1, n_qubits)
Z = cirq.PauliString(cirq.Z(qubits[-1]))
I = cirq.PauliString(cirq.I(qubits[-1]))
# Two observables, one for each PQC
observables_0 = [0.5*Z + 0.5*I]   # equivalent to |0⟩⟨0|
observables_1 = [-0.5*Z + 0.5*I]  # equivalent to |1⟩⟨1|

input_tensor = tf.keras.Input(shape=(n_pca,), dtype=tf.dtypes.float32, name='input')

PQC_1 = ReUploadingPQC(1, 1, n_pca, use_entanglement=False, use_terminal_entanglement=False, observables=observables_0, name='PQC_1')(input_tensor)
PQC_2 = ReUploadingPQC(1, 1, n_pca, use_entanglement=False, use_terminal_entanglement=False, observables=observables_1, name='PQC_2')(input_tensor)

output = tf.keras.layers.Concatenate()([PQC_1, PQC_2])

model = tf.keras.Model(inputs=[input_tensor], outputs=output)

# Optimizer
opt_adam = tf.keras.optimizers.Adam(lr=0.1)

# Compile the model with MSE loss
model.compile(loss='MSE',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])
model.summary()<jupyter_output>Model: "model_7"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input (InputLayer)              [(None, 6)]          0                                            
__________________________________________________________________________________________________
PQC_1 (ReUploadingPQC)          (None, 1)            12          input[0][0]                      
__________________________________________________________________________________________________
PQC_2 (ReUploadingPQC)          (None, 1)            12          input[0][0]                      
__________________________________________________________________________________________________
concatenate_7 (Concatenate)     (None, 2)            0           PQC_1[0][0]                [...]<jupyter_text>Train the Model<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train_transform, tf.keras.utils.to_categorical(Y_train), initial_epoch=last_epoch,
              batch_size=32,
              epochs=100,
              validation_data=(X_test_transform, tf.keras.utils.to_categorical(Y_test)),
              shuffle=True)

# Print and plot the best metrics obtained during the training
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/100
13/13 [==============================] - 2s 52ms/step - loss: 0.2758 - accuracy: 0.6442 - auc: 0.6152 - val_loss: 0.2102 - val_accuracy: 0.8300 - val_auc: 0.7716
Epoch 2/100
13/13 [==============================] - 0s 22ms/step - loss: 0.1805 - accuracy: 0.8093 - auc: 0.8105 - val_loss: 0.1146 - val_accuracy: 0.9200 - val_auc: 0.9155
Epoch 3/100
13/13 [==============================] - 0s 23ms/step - loss: 0.1147 - accuracy: 0.9218 - auc: 0.9126 - val_loss: 0.1194 - val_accuracy: 0.9050 - val_auc: 0.8937
Epoch 4/100
13/13 [==============================] - 0s 22ms/step - loss: 0.1037 - accuracy: 0.9344 - auc: 0.9326 - val_loss: 0.1148 - val_accuracy: 0.9050 - val_auc: 0.9276
Epoch 5/100
13/13 [==============================] - 0s 23ms/step - loss: 0.0977 - accuracy: 0.9510 - auc: 0.9411 - val_loss: 0.1095 - val_accuracy: 0.9450 - val_auc: 0.9404
Epoch 6/100
13/13 [==============================] - 0s 22ms/step - loss: 0.0891 - accuracy: 0.9618 - auc: 0.9503 - val_loss: 0.10[...]<jupyter_text>4. Hybrid Quantum-Classical QCNN In this fourth section, we will show how to build a QCNN model with quantum convolution followed by classical fully-connected layer. Since the quantum convolution layer (`QConv2D_DRC`) can take image data with multiple channels as its input, there is no need to flatten the image and reduce its dimension. Hence, for QCNN example, we will use the full size MNIST images.  Build the Model Using the `QConv2D_DRC` layer is similar to using the [`Conv2D` layer](https://keras.io/api/layers/convolution_layers/convolution2d/) from Keras. We just need to pass the output tensor of the previous layer to it, and pass its output tensor to the next layer.A small difference is that we need to use the method `call()` for the `QConv2D_DRC` layer to work. Let's build a model with two quantum convolution layers, a max pooling layer, followed by a classical fully-connected layer.<jupyter_code># Full size MNIST images as input with shape (28, 28, 1)
input_tensor = tf.keras.Input(shape=(28, 28, 1), dtype=tf.dtypes.float32, name='input')

drc_settings = {
    "n_qubits": [1],
    "n_layers": [1],
    "use_ent": [True],
    "use_terminal_ent": [True]
}

# Two quantum convolution layers
qconv_1 = QConv2D_DRC(1, (3,3), (2,2), drc_settings, 1, padding="valid").call(input_tensor)
qconv_2 = QConv2D_DRC(1, (3,3), (2,2), drc_settings, 2, padding="valid").call(qconv_1)

# A max pooling layer
pool = tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2,2), padding="valid")(qconv_2)

# Flatten the tensor
flat = tf.keras.layers.Flatten()(pool)

# A classical fully-connected layer
classical_dense = tf.keras.layers.Dense(2, activation='softmax')(flat)

model = tf.keras.Model(inputs=[input_tensor], outputs=classical_dense)
model.summary()<jupyter_output>Model: "model_12"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input (InputLayer)              [(None, 28, 28, 1)]  0                                            
__________________________________________________________________________________________________
tf.compat.v1.pad_8 (TFOpLambda) (None, 29, 29, 1)    0           input[0][0]                      
__________________________________________________________________________________________________
tf.__operators__.getitem_988 (S (None, 29, 29)       0           tf.compat.v1.pad_8[0][0]         
__________________________________________________________________________________________________
tf.__operators__.getitem_989 (S (None, 3, 3)         0           tf.__operators__.getitem_9[...]<jupyter_text>Since this model is more complicated, it may takes longer and harder to train. Therefore, for this example, we will use the Learning Rate Scheduler to change the learning rate during the training. We will also save both the model's state and the optimizer's state so we can continue the training from the save files later if needed. Also, instead of MSE, let's try different loss function. This time, we will try the cross-entropy loss.<jupyter_code># Training parameters
BATCH_SIZE = 32
EPOCHS = 100

# Print parameters for sanity check
print("Batch size, epochs:", BATCH_SIZE, EPOCHS)

# Optimizer with learning rate scheduler
opt_adam = tf.keras.optimizers.Adam(lr=lr_schedule(0, lr_init=0.01))

# Compile the model with cross-entropy loss
model.compile(loss='categorical_crossentropy',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])

# The directory on where we want to save the model
checkpoint_dir = main_dir + '/Hybrid'

# Initialize callbacks
# Let's use both callbacks that we have prepared just in case
cp_1 = CustomCallback(checkpoint_dir)
cp_2 = checkpoint_save_weights_only(checkpoint_dir)

# A list of callbacks to be passed on the Keras' model training API
callback_list = [cp_1, cp_2, lr_reducer, lr_scheduler]<jupyter_output><empty_output><jupyter_text>Train the Model<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train, tf.keras.utils.to_categorical(Y_train), initial_epoch=last_epoch,
              batch_size=BATCH_SIZE,
              epochs=EPOCHS,
              validation_data=(X_test, tf.keras.utils.to_categorical(Y_test)),
              shuffle=True,
              callbacks=callback_list)

# Print and plot the best metrics obtained during the training
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/100
Learning rate:  0.009999999776482582
13/13 [==============================] - 136s 4s/step - loss: 0.7437 - accuracy: 0.5028 - auc: 0.4980 - val_loss: 0.7027 - val_accuracy: 0.5000 - val_auc: 0.5055
Saved custom checkpoint for epoch 1.
Best acc, val acc, auc, val auc: [0.5099999904632568, 0.5, 0.5544000267982483, 0.5055000185966492]


Epoch 00001: saving model to /content/drive/My Drive/Projects/GSoC 2021/tutorials/Hybrid/01epoch.h5
Epoch 2/100
Learning rate:  0.009999999776482582
13/13 [==============================] - 28s 2s/step - loss: 0.6941 - accuracy: 0.4978 - auc: 0.6106 - val_loss: 0.6943 - val_accuracy: 0.3950 - val_auc: 0.4662
Saved custom checkpoint for epoch 2.
Best acc, val acc, auc, val auc: [0.5099999904632568, 0.5, 0.6250249743461609, 0.5055000185966492]


Epoch 00002: saving model to /content/drive/My Drive/Projects/GSoC 2021/tutorials/Hybrid/02epoch.h5
Epoch 3/100
Learning rate:  0.009999999776482582
13/13 [==============================] - 29s 2s/step -[...]<jupyter_text>If later we want to continue the training, to load the model's and optimizer's state, we can reconstruct and recompile the model (basically re-running the code up until before the training) and then running this code in a new cell:```checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))```After that, we can do the training like usual but remember to change the `initial_epoch` argument of Keras' model `fit()` method to the number of epoch of the last training session.For example, if the previous training session ends on epoch 195, then continue the training with this code:```last_epoch = 195H = model.fit(X_train, tf.keras.utils.to_categorical(Y_train),              initial_epoch=last_epoch,              batch_size=BATCH_SIZE,              epochs=EPOCHS,              validation_data=(X_test, tf.keras.utils.to_categorical(Y_test)),              shuffle=True,              callbacks=callback_list)```  5. Fully Quantum QCNN In this fifth section, we will show how to build a QCNN model with quantum convolution followed by a quantum classifier (no classical layer at all). This is basically as simple as replacing the classical fully-connected layer of model in section **4** with a quantum classifier explained in section **3**. Let's build a model with two quantum convolution layers, a max pooling layer, followed by a layer of quantum classifier.  Build the Model<jupyter_code>n_qubits = 2

# Construct the observable for the quantum classifier
qubits = cirq.GridQubit.rect(1, n_qubits)
Z_0 = cirq.PauliString(cirq.Z(qubits[0]))
I_0 = cirq.PauliString(cirq.I(qubits[0]))
Z_1 = cirq.PauliString(cirq.Z(qubits[1]))
I_1 = cirq.PauliString(cirq.I(qubits[1]))
observables = [0.5*Z_0 + 0.5*I_0, -0.5*Z_1 + 0.5*I_1]  # equivalent to |0⟩⟨0|⊗|1⟩⟨1|, or, |0⟩⟨0| for the first qubit and |1⟩⟨1| for the second qubit
# Full size MNIST images as input with shape (28, 28, 1)
input_tensor = tf.keras.Input(shape=(28, 28, 1), dtype=tf.dtypes.float32, name='input')

drc_settings = {
    "n_qubits": [1],
    "n_layers": [1],
    "use_ent": [True],
    "use_terminal_ent": [True]
}

# Two quantum convolution layers
qconv_1 = QConv2D_DRC(1, (3,3), (2,2), drc_settings, 1, padding="valid").call(input_tensor)
qconv_2 = QConv2D_DRC(1, (3,3), (2,2), drc_settings, 2, padding="valid").call(qconv_1)

# A max pooling layer
pool = tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2,2), padding="valid")(qconv_2)

# Flatten the tensor
flat = tf.keras.layers.Flatten()(pool)

# A layer of quantum classifier
quantum_classifier = ReUploadingPQC(n_qubits, 1, flat.shape[-1], use_entanglement=True, use_terminal_entanglement=True, observables=observables)(flat)

model = tf.keras.Model(inputs=[input_tensor], outputs=quantum_classifier)
model.summary()<jupyter_output>Model: "model_15"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input (InputLayer)              [(None, 28, 28, 1)]  0                                            
__________________________________________________________________________________________________
tf.compat.v1.pad_14 (TFOpLambda (None, 29, 29, 1)    0           input[0][0]                      
__________________________________________________________________________________________________
tf.__operators__.getitem_1729 ( (None, 29, 29)       0           tf.compat.v1.pad_14[0][0]        
__________________________________________________________________________________________________
tf.__operators__.getitem_1730 ( (None, 3, 3)         0           tf.__operators__.getitem_1[...]<jupyter_text>For this example, we again use the MSE loss function.<jupyter_code># Training parameters
BATCH_SIZE = 32
EPOCHS = 100

# Print parameters for sanity check
print("Batch size, epochs:", BATCH_SIZE, EPOCHS)

# Optimizer with learning rate scheduler
opt_adam = tf.keras.optimizers.Adam(lr=lr_schedule(0, lr_init=0.1))

# Compile the model with MSE loss
model.compile(loss='mse',
              optimizer=opt_adam,
              metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])

# The directory on where we want to save the model
checkpoint_dir = main_dir + '/Fully Quantum'

# Initialize callbacks
# Let's use both callbacks that we have prepared just in case
cp_1 = CustomCallback(checkpoint_dir)
cp_2 = checkpoint_save_weights_only(checkpoint_dir)

# A list of callbacks to be passed on the Keras' model training API
callback_list = [cp_1, cp_2, lr_reducer, lr_scheduler]<jupyter_output><empty_output><jupyter_text>Train the Model<jupyter_code>last_epoch = 0

# Train the model
H = model.fit(X_train, tf.keras.utils.to_categorical(Y_train), initial_epoch=last_epoch,
              batch_size=BATCH_SIZE,
              epochs=EPOCHS,
              validation_data=(X_test, tf.keras.utils.to_categorical(Y_test)),
              shuffle=True,
              callbacks=callback_list)

# Print and plot the best metrics obtained during the training
print("Best Train Accuracy:", max(H.history['accuracy']))
print("Best Val Accuracy:", max(H.history['val_accuracy']))
print("Best Train AUC:", max(H.history['auc']))
print("Best Val AUC", max(H.history['val_auc']))

plt.plot(H.history['accuracy'], label='Train Accuracy')
plt.plot(H.history['val_accuracy'], label='Val Accuracy')
plt.plot(H.history['auc'], label='Train AUC')
plt.plot(H.history['val_auc'], label='Val AUC')

plt.xlabel('Epoch')

plt.legend()
plt.show()<jupyter_output>Epoch 1/100
Learning rate:  0.10000000149011612
13/13 [==============================] - 141s 4s/step - loss: 0.3128 - accuracy: 0.5101 - auc: 0.5083 - val_loss: 0.2925 - val_accuracy: 0.5000 - val_auc: 0.6538
Saved custom checkpoint for epoch 1.
Best acc, val acc, auc, val auc: [0.5099999904632568, 0.5, 0.5130499601364136, 0.6538249850273132]


Epoch 00001: saving model to /content/drive/My Drive/Projects/GSoC 2021/tutorials/Fully Quantum/01epoch.h5
Epoch 2/100
Learning rate:  0.10000000149011612
13/13 [==============================] - 30s 2s/step - loss: 0.2553 - accuracy: 0.6044 - auc: 0.6008 - val_loss: 0.0775 - val_accuracy: 0.9050 - val_auc: 0.9755
Saved custom checkpoint for epoch 2.
Best acc, val acc, auc, val auc: [0.7149999737739563, 0.9049999713897705, 0.7424718737602234, 0.9755125045776367]


Epoch 00002: saving model to /content/drive/My Drive/Projects/GSoC 2021/tutorials/Fully Quantum/02epoch.h5
Epoch 3/100
Learning rate:  0.10000000149011612
13/13 [=====================[...]
{'fraction_non_alphanumeric': 0.15279610501888338, 'fraction_numerical': 0.05235018428356918, 'mean_word_length': 4.026300743281875, 'pattern_counts': {'":': 8, '<': 82, '<?xml version=': 0, '>': 93, 'https://': 12, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 7, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '24', 'dir': 'jupyter-structured-clean-dedup', 'id': '493608', 'license': 'MIT', 'n_tokens_mistral': 15920, 'n_tokens_neox': 14128, 'n_words': 3716, 'path': 'assets/qcnn_drc_tutorial.ipynb', 'repo_name': 'eraraya-ricardo/qcnn-hep', 'size': '43954'}