{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "33c0bc6b-0506-4e17-9823-07a644d3093d",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Quanten-Kernel-Methoden\"\n",
        "description: \"Quantenkernel werden zunächst verwendet, um ein Kernel-Matrixelement zu bestimmen. Eine vollständige Kernel-Matrix und die Schnittstelle zu klassischen Kernel-Tools werden vorgestellt.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore workstreams workstream xvals yvals xval Arunachalam scikit */}\n",
        "\n",
        "<span id=\"quantum-kernels\" />\n",
        "\n",
        "# Quantenkerne\n",
        "\n",
        "<span id=\"introduction-to-quantum-kernels\" />\n",
        "\n",
        "## Einführung in Quantenkernel\n",
        "\n",
        "Der Begriff „Quantenkernel-Methode“ bezeichnet jede Methode, bei der Quantencomputer zur Schätzung eines Kernels eingesetzt werden. In diesem Zusammenhang bezieht sich der Begriff „Kern“ auf die Kernmatrix oder einzelne Einträge darin. Zur Erinnerung: Eine Merkmalsabbildung $\\Phi(\\vec{x})$ ist eine Abbildung von $\\vec{x}\\in \\mathbb{R}^d$ auf $\\Phi(\\vec{x})\\in \\mathbb{R}^{d'}$, wobei in der Regel $d'>d$ gilt und das Ziel dieser Abbildung darin besteht, die Datenkategorien durch eine Hyperfläche trennbar zu machen. Die Kernel-Funktion nimmt Vektoren im Feature-Map-Raum als Argumente entgegen und gibt deren Skalarprodukt zurück, d. h. $K:\\mathbb{R}^d\\times\\mathbb{R}^d\\rightarrow \\mathbb{R}$ mit $K(x,y) = \\langle \\Phi(x)|\\Phi(y)\\rangle$. Klassischerweise interessieren uns Feature-Maps, für die sich die Kernel-Funktion leicht berechnen lässt. Dies bedeutet oft, eine Kernel-Funktion zu finden, für die sich das Skalarprodukt im Raum der Merkmalsabbildung als Funktion der ursprünglichen Datenvektoren darstellen lässt, ohne dass jemals $\\Phi(x)$ und $\\Phi(y)$ konstruiert werden müssen. Bei der Methode der Quantenkerne erfolgt die Merkmalsabbildung durch einen Quantenschaltkreis, und der Kernel wird anhand von Messungen an diesem Schaltkreis und den relativen Messwahrscheinlichkeiten geschätzt.\n",
        "\n",
        "In dieser Lektion werden wir die Tiefe von vorcodierten Verschlüsselungsschaltungen untersuchen, die eine erhebliche Verschränkung verwenden, und diese mit der Tiefe von Schaltungen vergleichen, die wir von Hand codieren. Es geht nicht darum, eine Methode gegenüber einer anderen zu bevorzugen. Es kann sein, dass die vorcodierten Schaltungen zu tief sind und die Verschränkung in der selbstgebauten Schaltung nicht ausreicht, um nützlich zu sein. Auch diese werden nur angezeigt, damit Sie sie erforschen können.\n",
        "\n",
        "Bevor wir eine Kernelmatrix-Schätzung im Detail durchgehen, wollen wir den Arbeitsablauf in der Sprache der Qiskit-Muster skizzieren.\n",
        "\n",
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "* Eingabe: Trainingsdatensatz\n",
        "* Ausgabe: Abstrakte Schaltung zur Berechnung eines Kernelmatrixeintrags\n",
        "\n",
        "Bei einem Datensatz besteht der Ausgangspunkt darin, die Daten in einen Quantenschaltkreis zu kodieren. Mit anderen Worten: Wir müssen unsere Daten auf den Hilbert-Raum der Zustände unseres Quantencomputers abbilden. Wir tun dies, indem wir eine datenabhängige Schaltung konstruieren. Es gibt viele Möglichkeiten, dies zu tun, und in der vorangegangenen Lektion wurde eine Reihe von Optionen vorgestellt. Sie können einen eigenen Schaltkreis zur Kodierung Ihrer Daten erstellen oder eine vorgefertigte Feature Map wie `zz_feature_map` verwenden. In dieser Lektion werden wir beides tun.\n",
        "\n",
        "Um ein einzelnes Element der Kernelmatrix zu berechnen, müssen zwei verschiedene Punkte kodiert werden, damit wir ihr inneres Produkt schätzen können. Ein vollständiger Quantenkernel-Workflow umfasst natürlich viele solcher inneren Produkte zwischen abgebildeten Datenvektoren sowie klassische maschinelle Lernmethoden. Der wichtigste Schritt bei der Iteration ist jedoch die Schätzung eines einzelnen Kernelmatrixelements. Dazu wählen wir eine datenabhängige Quantenschaltung und bilden zwei Datenvektoren im Merkmalsraum ab.\n",
        "\n",
        "![Klassischer\\_Rückblick\\_Hintergrund\\_Kernel\\_Kreislauf](https://quantum.cloud.ibm.com/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/classical-review-background-kernel-circuit.avif)\n",
        "\n",
        "Für die Aufgabe, eine Kernelmatrix zu erzeugen, sind wir besonders an der Wahrscheinlichkeit interessiert, den Zustand $|0\\rangle^{\\otimes N}$ zu messen, in dem sich alle $N$ Qubits im Zustand $|0\\rangle$ befinden. Zur Veranschaulichung: Die Schaltung, die für die Kodierung und Abbildung eines Datenvektors $\\vec{x}_i$ verantwortlich ist, kann als $\\Phi(\\vec{x}_i)$ geschrieben werden, und die Schaltung, die für die Kodierung und Abbildung von $\\vec{x}_j$ verantwortlich ist, ist $\\Phi(\\vec{x}_j)$, und bezeichnet die abgebildeten Zustände\n",
        "\n",
        "$$\n",
        "|\\psi(\\vec{x}_i)\\rangle = \\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}\n",
        "$$\n",
        "\n",
        "$$\n",
        "|\\psi(\\vec{x}_j)\\rangle = \\Phi(\\vec{x}_j)|0\\rangle^{\\otimes N}.\n",
        "$$\n",
        "\n",
        "Diese Zustände *sind* die Abbildung der Daten auf höhere Dimensionen, so dass unser gewünschter Kernel-Eintrag das innere Produkt ist\n",
        "\n",
        "$$\n",
        "\\langle\\psi(\\vec{x}_j)|\\psi(\\vec{x}_i)\\rangle = \\langle 0 |^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}.\n",
        "$$\n",
        "\n",
        "Wenn wir den Standardausgangszustand $|0\\rangle^{\\otimes N}$ mit den beiden Schaltkreisen $\\Phi^\\dagger(\\vec{x}_j)$ und $\\Phi(\\vec{x}_i)$ bearbeiten, ist die Wahrscheinlichkeit, dass wir den Zustand $|0\\rangle^{\\otimes N}$ messen, gleich\n",
        "\n",
        "$$\n",
        "P_0 = |\\langle0|^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}|^2.\n",
        "$$\n",
        "\n",
        "Dies ist genau der Wert, den wir wollen (bis zu $||^2$ ). Die Messschicht unserer Schaltung liefert Messwahrscheinlichkeiten (oder so genannte \"Quasi-Wahrscheinlichkeiten\", wenn bestimmte Methoden zur Fehlerbegrenzung verwendet werden). Die interessierende Wahrscheinlichkeit ist die des Nullzustandes, $|0\\rangle^{\\otimes N}$.\n",
        "\n",
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Schritt 2: Optimierung des Problems für die Quantenausführung\n",
        "\n",
        "* Eingabe: Abstrakte Schaltung, nicht optimiert für ein bestimmtes Backend\n",
        "* Ausgabe: Zielschaltung und Observable, optimiert für die ausgewählte QPU\n",
        "\n",
        "In diesem Schritt werden wir die Funktion `generate_preset_pass_manager` von Qiskit verwenden, um eine Optimierungsroutine für unsere Schaltung in Bezug auf den realen Quantencomputer zu spezifizieren, auf dem wir das Experiment durchführen wollen. Wir stellen `optimization_level=3` ein, was bedeutet, dass wir den voreingestellten Passmanager verwenden werden, der den höchsten Optimierungsgrad bietet. In diesem Zusammenhang bezieht sich der Begriff \"Optimierung\" auf die Optimierung der Implementierung des Schaltkreises auf einem echten Quantencomputer. Dazu gehören Überlegungen wie die Auswahl von physischen Qubits, die den Qubits im abstrakten Quantenschaltkreis entsprechen, um die Gattertiefe zu minimieren, oder die Auswahl von physischen Qubits mit den niedrigsten verfügbaren Fehlerraten. Dies steht nicht in direktem Zusammenhang mit der Optimierung des maschinellen Lernproblems (wie bei klassischen Optimierern wie COBYLA).\n",
        "\n",
        "Je nachdem, wie Sie Schritt 2 umsetzen, müssen Sie die Schaltung möglicherweise mehr als einmal optimieren, da jedes an einem Matrixelement beteiligte Punktepaar eine andere zu messende Schaltung ergibt.\n",
        "\n",
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven\n",
        "\n",
        "* Eingabe: Zielschaltung\n",
        "* Ausgabe: Wahrscheinlichkeitsverteilung\n",
        "\n",
        "Verwenden Sie die Primitive `Sampler` „ IBM Quantum “, um eine Wahrscheinlichkeitsverteilung der Zustände zu rekonstruieren, die sich aus der Abtastung der Schaltung ergibt. Beachten Sie, dass dieser Begriff manchmal auch als „Quasi-Wahrscheinlichkeitsverteilung“ bezeichnet wird. Dieser Begriff findet Anwendung, wenn Rauschen eine Rolle spielt und wenn zusätzliche Schritte eingeführt werden, beispielsweise zur Fehlerminderung. In solchen Fällen ergibt die Summe aller Wahrscheinlichkeiten möglicherweise nicht genau 1; daher der Begriff „Quasi-Wahrscheinlichkeit“.\n",
        "\n",
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben\n",
        "\n",
        "* Eingabe: Wahrscheinlichkeitsverteilung\n",
        "* Ausgabe: Ein einzelnes Kernelmatrixelement oder eine Kernelmatrix bei Wiederholung\n",
        "\n",
        "Berechnen Sie die Wahrscheinlichkeit der Messung von $|0\\rangle^{\\otimes N}$ auf der Quantenschaltung und füllen Sie die Kernelmatrix an der Stelle auf, die den beiden verwendeten Datenvektoren entspricht. Um die gesamte Kernelmatrix auszufüllen, müssen wir für jeden Eintrag ein Quantenexperiment durchführen. Sobald wir eine Kernelmatrix haben, können wir sie in vielen klassischen Algorithmen für maschinelles Lernen verwenden, die `pre-calculated kernels` akzeptieren. Beispiel: `qml_svc = SVC(kernel=\"precomputed\")`. Wir können dann klassische Workstreams verwenden, um unser Modell auf unsere Testdaten anzuwenden und eine Genauigkeitsbewertung zu erhalten. Je nachdem, wie zufrieden wir mit unserer Genauigkeitsbewertung sind, müssen wir möglicherweise einige Aspekte unserer Berechnung überarbeiten, z. B. unsere Merkmalskarte.\n",
        "\n",
        "<span id=\"lesson-outline\" />\n",
        "\n",
        "### Unterrichtsplan\n",
        "\n",
        "In dieser Lektion werden wir diese Schritte auf verschiedene Arten durchführen, um Ihre Zeit auf echten Quantencomputern optimal zu nutzen. Wir werden eine Quantenkernmethode anwenden, um\n",
        "\n",
        "* Ein einziger Kernelmatrixeintrag für Daten mit relativ wenigen Merkmalen, der ein echtes Backend verwendet, so dass wir leicht nachvollziehen können, was bei jedem Schritt geschieht.\n",
        "* Ein ganzer Datensatz mit relativ wenigen Merkmalen unter Verwendung eines simulierten Backends, damit wir sehen können, wie sich der Quanten-Workstream mit klassischen maschinellen Lernmethoden verbindet\n",
        "* Ein einziger Kernelmatrixeintrag für Daten mit vielen Merkmalen unter Verwendung eines echten Quantencomputers. Wir werden nicht die gesamte Kernelmatrix für einen großen Datensatz schätzen, um die Zeit auf IBM® Quantencomputern zu respektieren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2fedaf0c-efb4-4425-8e31-89ca0319375d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# If you have not already, install scikit learn\n",
        "#!pip install scikit-learn"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f05e564c-ae91-4365-88fb-a57183c525aa",
      "metadata": {},
      "source": [
        "<span id=\"single-kernel-matrix-entry\" />\n",
        "\n",
        "## Einzelner Kernel-Matrixeintrag\n",
        "\n",
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "Betrachten wir zunächst einen Datensatz mit nur wenigen Merkmalen, sagen wir 10. Der Datensatz kann beliebig groß sein, da wir die Elemente der Kernelmatrix einzeln berechnen. Wir brauchen mindestens zwei Punkte, also fangen wir damit an (im nächsten Beispiel werden wir einen vollständigen Datensatz importieren). Lassen Sie uns einige benötigte Pakete importieren:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "61f15d16-993a-4405-9454-685829cb08b4",
      "metadata": {},
      "outputs": [],
      "source": [
        "import pandas as pd\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "# Two mock data points, including category labels, as in training\n",
        "small_data = [\n",
        "    [-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],\n",
        "    [-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],\n",
        "]\n",
        "\n",
        "# Data points with labels removed, for inner product\n",
        "train_data = [small_data[0][:-1], small_data[1][:-1]]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e9acb5ee-8c7a-467d-a7e9-e6219f4d443b",
      "metadata": {},
      "source": [
        "Wir können versuchen, die `z_feature_map` zu verwenden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "9d8e424e-751a-4057-a879-cc39d83af953",
      "metadata": {},
      "outputs": [],
      "source": [
        "# from qiskit.circuit.library import zz_feature_map\n",
        "# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)\n",
        "\n",
        "from qiskit.circuit.library import z_feature_map\n",
        "\n",
        "fm = z_feature_map(feature_dimension=np.shape(train_data)[1])\n",
        "\n",
        "\n",
        "unitary1 = fm.assign_parameters(train_data[0])\n",
        "unitary2 = fm.assign_parameters(train_data[1])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2b41da49-ce41-45eb-83ff-cfcbaa8c2c92",
      "metadata": {},
      "source": [
        "Die beiden oben genannten Einheiten entsprechen genau den in der Einleitung beschriebenen $U_1$ und $U_2$. Wir können sie mit `unitary_overlap` kombinieren. Wie immer wollen wir die Tiefe unseres Kreislaufs im Auge behalten.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "4273dcf7-784c-4edb-a9cd-9b249d24dbb8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  9\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/4273dcf7-784c-4edb-a9cd-9b249d24dbb8-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()\n",
        "\n",
        "print(\"circuit depth = \", overlap_circ.decompose().depth())\n",
        "overlap_circ.decompose().draw(\"mpl\", scale=0.6, style=\"iqp\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0a9f9df0-d543-496a-99fd-8b66a53941a7",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Schritt 2: Optimierung des Problems für die Quantenausführung\n",
        "\n",
        "Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unsere Schaltung für die Ausführung auf diesem Backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "27f62452-2534-4f97-a738-52e4a19a7772",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_brisbane')>\n"
          ]
        }
      ],
      "source": [
        "# Import needed packages\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "# Get the least busy backend\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        ")\n",
        "print(backend)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "36cfd4e9-55c9-46c4-8afe-bd6cd0a40f45",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Apply level 3 optimization to our overlap circuit\n",
        "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n",
        "overlap_ibm = pm.run(overlap_circ)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "13def807-7e1a-44c8-a7d8-2db5ce0f75ec",
      "metadata": {},
      "source": [
        "Bei komplizierten Schaltkreisen erhöht dieser Schritt die Tiefe des Schaltkreises erheblich, da er sich auf native Gatter für echte Quantencomputer bezieht und Informationen möglicherweise von Qubit zu Qubit verschoben werden müssen. In diesem einfachen Fall wird die Tiefe kaum beeinflusst.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "79348ff5-4d6d-46d7-ae2b-e6ffbb22dd25",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  10\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "1"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "print(\"circuit depth = \", overlap_ibm.decompose().depth())\n",
        "overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ec97b263-4037-4ed9-9588-110335d2c51d",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven\n",
        "\n",
        "Die Syntax für die Ausführung auf einem Simulator ist unten auskommentiert. Für diesen Datensatz mit einer geringen Anzahl von Merkmalen ist die Ausführung auf einem Simulator immer noch eine Option. Für Berechnungen im Versorgungsmaßstab ist eine Simulation in der Regel nicht durchführbar. Simulatoren sollten nur zum Debuggen von verkleinertem Code verwendet werden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "36a81e65-d14f-4dc2-b64c-4acbd5a185cd",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Run this for a simulator\n",
        "\n",
        "# from qiskit.primitives import StatevectorSampler\n",
        "# from qiskit_ibm_runtime import Options, Session, Sampler\n",
        "# num_shots = 10000\n",
        "\n",
        "# Evaluate the problem using state vector-based primitives from Qiskit\n",
        "\n",
        "# sampler = StatevectorSampler()\n",
        "# results = sampler.run([overlap_circ], shots=num_shots).result()\n",
        "\n",
        "# .get_counts() returns counts associated with a state labeled by bit results\n",
        "# such as |001101...01>.\n",
        "\n",
        "# counts_bit = results[0].data.meas.get_counts()\n",
        "\n",
        "# .get_int_counts returns the same counts, but labeled by integer equivalent\n",
        "# of the above bit string.\n",
        "\n",
        "# counts = results[0].data.meas.get_int_counts()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1ad869b2-ad50-4a82-bc4c-70fd0ab802c5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.\n",
        "\n",
        "# Import the IBM Quantum primitive\n",
        "from qiskit_ibm_runtime import Session, SamplerV2 as Sampler\n",
        "\n",
        "num_shots = 10000\n",
        "\n",
        "# Use sampler and get the counts\n",
        "\n",
        "sampler = Sampler(mode=backend)\n",
        "results = sampler.run([overlap_ibm], shots=num_shots).result()\n",
        "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n",
        "counts_bit = results[0].data.meas.get_counts()\n",
        "# .get_int_counts returns the same counts, but labeled by integer equivalent\n",
        "# of the above bit string.\n",
        "counts = results[0].data.meas.get_int_counts()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16d66085-047a-4718-80fe-eb64c20bd398",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben\n",
        "\n",
        "Wie in der Einleitung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand zu messen $|00000\\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 62,
      "id": "33522847-21dc-48b8-9270-ce11fd1ec529",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "0.6525"
            ]
          },
          "execution_count": 62,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "counts.get(0, 0.0) / num_shots"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "65dfa3e4-bfe4-45d7-815f-e8c1aa24d916",
      "metadata": {},
      "source": [
        "Das ist das gewünschte Ergebnis: eine Schätzung des inneren Produkts (bis zum modifizierten Quadrat) der Vektoren, die zwei Datenpunkten entsprechen. Wenn wir uns die vollständige Verteilung der Messwahrscheinlichkeiten (oder Quasi-Wahrscheinlichkeiten) ansehen wollen, können wir dies mit der Funktion `plot_distribution` wie unten gezeigt tun. Man sieht, dass solche Bilder für eine große Anzahl von Qubits schnell unlösbar werden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "id": "29aaf5d6-ea7e-4373-b33c-9299699a3964",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/29aaf5d6-ea7e-4373-b33c-9299699a3964-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 30,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit.visualization import plot_distribution\n",
        "\n",
        "plot_distribution(counts_bit)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d0a52344-34f2-498b-8f46-489d581bd8ba",
      "metadata": {},
      "source": [
        "Alternativ könnte man auch eine Visualisierung wie die folgende definieren, um nur die 10 wahrscheinlichsten Messungen zu betrachten. Dies könnte für die Fehlersuche wichtig sein oder um ein besseres Gespür für die Daten zu bekommen. Aber die Messwahrscheinlichkeit des Nullzustands ist unser Kernelmatrixelement.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 29,
      "id": "c141e800-79eb-400a-9b8c-75e2393ce24e",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/c141e800-79eb-400a-9b8c-75e2393ce24e-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "def visualize_counts(probs, num_qubits):\n",
        "    \"\"\"Visualize the outputs from the Qiskit Sampler primitive.\"\"\"\n",
        "    zero_prob = probs.get(0, 0.0)\n",
        "    top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])\n",
        "    top_10.update({0: zero_prob})\n",
        "    by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))\n",
        "    xvals, yvals = list(zip(*by_key.items()))\n",
        "    xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]\n",
        "    plt.bar(xvals, yvals)\n",
        "    plt.xticks(rotation=75)\n",
        "    plt.title(\"Results of sampling\")\n",
        "    plt.xlabel(\"Measured bitstring\")\n",
        "    plt.ylabel(\"Counts\")\n",
        "    plt.show()\n",
        "\n",
        "\n",
        "visualize_counts(counts, overlap_circ.num_qubits)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "47e85d94-ee94-4ed9-8223-ccd5fc9442d4",
      "metadata": {},
      "source": [
        "Aus dieser Information über nur ein inneres Produkt zwischen zwei Datenpunkten im höherdimensionalen Merkmalsraum können wir nur schließen, dass ihre Überlappung im Vergleich zur maximalen Überlappung (die 1.0 wäre) ziemlich groß ist. Dies könnte ein Indikator dafür sein, dass diese beiden Datenpunkte in gewisser Weise ähnlich sind und in dieselben Klassen eingeteilt werden. Es könnte aber auch ein Indikator dafür sein, dass unsere Merkmalskarte nicht in der Lage ist, einen Raum abzubilden, in dem ähnliche Daten eine starke Überlappung und ungleiche Daten eine geringe Überlappung aufweisen. Um herauszufinden, was davon zutrifft, müssen wir unsere Merkmalskarte auf den gesamten Datensatz anwenden und sehen, ob die sich daraus ergebende Kernelmatrix so manipuliert werden kann, dass die Klassen mit hoher Genauigkeit getrennt werden können.\n",
        "\n",
        "Es ist erwähnenswert, dass wir die `z_feature_map` verwendet haben, die zu einer geringen Zwei-Qubit-Transpilierungstiefe führt (Tiefe 1, in der Tat). Wenn Ihre Schaltkreise zu tief werden, führt dies mit Sicherheit zu starkem Rauschen, so dass die Wahrscheinlichkeit, den Nullzustand zu messen, sehr gering ist, selbst wenn Ihre Merkmalskarte gut an Ihre Daten angepasst ist. Eine Wiederholung des obigen Prozesses mit `zz_feature_map` und `, entanglement='linear', reps=1` ergab beispielsweise `dist.get(0,0.0) = 0.0015` mit denselben Datenpunkten. Das liegt an den viel größeren Schaltungstiefen und Zwei-Qubit-Tiefen von `zz_feature_map`. Die folgende Abbildung zeigt die Wahrscheinlichkeitsverteilung für diese Berechnung.\n",
        "\n",
        "![Schlechte Ergebnisse aus einer zz-Featurekarte.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/zzfeaturemap-bad-results.avif)\n",
        "\n",
        "Es lohnt sich, mit einigen Datenpunkten aus derselben Kategorie zu spielen, um zu sehen, wie niedrig die Tiefe sein muss, um gute Ergebnisse zu erzielen. Die folgenden Hinweise sind grobe Ratschläge, von denen es sicher Ausnahmen gibt. Im Allgemeinen sollte eine transpilierte Zwei-Qubit-Tiefe von 10 oder weniger kein Problem darstellen. Eine transpilierte Zwei-Qubit-Tiefe von 50-60 ist Stand der Technik und erfordert unter anderem eine fortgeschrittene Fehlerminderung. Dazwischen können Ihre Ergebnisse je nach Datenähnlichkeit, Ausdruckskraft der Feature-Maps, Schaltkreisbreite und anderen Faktoren variieren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "33eda2a4-cd97-4bba-8acd-4017bfe2cd12",
      "metadata": {},
      "source": [
        "Normalerweise würde der Nachbearbeitungsschritt auch klassische maschinelle Lernverfahren umfassen. Im nächsten Abschnitt werden wir diesen Prozess auf einen ganzen Datensatz ausweiten und den klassischen Arbeitsablauf des maschinellen Lernens darstellen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "41850d1d-3800-4aa0-aa01-fe6e72c872be",
      "metadata": {},
      "source": [
        "<span id=\"check-your-understanding\" />\n",
        "\n",
        "#### Überprüfen Sie Ihr Verständnis\n",
        "\n",
        "Wie viele verschiedene Zustände können in einem 10-Qubit-Quantenschaltkreis überhaupt gemessen werden?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Antwort\">\n",
        "    $2^{10}$ oder 1024.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Nehmen wir einmal an, jemand, der sich mit Quantencomputern noch nicht auskennt, versucht, eine Quantenschaltung mit sehr hoher Zwei-Qubit-Tiefe zu verwenden, ohne dabei Fehlerminderungsmaßnahmen anzuwenden. Nehmen wir weiter an, dass dies zu einer Fehlerrate von 10 % bei jedem Qubit führt. Wenn das tatsächliche (fehlerfreie) Kernel-Matrixelement, das diesem Schaltkreis entspricht, sehr groß ist, beispielsweise 1.0, wie hoch wäre dann die Wahrscheinlichkeit, dass alle 10 Qubits in dem Zustand gemessen werden, in dem jedes Qubit den Wert $|0\\rangle$ annimmt?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Antwort\">\n",
        "    Die Wahrscheinlichkeit, dass jedes Qubit korrekt im Zustand |0> gefunden wird, ist 0.90. Die Wahrscheinlichkeit, dass alle 10 Qubits im richtigen Zustand gefunden werden, beträgt $0.90^{10}$ oder etwa 35 %.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Erläutern Sie mit eigenen Worten, warum es so wichtig ist, die Tiefe der Stromkreise zu überwachen. Dies gilt allgemein, aber erklären Sie es im Zusammenhang mit der Quanten-Kernel-Schätzung.\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Antwort\">\n",
        "    In diesem QKE-Arbeitsablauf basieren unsere Schätzungen auf den Messungen des Nullzustands, d. h. des Zustands, in dem sich jedes Qubit im Zustand $|0\\rangle$ befindet. Sehr tiefe Schaltungen führen zu hohen Fehlerquoten. Wenn sich diese Fehlerrate über viele Qubits summiert, verringert sich die Wahrscheinlichkeit, den Nullzustand zu messen, erheblich.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0fde5aca-0d54-40b1-82d4-9d1b2a1b67bb",
      "metadata": {},
      "source": [
        "<span id=\"full-kernel-matrix\" />\n",
        "\n",
        "## Vollständige Kernelmatrix\n",
        "\n",
        "In diesem Abschnitt wird das obige Verfahren auf die binäre Klassifizierung eines vollständigen Datensatzes ausgedehnt. Dadurch werden zwei wichtige Komponenten eingeführt: (1) wir können nun klassisches maschinelles Lernen im Post-Processing implementieren und (2) wir können Genauigkeitswerte für unser Training erhalten.\n",
        "\n",
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "Nun importieren wir einen bestehenden Datensatz für unsere Klassifizierung. Dieser Datensatz besteht aus 128 Zeilen (Datenpunkten) und 14 Merkmalen für jeden Punkt. Es gibt ein 15. Element, das die binäre Kategorie jedes Punktes angibt ( $\\pm 1$ ). Der Datensatz wird nachstehend importiert, oder Sie können [hier](https://github.com/qiskit-community/prototype-quantum-kernel-training/blob/main/data/dataset_graph7.csv) auf den Datensatz zugreifen und seine Struktur einsehen.\n",
        "\n",
        "Wir verwenden die ersten 90 Datenpunkte zum Training und die nächsten 30 Punkte zum Testen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "60f2699d-ef47-44f7-931a-4a70d4363c0e",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n",
            "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...\n",
            "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n",
            "HTTP request sent, awaiting response... 200 OK\n",
            "Length: 49405 (48K) [text/plain]\n",
            "Saving to: ‘dataset_graph7.csv.15’\n",
            "\n",
            "dataset_graph7.csv. 100%[===================>]  48.25K  --.-KB/s    in 0.02s   \n",
            "\n",
            "2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]\n",
            "\n"
          ]
        }
      ],
      "source": [
        "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n",
        "\n",
        "df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n",
        "\n",
        "# Prepare training data\n",
        "\n",
        "train_size = 90\n",
        "X_train = df.values[0:train_size, :-1]\n",
        "train_labels = df.values[0:train_size, -1]\n",
        "\n",
        "# Prepare testing data\n",
        "test_size = 30\n",
        "X_test = df.values[train_size : train_size + test_size, :-1]\n",
        "test_labels = df.values[train_size : train_size + test_size, -1]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4a665fcb-bd27-4713-9c0a-b7747de5fb45",
      "metadata": {},
      "source": [
        "Wir bereiten uns bereits auf die Speicherung mehrerer Ausgaben vor, indem wir eine Kernelmatrix und eine Testmatrix mit geeigneten Dimensionen konstruieren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "34d92041-0501-4c20-b780-ae5352510637",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Empty kernel matrix\n",
        "num_samples = np.shape(X_train)[0]\n",
        "kernel_matrix = np.full((num_samples, num_samples), np.nan)\n",
        "test_matrix = np.full((test_size, num_samples), np.nan)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "920233dd-cfac-498d-be88-746719083cc2",
      "metadata": {},
      "source": [
        "Nun erstellen wir eine Feature-Map, um unsere klassischen Daten in einem Quantenschaltkreis zu kodieren und abzubilden. Es steht uns frei, unsere eigene Feature Map zu erstellen oder eine vorgefertigte zu verwenden. Sie können die nachstehende Feature-Map gerne ändern oder zurück zu ZFeatureMap wechseln. Achten Sie aber immer auf die Schaltungstiefe. Erinnern Sie sich daran, dass im vorherigen 6-Qubit-Beispiel die Tiefe der transpilierten Schaltkreise bei Verwendung von `zz_feature_map` unüberwindbar hoch war. Mit zunehmender Größe und Komplexität der Schaltung könnte die Tiefe schnell so weit ansteigen, dass das Rauschen unsere Ergebnisse überlagert. Wenn Sie etwas über Ihre Datenstruktur wissen, das Aufschluss darüber gibt, welche Feature-Map-Struktur am nützlichsten ist, ist es ratsam, Ihre eigene benutzerdefinierte Feature-Map zu erstellen, die dieses Wissen nutzt.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "ca813500-5856-4b50-a213-a7e87d825c18",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n",
        "\n",
        "# Prepare feature map for computing overlap\n",
        "num_features = np.shape(X_train)[1]\n",
        "num_qubits = int(num_features / 2)\n",
        "\n",
        "# To use a custom feature map use the lines below.\n",
        "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n",
        "\n",
        "fm = QuantumCircuit(num_qubits)\n",
        "training_param = Parameter(\"θ\")\n",
        "feature_params = ParameterVector(\"x\", num_qubits * 2)\n",
        "fm.ry(training_param, fm.qubits)\n",
        "for cz in entangler_map:\n",
        "    fm.cz(cz[0], cz[1])\n",
        "for i in range(num_qubits):\n",
        "    fm.rz(-2 * feature_params[2 * i + 1], i)\n",
        "    fm.rx(-2 * feature_params[2 * i], i)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3c6416b5-9eb1-40a4-b72c-8912df931139",
      "metadata": {},
      "source": [
        "<span id=\"steps-2-and-3-optimize-problem-and-execute-using-primitives\" />\n",
        "\n",
        "### Schritte 2 und 3: Problem optimieren und mit Primitiven ausführen\n",
        "\n",
        "Wir werden eine Überlappungsschaltung konstruieren, und wenn wir in diesem Beispiel auf einem echten Quantencomputer arbeiten würden, würden wir sie wie zuvor für die Ausführung optimieren. In diesem Fall beabsichtigen wir jedoch, über alle Datenpunkte zu gehen und die vollständige Kernelmatrix zu berechnen. Für jedes Paar von Datenvektoren $\\vec{x}_i$ und $\\vec{x}_j$ erstellen wir eine andere Überlappungsschaltung. Wir müssen also unsere Schaltung für jedes Datenpunktpaar optimieren. Die Schritte 2 und 3 werden also in mehreren Iterationen zusammen durchgeführt.\n",
        "\n",
        "Die nachstehende Codezelle führt genau denselben Prozess wie zuvor für ein einzelnes Datenpunktpaar aus. Diesmal wird sie einfach innerhalb von zwei `for` -Schleifen ausgeführt, und am Ende befindet sich die zusätzliche Zeile `kernel_matrix[x_1,x_2] = ...` , in der die Ergebnisse der einzelnen Berechnungen gespeichert werden. Beachten Sie, dass wir die Symmetrie einer Kernelmatrix genutzt haben, um die Anzahl der Berechnungen um 1/2 zu reduzieren. Wir haben auch die Diagonalelemente einfach auf 1 gesetzt, so wie sie bei Abwesenheit von Rauschen sein sollten. Je nach Ihrer Implementierung und der erforderlichen Genauigkeit können Sie die Diagonalelemente auch zur Schätzung des Rauschens verwenden oder sich über das Rauschen informieren, um Fehler zu minimieren.\n",
        "\n",
        "Sobald die Kernelmatrix vollständig ausgefüllt ist, wiederholen wir den Vorgang für die Testdaten und füllen die test\\_matrix aus. Dies ist eigentlich auch eine Kernelmatrix; wir geben ihr nur einen anderen Namen, um die beiden zu unterscheiden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7995748f-fc97-4eda-bed6-8b189c1e65ea",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "training done\n",
            "test matrix done\n"
          ]
        }
      ],
      "source": [
        "# To use a simulator\n",
        "from qiskit.primitives import StatevectorSampler\n",
        "\n",
        "# Remember to insert your token in the QiskitRuntimeService constructor\n",
        "# to use real quantum computers\n",
        "\n",
        "# service = QiskitRuntimeService()\n",
        "# backend = service.least_busy(\n",
        "#    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        "# )\n",
        "\n",
        "num_shots = 10000\n",
        "\n",
        "# Evaluate the problem using state vector-based primitives from Qiskit.\n",
        "sampler = StatevectorSampler()\n",
        "\n",
        "for x1 in range(0, train_size):\n",
        "    for x2 in range(x1 + 1, train_size):\n",
        "        unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n",
        "        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n",
        "\n",
        "        # Create the overlap circuit\n",
        "        overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "        overlap_circ.measure_all()\n",
        "\n",
        "        # These lines run the qiskit sampler primitive.\n",
        "        counts = (\n",
        "            sampler.run([overlap_circ], shots=num_shots)\n",
        "            .result()[0]\n",
        "            .data.meas.get_int_counts()\n",
        "        )\n",
        "\n",
        "        # Assign the probability of the 0 state to the kernel matrix, and the transposed element\n",
        "        # (since this is an inner product)\n",
        "        kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n",
        "        kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots\n",
        "    # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to\n",
        "    # probability (or alter the code to check these entries and verify they yield 1)\n",
        "    kernel_matrix[x1, x1] = 1\n",
        "\n",
        "print(\"training done\")\n",
        "\n",
        "# Similar process to above, but for testing data.\n",
        "for x1 in range(0, test_size):\n",
        "    for x2 in range(0, train_size):\n",
        "        unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])\n",
        "        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n",
        "\n",
        "        # Create the overlap circuit\n",
        "        overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "        overlap_circ.measure_all()\n",
        "\n",
        "        counts = (\n",
        "            sampler.run([overlap_circ], shots=num_shots)\n",
        "            .result()[0]\n",
        "            .data.meas.get_int_counts()\n",
        "        )\n",
        "\n",
        "        test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n",
        "\n",
        "print(\"test matrix done\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ea65cd41-4ba3-423c-a98f-2f8e3751adf4",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben\n",
        "\n",
        "Da wir nun eine Kernel-Matrix und eine ähnlich formatierte test\\_matrix aus Quantenkernel-Methoden haben, können wir klassische Algorithmen des maschinellen Lernens anwenden, um Vorhersagen über unsere Testdaten zu treffen und ihre Genauigkeit zu überprüfen. Wir beginnen mit dem Import von Scikit-Learn's `sklearn.svc`, einem Support-Vektor-Klassifikator (SVC). Wir müssen angeben, dass der SVC unseren vorberechneten Kernel unter `kernel = precomputed` verwenden soll.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "78d9e291-54c3-4c45-827e-066c6d012da5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# import a support vector classifier from a classical ML package.\n",
        "from sklearn.svm import SVC\n",
        "\n",
        "# Specify that you want to use a pre-computed kernel matrix\n",
        "qml_svc = SVC(kernel=\"precomputed\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9bd69e5f-dafd-492c-bd5d-f5e16925f611",
      "metadata": {},
      "source": [
        "Mit `SVC.fit` können wir nun die Kernelmatrix und die Trainingsmarkierungen eingeben, um eine Anpassung zu erhalten. `SVC.score` bewertet dann unsere Testdaten anhand dieser Anpassung unter Verwendung unserer test\\_matrix und gibt unsere Genauigkeit zurück.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d8708973-c97c-494c-af9c-e4a0fe430452",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Precomputed kernel classification test score: 1.0\n"
          ]
        }
      ],
      "source": [
        "# Feed in the pre-computed matrix and the labels of the training data.\n",
        "# The classical algorithm gives you a fit.\n",
        "qml_svc.fit(kernel_matrix, train_labels)\n",
        "\n",
        "# Now use the .score to test your data, using the matrix of test data,\n",
        "# and test labels as your inputs.\n",
        "qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)\n",
        "print(f\"Precomputed kernel classification test score: {qml_score_precomputed_kernel}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0200dceb-6d61-4e16-8c3d-d08dbc5529e5",
      "metadata": {},
      "source": [
        "Wir sehen, dass die Genauigkeit unseres trainierten Modells 100 % beträgt. Das ist großartig und zeigt, dass QKE funktionieren kann. Aber das ist etwas ganz anderes als ein Quantenvorteil. Klassische Kernel wären wahrscheinlich auch in der Lage gewesen, dieses Klassifizierungsproblem mit 100%iger Genauigkeit zu lösen. Es gibt noch viel zu tun, um die verschiedenen Datentypen und Datenbeziehungen zu charakterisieren, um herauszufinden, wo Quantenkerne in der gegenwärtigen Ära der Versorgungswirtschaft am nützlichsten sind.\n",
        "Wir überlassen es den Lernenden, Teile dieses Arbeitsablaufs zu ändern und die Wirksamkeit verschiedener Quantenmerkmalskarten zu untersuchen. Hier sind ein paar Dinge zu beachten:\n",
        "\n",
        "* Wie zuverlässig ist die Genauigkeit? Gilt dies für alle Arten von Daten oder nur für diese speziellen Trainingsdaten?\n",
        "* Welche Struktur in Ihren Daten lässt Sie vermuten, dass eine Quantenmerkmalskarte nützlich ist?\n",
        "* Wie wird die Genauigkeit durch die Erhöhung/Verringerung der Trainingsdatenmenge beeinflusst?\n",
        "* Welche Feature-Maps können Sie verwenden und wie variieren die Ergebnisse mit Feature-Maps?\n",
        "* Wie werden die Genauigkeit und die Laufzeit durch die Erhöhung der Anzahl der Merkmale beeinflusst?\n",
        "* Welche Trends, wenn überhaupt, erwarten Sie bei echten Quantencomputern?\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "da2427c4-a1a1-4284-b10f-95d5c5230755",
      "metadata": {},
      "source": [
        "<span id=\"scaling-to-more-features-and-qubits\" />\n",
        "\n",
        "## Skalierung auf mehr Funktionen und Qubits\n",
        "\n",
        "In diesem Abschnitt wiederholen wir die Berechnung eines einzelnen Matrixelements, aber für eine viel größere Anzahl von Merkmalen, und skizzieren den Weg zur Skalierung in Richtung Nutzen. Die Beschränkung auf ein einziges Matrizenelement dient dazu, den Prozess zu veranschaulichen, ohne zu viel Zeit auf Quantencomputern zu verbrauchen.\n",
        "\n",
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "Wir gehen von einem Datensatz aus, in dem jeder Datenpunkt 42 Merkmale aufweist. Wie im ersten Beispiel wird ein einzelnes Kernelmatrixelement berechnet, wofür zwei Datenpunkte benötigt werden. Die beiden folgenden Punkte haben 42 Merkmale und eine einzige Kategorievariable ( $\\pm 1$ ).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 79,
      "id": "014ba70a-5202-4c86-b886-7ff27ae9a1b3",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Two mock data points, including category labels, as in training\n",
        "\n",
        "large_data = [\n",
        "    [\n",
        "        -0.028,\n",
        "        -1.49,\n",
        "        -1.698,\n",
        "        0.107,\n",
        "        -1.536,\n",
        "        -1.538,\n",
        "        -1.356,\n",
        "        -1.514,\n",
        "        -0.109,\n",
        "        -1.8,\n",
        "        -0.122,\n",
        "        -1.651,\n",
        "        -1.955,\n",
        "        -0.123,\n",
        "        -1.732,\n",
        "        0.091,\n",
        "        -0.048,\n",
        "        -0.128,\n",
        "        -0.026,\n",
        "        0.082,\n",
        "        -1.263,\n",
        "        0.065,\n",
        "        0.004,\n",
        "        -0.055,\n",
        "        -0.08,\n",
        "        -0.173,\n",
        "        -1.734,\n",
        "        -0.39,\n",
        "        -1.451,\n",
        "        0.078,\n",
        "        -1.578,\n",
        "        -0.025,\n",
        "        -0.184,\n",
        "        -0.119,\n",
        "        -1.336,\n",
        "        0.055,\n",
        "        -0.204,\n",
        "        -1.578,\n",
        "        0.132,\n",
        "        -0.121,\n",
        "        -1.599,\n",
        "        -0.187,\n",
        "        -1,\n",
        "    ],\n",
        "    [\n",
        "        -1.414,\n",
        "        -1.439,\n",
        "        -1.606,\n",
        "        0.246,\n",
        "        -1.673,\n",
        "        0.002,\n",
        "        -1.317,\n",
        "        -1.262,\n",
        "        -0.178,\n",
        "        -1.814,\n",
        "        0.013,\n",
        "        -1.619,\n",
        "        -1.86,\n",
        "        -0.25,\n",
        "        -0.212,\n",
        "        -0.214,\n",
        "        -0.033,\n",
        "        0.071,\n",
        "        -0.11,\n",
        "        -1.607,\n",
        "        0.441,\n",
        "        -0.143,\n",
        "        -0.009,\n",
        "        -1.655,\n",
        "        -1.579,\n",
        "        0.381,\n",
        "        -1.86,\n",
        "        -0.079,\n",
        "        -0.088,\n",
        "        -0.058,\n",
        "        -1.481,\n",
        "        -0.064,\n",
        "        -0.065,\n",
        "        -1.507,\n",
        "        0.177,\n",
        "        -0.131,\n",
        "        -0.153,\n",
        "        0.07,\n",
        "        -1.627,\n",
        "        0.593,\n",
        "        -1.547,\n",
        "        -0.16,\n",
        "        -1,\n",
        "    ],\n",
        "]\n",
        "train_data = [large_data[0][:-1], large_data[1][:-1]]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3e4dd6a7-e51c-4dc2-99ea-66e2746e411d",
      "metadata": {},
      "source": [
        "Es sei daran erinnert, dass die `zz_feature_map` bei relativ wenigen Merkmalen (14 Merkmale) recht tiefe Schaltkreise erzeugt. Wenn wir die Anzahl der Funktionen erhöhen, müssen wir die Schaltungstiefe genau überwachen. Um dies zu veranschaulichen, werden wir zunächst versuchen, die `zz_feature_map` zu verwenden und die Tiefe der resultierenden Schaltung zu überprüfen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bdb1ecc4-7827-4eb7-bf7b-5a09838775ca",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit.library import zz_feature_map\n",
        "\n",
        "fm = zz_feature_map(\n",
        "    feature_dimension=np.shape(train_data)[1], entanglement=\"linear\", reps=1\n",
        ")\n",
        "\n",
        "unitary1 = fm.assign_parameters(train_data[0])\n",
        "unitary2 = fm.assign_parameters(train_data[1])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5bc865cb-8f8b-4812-9a05-1bd34dbcbcb9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  251\n",
            "two-qubit depth 165\n"
          ]
        }
      ],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()\n",
        "\n",
        "print(\"circuit depth = \", overlap_circ.decompose(reps=2).depth())\n",
        "print(\n",
        "    \"two-qubit depth\",\n",
        "    overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),\n",
        ")\n",
        "# overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b3348ab6-ef4e-40d2-b803-e86484e0b8f3",
      "metadata": {},
      "source": [
        "Wie bereits beschrieben, ist die Frage, wie tief man zu tief ist, sehr differenziert. Aber eine Zwei-Qubit-Tiefe von mehr als 100, selbst vor der Transpilation, ist ein absolutes No-Go. Aus diesem Grund wurde in dieser Lektion der Schwerpunkt auf benutzerdefinierte Feature-Maps gelegt. Wenn Sie etwas über die Struktur Ihres gesamten Datensatzes wissen, sollten Sie eine Verflechtungskarte mit Blick auf diese Struktur entwerfen. Da wir hier nur das innere Produkt zwischen zwei solchen Datenpunkten berechnen, haben wir einer geringen Schaltungstiefe gegenüber einer detaillierten Betrachtung der Datenstruktur den Vorrang gegeben.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 95,
      "id": "784f8a68-0d77-433b-8482-bac38ddb8adc",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n",
        "\n",
        "# Prepare feature map for computing overlap\n",
        "\n",
        "entangler_map = [\n",
        "    [3, 4],\n",
        "    [2, 5],\n",
        "    [1, 4],\n",
        "    [2, 3],\n",
        "    [4, 6],\n",
        "    [7, 9],\n",
        "    [10, 11],\n",
        "    [9, 12],\n",
        "    [8, 11],\n",
        "    [9, 10],\n",
        "    [11, 13],\n",
        "    [14, 16],\n",
        "    [17, 18],\n",
        "    [16, 19],\n",
        "    [15, 18],\n",
        "    [16, 17],\n",
        "    [18, 20],\n",
        "]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 96,
      "id": "587daff1-7c5b-4505-8064-827c1fbdc742",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Use the entangler map above to build a feature map\n",
        "\n",
        "num_features = np.shape(train_data)[1]\n",
        "num_qubits = int(num_features / 2)\n",
        "\n",
        "fm = QuantumCircuit(num_qubits)\n",
        "training_param = Parameter(\"θ\")\n",
        "feature_params = ParameterVector(\"x\", num_qubits * 2)\n",
        "fm.ry(training_param, fm.qubits)\n",
        "for cz in entangler_map:\n",
        "    fm.cz(cz[0], cz[1])\n",
        "for i in range(num_qubits):\n",
        "    fm.rz(-2 * feature_params[2 * i + 1], i)\n",
        "    fm.rx(-2 * feature_params[2 * i], i)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "02576ae9-8f24-4f47-8c37-ae0c3f710d4c",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "# Assign features of each data point to a unitary, an instance of the general feature map.\n",
        "\n",
        "unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])\n",
        "unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])\n",
        "\n",
        "# Create the overlap circuit\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fbe9ba90-6463-488d-b6f2-e9d4ee8f0871",
      "metadata": {},
      "source": [
        "Wir machen uns noch nicht die Mühe, die Tiefen zu überprüfen, denn was wirklich zählt, ist die transpilierte Zwei-Qubit-Tiefe.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9a5c14fc-aa58-461f-a1d5-fc644e042ecc",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Schritt 2: Optimierung des Problems für die Quantenausführung\n",
        "\n",
        "Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unsere Schaltung für die Ausführung auf diesem Backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "99e3a8c7-9aba-4890-a492-cb3779b5de03",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_brisbane')>\n"
          ]
        }
      ],
      "source": [
        "# Import needed packages\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "# Get the least busy backend\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        ")\n",
        "print(backend)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0b524533-7422-4d91-a3d1-82dfdeaf1588",
      "metadata": {},
      "source": [
        "Bei kleineren Aufträgen liefert ein voreingestellter Passmanager oft zuverlässig den gleichen Schaltkreis mit der gleichen Tiefe. Bei sehr großen, komplexen Schaltkreisen kann der Passmanager jedoch bei jedem Durchlauf andere transpilierte Schaltkreise zurückgeben. Dies liegt daran, dass Heuristiken verwendet werden und dass sehr große Schaltungen eine komplizierte Landschaft möglicher Optimierungen aufweisen. Oft ist es sinnvoll, ein paar Mal umzudrehen und die flachste Strecke zu nehmen. Dies führt nur zu einem klassischen Overhead und kann die Ergebnisse des Quantencomputers erheblich verbessern.\n",
        "\n",
        "Hier transpilieren wir die unitäre Überlappungsschaltung 20 Mal und betrachten die Tiefe der erhaltenen Schaltungen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 99,
      "id": "f5f8f833-8ac2-4df0-8268-4d83f841fd16",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  61\n"
          ]
        }
      ],
      "source": [
        "# Apply level 3 optimization to our overlap circuit\n",
        "transpiled_qcs = []\n",
        "transpiled_depths = []\n",
        "transpiled_twoqubit_depths = []\n",
        "for i in range(1, 20):\n",
        "    pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n",
        "    overlap_ibm = pm.run(overlap_circ)\n",
        "    transpiled_qcs.append(overlap_ibm)\n",
        "    transpiled_depths.append(overlap_ibm.decompose().depth())\n",
        "    transpiled_twoqubit_depths.append(\n",
        "        overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n",
        "    )\n",
        "\n",
        "print(\"circuit depth = \", overlap_ibm.decompose().depth())"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 100,
      "id": "fd604f87-32d1-42dd-a1fb-f2315cb4dfdc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]\n",
            "[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]\n"
          ]
        }
      ],
      "source": [
        "print(transpiled_depths)\n",
        "print(transpiled_twoqubit_depths)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "30fad0d8-f5e4-4dcf-946a-956f23334e9c",
      "metadata": {},
      "source": [
        "Hier können Sie sehen, dass die Gesamttiefe des Gates bei verschiedenen Transpilationsdurchläufen variiert. Unsere Schaltung ist noch nicht tief/weit genug, um Variationen in den transponierten Zwei-Qubit-Tiefen zu sehen. Wir werden die `transpiled_qcs[1]` verwenden, die eine Tiefe von 60 aufweist, die nur geringfügig niedriger ist als die Tiefe des tiefsten erhaltenen Schaltkreises, die 77 betrug.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 101,
      "id": "28f00388-0e6e-41db-a0b4-9200d58ba5cb",
      "metadata": {},
      "outputs": [],
      "source": [
        "overlap_ibm = transpiled_qcs[1]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e96cee51-e79f-4eab-829b-397645bc1080",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven\n",
        "\n",
        "Wenn wir uns dem Nutzen annähern, werden Simulatoren nicht mehr nützlich sein. Hier wird nur die Syntax für echte Quantencomputer gezeigt.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 102,
      "id": "7eedcf5f-5ead-4eb0-b423-669901f572ba",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Run on ibm_osaka, 7-12-24, required 22 sec.\n",
        "\n",
        "# Import our IBM Quantum primitive\n",
        "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
        "\n",
        "# Open a session:\n",
        "session = Session(backend=backend)\n",
        "num_shots = 10000\n",
        "# Use sampler and get the counts\n",
        "\n",
        "sampler = Sampler(mode=session)\n",
        "options = sampler.options\n",
        "options.dynamical_decoupling.enable = True\n",
        "options.twirling.enable_gates = True\n",
        "counts = (\n",
        "    sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()\n",
        ")\n",
        "\n",
        "# Close session after done\n",
        "session.close()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f40540b8-146d-4165-af12-db902232300d",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben\n",
        "\n",
        "Wie in der Einleitung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand zu messen $|00000\\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 103,
      "id": "ce9f4dc1-b562-437e-b9fe-5a1781b67bfe",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "0.0138"
            ]
          },
          "execution_count": 103,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "counts.get(0, 0.0) / num_shots"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9b6c78b4-6788-404f-bd90-d98d7b288d42",
      "metadata": {},
      "source": [
        "Dieses Verfahren für das einzelne Kernelmatrixelement kann für andere Datenpaare in Ihrem Satz wiederholt werden, um die vollständige Kernelmatrix zu erhalten. Die Dimension der Kernelmatrix wird durch die Anzahl der Punkte in Ihren Trainingsdaten bestimmt, nicht durch die Anzahl der Merkmale. Der Rechenaufwand für die Umwandlung der Kernelmatrix in ein Vorhersagemodell skaliert also nicht mit der Anzahl der Merkmale oder Qubits. Selbst bei relativ kleinen Datensätzen mit einer großen Anzahl von Merkmalen müssten die Daten noch mit einer Merkmalskarte abgeglichen werden, die eine effektive Klassifizierung ermöglicht.\n",
        "\n",
        "<span id=\"scaling-and-future-work\" />\n",
        "\n",
        "### Skalierung und zukünftige Arbeiten\n",
        "\n",
        "Die Kernel-Methode setzt voraus, dass wir die „ $|0\\rangle$ “ so genau wie möglich messen. Gate-Fehler und Auslesefehler bedeuten jedoch, dass eine von Null verschiedene Wahrscheinlichkeit $p$ besteht, dass ein beliebiges Qubit fälschlicherweise als im Zustand $|1\\rangle$ gemessen wird. Selbst unter der stark vereinfachten Annahme, dass die Wahrscheinlichkeit von „ $|0\\rangle$ “ gleich „ $100\\%$ “ sein sollte, reduziert sich bei vielen Merkmalen, die beispielsweise auf „ $N$ “ Bits kodiert sind, die Wahrscheinlichkeit, alle Bits korrekt als „ $|0\\rangle$ “ zu messen, auf „ $(1-p)^N$ “. Mit zunehmendem „ $N$ “ verliert diese Methode zunehmend an Zuverlässigkeit. Die Überwindung dieser Schwierigkeit und die Ausweitung der Kernel-Schätzung auf immer mehr Merkmale ist ein aktueller Forschungsschwerpunkt. Weitere Informationen zu diesem Thema finden Sie in dieser Arbeit von [Thanasilp, Wang, Cerezo und Holmes](https://www.nature.com/articles/s41467-024-49287-w). Wir empfehlen Ihnen, sich damit auseinanderzusetzen, was mit den heutigen Quantencomputern bereits möglich ist, und sich gleichzeitig darauf zu freuen, welche Möglichkeiten sich im Zeitalter der Fehlerkorrektur eröffnen werden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "556eb119-2b22-4bd9-b82e-c3a6ea31cfa1",
      "metadata": {},
      "source": [
        "<span id=\"review\" />\n",
        "\n",
        "### Prüfen\n",
        "\n",
        "Die Berechnung eines Quantenkerns umfasst\n",
        "\n",
        "* berechnung der Einträge der Kernelmatrix unter Verwendung von Paaren von Trainingsdatenpunkten\n",
        "* kodierung der Daten und deren Abbildung über ein Feature Mapping\n",
        "* optimierung Ihrer Schaltung für den Betrieb auf echten Quantencomputern / Backends\n",
        "\n",
        "Der Quantenkern kann dann, wie in dieser Lektion, in klassischen Algorithmen für maschinelles Lernen verwendet werden.\n",
        "\n",
        "Bei der Verwendung von Quantenkernen sind einige wichtige Dinge zu beachten:\n",
        "\n",
        "* Ist es wahrscheinlich, dass der Datensatz von Quantenkernel-Methoden profitieren wird?\n",
        "* Probieren Sie verschiedene Merkmalskarten und Verschränkungsschemata aus.\n",
        "* Ist die Schaltungstiefe akzeptabel?\n",
        "* Versuchen Sie, einen Passmanager mehrfach zu verwenden, und nutzen Sie die kleinstmögliche Schaltung.\n",
        "\n",
        "Quanten-Kernel-Methoden sind potenziell leistungsstarke Werkzeuge, sofern die Datensätze mit quantenbestimmbaren Merkmalen und einer geeigneten Quantenmerkmalskarte richtig aufeinander abgestimmt sind. Um besser zu verstehen, wo Quantenkerne wahrscheinlich nützlich sind, empfehlen wir die Lektüre von [Liu, Arunachalam & Temme (2021)](https://www.nature.com/articles/s41567-021-01287-z).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "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"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}