{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Wprowadzenie\n",
    "\n",
    "W ramach tego notatnika przyjrzymy się, w jaki sposób można wykorzystać warstwy splotu grafowego, jako część większej architektury. W szczególności sprawdzimy:\n",
    "1. Jak połączyć kilka warstw splotu jedną po drugiej.\n",
    "2. Jak podłączyć je klasycznej sieci neuronowej, w ramach klasyfikacji wierzchołka."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Import bibliotek"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import torch_geometric as pyg\n",
    "import torch_geometric.datasets as pyg_datasets\n",
    "import torch_geometric.nn as pyg_nn\n",
    "import torch_geometric.transforms as pyg_t\n",
    "import torch_geometric.loader as pyg_loader\n",
    "import torch as th\n",
    "import torch.nn as nn\n",
    "import matplotlib.pyplot as plt\n",
    "import networkx as nx\n",
    "from tqdm.auto import tqdm\n",
    "from sklearn.metrics import classification_report\n",
    "\n",
    "from typing import Tuple\n",
    "from pathlib import Path\n",
    "\n",
    "import sys\n",
    "sys.path.append('..')\n",
    "import utils.vis_utils as vis_utils"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Wczytanie danych\n",
    "\n",
    "W ramach tego notebooka wykorzystamy zbiór danych `Cora`, wbudowany w bibliotekę Pytorch Geometric. Zbiór ten zawiera informacje o artykułach naukowych, w których wierzchołkami są artykuły, a krawędziami są cytowania między nimi. Każdy artykuł jest przypisany do jednej z 7 kategorii (klas). Dodatkowo, wierzchołki posiadają atrybuty opisujące słowa kluczowe artykułów.\n",
    "\n",
    "Pytorch Geometric umożliwia wstępne przetworzenie danych wierzchołków. Dla ułatwienia sobie zadania wykorzystamy transformatę normalizującą cechy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.x\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.tx\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.allx\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.y\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.ty\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.ally\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.graph\n",
      "Downloading https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.test.index\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Zbiór danych: Cora():\n",
      "---\n",
      "Ilość cech wierzchołków: 1433\n",
      "Ilość klas: 7\n",
      "--- Analiza grafu ---\n",
      "Wierzchołki: 2708\n",
      "Krawędzie: 10556\n",
      "Średni stopień wierzchołka: 3.90\n",
      "Czy graf jest nieskierowany? True\n",
      "Czy graf ma niepołączone wierzchołki? False\n",
      "Czy wierzchołki mają krawędzie do samych siebie? False\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Processing...\n",
      "Done!\n"
     ]
    }
   ],
   "source": [
    "dataset = pyg_datasets.Planetoid(root=Path(os.pardir, \"data\", \"Planetoid\"), name='Cora', transform=pyg_t.NormalizeFeatures())\n",
    "cora = dataset[0]\n",
    "\n",
    "print()\n",
    "print(f'Zbiór danych: {dataset}:')\n",
    "print('---')\n",
    "print(f'Ilość cech wierzchołków: {dataset.num_features}')\n",
    "print(f'Ilość klas: {dataset.num_classes}')\n",
    "print(\"--- Analiza grafu ---\")\n",
    "\n",
    "print(f'Wierzchołki: {cora.num_nodes}')\n",
    "print(f'Krawędzie: {cora.num_edges}')\n",
    "print(f'Średni stopień wierzchołka: {cora.num_edges / cora.num_nodes:.2f}')\n",
    "\n",
    "print(f'Czy graf jest nieskierowany? {cora.is_undirected()}')\n",
    "print(f'Czy graf ma niepołączone wierzchołki? {cora.has_isolated_nodes()}')\n",
    "print(f'Czy wierzchołki mają krawędzie do samych siebie? {cora.has_self_loops()}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Data(x=[2708, 1433], edge_index=[2, 10556], y=[2708], train_mask=[2708], val_mask=[2708], test_mask=[2708])"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "cora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wizualizacja zbioru danych Cora"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "cora_nx = pyg.utils.to_networkx(cora, node_attrs='y')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 128340 characters]",
      "text/plain": [
       "<Figure size 472.441x393.701 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "pos = nx.spring_layout(cora_nx, seed=333)\n",
    "\n",
    "# Wielkość wierzchołków - stopień wierzchołka\n",
    "cent = nx.degree_centrality(cora_nx)\n",
    "node_size = list(map(lambda x: x * 200, cent.values()))\n",
    "\n",
    "# Zmniejszamy wizualnie wierzchołki słabo połączone, o małym stopniu.\n",
    "# Wizualnie największy rozmiar uzyskaja 20 wierzchołków o najwyższym stopniu\n",
    "cent_array = np.array(list(cent.values()))\n",
    "threshold = sorted(cent_array, reverse=True)[20]\n",
    "\n",
    "# zmniejszamy\n",
    "cent_bin = np.where(cent_array >= threshold, 1, 0.1)\n",
    "\n",
    "# Kolory wierzchołków - odpowiadające klasom\n",
    "node_colors = [cora_nx.nodes[node]['y'] for node in cora_nx.nodes]\n",
    "\n",
    "# Ustaw rozmiar wykresu na 12x12 cm za pomocą vis_utils\n",
    "fig, ax = plt.subplots(figsize=vis_utils.image_size_in_cm(12, 10))\n",
    "\n",
    "nodes = nx.draw_networkx_nodes(\n",
    "    cora_nx, pos, ax=ax,\n",
    "    node_size=node_size,\n",
    "    cmap=plt.cm.plasma,\n",
    "    node_color=node_colors,\n",
    "    nodelist=list(cent.keys()),\n",
    "    alpha=cent_bin\n",
    ")\n",
    "edges = nx.draw_networkx_edges(cora_nx, pos, ax=ax, width=0.25, alpha=0.3)\n",
    "ax.set_xlabel(\"Wymiar X\")\n",
    "ax.set_ylabel(\"Wymiar Y\")\n",
    "fig.savefig(\"rys_3.11.tiff\", pil_kwargs={\"compression\": \"tiff_lzw\"})\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pytorch Geometric, konstruując zbiory danych od razu przygotowuje maski treningowe/testowe i walidacyjne. Ich wykorzystanie jest takie samo, jak w przypadku klasycznych\n",
    "sieci neuronowych."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Prosta warstwa splotu\n",
    "\n",
    "Zbudujmy prostą warstwę splotu, która będzie posiadać następne konkretyzacje abstrakcyjnych funkcji `msg, agg, update`:\n",
    "1. `msg` - wiadomość od sąsiadów - bez przekształceń.\n",
    "2. `agg` - agregacja wiadomości - sumowanie.\n",
    "3. `update` - aktualizacja wierzchołka: suma dotychczasowego stanu i wiadomości.\n",
    "\n",
    "Nie będziemy dodawać żadnej nieliniowości na zakończenie przetwarzania."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SimpleGNN(pyg_nn.MessagePassing):\n",
    "\n",
    "    def __init__(self, dim_in: int, dim_out: int):\n",
    "        super(SimpleGNN, self).__init__(aggr='add')\n",
    "        self.linear = nn.Linear(dim_in, dim_out)\n",
    "\n",
    "    def forward(self, x: th.Tensor, edge_index: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Rozpoczęcie propagacji wiadomości\"\"\"\n",
    "        return self.propagate(edge_index, x=x)\n",
    "\n",
    "    def message(self, x_j: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Bardzo prosty sposób konstrukcji wiadomości - zwracamy po prostu x_j\"\"\"\n",
    "        return x_j\n",
    "\n",
    "    def update(self, aggr_out: th.Tensor, x: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Aktualizacja stanu wierzchołka: ReLu z sumy wiadomości sąsiadów i dotychczasowego stanu wierzchołka\"\"\"\n",
    "        return self.linear(aggr_out + x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sprawdźmy, czy implementacja się sprawdza."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([2708, 16])"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with th.no_grad():\n",
    "    simple_gnn = SimpleGNN(dataset.num_features, 16)\n",
    "    yhat = simple_gnn(cora.x, cora.edge_index)\n",
    "yhat.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Model klasyfikacji wierzchołków\n",
    "\n",
    "Teraz możemy stworzyć prosty model klasyfikacji wierzchołków. Jego ogólna architektura zawsze będzie z grubsza podobna:\n",
    "\n",
    "1. **Wejście:** macierz cech wierzchołków `X`, macierz listy sąsiedztwa: `edge_index`.\n",
    "2. **Warstwy splotu grafowego:** kilka warstw splotu, których wyjściem jest wektor osadzeń będący nową reprezentacją wierzchołków: `H`.\n",
    "3. **Klasyczna sieć neuronowa:** kilka warstw liniowych z aktywacjami ReLU, które przetworzą wektor osadzeń w wektor wynikowy do klasyfikacji: `yhat`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClassificationNN(nn.Module):\n",
    "\n",
    "    def __init__(self, gnn_sizes: Tuple[int, ...], mlp_sizes: Tuple[int, ...]):\n",
    "        super().__init__()\n",
    "        # Budowa warstw GNN\n",
    "        self._build_gnns(gnn_sizes)\n",
    "\n",
    "        # Budowa warstw MLP\n",
    "        self._build_lin_layers(mlp_sizes)\n",
    "\n",
    "    def _build_lin_layers(self, mlp_sizes: Tuple[int, ...]):\n",
    "        self.lin_layers = []\n",
    "        \"\"\"Budowanie kolejnych warstw liniowych - wejście do następnej jest\n",
    "        wyjściem z poprzedniej warstwy\"\"\"\n",
    "        for dim_in, dim_out in zip(mlp_sizes[:-1], mlp_sizes[1:]):\n",
    "            lin = nn.Linear(dim_in, dim_out)\n",
    "            self.lin_layers.append(lin)\n",
    "            self.lin_layers.append(nn.ReLU())\n",
    "        self.lin_layers = nn.ModuleList(self.lin_layers)\n",
    "\n",
    "    def _build_gnns(self, gnn_sizes: Tuple[int, ...]):\n",
    "        \"\"\"Budowanie warstw GNN. Rozmiary kolejnych warstw zależą od warstw poprzednich\"\"\"\n",
    "        self.gnns = []\n",
    "        for dim_in, dim_out in zip(gnn_sizes[:-1], gnn_sizes[1:]):\n",
    "            self.gnns.append(SimpleGNN(dim_in, dim_out))\n",
    "        self.gnns = nn.ModuleList(self.gnns)\n",
    "\n",
    "    def forward(self, x:th.Tensor, edge_index: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Klasyfikacja wierzchołków - najpierw przetworzenie grafu przez warstwy splotu grafowego,\n",
    "        potem - przekazanie wyniku ich działania do klasycznej sieci neuronowej.\"\"\"\n",
    "        last_embeddig = x\n",
    "\n",
    "        # Rozpoczęcie od warstw GNN - wejście: ostatnia reprezentacja wierzchołków oraz macierz sąsiedztwa.\n",
    "        # Wyjście: nowa representacja wierzchołków.\n",
    "        for gnn in self.gnns:\n",
    "            last_embeddig = gnn(last_embeddig, edge_index)\n",
    "\n",
    "        # Klasyczna sieć MLP - wejście: ostatnia reprezentacja wierzchołków.\n",
    "        # Wyjście: klasyfikacja wierzchołków.\n",
    "        lin_input = last_embeddig\n",
    "        for lin in self.lin_layers:\n",
    "            lin_input = lin(lin_input)\n",
    "        return lin_input"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Graf w przedstawionym przypadku jest relatywnie mały, został też podzielony wstępnie na maski treningowe, walidacyjne i testowe. Zastosowanie podzbiorów treningowych (ang. *mini-batch*) dla grafów omówimy w kolejnych rozdziałach, dlatego teraz, dla uproszczenia, przeprowadzimy uczenie na całym zbiorze treningowym, sprawdzając jego skuteczność na zbiorze walidacyjnym."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funkcje pomocnicze"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_losses_plot(train_loss: list, val_loss: list, plot_name: str):\n",
    "    fig, ax = plt.subplots(figsize=vis_utils.image_size_in_cm(12, 11))\n",
    "    # Użycie palety bardziej czytelnej w odcieniach szarości:\n",
    "    ax.plot(train_loss, label='Zb. treningowy', linestyle='-', color='black', linewidth=2)\n",
    "    ax.plot(val_loss, label='Zb. walidacyjny', linestyle='--', color='grey', linewidth=2)\n",
    "    ax.legend(fontsize=12)\n",
    "    vis_utils.change_plot_comma_separator(ax)\n",
    "    ax.set_xlabel('Epoka', fontsize=12)\n",
    "    ax.set_ylabel('Funkcja kosztu', fontsize=12)\n",
    "    ax.tick_params(axis='both', which='major', labelsize=12)\n",
    "    plt.tight_layout()\n",
    "    plt.savefig(plot_name, pil_kwargs={\"compression\": \"tiff_lzw\"})\n",
    "    plt.show()   "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model z jedną warstwą splotu\n",
    "\n",
    "Zaczniemy, ponownie, od bardzo prostego modelu z jedną warstwą splotu.\n",
    "\n",
    "\n",
    "1. Wejście: oryginalna macierz wierzchołków X: `N x 1433`\n",
    "2. Pierwsza warstwa splotu, wyjście: `N x 64`\n",
    "3. Sieć MLP, warstwy:\n",
    "   1. Pierwsza: `N x 128 -> N x 64`\n",
    "   2. Druga: `N x 64 -> N x 32`\n",
    "   3. Wyjście: `N x 32 -> N x 7`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "daba7a902a584c11ad616a4f4437731f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/20 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 41196 characters]",
      "text/plain": [
       "<Figure size 472.441x433.071 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "pyg.seed_everything(123)\n",
    "single_gnn_classifier = ClassificationNN(gnn_sizes=(dataset.num_features, 64), mlp_sizes=(64, 32, dataset.num_classes))\n",
    "optimizer = th.optim.Adam(single_gnn_classifier.parameters(), lr=0.005)\n",
    "\n",
    "yhat_single_before_train = single_gnn_classifier(cora.x, cora.edge_index)[cora.val_mask]\n",
    "\n",
    "\n",
    "# Trening\n",
    "epochs = 20\n",
    "epoch_losses= []\n",
    "val_losses = []\n",
    "\n",
    "for epoch in tqdm(range(epochs)):\n",
    "    single_gnn_classifier.train()\n",
    "    optimizer.zero_grad()\n",
    "    out = single_gnn_classifier(cora.x, cora.edge_index)\n",
    "    loss = nn.CrossEntropyLoss()(out[cora.train_mask], cora.y[cora.train_mask])\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "    epoch_losses.append(loss.item())\n",
    "    with th.no_grad():\n",
    "        single_gnn_classifier.eval()\n",
    "        yhat = single_gnn_classifier(cora.x, cora.edge_index)\n",
    "        val_loss = nn.CrossEntropyLoss()(yhat[cora.val_mask], cora.y[cora.val_mask])\n",
    "        val_losses.append(val_loss.item())\n",
    "\n",
    "yhat_single_after_train = single_gnn_classifier(cora.x, cora.edge_index)[cora.val_mask]\n",
    "\n",
    "make_losses_plot(epoch_losses, val_losses, plot_name=\"rys_3.12.tiff\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.00      0.00      0.00        61\n",
      "           1       0.00      0.00      0.00        36\n",
      "           2       0.00      0.00      0.00        78\n",
      "           3       0.00      0.00      0.00       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.06      1.00      0.11        29\n",
      "\n",
      "    accuracy                           0.06       500\n",
      "   macro avg       0.01      0.14      0.02       500\n",
      "weighted avg       0.00      0.06      0.01       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Raport klasyfikacji przed treningiem\n",
    "print(classification_report(cora.y[cora.val_mask].numpy(), th.argmax(yhat_single_before_train, dim=1).numpy()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.36      0.89      0.51        61\n",
      "           1       0.15      0.89      0.25        36\n",
      "           2       0.00      0.00      0.00        78\n",
      "           3       0.88      0.18      0.29       158\n",
      "           4       0.75      0.53      0.62        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.50      0.76      0.60        29\n",
      "\n",
      "    accuracy                           0.36       500\n",
      "   macro avg       0.38      0.46      0.33       500\n",
      "weighted avg       0.48      0.36      0.31       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Raport klasyfikacji po treningu\n",
    "print(classification_report(cora.y[cora.val_mask].numpy(), th.argmax(yhat_single_after_train, dim=1).numpy()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Struktura pierwszej sieci:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------------------+--------------------------+----------------+----------+\n",
      "| Layer                    | Input Shape              | Output Shape   | #Param   |\n",
      "|--------------------------+--------------------------+----------------+----------|\n",
      "| ClassificationNN         | [2708, 1433], [2, 10556] | [2708, 7]      | 94,087   |\n",
      "| ├─(gnns)ModuleList       | --                       | --             | 91,776   |\n",
      "| │    └─(0)SimpleGNN      | [2708, 1433], [2, 10556] | [2708, 64]     | 91,776   |\n",
      "| ├─(lin_layers)ModuleList | --                       | --             | 2,311    |\n",
      "| │    └─(0)Linear         | [2708, 64]               | [2708, 32]     | 2,080    |\n",
      "| │    └─(1)ReLU           | [2708, 32]               | [2708, 32]     | --       |\n",
      "| │    └─(2)Linear         | [2708, 32]               | [2708, 7]      | 231      |\n",
      "| │    └─(3)ReLU           | [2708, 7]                | [2708, 7]      | --       |\n",
      "+--------------------------+--------------------------+----------------+----------+\n"
     ]
    }
   ],
   "source": [
    "print(pyg_nn.summary(single_gnn_classifier, cora.x, cora.edge_index))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model z kilkoma warstwami splotu\n",
    "\n",
    "Następnie zbudujemy model z kilkoma warstwami splotu. W tym przypadku, zastosujemy dwie warstwy splotu.\n",
    "\n",
    "1. Wejście: oryginalna macierz wierzchołków X: `N x 1433`\n",
    "2. Pierwsza warstwa splotu, wyjście: `N x 64`\n",
    "3. Druga warstwa splotu, wyjście: `N x 32`\n",
    "4. Sieć MLP, warstwy:\n",
    "   1. Pierwsza: `N x 32 -> N x 16`\n",
    "   2. Wyjście: `N x 16 -> N x 7`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a033eea644c2461ebd68b5c6ad518c0d",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/20 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 39708 characters]",
      "text/plain": [
       "<Figure size 472.441x433.071 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "pyg.seed_everything(123)\n",
    "multi_gnn_classifier = ClassificationNN(gnn_sizes=(dataset.num_features, 64, 32), mlp_sizes=(32, 16, dataset.num_classes))\n",
    "optimizer = th.optim.Adam(multi_gnn_classifier.parameters(), lr=0.005)\n",
    "\n",
    "yhat_multi_before_train = multi_gnn_classifier(cora.x, cora.edge_index)[cora.val_mask]\n",
    "\n",
    "# Trening\n",
    "epochs = 20\n",
    "gnn2_epoch_losses= []\n",
    "gnn2_val_losses = []\n",
    "\n",
    "for epoch in tqdm(range(epochs)):\n",
    "    multi_gnn_classifier.train()\n",
    "    optimizer.zero_grad()\n",
    "    out = multi_gnn_classifier(cora.x, cora.edge_index)\n",
    "    loss = nn.CrossEntropyLoss()(out[cora.train_mask], cora.y[cora.train_mask])\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "    gnn2_epoch_losses.append(loss.item())\n",
    "    with th.no_grad():\n",
    "        multi_gnn_classifier.eval()\n",
    "        yhat = multi_gnn_classifier(cora.x, cora.edge_index)\n",
    "        val_loss = nn.CrossEntropyLoss()(yhat[cora.val_mask], cora.y[cora.val_mask])\n",
    "        gnn2_val_losses.append(val_loss.item())\n",
    "\n",
    "yhat_multi_after_train = multi_gnn_classifier(cora.x, cora.edge_index)[cora.val_mask]\n",
    "\n",
    "make_losses_plot(gnn2_epoch_losses, gnn2_val_losses, plot_name=\"rys_3.13.tiff\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Struktura tej sieci"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------------------+--------------------------+----------------+----------+\n",
      "| Layer                    | Input Shape              | Output Shape   | #Param   |\n",
      "|--------------------------+--------------------------+----------------+----------|\n",
      "| ClassificationNN         | [2708, 1433], [2, 10556] | [2708, 7]      | 94,503   |\n",
      "| ├─(gnns)ModuleList       | --                       | --             | 93,856   |\n",
      "| │    └─(0)SimpleGNN      | [2708, 1433], [2, 10556] | [2708, 64]     | 91,776   |\n",
      "| │    └─(1)SimpleGNN      | [2708, 64], [2, 10556]   | [2708, 32]     | 2,080    |\n",
      "| ├─(lin_layers)ModuleList | --                       | --             | 647      |\n",
      "| │    └─(0)Linear         | [2708, 32]               | [2708, 16]     | 528      |\n",
      "| │    └─(1)ReLU           | [2708, 16]               | [2708, 16]     | --       |\n",
      "| │    └─(2)Linear         | [2708, 16]               | [2708, 7]      | 119      |\n",
      "| │    └─(3)ReLU           | [2708, 7]                | [2708, 7]      | --       |\n",
      "+--------------------------+--------------------------+----------------+----------+\n"
     ]
    }
   ],
   "source": [
    "print(pyg_nn.summary(multi_gnn_classifier, cora.x, cora.edge_index))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.00      0.00      0.00        61\n",
      "           1       0.00      0.00      0.00        36\n",
      "           2       0.00      0.00      0.00        78\n",
      "           3       0.00      0.00      0.00       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.06      1.00      0.11        29\n",
      "\n",
      "    accuracy                           0.06       500\n",
      "   macro avg       0.01      0.14      0.02       500\n",
      "weighted avg       0.00      0.06      0.01       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Raport klasyfikacji przed treningiem\n",
    "print(classification_report(cora.y[cora.val_mask].numpy(), th.argmax(yhat_multi_before_train, dim=1).numpy()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.35      0.72      0.47        61\n",
      "           1       0.52      0.89      0.65        36\n",
      "           2       0.73      0.88      0.80        78\n",
      "           3       0.77      0.71      0.74       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.37      0.90      0.53        29\n",
      "\n",
      "    accuracy                           0.57       500\n",
      "   macro avg       0.39      0.59      0.45       500\n",
      "weighted avg       0.46      0.57      0.49       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Raport klasyfikacji po treningu\n",
    "print(classification_report(cora.y[cora.val_mask].numpy(), th.argmax(yhat_multi_after_train, dim=1).numpy()))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
