{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp models.siren"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# SiReN\n",
    "> Sign-Aware Recommendation Systems with Graph Neural Networks (SiReN).\n",
    "\n",
    "SiRen first constructs a signed bipartite graph $\\mathcal{E}^s = (u,v,w^s_{uv})|w^s_{uv} = w_{uv} − w_o,(u,v,w_{uv}) ∈ \\mathcal{E}$. Then it split this into 2 graphs. $\\mathcal{E}^p = (u,v,w^s_{uv})|w^s_{uv} > 0,\\  (u,v,w^s_{uv}) ∈ \\mathcal{E}^s$, and $\\mathcal{E}^n = (u,v,w^s_{uv})|w^s_{uv} < 0,\\ (u,v,w^s_{uv}) ∈ \\mathcal{E}^s$. The purpose of this graph partitioning is to make the graphs $G^p$ and $G^n$, respectively, assortative and disassortative so that each partitioned graph is used as input to the most appropriate learning model.\n",
    "\n",
    "For assortative relation learning, we use GNN network and for learning the disassortative relations, MLP Network is a better candidate. The resultant embeddings $Z^p$ and $Z^n$ are then reweighted using attention mechanism. Finally, the whole network parameters is optimized using a sign-aware BPR loss function.\n",
    "\n",
    "Note: For the graph with negative edges, we adopt a multi-layer perceptron (MLP) due to the fact that negative edges can weaken the homophily and thus message passing to such dissimilar nodes would not be feasible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "import torch\n",
    "from torch import nn\n",
    "import torch.nn.functional as F\n",
    "from torch_geometric.data import Data\n",
    "\n",
    "from recohut.models.layers.message_passing import LightGConv, LRGCCF"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class SiReN(nn.Module):\n",
    "    def __init__(self,\n",
    "                 train,\n",
    "                 num_u,\n",
    "                 num_v,\n",
    "                 offset,\n",
    "                 num_layers = 2,\n",
    "                 MLP_layers = 2,\n",
    "                 dim = 64,\n",
    "                 reg = 1e-4,\n",
    "                 device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\"),\n",
    "                 graph_enc = 'lightgcn',\n",
    "                 user_col = 'userId',\n",
    "                 item_col = 'itemId',\n",
    "                 rating_col = 'rating'):\n",
    "        super(SiReN,self).__init__()\n",
    "        self.M = num_u; self.N = num_v;\n",
    "        self.num_layers = num_layers\n",
    "        self.MLP_layers = MLP_layers\n",
    "        self.device = device\n",
    "        self.reg = reg\n",
    "        self.embed_dim = dim\n",
    "        edge_user = torch.tensor(train[train[rating_col]>offset][user_col].values-1)\n",
    "        edge_item = torch.tensor(train[train[rating_col]>offset][item_col].values-1)+self.M\n",
    "        edge_ = torch.stack((torch.cat((edge_user,edge_item),0),torch.cat((edge_item,edge_user),0)),0)\n",
    "        self.data_p=Data(edge_index=edge_)\n",
    "        # For the graph with positive edges\n",
    "        self.E = nn.Parameter(torch.empty(self.M + self.N, dim))\n",
    "        nn.init.xavier_normal_(self.E.data)\n",
    "        self.convs = nn.ModuleList()\n",
    "        self.mlps = nn.ModuleList()\n",
    "        for _ in range(num_layers):\n",
    "            if graph_enc == 'lightgcn':\n",
    "                self.convs.append(LightGConv()) \n",
    "            else:\n",
    "                self.convs.append(LRGCCF(dim,dim))\n",
    "        # For the graph with negative edges\n",
    "        self.E2 = nn.Parameter(torch.empty(self.M + self.N, dim))\n",
    "        nn.init.xavier_normal_(self.E2.data)\n",
    "        for _ in range(MLP_layers):\n",
    "            self.mlps.append(nn.Linear(dim,dim,bias=True))\n",
    "            nn.init.xavier_normal_(self.mlps[-1].weight.data)\n",
    "        # Attntion model\n",
    "        self.attn = nn.Linear(dim,dim,bias=True)\n",
    "        self.q = nn.Linear(dim,1,bias=False)\n",
    "        self.attn_softmax = nn.Softmax(dim=1)\n",
    "        \n",
    "    def aggregate(self):\n",
    "        # Generate embeddings z_p\n",
    "        B=[]; B.append(self.E)\n",
    "        x = self.convs[0](self.E,self.data_p.edge_index)\n",
    "        B.append(x)\n",
    "        for i in range(1,self.num_layers):\n",
    "            x = self.convs[i](x,self.data_p.edge_index)\n",
    "            B.append(x)\n",
    "        z_p = sum(B)/len(B) \n",
    "        # Generate embeddings z_n\n",
    "        C = []; C.append(self.E2)\n",
    "        x = F.dropout(F.relu(self.mlps[0](self.E2)),p=0.5,training=self.training)\n",
    "        for i in range(1,self.MLP_layers):\n",
    "            x = self.mlps[i](x);\n",
    "            x = F.relu(x)\n",
    "            x = F.dropout(x,p=0.5,training=self.training)\n",
    "            C.append(x)\n",
    "        z_n = C[-1]\n",
    "        # Attntion for final embeddings Z\n",
    "        w_p = self.q(F.dropout(torch.tanh((self.attn(z_p))),p=0.5,training=self.training))\n",
    "        w_n = self.q(F.dropout(torch.tanh((self.attn(z_n))),p=0.5,training=self.training))\n",
    "        alpha_ = self.attn_softmax(torch.cat([w_p,w_n],dim=1))\n",
    "        Z = alpha_[:,0].view(len(z_p),1) * z_p + alpha_[:,1].view(len(z_p),1) * z_n\n",
    "        return Z\n",
    "    \n",
    "    def forward(self,u,v,w,n,device):\n",
    "        emb = self.aggregate()\n",
    "        u_ = emb[u].to(device);\n",
    "        v_ = emb[v].to(device);\n",
    "        n_ = emb[n].to(device);\n",
    "        w_ = w.to(device)\n",
    "        positivebatch = torch.mul(u_ , v_ ); \n",
    "        negativebatch = torch.mul(u_.view(len(u_),1,self.embed_dim),n_)  \n",
    "        sBPR_loss =  F.logsigmoid((torch.sign(w_).view(len(u_),1) * (positivebatch.sum(dim=1).view(len(u_),1))) - negativebatch.sum(dim=2)).sum(dim=1)\n",
    "        reg_loss = u_.norm(dim=1).pow(2).sum() + v_.norm(dim=1).pow(2).sum() + n_.norm(dim=2).pow(2).sum();\n",
    "        return -torch.sum(sBPR_loss) + self.reg * reg_loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "  <div id=\"df-7e2ba319-eba9-4588-880b-132485169e2f\">\n",
       "    <div class=\"colab-df-container\">\n",
       "      <div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>userId</th>\n",
       "      <th>itemId</th>\n",
       "      <th>rating</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>4</td>\n",
       "      <td>4</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>5</td>\n",
       "      <td>5</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>\n",
       "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-7e2ba319-eba9-4588-880b-132485169e2f')\"\n",
       "              title=\"Convert this dataframe to an interactive table.\"\n",
       "              style=\"display:none;\">\n",
       "        \n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
       "       width=\"24px\">\n",
       "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
       "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
       "  </svg>\n",
       "      </button>\n",
       "      \n",
       "  <style>\n",
       "    .colab-df-container {\n",
       "      display:flex;\n",
       "      flex-wrap:wrap;\n",
       "      gap: 12px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert {\n",
       "      background-color: #E8F0FE;\n",
       "      border: none;\n",
       "      border-radius: 50%;\n",
       "      cursor: pointer;\n",
       "      display: none;\n",
       "      fill: #1967D2;\n",
       "      height: 32px;\n",
       "      padding: 0 0 0 0;\n",
       "      width: 32px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert:hover {\n",
       "      background-color: #E2EBFA;\n",
       "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "      fill: #174EA6;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert {\n",
       "      background-color: #3B4455;\n",
       "      fill: #D2E3FC;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert:hover {\n",
       "      background-color: #434B5C;\n",
       "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "      fill: #FFFFFF;\n",
       "    }\n",
       "  </style>\n",
       "\n",
       "      <script>\n",
       "        const buttonEl =\n",
       "          document.querySelector('#df-7e2ba319-eba9-4588-880b-132485169e2f button.colab-df-convert');\n",
       "        buttonEl.style.display =\n",
       "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "        async function convertToInteractive(key) {\n",
       "          const element = document.querySelector('#df-7e2ba319-eba9-4588-880b-132485169e2f');\n",
       "          const dataTable =\n",
       "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
       "                                                     [key], {});\n",
       "          if (!dataTable) return;\n",
       "\n",
       "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
       "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
       "            + ' to learn more about interactive tables.';\n",
       "          element.innerHTML = '';\n",
       "          dataTable['output_type'] = 'display_data';\n",
       "          await google.colab.output.renderOutput(dataTable, element);\n",
       "          const docLink = document.createElement('div');\n",
       "          docLink.innerHTML = docLinkHtml;\n",
       "          element.appendChild(docLink);\n",
       "        }\n",
       "      </script>\n",
       "    </div>\n",
       "  </div>\n",
       "  "
      ],
      "text/plain": [
       "   userId  itemId  rating\n",
       "0       1       1       4\n",
       "1       1       2       5\n",
       "2       2       1       2\n",
       "3       2       3       5\n",
       "4       3       2       3\n",
       "5       4       4       2\n",
       "6       5       5       4"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "train = pd.DataFrame(\n",
    "    {'userId':[1,1,2,2,3,4,5],\n",
    "     'itemId':[1,2,1,3,2,4,5],\n",
    "     'rating':[4,5,2,5,3,2,4]}\n",
    ")\n",
    "\n",
    "train"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = SiReN(train,\n",
    "                num_u = 5,\n",
    "                num_v = 5,\n",
    "                offset = 3.5,\n",
    "                num_layers = 1,\n",
    "                MLP_layers = 1,\n",
    "                dim = 2,\n",
    "                reg = 1e-4,\n",
    "                device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\"),\n",
    "                graph_enc = 'lightgcn',\n",
    "                user_col = 'userId',\n",
    "                item_col = 'itemId',\n",
    "                rating_col = 'rating')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 0.3599, -0.6117],\n",
       "        [ 0.0643,  0.1232],\n",
       "        [-0.0828,  0.1913],\n",
       "        [-0.3257, -0.1678],\n",
       "        [ 0.2391, -0.0063],\n",
       "        [-0.0721, -0.3664],\n",
       "        [-0.0213, -0.1475],\n",
       "        [ 0.1260, -0.0493],\n",
       "        [-0.0176, -0.1874],\n",
       "        [ 0.0924,  0.2289]], grad_fn=<AddBackward0>)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "torch.random.manual_seed(0)\n",
    "model.aggregate()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Sparsh A.\n",
      "\n",
      "Last updated: 2021-12-20 05:06:04\n",
      "\n",
      "torch_geometric: 2.0.2\n",
      "\n",
      "Compiler    : GCC 7.5.0\n",
      "OS          : Linux\n",
      "Release     : 5.4.104+\n",
      "Machine     : x86_64\n",
      "Processor   : x86_64\n",
      "CPU cores   : 2\n",
      "Architecture: 64bit\n",
      "\n",
      "pandas : 1.1.5\n",
      "torch  : 1.10.0+cu111\n",
      "IPython: 5.5.0\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "!pip install -q watermark\n",
    "%reload_ext watermark\n",
    "%watermark -a \"Sparsh A.\" -m -iv -u -t -d -p torch_geometric"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
