{ "cells": [ { "cell_type": "markdown", "id": "c7a956c2", "metadata": {}, "source": [ "# Hello World: Quantum Machine Learning with Merlin (Processor)\n", "\n", "This notebook uses Merlin to build a quantum reservoir classifier for the Iris dataset. The reservoir is evaluated through `MerlinProcessor`: local Perceval SLOS is the default path, and a commented cloud simulator path is provided next to it.\n", "\n", "Gradients are not propagated through processor execution, so the reservoir outputs are computed once and the training loop only updates the classical PyTorch head." ] }, { "cell_type": "markdown", "id": "34a189db", "metadata": {}, "source": [ "## 1. Install and Import Dependencies\n", "\n", "First, let's make sure all required packages are installed and import them. \n", "If you haven't installed Merlin yet, run: \n", "`pip install merlinquantum` in your terminal or `!pip install merlinquantum` in your notebook." ] }, { "cell_type": "code", "execution_count": 22, "id": "2ed0f1a9", "metadata": {}, "outputs": [], "source": [ "import random\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import torch\n", "import torch.nn as nn\n", "import merlin as ML\n", "import perceval as pcvl\n", "from merlin import MerlinProcessor\n", "from merlin.datasets import iris\n", "from perceval import Processor\n", "from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix" ] }, { "cell_type": "markdown", "id": "19e0d5db", "metadata": {}, "source": [ "## 2. Define the Processor (Cloud `sim:slos` or Local SLOS)\n", "\n", "Use the local SLOS processor by default. To run on Quandela Cloud, comment the local block and uncomment the `RemoteProcessor` block.\n", "\n", "`sim:slos` is a noise-free cloud simulator. Use `sim:belenos` when you want a simulator that reproduces the noise of the `qpu:belenos` QPU. For Scaleway-hosted platforms and future session-based providers, use `pcvl.providers.scaleway` instead of `pcvl.RemoteProcessor`." ] }, { "cell_type": "code", "execution_count": 15, "id": "cloud-processor-path", "metadata": {}, "outputs": [], "source": [ "# Cloud simulator path. Use this block instead of the local SLOS block below.\n", "# Save CLOUD_TOKEN in a .env file next to this notebook, or set it in your environment.\n", "# from dotenv import load_dotenv\n", "# import os\n", "# load_dotenv()\n", "# pcvl.RemoteConfig.set_token(os.getenv(\"CLOUD_TOKEN\"))\n", "# remote_processor = pcvl.RemoteProcessor(\"sim:slos\")\n", "\n", "# proc = MerlinProcessor(\n", "# processor=remote_processor,\n", "# microbatch_size=32,\n", "# timeout=3600.0,\n", "# max_shots_per_call=None,\n", "# chunk_concurrency=1,\n", "# )" ] }, { "cell_type": "code", "execution_count": 16, "id": "local-slos", "metadata": {}, "outputs": [], "source": [ "# Local SLOS path. This is the default path for running without cloud credentials.\n", "local_processor = Processor(\"SLOS\")\n", "proc = MerlinProcessor(processor=local_processor)" ] }, { "cell_type": "markdown", "id": "5c13b34f", "metadata": {}, "source": [ "## 3. Load and Prepare the Iris Dataset\n", "\n", "We'll use the classic Iris dataset, a simple and well-known benchmark for classification. \n", "Let's load the data and convert it to PyTorch tensors for training." ] }, { "cell_type": "code", "execution_count": 17, "id": "c71851b3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training samples: 120\n", "Test samples: 30\n", "Features: 4\n", "Classes: 3\n" ] } ], "source": [ "train_features, train_labels, train_metadata = iris.get_data_train()\n", "test_features, test_labels, test_metadata = iris.get_data_test()\n", "\n", "X_train = torch.FloatTensor(train_features)\n", "y_train = torch.LongTensor(train_labels)\n", "X_test = torch.FloatTensor(test_features)\n", "y_test = torch.LongTensor(test_labels)\n", "\n", "print(f\"Training samples: {X_train.shape[0]}\")\n", "print(f\"Test samples: {X_test.shape[0]}\")\n", "print(f\"Features: {X_train.shape[1]}\")\n", "print(f\"Classes: {len(torch.unique(y_train))}\")" ] }, { "cell_type": "markdown", "id": "74a16ed3", "metadata": {}, "source": [ "![iris](../_static/img/Iris_pipeline.png)" ] }, { "cell_type": "markdown", "id": "c80a7e7d", "metadata": {}, "source": [ "## 4. Define the Hybrid Model\n", "\n", "The quantum reservoir is fixed. The processor evaluates it once on the train and test inputs, then the PyTorch model trains only the classical head on those reservoir outputs." ] }, { "cell_type": "code", "execution_count": null, "id": "hybrid-model", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Reservoir output size: 10\n", "Train reservoir outputs: (120, 10)\n", "Test reservoir outputs: (30, 10)\n" ] } ], "source": [ "# Define a simple sandwich reservoir layer\n", "# The number of modes and photons is determined by the number of features in the dataset, and the output size is determined by the reservoir design. \n", "# The reservoir will be used to transform the input features into a higher-dimensional space before feeding them into a classical classifier head.\n", "reservoir = ML.QuantumLayer.simple(input_size=X_train.shape[1]).eval()\n", "output_size = reservoir.output_size\n", "number_of_classes = len(torch.unique(y_train))\n", "print(f\"Reservoir output size: {output_size}\")\n", "\n", "# Process the training and test data through the reservoir to obtain the reservoir outputs. \n", "# These outputs will be used as features for training the classical classifier head.\n", "train_reservoir_outputs = proc.forward(reservoir, X_train)\n", "test_reservoir_outputs = proc.forward(reservoir, X_test)\n", "print(f\"Train reservoir outputs: {tuple(train_reservoir_outputs.shape)}\")\n", "print(f\"Test reservoir outputs: {tuple(test_reservoir_outputs.shape)}\")\n", "\n", "# Simple MLP to process the reservoir outputs and perform classification.\n", "class HybridIrisClassifier(nn.Module):\n", " \"\"\"Classical head trained on processor-generated reservoir outputs.\"\"\"\n", "\n", " def __init__(self, input_size: int, number_of_classes: int):\n", " super().__init__()\n", " self.classifier = nn.Sequential(\n", " nn.Linear(input_size, 8),\n", " nn.ReLU(),\n", " nn.Dropout(0.1),\n", " nn.Linear(8, number_of_classes),\n", " )\n", "\n", " def forward(self, reservoir_outputs: torch.Tensor) -> torch.Tensor:\n", " return self.classifier(reservoir_outputs)" ] }, { "cell_type": "markdown", "id": "0b96210f", "metadata": {}, "source": [ "## 5. Training Loop\n", "\n", "The training loop uses the local processor outputs by default. Switching the processor cell to the cloud path changes only where the reservoir outputs are computed." ] }, { "cell_type": "code", "execution_count": 19, "id": "training-loop", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 20: loss=1.0620, test accuracy=0.2000\n", "Epoch 40: loss=0.9501, test accuracy=0.6333\n", "Epoch 60: loss=0.7851, test accuracy=0.6333\n", "Epoch 80: loss=0.6372, test accuracy=0.8667\n", "Epoch 100: loss=0.4931, test accuracy=0.8667\n", "Epoch 120: loss=0.3959, test accuracy=0.8667\n", "Epoch 140: loss=0.3717, test accuracy=0.8333\n", "Epoch 160: loss=0.3562, test accuracy=0.8667\n", "Epoch 180: loss=0.2730, test accuracy=0.8667\n", "Epoch 200: loss=0.2365, test accuracy=0.9000\n" ] } ], "source": [ "learning_rate = 0.01\n", "number_of_epochs = 200\n", "\n", "\n", "def reset_seeds(seed: int) -> None:\n", " random.seed(seed)\n", " np.random.seed(seed)\n", " torch.manual_seed(seed)\n", " torch.cuda.manual_seed_all(seed)\n", " torch.backends.cudnn.deterministic = True\n", " torch.backends.cudnn.benchmark = False\n", "\n", "\n", "def evaluate_accuracy(\n", " model: nn.Module, reservoir_outputs: torch.Tensor, labels: torch.Tensor\n", ") -> float:\n", " model.eval()\n", " with torch.no_grad():\n", " predictions = model(reservoir_outputs).argmax(dim=1)\n", " return (predictions == labels).float().mean().item()\n", "\n", "\n", "def train_classifier(seed: int, show_progress: bool = False) -> HybridIrisClassifier:\n", " reset_seeds(seed)\n", " model = HybridIrisClassifier(\n", " input_size=output_size,\n", " number_of_classes=number_of_classes,\n", " )\n", " optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)\n", " criterion = nn.CrossEntropyLoss()\n", "\n", " model.train()\n", " for epoch in range(number_of_epochs):\n", " optimizer.zero_grad()\n", " loss = criterion(model(train_reservoir_outputs), y_train)\n", " loss.backward()\n", " optimizer.step()\n", "\n", " if show_progress and (epoch + 1) % 20 == 0:\n", " accuracy = evaluate_accuracy(model, test_reservoir_outputs, y_test)\n", " model.train()\n", " print(\n", " f\"Epoch {epoch + 1}: loss={loss.item():.4f}, \"\n", " f\"test accuracy={accuracy:.4f}\"\n", " )\n", "\n", " return model\n", "\n", "\n", "model = train_classifier(seed=123, show_progress=True)" ] }, { "cell_type": "markdown", "id": "e4d0bc29", "metadata": {}, "source": [ "## 6. Observe Results\n", "\n", "Evaluate the trained model once, then repeat the classical training loop across several seeds to observe the variation from classical initialization and dropout." ] }, { "cell_type": "code", "execution_count": 20, "id": "observe-results", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Test accuracy: 0.9000\n", "Average accuracy over 10 runs: 0.8533 +/- 0.0233\n" ] } ], "source": [ "test_accuracy = evaluate_accuracy(model, test_reservoir_outputs, y_test)\n", "print(f\"Test accuracy: {test_accuracy:.4f}\")\n", "\n", "number_of_runs = 10\n", "accuracies = []\n", "\n", "for run_index in range(number_of_runs):\n", " trained_model = train_classifier(seed=123 + run_index)\n", " accuracies.append(evaluate_accuracy(trained_model, test_reservoir_outputs, y_test))\n", "\n", "accuracies_tensor = torch.tensor(accuracies)\n", "average_accuracy = accuracies_tensor.mean().item()\n", "std_accuracy = accuracies_tensor.std(unbiased=True).item()\n", "print(f\"Average accuracy over {number_of_runs} runs: {average_accuracy:.4f} +/- {std_accuracy:.4f}\")" ] }, { "cell_type": "markdown", "id": "confusion-matrix-note", "metadata": {}, "source": [ "The confusion matrix shows which classes the final trained model confuses on the test split." ] }, { "cell_type": "code", "execution_count": 23, "id": "confusion-matrix", "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfIAAAHHCAYAAABEJtrOAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAOHRJREFUeJzt3Qd8FGX6wPFnNkBCSyBIJxAEpAuKwIkKeCL8USl66ql45kCwAYJYAJUmCLZDVBCwYqN4CIp46iFVBQvNw0JREYOCdAJBkpDs//O83u5lIUA2u5vdmfl9/YzJzs7svDu75Jn3ectYXq/XKwAAwJY80S4AAAAoOgI5AAA2RiAHAMDGCOQAANgYgRwAABsjkAMAYGMEcgAAbIxADgCAjRHIAQCwMQI5iuTvf/+7pKamcvbC7PDhw9K3b1+pVq2aWJYlgwcPDvs51s9NPz8E4jsNuyKQw5gxY4YJHKtXry7WM3LkyBEZPXq0LFu2LKLH+de//mWOE+vGjx9vPovbb79dXnvtNfnb3/4W7SLZTnF9p4BYUSLaBYA9Pf/885KXlxeWP7pjxowxv3fs2FEiGcinTJkS88F8yZIl8qc//UlGjRoVsWNs2rRJPB7nXsMX9TsVru80UNyc+68ZEZGZmWl+lixZUuLj4znLYbZr1y6pUKFCRM+rfm76+eEPfKdhdwRynLLNsFy5cvLDDz/IZZddJuXLl5devXqdtI189uzZ0qpVK7NdYmKiNG/eXJ566qmTvv5PP/0klStXNr9rDUpT+7rkrzVv3LhRrr76aklOTpaEhAQ577zzZMGCBQGvk5OTY/Zv0KCB2aZSpUpy4YUXyqJFi/xl1dq48h1Dl9N5//33pUOHDv7307p1a5k5c2bANv/85z/Ney5durScccYZcuONN8ovv/xS4HnU9T179jS/6/u+5557JDc312yjaWAt09atW+W9997zl1HPka/ZQ3/Pz7dP/hTyli1b5C9/+YtpY9dzUatWLbnuuuvk4MGDp2wj//HHH+Waa64x57lMmTImK6DlKOh4b775pjz88MPmtfUYl1xyiXz//fenPZ/6uer+mzdvNucpKSnJnIcRI0aI3oQxPT1devToYc61lv8f//hHwP7Z2dkycuRIc75137Jly8pFF10kS5cuLfR3KpjvtGZFNHOxePHigHLccsstUqpUKfnqq69O+56B4kBqHad07Ngx6dKliwmMTzzxhPkjXxANmtdff735o/7oo4+add999518+umnMmjQoAL30T+4U6dONe3BV155pVx11VVm/dlnn21+fvPNN3LBBRdIzZo1ZdiwYeYPtwYRDYZvvfWW2UfpH+kJEyaYTmJt2rSRjIwM09a/du1aufTSS+XWW2+VX3/91ZRR250LQ4Nnnz59pGnTpjJ8+HBTS163bp188MEHcsMNN/i36d27twnwevzffvvNXLjoe9Zt89esNWDreWzbtq05jx999JEJVPXq1TPvv3HjxqZsd911lwmQd999t/8cFZYGOj1GVlaWDBw40ARDvXhYuHChHDhwwAS/gmi527VrZ1LSd955p7kQeuWVV6R79+4yd+5c/3n2eeSRR0yA0wsRvUB47LHHTDD8/PPPC1XOv/71r+b96uvoxcK4cePMBcT06dPlz3/+s/n+vPHGG+b19dy2b9/e7Kef6wsvvGC+Z/369ZNDhw7Jiy++aN7zF198IS1btjztdyqY7/SDDz4o7777rtx8882yYcMGE/Q//PBDk4IfO3astGjRotCfDRBRej9y4OWXX9b70nu//PJL/8lIS0sz64YNG3bCCdLn6tSp4388aNAgb2JiovfYsWNBnczdu3ebY4waNeqE5y655BJv8+bNvUePHvWvy8vL87Zr187boEED/7oWLVp4L7/88lMep3///uY4hXHgwAFv+fLlvW3btvX+/vvvAc/p8VV2dra3SpUq3mbNmgVss3DhQnOckSNHnnAeH3rooYDXOuecc7ytWrUKWKfn9Pj34vtstm7dGrB+6dKlZr3+VOvWrTOP//nPf57y/ekxtEw+gwcPNvt9/PHH/nWHDh3y1q1b15uamurNzc0NOF7jxo29WVlZ/m2feuops37Dhg2nPK5+xrrdLbfc4l+n35datWp5LcvyPvLII/71+/fv95YuXTqgnLpt/uP6tqtataq3T58+hfpOBfOdVvqeSpUq5e3bt685Vs2aNb3nnXeeNycn55TvFShOpNZxWlq7OR2tfWpboy+dHap9+/aZjl/XXnutqXnt2bPHLHv37jW1KU0h+1LYemytveu6cND3oMfULICmjvPzpeS1xq/t2XfccUfANpdffrk0atTohLS0uu222wIea1pYU9rh4qtxa61Ra9fBdATUTIbWUH00/awpZE1Vf/vttwHb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czMPHDhQhg0bFu3iIUK0Rj5//nz/FI9wNp08RGvmGuDbt28f7eLABqiR20Rx3dcWQHQdPHjQ/ExOTuajQKEQyG3iVPe11Wn/ANifZtkGDx4sF1xwgTRr1izaxYFNMEUrAMQIbSv/+uuv5ZNPPol2UWAjBHKbKK772gKIDp2Le+HChWbEgptvz4zgkVq3ifz3tfXx3df2/PPPj2rZABSd3u5Cg7h2aFyyZInUrVuX04mgUCO3keK4ry1iw+HDh+X777/3P966dausX7/edICqXbt2VMuG8KfTZ86cKe+8844ZS+7r85KUlCSlS5fmdOO0GH5mMzr07PHHH/ff1/bpp582w9LgLMuWLZOLL774hPV6ITdjxoyolAmRG15YkJdffln+/ve/c9pxWgRyAABsjDZyAABsjEAOAICNEcgBALAxAjkAADZGIAcAwMYI5AAA2BiBHAAAGyOQAyHSSTvy3yu8Y8eO5g5W0ZhERicXOXDgwEm30efffvvtQr/m6NGjzcRDofjpp5/McXVmOgDhRyCHY4OrBg9ddJ76+vXry0MPPSTHjh2L+LHnzZsnY8eODVvwBYBTYa51ONb//d//mWkus7Ky5F//+peZ07pkyZIyfPjwE7bNzs42AT8cdD50ACgu1MjhWPHx8eYWr3Xq1JHbb79dOnXqJAsWLAhIhz/88MNSo0YNadiwoVmfnp4u1157rVSoUMEE5B49epjUsE9ubq65eY0+X6lSJbnvvvvM3avyOz61rhcSQ4cOlZSUFFMmzQ68+OKL5nV986lXrFjR1Mx9c2vrne0mTJhg7oSlN85o0aKFzJ07N+A4enFy1llnmef1dfKXs7C0XPoaZcqUkTPPPFNGjBghOTk5J2w3ffp0U37dTs/PwYMHA55/4YUXpHHjxpKQkCCNGjWSZ599NuiyACgaAjlcQwOe1rx99BawmzZtkkWLFpn7QGsA69Kli7kD1ccffyyffvqplCtXztTsffv94x//MDcteemll+STTz6Rffv2mdtPnspNN90ks2bNMje4+e6770xQ1NfVwPjWW2+ZbbQcO3bskKeeeso81iD+6quvyrRp0+Sbb76Ru+66S2688UZZvny5/4Ljqquukm7dupm25759+8qwYcOCPif6XvX9fPvtt+bYzz//vDz55JMB2+hd2N58801599135YMPPpB169bJHXfc4X/+jTfekJEjR5qLIn1/48ePNxcEr7zyStDlAVAEXsCB0tLSvD169DC/5+XleRctWuSNj4/33nPPPf7nq1at6s3KyvLv89prr3kbNmxotvfR50uXLu398MMPzePq1at7H3vsMf/zOTk53lq1avmPpTp06OAdNGiQ+X3Tpk1aXTfHL8jSpUvN8/v37/evO3r0qLdMmTLelStXBmx78803e6+//nrz+/Dhw71NmjQJeH7o0KEnvNbx9Pn58+ef9PnHH3/c26pVK//jUaNGeePi4rzbt2/3r3v//fe9Ho/Hu2PHDvO4Xr163pkzZwa8ztixY73nn3+++X3r1q3muOvWrTvpcQEUHW3kcCytZWvNV2vamqq+4YYbTC9sn+bNmwe0i3/11Vem9qm11PyOHj0qP/zwg0kna605/21jS5QoYe4Pf3x63Udry3FxcdKhQ4dCl1vLcOTIEbn00ksD1mtW4JxzzjG/a833+NvXnn/++RKsOXPmmEyBvj+9B7p2BkxMTAzYRu9/XrNmzYDj6PnULIKeK9335ptvln79+vm30dfR+2kDiDwCORxL242nTp1qgrW2g2vQza9s2bIBjzWQtWrVyqSKj1e5cuUip/ODpeVQ7733XkAAVdrGHi6rVq2SXr16yZgxY0yTggbe2bNnm+aDYMuqKfnjLyz0AgZA5BHI4VgaqLVjWWGde+65poZapUqVE2qlPtWrV5fPP/9c2rdv7695rlmzxuxbEK31a+1V27a1s93xfBkB7UTn06RJExOwf/7555PW5LVjma/jns9nn30mwVi5cqXpCPjAAw/4123btu2E7bQcv/76q7kY8h3H4/GYDoJVq1Y163/88UdzUQCg+NHZDfgvDURnnHGG6amund22bt1qxnnfeeedsn37drPNoEGD5JFHHjGTqmzcuNF0+jrVGPDU1FRJS0uTPn36mH18r6mdx5QGUu2trs0Au3fvNjVcTVffc889poObdhjT1PXatWvlmWee8Xcgu+2222TLli1y7733mhT3zJkzTae1YDRo0MAEaa2F6zE0xV5Qxz3tia7vQZse9Lzo+dCe6zoiQGmNXjvn6f6bN2+WDRs2mGF/EydO5LsFFAMCOfBfOrRqxYoVpk1Ye4RrrVfbfrWN3FdDv/vuu+Vvf/ubCWzaVqxB98orrzzlOdT0/tVXX22Cvg7N0rbkzMxM85ymzjUQao9zrd0OGDDArNcJZbTntwZILYf2nNdUuw5HU1pG7fGuFwc6NE17t2tv8WB0797dXCzoMXX2Nq2h6zGPp1kNPR+XXXaZdO7cWc4+++yA4WXaY16Hn2nw1gyEZhH0osJXVgCRZWmPtwgfAwAARAg1cgAAbIxADgCAjRHIAQCwMQI5AAA2RiAHAMDGCOQAANgYgRwAABsjkAMAYGMEcgAAbIxADgCAjRHIAQCwMQI5AABiX/8PoDV9FHWwZLIAAAAASUVORK5CYII=", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "model.eval()\n", "with torch.no_grad():\n", " test_predictions = model(test_reservoir_outputs).argmax(dim=1)\n", "\n", "class_labels = [str(label) for label in range(number_of_classes)]\n", "confusion = confusion_matrix(\n", " y_test.numpy(),\n", " test_predictions.numpy(),\n", " labels=list(range(number_of_classes)),\n", ")\n", "\n", "display = ConfusionMatrixDisplay(\n", " confusion_matrix=confusion,\n", " display_labels=class_labels,\n", ")\n", "display.plot(cmap=\"Blues\", values_format=\"d\")\n", "plt.title(\"Iris test confusion matrix\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "7f6dd19d", "metadata": {}, "source": [ "# Conclusion\n", "\n", "This notebook defines a processor once, uses it to evaluate a fixed quantum reservoir, and trains only the classical classifier. To move from local SLOS to a cloud simulator or QPU-backed processor, change the processor definition in section 2 and keep the rest of the notebook unchanged." ] } ], "metadata": { "kernelspec": { "display_name": "venv-0.4", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.7" } }, "nbformat": 4, "nbformat_minor": 5 }