{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"%pip install -q \"qiskit>=1.2\" \"qiskit-machine-learning>=0.8\" seaborn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:33:05.323804Z","iopub.execute_input":"2026-10-15T08:33:05.324296Z","iopub.status.idle":"2026-10-15T08:33:12.777481Z","shell.execute_reply.started":"2026-10-15T08:33:05.324265Z","shell.execute_reply":"2026-10-15T08:33:12.776665Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%pip install -q pennylane","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:36:33.903285Z","iopub.execute_input":"2026-10-15T08:36:33.903785Z","iopub.status.idle":"2026-10-15T08:36:46.197051Z","shell.execute_reply.started":"2026-10-15T08:36:33.903756Z","shell.execute_reply":"2026-10-15T08:36:46.19607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nDATA_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nIMAGE_DIR = os.path.join(DATA_DIR, \"train_images\")\nCSV_PATH = os.path.join(DATA_DIR, \"train.csv\")\n\ndf = pd.read_csv(CSV_PATH)\ndf[\"image_path\"] = df[\"id_code\"].apply(\n    lambda x: os.path.join(IMAGE_DIR, str(x) + \".png\")\n)\ndf = df[df[\"image_path\"].map(os.path.isfile)].reset_index(drop=True)\n\nprint(\"Images found:\", len(df))\nprint(\"Dataset shape:\", df.shape)\nprint(\"\\nDiagnosis distribution:\")\nprint(df[\"diagnosis\"].value_counts().sort_index())\n\nrow = df.iloc[0]\nplt.figure(figsize=(5, 5))\nplt.imshow(Image.open(row[\"image_path\"]))\nplt.title(f\"Diagnosis: {row['diagnosis']}\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:36:46.458769Z","iopub.execute_input":"2026-10-15T08:36:46.459434Z","iopub.status.idle":"2026-10-15T08:37:10.487586Z","shell.execute_reply.started":"2026-10-15T08:36:46.459403Z","shell.execute_reply":"2026-10-15T08:37:10.48675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score,\n    classification_report,\n    confusion_matrix,\n    cohen_kappa_score\n)\n\nSEED = 42\nSAMPLES_PER_CLASS = 100\nIMG_SIZE = 32\nNUM_QUBITS = 8\n\ntf.keras.utils.set_random_seed(SEED)\nnp.random.seed(SEED)\n\nsample_df = (\n    df.groupby(\"diagnosis\", group_keys=False)\n      .apply(\n          lambda g: g.sample(\n              n=min(len(g), SAMPLES_PER_CLASS),\n              random_state=SEED\n          )\n      )\n      .reset_index(drop=True)\n)\n\ndef extract_patch_features(image_path):\n    image = tf.io.read_file(image_path)\n    image = tf.io.decode_png(image, channels=3)\n    image = tf.image.rgb_to_grayscale(image)\n    image = tf.image.resize(image, [IMG_SIZE, IMG_SIZE])\n    image = tf.cast(image, tf.float32) / 255.0\n\n    patches = []\n    for r in range(2):\n        for c in range(4):\n            patch = image[\n                r * 16:(r + 1) * 16,\n                c * 8:(c + 1) * 8,\n                :\n            ]\n            patches.append(float(tf.reduce_mean(patch)))\n\n    return np.array(patches, dtype=np.float32)\n\nX = np.stack([\n    extract_patch_features(p)\n    for p in sample_df[\"image_path\"]\n])\n\ny = sample_df[\"diagnosis\"].to_numpy(dtype=np.int64)\n\n# Map image intensities to quantum rotation angles.\nX = X * np.pi\n\nX_train, X_test, y_train, y_test = train_test_split(\n    X,\n    y,\n    test_size=0.25,\n    random_state=SEED,\n    stratify=y\n)\n\nprint(\"Input features:\", X.shape)\nprint(\"Training set:\", X_train.shape)\nprint(\"Testing set:\", X_test.shape)\nprint(\"Training class counts:\", np.bincount(y_train, minlength=5))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:38:15.856406Z","iopub.execute_input":"2026-10-15T08:38:15.85726Z","iopub.status.idle":"2026-10-15T08:38:40.083905Z","shell.execute_reply.started":"2026-10-15T08:38:15.857229Z","shell.execute_reply":"2026-10-15T08:38:40.083147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pennylane as qml\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader, TensorDataset\n\ntorch.manual_seed(SEED)\n\ndev = qml.device(\"default.qubit\", wires=NUM_QUBITS)\n\ndef quantum_convolution(weights, wires):\n    for i, wire in enumerate(wires):\n        qml.Rot(\n            weights[i, 0],\n            weights[i, 1],\n            weights[i, 2],\n            wires=wire\n        )\n\n    for i in range(len(wires) - 1):\n        qml.CNOT(wires=[wires[i], wires[i + 1]])\n\ndef quantum_pooling(weights, pairs):\n    for i, (control, target) in enumerate(pairs):\n        qml.CNOT(wires=[control, target])\n        qml.CRY(weights[i], wires=[target, control])\n\n@qml.qnode(dev, interface=\"torch\", diff_method=\"backprop\")\ndef qcnn_circuit(inputs, conv1, pool1, conv2, pool2):\n    for i in range(NUM_QUBITS):\n        qml.RY(inputs[i], wires=i)\n\n    # Quantum convolution layer 1\n    quantum_convolution(conv1, list(range(8)))\n\n    # Quantum pooling layer 1\n    quantum_pooling(\n        pool1,\n        [(0, 1), (2, 3), (4, 5), (6, 7)]\n    )\n\n    # Convolution on the retained qubits\n    quantum_convolution(conv2, [1, 3, 5, 7])\n\n    # Final pooling stage\n    quantum_pooling(\n        pool2,\n        [(1, 3), (5, 7)]\n    )\n\n    return [\n        qml.expval(qml.PauliZ(3)),\n        qml.expval(qml.PauliZ(7))\n    ]\n\nclass QCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.conv1 = nn.Parameter(0.1 * torch.randn(8, 3))\n        self.pool1 = nn.Parameter(0.1 * torch.randn(4))\n        self.conv2 = nn.Parameter(0.1 * torch.randn(4, 3))\n        self.pool2 = nn.Parameter(0.1 * torch.randn(2))\n\n        self.classifier = nn.Linear(2, 5)\n\n    def forward(self, x):\n        quantum_outputs = []\n\n        for sample in x:\n            qout = qcnn_circuit(\n                sample,\n                self.conv1,\n                self.pool1,\n                self.conv2,\n                self.pool2\n            )\n            quantum_outputs.append(torch.stack(qout))\n\n        quantum_outputs = torch.stack(quantum_outputs).float()\n        return self.classifier(quantum_outputs)\n\nmodel = QCNN()\nprint(model)\nprint(\"Trainable parameters:\", sum(p.numel() for p in model.parameters()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:38:51.044555Z","iopub.execute_input":"2026-10-15T08:38:51.044984Z","iopub.status.idle":"2026-10-15T08:38:55.289028Z","shell.execute_reply.started":"2026-10-15T08:38:51.044954Z","shell.execute_reply":"2026-10-15T08:38:55.288312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cpu\")\n\nmodel = model.to(device)\n\nX_train_t = torch.tensor(X_train, dtype=torch.float32)\ny_train_t = torch.tensor(y_train, dtype=torch.long)\nX_test_t = torch.tensor(X_test, dtype=torch.float32)\ny_test_t = torch.tensor(y_test, dtype=torch.long)\n\ntrain_loader = DataLoader(\n    TensorDataset(X_train_t, y_train_t),\n    batch_size=4,\n    shuffle=True\n)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=0.01)\n\nEPOCHS = 5\nhistory = {\"loss\": [], \"accuracy\": []}\n\nfor epoch in range(EPOCHS):\n    model.train()\n    total_loss = 0.0\n    correct = 0\n    total = 0\n\n    for xb, yb in train_loader:\n        optimizer.zero_grad()\n\n        logits = model(xb)\n        loss = criterion(logits, yb)\n\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item() * len(yb)\n        correct += (logits.argmax(dim=1) == yb).sum().item()\n        total += len(yb)\n\n    epoch_loss = total_loss / total\n    epoch_accuracy = correct / total\n\n    history[\"loss\"].append(epoch_loss)\n    history[\"accuracy\"].append(epoch_accuracy)\n\n    print(\n        f\"Epoch {epoch + 1}/{EPOCHS} | \"\n        f\"Loss: {epoch_loss:.4f} | \"\n        f\"Training accuracy: {epoch_accuracy:.4f}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:39:16.974554Z","iopub.execute_input":"2026-10-15T08:39:16.975037Z","iopub.status.idle":"2026-10-15T08:39:47.953438Z","shell.execute_reply.started":"2026-10-15T08:39:16.975006Z","shell.execute_reply":"2026-10-15T08:39:47.952644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\nwith torch.no_grad():\n    logits = model(X_test_t)\n    y_pred = logits.argmax(dim=1).cpu().numpy()\n\naccuracy = accuracy_score(y_test, y_pred)\nkappa = cohen_kappa_score(y_test, y_pred, weights=\"quadratic\")\n\nprint(\"QCNN test accuracy:\", round(accuracy, 4))\nprint(\"Quadratic weighted kappa:\", round(kappa, 4))\n\nprint(\"\\nClassification report:\")\nprint(classification_report(\n    y_test,\n    y_pred,\n    labels=[0, 1, 2, 3, 4],\n    target_names=[\n        \"No DR\",\n        \"Mild\",\n        \"Moderate\",\n        \"Severe\",\n        \"Proliferative DR\"\n    ],\n    zero_division=0\n))\n\ncm = confusion_matrix(y_test, y_pred, labels=[0, 1, 2, 3, 4])\n\nplt.figure(figsize=(7, 6))\nplt.imshow(cm, interpolation=\"nearest\", cmap=\"Blues\")\nplt.title(\"Quantum CNN Confusion Matrix\")\nplt.colorbar()\nplt.xticks(range(5), range(5))\nplt.yticks(range(5), range(5))\nplt.xlabel(\"Predicted grade\")\nplt.ylabel(\"Actual grade\")\n\nfor i in range(5):\n    for j in range(5):\n        plt.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\")\n\nplt.tight_layout()\nplt.show()\n\nplt.figure(figsize=(8, 4))\nplt.plot(history[\"loss\"], marker=\"o\", label=\"Training loss\")\nplt.plot(history[\"accuracy\"], marker=\"s\", label=\"Training accuracy\")\nplt.xlabel(\"Epoch\")\nplt.title(\"QCNN Training History\")\nplt.legend()\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:39:55.535022Z","iopub.execute_input":"2026-10-15T08:39:55.535687Z","iopub.status.idle":"2026-10-15T08:39:56.886554Z","shell.execute_reply.started":"2026-10-15T08:39:55.53566Z","shell.execute_reply":"2026-10-15T08:39:56.885628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"os.makedirs(\"/kaggle/working\", exist_ok=True)\n\npd.DataFrame({\n    \"actual_diagnosis\": y_test,\n    \"predicted_diagnosis\": y_pred\n}).to_csv(\"/kaggle/working/qcnn_predictions.csv\", index=False)\n\nresults = pd.DataFrame([{\n    \"model\": \"8-qubit QCNN-inspired hybrid classifier\",\n    \"dataset\": \"APTOS 2019\",\n    \"sample_count\": len(sample_df),\n    \"qubits\": NUM_QUBITS,\n    \"epochs\": EPOCHS,\n    \"test_accuracy\": accuracy,\n    \"quadratic_weighted_kappa\": kappa\n}])\n\nresults.to_csv(\"/kaggle/working/qcnn_results.csv\", index=False)\ntorch.save(model.state_dict(), \"/kaggle/working/qcnn_model.pth\")\n\nprint(results.to_string(index=False))\nprint(\"\\nFiles saved in /kaggle/working/\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-15T08:40:31.906019Z","iopub.execute_input":"2026-10-15T08:40:31.906535Z","iopub.status.idle":"2026-10-15T08:40:31.936216Z","shell.execute_reply.started":"2026-10-15T08:40:31.906507Z","shell.execute_reply":"2026-10-15T08:40:31.935457Z"}},"outputs":[],"execution_count":null}]}