{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Wprowadzenie\n",
    "\n",
    "Kod w tej części stanowi uzupełnienie i ilustrację dla rozdziału książki poświęconego podstawowym założeniom grafowych sieci neuronowych."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Import bibliotek"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import networkx as nx\n",
    "import torch as th\n",
    "import random\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import sys\n",
    "sys.path.append('..')\n",
    "import utils.vis_utils as vis_utils"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Przygotowanie danych\n",
    "\n",
    "Skonstruujmy bardzo prosty graf nieskierowany złożony z kilku wierzchołków z określonymi wartościami atrybutów."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "g = nx.Graph()\n",
    "g.add_edges_from([\n",
    "    ('a', 'b'),\n",
    "    ('a', 'c'),\n",
    "    ('b', 'c'),\n",
    "    ('c', 'd'),\n",
    "    ('d', 'e')\n",
    "])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 15092 characters]",
      "text/plain": [
       "<Figure size 472.441x393.701 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "pos = nx.spring_layout(g, seed=3)\n",
    "fig, ax = plt.subplots(figsize=vis_utils.image_size_in_cm(12, 10))\n",
    "nx.draw(g, pos, with_labels=True, font_size=12, ax=ax, node_color='lightblue')\n",
    "plt.savefig(\"rys_3.4.tiff\", pil_kwargs={\"compression\": \"tiff_lzw\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Odporność na permutacje"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Podejście naiwne"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "W podejściu naiwnym wejściem do sieci neuronowej jest macierz sąsiedztwa. Sprawdźmy jej zawartość."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0, 1, 1, 0, 0],\n",
       "       [1, 0, 1, 0, 0],\n",
       "       [1, 1, 0, 1, 0],\n",
       "       [0, 0, 1, 0, 1],\n",
       "       [0, 0, 0, 1, 0]])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A = nx.adjacency_matrix(g).todense()\n",
    "A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['a', 'b', 'c', 'd', 'e']"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "node_list = list(g.nodes())\n",
    "node_list"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Przygotujmy prosty model sieci neuronowej, który będzie przyjmował na wejściu konkatenację wektorów macierzy sąsiedztwa dla poszczególnych wierzchołków."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NaiveMLP(th.nn.Module):\n",
    "    def __init__(self, in_dim, hidden_dim1, hidden_dim2, out_dim):\n",
    "        super(NaiveMLP, self).__init__()\n",
    "        self.fc1 = th.nn.Linear(in_dim, hidden_dim1)\n",
    "        self.fc2 = th.nn.Linear(hidden_dim1, hidden_dim2)\n",
    "        self.out = th.nn.Linear(hidden_dim2, out_dim)\n",
    "\n",
    "    def forward(self, x):\n",
    "        h1 = self.fc1(x).relu()\n",
    "        h2 = self.fc2(h1).relu()\n",
    "        out = self.out(h2)\n",
    "        return out"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "th.manual_seed(333)\n",
    "naive_mlp = NaiveMLP(5, 8, 4, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Pierwsza predykcja** - zastosujmy alfabetyczną kolejność wierzchołków, przygotowując macierz wejściową. Biblioteka **networkx** buduje macierz sąsiedztwa zgodnie z naturalną kolejnością wierzchołków, więc możemy wykorzystać tę macierz bezpośrednio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[0., 1., 1., 0., 0.],\n",
       "        [1., 0., 1., 0., 0.],\n",
       "        [1., 1., 0., 1., 0.],\n",
       "        [0., 0., 1., 0., 1.],\n",
       "        [0., 0., 0., 1., 0.]])"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = th.tensor(A).to(th.float)\n",
    "X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[0.3263, 0.3392],\n",
       "        [0.3252, 0.0212],\n",
       "        [0.3895, 0.1369],\n",
       "        [0.2752, 0.2046],\n",
       "        [0.3345, 0.0871]])"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with th.no_grad():\n",
    "    yhat1 = naive_mlp(X)\n",
    "yhat1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Druga predykcja** - zmieńmy teraz kolejność wierzchołków - nie mamy przecież gwarancji, że zawsze będą one układane w alfabetycznej kolejności. Zobaczmy, jak zmieni się wynik."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['b', 'a', 'e', 'c', 'd']"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A = nx.adjacency_matrix(g).todense()\n",
    "np.random.seed(222)\n",
    "permuted_indices = np.random.permutation(A.shape[0])\n",
    "permuted_node_names = [node_list[i] for i in permuted_indices]\n",
    "\n",
    "permuted_node_names"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 24564 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "A_perm = A[permuted_indices][:, permuted_indices]\n",
    "g_perm = nx.from_numpy_array(A_perm)\n",
    "g_perm = nx.relabel_nodes(g_perm, {i: permuted_node_names[i] for i in range(A.shape[0])}, copy=True)\n",
    "\n",
    "nx.draw(g_perm, pos, with_labels=True)\n",
    "plt.savefig(\"permuted_graph.svg\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0, 1, 0, 1, 0],\n",
       "       [1, 0, 0, 1, 0],\n",
       "       [0, 0, 0, 0, 1],\n",
       "       [1, 1, 0, 0, 1],\n",
       "       [0, 0, 1, 1, 0]])"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A_perm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Graf z przestawionymi wierzchołkami wygląda niemal identycznie, a jednak macierz sąsiedztwa jest zupełnie inna, niż na początku."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oryginalna macierz sąsiedztwa\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "array([[0, 1, 1, 0, 0],\n",
       "       [1, 0, 1, 0, 0],\n",
       "       [1, 1, 0, 1, 0],\n",
       "       [0, 0, 1, 0, 1],\n",
       "       [0, 0, 0, 1, 0]])"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(\"Oryginalna macierz sąsiedztwa\")\n",
    "A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Macierz sąsiedztwa po permutacji\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "array([[0, 1, 0, 1, 0],\n",
       "       [1, 0, 0, 1, 0],\n",
       "       [0, 0, 0, 0, 1],\n",
       "       [1, 1, 0, 0, 1],\n",
       "       [0, 0, 1, 1, 0]])"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(\"Macierz sąsiedztwa po permutacji\")\n",
    "A_perm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[0.3755, 0.2291],\n",
       "        [0.3697, 0.0465],\n",
       "        [0.3347, 0.0676],\n",
       "        [0.3833, 0.0638],\n",
       "        [0.3107, 0.1671]])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A_perm = th.tensor(A_perm).to(th.float)\n",
    "with th.no_grad():\n",
    "    yhat2 = naive_mlp(A_perm)\n",
    "yhat2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nawet pomimo zmiany kolejności wierzchołków - predykcje w obu przypadkach są zupełnie różne."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Początkowe predykcje\n",
      "tensor([[0.3263, 0.3392],\n",
      "        [0.3252, 0.0212],\n",
      "        [0.3895, 0.1369],\n",
      "        [0.2752, 0.2046],\n",
      "        [0.3345, 0.0871]])\n"
     ]
    }
   ],
   "source": [
    "print(\"Początkowe predykcje\")\n",
    "print(yhat1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predykcje po permutacji\n",
      "tensor([[0.3755, 0.2291],\n",
      "        [0.3697, 0.0465],\n",
      "        [0.3347, 0.0676],\n",
      "        [0.3833, 0.0638],\n",
      "        [0.3107, 0.1671]])\n"
     ]
    }
   ],
   "source": [
    "print(\"Predykcje po permutacji\")\n",
    "print(yhat2)"
   ]
  }
 ],
 "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
}
