{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Wprowadzenie\n",
    "\n",
    "Czas przeanalizować kolejną warstwę splotu grafowego - GCN (ang. *Graph Convolutional Network*). Przez długi czas była to jedna z najbardziej popularnych warstw splotu w grafowych sieciach neuronowych.\n",
    "\n",
    "Oryginalne publikacje:\n",
    "\n",
    "> Kipf, T. N., & Welling, M. (2016). Semi-supervised classification with graph convolutional networks. arXiv preprint arXiv:1609.02907."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Import bibliotek"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import torch as th\n",
    "import torch.nn as nn\n",
    "import torch_geometric as pyg\n",
    "import torch_geometric.nn as pyg_nn\n",
    "import torch_geometric.utils as pyg_utils\n",
    "import torch_geometric.transforms as pyg_t\n",
    "import torch_geometric.datasets as pyg_datasets\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from pathlib import Path\n",
    "from typing import Tuple, Union\n",
    "from tqdm.auto import tqdm\n",
    "from sklearn.metrics import classification_report"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import utilities as utils\n",
    "from base_models import NodesClassifierModel, NaiveGraphConv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Przygotowanie danych"
   ]
  },
  {
   "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": [
    "dataset = pyg_datasets.Planetoid(root=Path(os.pardir, \"../data\", \"Planetoid\"), name='Cora', transform=pyg_t.NormalizeFeatures())\n",
    "cora = dataset[0]\n",
    "cora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Splot GCN\n",
    "\n",
    "Oryginalna formalizacja tej warstwy splotu z publikacji (Kipf & Welling, 2016) wykorzystywała notację macierzową, zaczerpniętą z analizy spektralnej grafu. Zarówno w tym notatniku, jak i w części książkowej, będziemy korzystać z notacji uproszczonej, opisujacej działania na poziomie pojedynczych wierzchołków, tak jak to miało miejsce w przypadku warstw, omawianych w poprzednich rozdziałach.\n",
    "\n",
    "Formalizację zbliżoną do przedstawionej znajdziemy m.in. w:\n",
    "\n",
    "> Xu, K., Hu, W., Leskovec, J., & Jegelka, S. (2018). How powerful are graph neural networks?. arXiv preprint arXiv:1810.00826\n",
    "\n",
    "> Hamilton, W. L. (2020). Graph representation learning. Morgan & Claypool Publishers."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$$\n",
    "\\mathbf{h}_v^{(k)} = \\sigma \\left(\\mathbf{W}^{(k)} \\sum_{u \\in \\mathcal{N}(v) \\cup \\left\\{v\\right\\}} \\frac{\\mathbf{h}_u^{(k-1)}}{\\sqrt{|N(u)||N(v)|}}\\right)\n",
    "$$\n",
    "\n",
    "\n",
    "gdzie:\n",
    "\n",
    "- $\\mathbf{h}_v^{(k)}$ to reprezentacja wierzchołka $v$ w warstwie $k$;\n",
    "- $\\mathbf{W}^{(k)}$ to macierz wag warstwy;\n",
    "- $\\mathcal{N}(v)$ - zbiór sąsiadów wierzchołka $v$;\n",
    "- $\\mathcal{N}(v) \\cup \\left\\{v\\right\\}$ - zbiór sąsiadów wierzchołka $v$ wraz z wierzchołkiem $v$ (pętla do samego siebie);\n",
    "- $\\sigma$ funkcja aktywacji - zwykle nieliniowa (jak np. ReLU). Można ją opuścić, jeśli sieć GNN jest częścią większej architektury.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naiwna implementacja"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NaiveGCN(NaiveGraphConv):\n",
    "\n",
    "    def __init__(self, dim_in: int, dim_out: int, agg_fn=th.sum, sigma=nn.Identity()):\n",
    "        super().__init__(dim_in, dim_out, agg_fn, sigma)\n",
    "        self.W = nn.Linear(dim_in, dim_out)\n",
    "        pyg_nn.inits.glorot(self.W.weight)\n",
    "\n",
    "    def reset_parameters(self):\n",
    "        super().reset_parameters()\n",
    "        pyg_nn.inits.glorot(self.W.weight)\n",
    "\n",
    "    def message(self, v_neighbors: th.Tensor, normalization_coefs: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Funkcja budująca wiadomości od sąsiadów, implementacja abstrakcyjnej\n",
    "        funkcji `message` z architektury MPNN. Przyjmuje jako wejście\n",
    "        macierz cech sąsiadów wierzchołka v:  `u in N(v)` i buduje z niej wiadomości.\n",
    "        W tym przypadku - wymnaża wiadomości przez współczynnik normalizacji.\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        v_neighbors : th.Tensor\n",
    "            Macierz cech sąsiadów wierzchołka v: `N(v)`.\n",
    "            Wymiar: `|N(v)| x dim_in`.\n",
    "\n",
    "        normalization_coefs : th.Tensor\n",
    "            Macierz ze współczynnikami normalizacji - zgodnie z równaniem: `sqrt(deg(u) * deg(v))`.\n",
    "            Wymiar: `|V| x 1`.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        th.Tensor\n",
    "            Macierz wiadomości od sąsiadów.\n",
    "            Wymiar: `|N(v)| x dim_out`.\n",
    "        \"\"\"\n",
    "        return v_neighbors * normalization_coefs\n",
    "\n",
    "    def aggregate(self, m_nv: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Funkcja agregująca wiadomości od sąsiadów, implementacja abstrakcyjnej\n",
    "        funkcji `aggregate` z architektury MPNN. Przyjmuje jako wejście\n",
    "        macierz wiadomości od sąsiadów wierzchołka v i agreguje je.\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        m_nv : th.Tensor\n",
    "            Macierz wiadomości od sąsiadów.\n",
    "            Wymiar: `|N(v)| x dim_out`.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        th.Tensor\n",
    "            Zagregowane wiadomości. Wymiar: `1 x dim_out`.\n",
    "        \"\"\"\n",
    "        # Po agregacji upewniamy się, że wynikowy wektor zachowa wymiar (1 x dim out).\n",
    "        return self.aggr(m_nv, dim=0, keepdim=True)\n",
    "\n",
    "    def update(self, v: th.Tensor, agg: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Implementacja funkcji `update` z architektury MPNN. Aktualizuje\n",
    "        reprezentacje wierzchołka v na podstawie zagregowanych wiadomości\n",
    "        od sąsiadów. W przypadku splotu GCN - wykorzystuje wyłącznie zagregowane\n",
    "        wartości sąsiadów (zawierające pętlę wierzchołka v do samego siebie).\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        v : th.Tensor\n",
    "            Aktualna reprezentacja wierzchołka v. Wymiar: `1 x dim_in`.\n",
    "        agg : th.Tensor\n",
    "            Zagregowane wiadomości sąsiadów. Wymiar: `1 x dim_out`.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        th.Tensor\n",
    "            Zaktualizowana reprezentacja wierzchołka v. Wymiar: `1 x dim_out`.\n",
    "        \"\"\"\n",
    "        v_new = self.W(agg)\n",
    "        v_new_nonlin = self.sigma(v_new)\n",
    "        return v_new_nonlin\n",
    "\n",
    "    def forward(self, x: th.Tensor, edge_index: th.Tensor) -> th.Tensor:\n",
    "        \"\"\"Funkcja przepływu danych w warstwie splotu grafowego. Rozpoczyna cykl\n",
    "        przekazywania wiadomości, dla każdego wierzchołka w grafie: v po kolei:\n",
    "        1. Znajdując sąsiadów;\n",
    "        2. Budując wiadomości od sąsiadów;\n",
    "        3. Agregując wiadomości od sąsiadów;\n",
    "        4. Aktualizując reprezentację wierzchołka.\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        x : th.Tensor\n",
    "            Macierz cech wierzchołków.\n",
    "\n",
    "        edge_index : th.Tensor\n",
    "            Macierz sąsiedztwa grafu.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        th.Tensor\n",
    "            Zaktualizowane cechy wierzchołków.\n",
    "        \"\"\"\n",
    "        # Alokacja nowej macierzy reprezentacji wierzchołków\n",
    "        N = x.shape[0]\n",
    "        h_new = th.zeros(N, self.dim_out).to(th.float32)\n",
    "\n",
    "        # Przygotowanie macierzy stopni wierzchołka\n",
    "        node_deg_matrix = th.zeros(N, 1).to(th.float32)\n",
    "        for v in range(N):\n",
    "            # Dla każdego wierzchołka v w grafie - zsumuj krawędzie wchodzące do- i wychodzące z- niego\n",
    "            # Równie dobrze można wykorzystać do tego funkcję z modułu pyg_utils.degree\n",
    "            node_deg_matrix[v] = (edge_index[0] == v).sum() + (edge_index[1] == v).sum()\n",
    "\n",
    "        # Dla każdego wierzchołka v w grafie\n",
    "        for v in range(x.shape[0]):\n",
    "            # Znajdź indeksy w macierzy sąsiedztwa, gdzie odbiera on wiadomość\n",
    "            v_index_as_receiver = th.where(edge_index[1] == v)[0]\n",
    "\n",
    "            # Znajdź sąsiadów, którzy wysyłają tę wiadomość\n",
    "            # |N(v)| x dim_in\n",
    "            neighbors = edge_index[0, v_index_as_receiver]\n",
    "            # Dodanie wierzchołka v jako własnego sąsiada - pętla do samego siebie\n",
    "            neighbors = th.cat((neighbors, th.tensor([v])))\n",
    "\n",
    "            # Zbuduj wiadomości od sąsiadów\n",
    "            # |N(v)| x dim_out\n",
    "            neighbors_x = x[neighbors]\n",
    "\n",
    "            # Wyliczenie wskaźnika normalizacji zgodnie z równaniem: sqrt(deg(u) * deg(v))\n",
    "            deg_v = node_deg_matrix[v]\n",
    "            deg_u = node_deg_matrix[neighbors]\n",
    "            norm_coef = 1/th.sqrt(deg_v * deg_u)\n",
    "\n",
    "            m_nv = self.message(neighbors_x, normalization_coefs=norm_coef)\n",
    "\n",
    "            # Agreguj wiadomości sąsiadów\n",
    "            # 1 x dim_out\n",
    "            agg = self.aggregate(m_nv)\n",
    "\n",
    "            # Zaktualizuj wierzchołek v\n",
    "            # 1 x dim_out\n",
    "            new_v = self.update(x[v], agg)\n",
    "            h_new[v] = new_v\n",
    "        return h_new\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sprawdzenie, czy implementacja działa:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([2708, 16])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with th.no_grad():\n",
    "    naive_gcn = NaiveGCN(dim_in=cora.num_features, dim_out=16)\n",
    "    naive_gcn.eval()\n",
    "    h = naive_gcn(cora.x, cora.edge_index)\n",
    "h.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model klasyfikacji z naiwną implementacją GCN"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-warning\">\n",
    "⚠️ Uwaga, ze względu na fakt, iż implementacja naiwna nie jest zoptymalizowana, może być konieczne dłuższe oczekiwanie na wyniki. W zależności od posiadanego sprzętu i procesora, całość może zająć chwilę. Jeśli czas oczekiwani jest za długi - warto zmniejszyć ilość epok.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyg.seed_everything(123)\n",
    "naive_gcn1 = NaiveGCN(dim_in=cora.num_features, dim_out=64)\n",
    "naive_gcn2 = NaiveGCN(dim_in=64, dim_out=32)\n",
    "\n",
    "naive_clf = NodesClassifierModel(gnn_convs=(naive_gcn1, naive_gcn2), lin_sizes=(32, 16, 7))\n",
    "optim = th.optim.Adam(naive_clf.parameters(), lr=0.01)\n",
    "loss_fn = nn.CrossEntropyLoss()\n",
    "\n",
    "naive_clf.eval()\n",
    "yhat_naive_before_training = naive_clf(cora.x, cora.edge_index)\n",
    "\n",
    "\n",
    "epoch_losses_naive = []\n",
    "val_losses_naive = []\n",
    "\n",
    "for epoch in tqdm(range(35)):\n",
    "    naive_clf.train()\n",
    "    optim.zero_grad()\n",
    "    out = naive_clf(cora.x, cora.edge_index)\n",
    "    loss = loss_fn(out[cora.train_mask], cora.y[cora.train_mask])\n",
    "    loss.backward()\n",
    "    optim.step()\n",
    "    epoch_losses_naive.append(loss.item())\n",
    "\n",
    "    naive_clf.eval()\n",
    "    with th.no_grad():\n",
    "        val_loss = loss_fn(out[cora.val_mask], cora.y[cora.val_mask])\n",
    "        val_losses_naive.append(val_loss.item())\n",
    "\n",
    "naive_clf.eval()\n",
    "yhat_naive_after_training = naive_clf(cora.x, cora.edge_index)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+-----------------------------+--------------------------+----------------+----------+\n",
      "| Layer                       | Input Shape              | Output Shape   | #Param   |\n",
      "|-----------------------------+--------------------------+----------------+----------|\n",
      "| NodesClassifierModel        | [2708, 1433], [2, 10556] | [2708, 7]      | 94,503   |\n",
      "| ├─(gnn_convs)ModuleList     | --                       | --             | 93,856   |\n",
      "| │    └─(0)NaiveGCN          | [2708, 1433], [2, 10556] | [2708, 64]     | 91,776   |\n",
      "| │    │    └─(sigma)Identity | [1, 64]                  | [1, 64]        | --       |\n",
      "| │    │    └─(W)Linear       | [1, 1433]                | [1, 64]        | 91,776   |\n",
      "| │    └─(1)NaiveGCN          | [2708, 64], [2, 10556]   | [2708, 32]     | 2,080    |\n",
      "| │    │    └─(sigma)Identity | [1, 64]                  | [1, 64]        | --       |\n",
      "| │    │    └─(W)Linear       | [1, 64]                  | [1, 32]        | 2,080    |\n",
      "| ├─(lin_layers)Sequential    | [2708, 32]               | [2708, 7]      | 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(naive_clf, cora.x, cora.edge_index))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Wynik klasyfikacji modelu Naiwny GCN przed trenowaniem:\n",
      "              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.16      1.00      0.27        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.00      0.00      0.00        29\n",
      "\n",
      "    accuracy                           0.16       500\n",
      "   macro avg       0.02      0.14      0.04       500\n",
      "weighted avg       0.02      0.16      0.04       500\n",
      "\n",
      "\n",
      "\n",
      "\n",
      "\n",
      "Wynik klasyfikacji modelu Naiwny GCN po trenowaniu:\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.21      0.57      0.31        61\n",
      "           1       0.00      0.00      0.00        36\n",
      "           2       0.59      0.96      0.73        78\n",
      "           3       0.80      0.70      0.74       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.36      0.83      0.50        29\n",
      "\n",
      "    accuracy                           0.49       500\n",
      "   macro avg       0.28      0.44      0.33       500\n",
      "weighted avg       0.39      0.49      0.41       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "utils.val_scores_before_after_train(\n",
    "    cora.y[cora.val_mask],\n",
    "    yhat_naive_before_training[cora.val_mask],\n",
    "    yhat_naive_after_training[cora.val_mask],\n",
    "    \"Naiwny GCN\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 37004 characters]",
      "text/plain": [
       "<Figure size 472.441x472.441 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "utils.plot_results(\n",
    "    epoch_losses_naive,\n",
    "    val_losses_naive,\n",
    "    plot_name=\"rys_4.2-naive.tiff\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model klasyfikacji z warstwą GCN z PyG"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyg.seed_everything(123)\n",
    "pyg_gcn1 = pyg_nn.GCNConv(in_channels=cora.num_features, out_channels=64, normalize=True, cached=True)\n",
    "pyg_gcn2 = pyg_nn.GCNConv(in_channels=64, out_channels=32, normalize=True, cached=True)\n",
    "\n",
    "pyg_clf = NodesClassifierModel(gnn_convs=(pyg_gcn1, pyg_gcn2), lin_sizes=(32, 16, 7))\n",
    "optim = th.optim.Adam(pyg_clf.parameters(), lr=0.01)\n",
    "loss_fn = nn.CrossEntropyLoss()\n",
    "\n",
    "pyg_clf.eval()\n",
    "yhat_pyg_before_training = pyg_clf(cora.x, cora.edge_index)\n",
    "\n",
    "\n",
    "epoch_losses_pyg = []\n",
    "val_losses_pyg = []\n",
    "\n",
    "for epoch in tqdm(range(25)):\n",
    "    pyg_clf.train()\n",
    "    optim.zero_grad()\n",
    "    out = pyg_clf(cora.x, cora.edge_index)\n",
    "    loss = loss_fn(out[cora.train_mask], cora.y[cora.train_mask])\n",
    "    loss.backward()\n",
    "    optim.step()\n",
    "    epoch_losses_pyg.append(loss.item())\n",
    "\n",
    "    pyg_clf.eval()\n",
    "    with th.no_grad():\n",
    "        val_loss = loss_fn(out[cora.val_mask], cora.y[cora.val_mask])\n",
    "        val_losses_pyg.append(val_loss.item())\n",
    "\n",
    "pyg_clf.eval()\n",
    "yhat_pyg_after_training = pyg_clf(cora.x, cora.edge_index)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+--------------------------+--------------------------+----------------+----------+\n",
      "| Layer                    | Input Shape              | Output Shape   | #Param   |\n",
      "|--------------------------+--------------------------+----------------+----------|\n",
      "| NodesClassifierModel     | [2708, 1433], [2, 10556] | [2708, 7]      | 94,503   |\n",
      "| ├─(gnn_convs)ModuleList  | --                       | --             | 93,856   |\n",
      "| │    └─(0)GCNConv        | [2708, 1433], [2, 10556] | [2708, 64]     | 91,776   |\n",
      "| │    └─(1)GCNConv        | [2708, 64], [2, 10556]   | [2708, 32]     | 2,080    |\n",
      "| ├─(lin_layers)Sequential | [2708, 32]               | [2708, 7]      | 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(pyg_clf, cora.x, cora.edge_index))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Wynik klasyfikacji modelu PyG GCN przed trenowaniem:\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.00      0.00      0.00        61\n",
      "           1       0.07      1.00      0.13        36\n",
      "           2       0.00      0.00      0.00        78\n",
      "           3       0.50      0.01      0.01       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.00      0.00      0.00        57\n",
      "           6       0.00      0.00      0.00        29\n",
      "\n",
      "    accuracy                           0.07       500\n",
      "   macro avg       0.08      0.14      0.02       500\n",
      "weighted avg       0.16      0.07      0.01       500\n",
      "\n",
      "\n",
      "\n",
      "\n",
      "\n",
      "Wynik klasyfikacji modelu PyG GCN po trenowaniu:\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.46      0.69      0.55        61\n",
      "           1       0.51      0.75      0.61        36\n",
      "           2       0.59      0.83      0.69        78\n",
      "           3       0.73      0.63      0.68       158\n",
      "           4       0.00      0.00      0.00        81\n",
      "           5       0.47      0.70      0.56        57\n",
      "           6       0.62      0.52      0.57        29\n",
      "\n",
      "    accuracy                           0.58       500\n",
      "   macro avg       0.48      0.59      0.52       500\n",
      "weighted avg       0.51      0.58      0.53       500\n",
      "\n"
     ]
    }
   ],
   "source": [
    "utils.val_scores_before_after_train(\n",
    "    cora.y[cora.val_mask],\n",
    "    yhat_pyg_before_training[cora.val_mask],\n",
    "    yhat_pyg_after_training[cora.val_mask],\n",
    "    \"PyG GCN\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 41560 characters]",
      "text/plain": [
       "<Figure size 472.441x472.441 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "utils.plot_results(\n",
    "    epoch_losses_pyg,\n",
    "    val_losses_pyg,\n",
    "    plot_name=\"rys_4.2.tiff\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Alternatywna forma implementacji PyG\n",
    "\n",
    "Biblioteka PyG umożliwia tworzenie warstw splotu na dwa sposoby:\n",
    "1. Albo jako pojedyncze warstwy - każda z nich jest osobnym obiektem, który można wykorzystać w modelu (odmiennie ustawiając parametry itd.). Wtedy warstwy są łączone w modelu, tak jak w przypadku modelu z naiwną implementacją.\n",
    "2. Albo jako jeden model, który jest automatycznie konstruowany przez PyG z kilku warstw splotu zestawionych razem. \n",
    "\n",
    "\n",
    "W przedstawionym przykładzie zastosowaliśmy podejście (1) - samodzielnie opisując każdą warstwę. Poniżej przedstawiony jest przykład podejścia (2): PyG zbuduje jeden model grafowy, z dwiema warstwami GCN. Wejściem do każdej będzie:\n",
    "* macierz cech wierzchołków (wyjście z poprzedniej warstwy lub oryginalne cechy wierzchołków),\n",
    "* macierz sąsiedztwa grafu (nie ulega zmianie).\n",
    "\n",
    "Określamy specyficzne dla GCN parametry, które będą miały zastosowanie **do wszystkich warstw**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "+---------------------+--------------------------+----------------+----------+\n",
      "| Layer               | Input Shape              | Output Shape   | #Param   |\n",
      "|---------------------+--------------------------+----------------+----------|\n",
      "| GCN                 | [2708, 1433], [2, 10556] | [2708, 32]     | 93,856   |\n",
      "| ├─(dropout)Dropout  | [2708, 64]               | [2708, 64]     | --       |\n",
      "| ├─(act)ReLU         | [2708, 64]               | [2708, 64]     | --       |\n",
      "| ├─(convs)ModuleList | --                       | --             | 93,856   |\n",
      "| │    └─(0)GCNConv   | [2708, 1433], [2, 10556] | [2708, 64]     | 91,776   |\n",
      "| │    └─(1)GCNConv   | [2708, 64], [2, 10556]   | [2708, 32]     | 2,080    |\n",
      "| ├─(norms)ModuleList | --                       | --             | --       |\n",
      "| │    └─(0)Identity  | [2708, 64]               | [2708, 64]     | --       |\n",
      "| │    └─(1)Identity  | --                       | --             | --       |\n",
      "+---------------------+--------------------------+----------------+----------+\n"
     ]
    }
   ],
   "source": [
    "gcn_model = pyg_nn.GCN(\n",
    "    in_channels=cora.num_features, # Ilość cech wejściowych - oryginalnych\n",
    "    hidden_channels=64,            # Ilość cech w warstwie ukrytej\n",
    "    num_layers=2,                  # Ilość warstw ukrytych - każda warstwa jest warstwą typu GCNConv\n",
    "    out_channels=32,               # Ilość klas do przewidzenia\n",
    "    # Dodatkowe hiperparametry dla każdej warstwy\n",
    "    normalize=True,\n",
    "    cached=True\n",
    ")\n",
    "print(pyg_nn.summary(gcn_model, cora.x, cora.edge_index))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Taki model można używać identycznie, jak wcześniej."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "with th.no_grad():\n",
    "    yhat = gcn_model(cora.x, cora.edge_index)\n",
    "    classifier = NodesClassifierModel(gnn_convs=(gcn_model,), lin_sizes=(32, 16, 7))\n",
    "    yhat_classifier = classifier(cora.x, cora.edge_index)"
   ]
  }
 ],
 "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
}
