{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install dgl-cu113 -f https://data.dgl.ai/wheels/repo.html","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:45:06.183977Z","iopub.execute_input":"2022-05-25T11:45:06.184307Z","iopub.status.idle":"2022-05-25T11:45:25.204787Z","shell.execute_reply.started":"2022-05-25T11:45:06.184265Z","shell.execute_reply":"2022-05-25T11:45:25.203944Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import dgl\nfrom dgl.data.utils import save_graphs, load_graphs\n\nimport torch\n\nimport pandas as pd\nimport numpy as np\n\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:45:25.208352Z","iopub.execute_input":"2022-05-25T11:45:25.208584Z","iopub.status.idle":"2022-05-25T11:45:27.5383Z","shell.execute_reply.started":"2022-05-25T11:45:25.208558Z","shell.execute_reply":"2022-05-25T11:45:27.53754Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def product_data():\n    prod_df = pd.read_csv(\"../input/h-and-m-personalized-fashion-recommendations/articles.csv\")\n    prod_df = prod_df.reset_index()\n    prod_df = prod_df.rename(columns={\"index\":\"product_node_id\"})\n\n    # Group products under same index group + product group\n    prod_df['article_id_str'] = prod_df['article_id'].astype(str)\n    sim_idx_prdgpnm = prod_df.groupby(['index_group_name', 'product_group_name'])['article_id_str'].apply(lambda  x: ','.join(x)).reset_index()\n    sim_idx_prdgpnm.to_parquet('./index_prod_group.parquet', index=False)\n\n    ## Creating interactions between products of same index group + product group\n    s = sim_idx_prdgpnm['article_id_str'].str.split(',').apply(pd.Series, 1).stack()\n    s.index = s.index.droplevel(-1)\n    s.name = 'article_id'\n    s = s.reset_index()\n    s['article_id'] = s['article_id'].astype(int)\n    s = s.merge(prod_df[['article_id', 'product_node_id']], on = 'article_id')\n    s.to_parquet('./sim_prod_interaction.parquet', index=False)\n\n    ## Subsetting numerical values and replacing it with inverse frequency,\n    subset_columns = ['product_node_id', 'article_id', 'product_code', 'product_type_no',\n                      'graphical_appearance_no','colour_group_code',\n                      'department_no','index_group_no', 'section_no',\n                      'garment_group_no']\n    prod_df = prod_df[subset_columns]\n    norm_prod_df = prod_df.copy()\n    for c in subset_columns[2:]:\n        norm_prod_df[c] = norm_prod_df[c].map(dict(100/prod_df[c].value_counts()))\n    norm_prod_df.to_parquet('./product.parquet', index=False)\n\n    print('Done!')\n\n    return  norm_prod_df\n\nprod_df = product_data()\n\n\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:45:32.966249Z","iopub.execute_input":"2022-05-25T11:45:32.969254Z","iopub.status.idle":"2022-05-25T11:45:36.131048Z","shell.execute_reply.started":"2022-05-25T11:45:32.969123Z","shell.execute_reply":"2022-05-25T11:45:36.130305Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def user_data():\n    user_df = pd.read_csv(\"../input/h-and-m-personalized-fashion-recommendations/customers.csv\")\n    user_df['Active'] = user_df['Active'].fillna(value=0)\n    user_df['FN'] = user_df['FN'].fillna(value=0)\n\n    user_df['club_member_status'] = user_df['club_member_status'].fillna(value='NotAvailable')\n    user_df['club_member_status'] = user_df['club_member_status'].astype('category').cat.codes\n\n    user_df['fashion_news_frequency'] = user_df['fashion_news_frequency'].replace('None', 'NONE')\n    user_df['fashion_news_frequency'] = user_df['fashion_news_frequency'].fillna(value='NONE')\n    user_df['fashion_news_frequency'] = user_df['fashion_news_frequency'].astype('category').cat.codes\n\n    user_df['age'] = user_df['age'].fillna(value=-1)\n\n    user_df = user_df.reset_index()\n    user_df = user_df.rename(columns={\"index\":\"customer_node_id\"})\n    user_df.to_parquet('./user.parquet', index=False)\n    print(\"Done!\")\n    return  user_df\n\nuser_df = user_data()","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:45:36.132665Z","iopub.execute_input":"2022-05-25T11:45:36.133078Z","iopub.status.idle":"2022-05-25T11:45:43.385084Z","shell.execute_reply.started":"2022-05-25T11:45:36.13304Z","shell.execute_reply":"2022-05-25T11:45:43.384308Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def decay_function(x):\n    return np.exp(1)**(-(x**2)/2000)\n\ndef transaction_data(user_df, prod_df):\n\n    prch_df = pd.read_csv(\"../input/h-and-m-personalized-fashion-recommendations/transactions_train.csv\")\n    prch_df['t_max'] = prch_df['t_dat'].max()\n    prch_df['days_frm_prch'] = (pd.to_datetime(prch_df['t_max']) - pd.to_datetime(prch_df['t_dat'])).dt.days\n    prch_df = prch_df[prch_df['days_frm_prch']<367]\n\n    prch_df['decay_cost'] = np.round(prch_df['days_frm_prch'].map(decay_function), 3) + 0.01\n\n    prch_df = prch_df[['customer_id', 'article_id', 'price', 'decay_cost']]\n\n    prch_df = prch_df.merge(user_df[[\"customer_id\",\"customer_node_id\"]], on='customer_id', how='left').merge(prod_df[[\"article_id\", \"product_node_id\"]], on='article_id', how='left')\n\n    prch_df.to_parquet('./transaction.parquet', index=False)\n    print('Done!')\n    return prch_df\n\nprch_df = transaction_data(user_df, prod_df)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:45:43.386783Z","iopub.execute_input":"2022-05-25T11:45:43.387109Z","iopub.status.idle":"2022-05-25T11:46:40.97553Z","shell.execute_reply.started":"2022-05-25T11:45:43.387072Z","shell.execute_reply":"2022-05-25T11:46:40.973724Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Making Graph","metadata":{}},{"cell_type":"code","source":"import dgl\nimport torch\nimport pandas as pd\n\nfrom dgl.data.utils import save_graphs, load_graphs","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.976703Z","iopub.status.idle":"2022-05-25T11:46:40.977284Z","shell.execute_reply.started":"2022-05-25T11:46:40.977024Z","shell.execute_reply":"2022-05-25T11:46:40.977052Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"customer_df = pd.read_parquet(\"./user.parquet\")\nproduct_df = pd.read_parquet(\"./product.parquet\")\npurchased_df = pd.read_parquet(\"./transaction.parquet\")\nsim_prod_df = pd.read_parquet(\"./sim_prod_interaction.parquet\")","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.978422Z","iopub.status.idle":"2022-05-25T11:46:40.978965Z","shell.execute_reply.started":"2022-05-25T11:46:40.978725Z","shell.execute_reply":"2022-05-25T11:46:40.978751Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_prch = purchased_df[purchased_df['decay_cost']<0.96]\ntest_prch = purchased_df[purchased_df['decay_cost']>=0.96]\nprint(train_prch.shape, test_prch.shape)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.980022Z","iopub.status.idle":"2022-05-25T11:46:40.980582Z","shell.execute_reply.started":"2022-05-25T11:46:40.980348Z","shell.execute_reply":"2022-05-25T11:46:40.980374Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"num_nodes = {\"customer\": customer_df.shape[0],\n             \"product\": product_df.shape[0],\n             \"prod_group\": sim_prod_df['index'].nunique()}\n\ntrain_data_dict = {\n    (\"customer\", \"purchased\", \"product\") : list(train_prch[[\"customer_node_id\", \"product_node_id\"]].to_records(index=False)),\n    (\"product\", \"rev_purchased\", \"customer\") : list(train_prch[[\"product_node_id\", \"customer_node_id\"]].to_records(index=False)),\n    (\"product\", \"same_product_group\", \"prod_group\"): list(sim_prod_df[['product_node_id', 'index']].to_records(index=False)),\n    (\"prod_group\", \"rev_same_product_group\", \"product\"): list(sim_prod_df[['index', 'product_node_id']].to_records(index=False))\n}\n\ntest_data_dict = {\n    (\"customer\", \"purchased\", \"product\") : list(test_prch[[\"customer_node_id\", \"product_node_id\"]].to_records(index=False)),\n    (\"product\", \"rev_purchased\", \"customer\") : list(test_prch[[\"product_node_id\", \"customer_node_id\"]].to_records(index=False)),\n    (\"product\", \"same_product_group\", \"prod_group\"): list(sim_prod_df[['product_node_id', 'index']].to_records(index=False)),\n    (\"prod_group\", \"rev_same_product_group\", \"product\"): list(sim_prod_df[['index', 'product_node_id']].to_records(index=False))\n\n}\n\n\ntrain_graph = dgl.heterograph(train_data_dict, num_nodes)\n\ntest_graph = dgl.heterograph(test_data_dict, num_nodes)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.981686Z","iopub.status.idle":"2022-05-25T11:46:40.982251Z","shell.execute_reply.started":"2022-05-25T11:46:40.981999Z","shell.execute_reply":"2022-05-25T11:46:40.982025Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"for c in ['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age']:\n    customer_df[c] = customer_df[c].astype('float32')\n\nfor c in product_df.drop(['product_node_id', 'article_id'], axis=1).columns:\n    product_df[c] = product_df[c].astype('float32')\n\n# add node features\n\ntrain_graph.ndata[\"feats\"] = {\"customer\":torch.tensor(customer_df[['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age']].values),\n                              \"product\":torch.tensor(product_df.drop(['product_node_id', 'article_id'], axis=1).values)}\n\n\ntest_graph.ndata[\"feats\"] = {\"customer\":torch.tensor(customer_df[['FN', 'Active', 'club_member_status', 'fashion_news_frequency', 'age']].values),\n                          \"product\":torch.tensor(product_df.drop(['product_node_id', 'article_id'], axis=1).values)}\n\n#add edge features\n\ntrain_graph.edges[\"purchased\"].data[\"price\"] = torch.tensor(train_prch[[\"price\"]].astype('float32').values)\ntrain_graph.edges[\"purchased\"].data[\"weight\"] = torch.tensor(train_prch[[\"decay_cost\"]].astype('float32').values)\n\ntrain_graph.edges[\"rev_purchased\"].data[\"price\"] = torch.tensor(train_prch[[\"price\"]].astype('float32').values)\ntrain_graph.edges[\"rev_purchased\"].data[\"weight\"] = torch.tensor(train_prch[[\"decay_cost\"]].astype('float32').values)\n\n\ntest_graph.edges[\"purchased\"].data[\"price\"] = torch.tensor(test_prch[[\"price\"]].astype('float32').values)\ntest_graph.edges[\"purchased\"].data[\"weight\"] = torch.tensor(test_prch[[\"decay_cost\"]].astype('float32').values)\n\ntest_graph.edges[\"rev_purchased\"].data[\"price\"] = torch.tensor(test_prch[[\"price\"]].astype('float32').values)\ntest_graph.edges[\"rev_purchased\"].data[\"weight\"] = torch.tensor(test_prch[[\"decay_cost\"]].astype('float32').values)\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.983348Z","iopub.status.idle":"2022-05-25T11:46:40.983893Z","shell.execute_reply.started":"2022-05-25T11:46:40.983657Z","shell.execute_reply":"2022-05-25T11:46:40.983683Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"save_graphs(\"./h&m_train.bin\", train_graph)\nsave_graphs(\"./h&m_test.bin\", test_graph)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.984954Z","iopub.status.idle":"2022-05-25T11:46:40.985506Z","shell.execute_reply.started":"2022-05-25T11:46:40.985274Z","shell.execute_reply":"2022-05-25T11:46:40.9853Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f\"Number of Nodes: {train_graph.num_nodes()}\")\nfor ntype in train_graph.ntypes:\n    print(f\"{ntype} : {train_graph.num_nodes(ntype)}\")","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.986552Z","iopub.status.idle":"2022-05-25T11:46:40.98709Z","shell.execute_reply.started":"2022-05-25T11:46:40.986851Z","shell.execute_reply":"2022-05-25T11:46:40.986877Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f\"Total edges: {train_graph.num_edges()}\")\nfor etype in train_graph.etypes:\n    print(f\"{etype} : {train_graph.num_edges(etype)}\")","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.988179Z","iopub.status.idle":"2022-05-25T11:46:40.988708Z","shell.execute_reply.started":"2022-05-25T11:46:40.988479Z","shell.execute_reply":"2022-05-25T11:46:40.988505Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Loss and Metrics","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport torch\nimport torch.nn.functional as F\nfrom sklearn.metrics import roc_auc_score\n\n\ndef compute_loss_bcelogits(pos_score, neg_score):\n\n    # BCE with logits\n    score = torch.cat([pos_score, neg_score])\n    labels = torch.cat([torch.ones_like(pos_score), torch.zeros_like(neg_score)])\n    bcelogits_loss = F.binary_cross_entropy_with_logits(score, labels)\n    return bcelogits_loss\n\ndef compute_auc(pos_score, neg_score):\n\n    score = torch.cat([pos_score, neg_score]).clone().cpu().detach().numpy()\n    labels = torch.cat([torch.ones_like(pos_score), torch.zeros_like(neg_score)]).clone().cpu().detach().numpy()\n\n    roc_score = roc_auc_score(labels, score)\n    return roc_score\n\n\ndef precision_k(y_pred, y_true, k=12):\n    x = np.intersect1d(y_true, y_pred[:k])\n    return len(x)/k\n\n\ndef relevence_k(y_pred, y_true, k=12):\n    if y_pred[k-1] in y_true:\n        return 1\n    else:\n        return 0\n\n\ndef avg_precision_k(y_pred, y_true, k=12):\n    ap = 0.0\n    n = len(y_pred)\n    k = min(n, k)\n    for i in range(1, k+1):\n        ap += precision_k(y_true, y_pred, i) * relevence_k(y_true, y_pred, i)\n\n    return ap/min(len(y_true), k)\n\n\ndef mean_avg_precision_k(y_true, y_pred, k=12):\n    return np.mean([avg_precision_k(yt, yp, k) for yt, yp in zip(y_true, y_pred)])","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.989778Z","iopub.status.idle":"2022-05-25T11:46:40.990337Z","shell.execute_reply.started":"2022-05-25T11:46:40.990096Z","shell.execute_reply":"2022-05-25T11:46:40.990122Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"code","source":"from typing import Tuple\n\nimport dgl.function as fn\nimport dgl.nn as dglnn\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass NodeEmbedding(nn.Module):\n    \"\"\"\n    To project input features to same emdedding space\n    \"\"\"\n    def reset(self):\n        nn.init.xavier_uniform_(self.proj_ip.weight)\n\n    def __init__(self, in_feats: int, embd_dim: int, hid_units: int) -> None:\n        super().__init__()\n        self.proj_ip = nn.Linear(in_feats, embd_dim)\n        self.proj_ip2 = nn.Linear(embd_dim, hid_units)\n        self.lrelu = nn.LeakyReLU()\n        self.reset()\n\n    def forward(self, node_feats):\n        x = self.proj_ip(node_feats)\n        x = self.lrelu(self.proj_ip2(x))\n        return x\n\n\nclass ConvLayer(nn.Module):\n    \"\"\"\n    1 layer of message passing and aggregation\n    \"\"\"\n\n    def reset_params(self):\n        gain = nn.init.calculate_gain('relu')\n        nn.init.xavier_uniform_(self.fc_neigh.weight, gain)\n        nn.init.xavier_uniform_(self.fc_self.weight, gain)\n\n    def __init__(self,\n                 in_feats: Tuple[int, int],\n                 out_size: int,\n                 aggregation: str,\n                 dropout: float,\n                 norm: bool):\n        \"\"\":parameter\n\n        in_feats:\n            Dimension of the message (aka features) of the node type neighbor and of the node type. E.g. if the\n            ConvLayer is initialized for the edge type (user, clicks, item), in_feats should be\n            (dimension_of_item_features, dimension_of_user_features). Note that usually features will first go\n            through embedding layer, thus dimension might be equal to the embedding dimension.\n\n        out_size:\n            output dimension of node features after message passing\n\n        aggregation:\n            aggregation function to use for messages (mean, sum)\n\n        dropout:\n            dropout applied to input features\n            \n        norm:\n            apply normalization after message passing and aggregation\n        \"\"\"\n        super().__init__()\n        self._in_neigh_feats, self._in_self_feats = in_feats\n        self.fc_neigh = nn.Linear(self._in_neigh_feats, out_size, bias=False)\n        self.fc_self = nn.Linear(self._in_self_feats, out_size, bias=False)\n        self.dropout_fn = nn.Dropout(dropout)\n        self._aggregation = aggregation\n        self.norm = norm\n        self.reset_params()\n\n    def forward(self, g, x):\n        \"\"\"\n        g: input graph/blocks\n        x: input features/node embeddings\n\n        :output\n        h: updated node embeddings after message passing and aggregation\n        \"\"\"\n\n        h_neigh, h_self = x\n        h_neigh = self.dropout_fn(h_neigh)\n        h_self = self.dropout_fn(h_self)\n        g.srcdata['h'] = h_neigh\n\n        if self._aggregation == 'mean':\n            g.update_all(\n                fn.copy_src('h', 'm'),\n                fn.mean('m', 'neigh'))\n            h_neigh = g.dstdata['neigh']\n\n        elif self._aggregation == 'sum':\n            g.update_all(\n                fn.copy_src('h', 'm'),\n                fn.sum('m', 'neigh'))\n            h_neigh = g.dstdata['neigh']\n\n        else:\n            raise KeyError(f'Aggregation function {self._aggregation} not supported...')\n\n        z = F.relu(self.fc_self(h_self) + self.fc_neigh(h_neigh))\n\n        if self.norm:\n            z_norm = z.norm(2, 1, keepdim=True)\n            z_norm = torch.where(z_norm == 0, torch.tensor(1.0).to(z_norm), z_norm)\n            z = z / z_norm\n\n        return z\n\n\nclass PredictionLayer(nn.Module):\n    \"\"\":returns\n\n    Neural Net to compute link score between customer and product\n    \"\"\"\n\n    def reset_params(self):\n        gain_relu = nn.init.calculate_gain('selu')\n        gain_sigmoid = nn.init.calculate_gain('sigmoid')\n        nn.init.xavier_uniform_(self.hidden_layer1.weight, gain_relu)\n        nn.init.xavier_uniform_(self.hidden_layer2.weight, gain_relu)\n        nn.init.xavier_uniform_(self.output_layer.weight, gain_sigmoid)\n\n    def __init__(self, embd_dim: int):\n        super(PredictionLayer, self).__init__()\n        self.hidden_layer1 = nn.Linear(embd_dim * 2, 16)\n        self.hidden_layer2 = nn.Linear(16, 8)\n        self.output_layer = nn.Linear(8, 1)\n        self.relu = nn.SELU()\n        self.sigmoid = nn.Sigmoid()\n        self.reset_params()\n\n    def forward(self, x):\n        x = self.relu(self.hidden_layer1(x))\n        x = self.relu(self.hidden_layer2(x))\n        x = self.output_layer(x)\n        #x = self.sigmoid(x)\n\n        return x\n\n\nclass PredictionModule(nn.Module):\n\n    def __init__(self, predicting_layer, embd_dim: int):\n        super(PredictionModule, self).__init__()\n        self.layer_nn = predicting_layer(embd_dim)\n\n    def forward(self, g, h):\n        score_dict = {}\n\n        for src, etype, dst in g.canonical_etypes:\n            if src in ['customer', 'product'] and dst in ['customer', 'product']:\n                src_nid, dst_nid = g.all_edges(etype=etype)\n                embd_head = h[src][src_nid]\n                embd_tail = h[dst][dst_nid]\n                cat_embd = torch.cat((embd_head, embd_tail), 1)\n                score = self.layer_nn(cat_embd)\n                score_dict[etype] = torch.flatten(score)\n\n        #print(score_dict, score)\n        score_dict = {key: torch.unsqueeze(score_dict[key], 1) for key in score_dict.keys()}\n        return score_dict\n\n\nclass GNNModel(nn.Module):\n    \"\"\":returns\n    Combined model which includes embedding layer, conv layers and prediction layer.\n    \"\"\"\n\n    def __init__(self,\n                 g,\n                 dim_dict,\n                 n_layers: int = 2,\n                 norm: bool = True,\n                 dropout: float = 0.1,\n                 aggregation: str = 'mean',\n                 aggregator_hetero: str = 'sum'\n                 ):\n        super().__init__()\n\n        self.customer_embd = NodeEmbedding(dim_dict['customer'], dim_dict['out'], dim_dict['hidden'])\n        self.product_embd = NodeEmbedding(dim_dict['product'], dim_dict['out'], dim_dict['hidden'])\n        self.prod_group_embd = NodeEmbedding(dim_dict['prod_group'], dim_dict['out'], dim_dict['hidden'])\n\n        self.layers = nn.ModuleList()\n\n        # connect embedding layer to hidden layer\n        for i in range(n_layers - 1):\n            self.layers.append(\n                dglnn.HeteroGraphConv(\n                    {etype: ConvLayer((dim_dict['hidden'], dim_dict['hidden']),\n                                      dim_dict['hidden'],\n                                      aggregation,\n                                      dropout,\n                                      norm)\n                     for etype in g.etypes}, aggregate=aggregator_hetero)\n            )\n\n        # output layer\n        self.layers.append(\n            dglnn.HeteroGraphConv(\n                {etype: ConvLayer((dim_dict['hidden'], dim_dict['hidden']),\n                                  dim_dict['out'],\n                                  aggregation,\n                                  dropout,\n                                  norm)\n                 for etype in g.etypes}, aggregate=aggregator_hetero)\n        )\n\n        self.pred_fn = PredictionModule(PredictionLayer, dim_dict['out'])\n\n    def get_node_representations(self, blocks, h):\n        for i in range(len(blocks)):\n            layer = self.layers[i]\n            h = layer(blocks[i], h)\n        return h\n\n    def forward(self, blocks, h, pos_graph, neg_graph):\n        h['customer'] = self.customer_embd(h['customer'])\n        h['product'] = self.product_embd(h['product'])\n        h['prod_group'] = self.prod_group_embd(h['prod_group'])\n        h = self.get_node_representations(blocks, h)\n        pos_score = self.pred_fn(pos_graph, h)\n        neg_score = self.pred_fn(neg_graph, h)\n\n        return h, pos_score, neg_score\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.991524Z","iopub.status.idle":"2022-05-25T11:46:40.992076Z","shell.execute_reply.started":"2022-05-25T11:46:40.991831Z","shell.execute_reply":"2022-05-25T11:46:40.991856Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Util funtions","metadata":{}},{"cell_type":"code","source":"import os\nimport time\n\nimport dgl\nimport torch\nimport pandas as pd\nimport tqdm\nimport numpy as np\nimport math\n\n\ndef get_node_loader(g,\n                    customer_ids,\n                    all_product_ids,\n                    prod_gp_ids,\n                    pos_neigh_sample,\n                    n_layers,\n                    batch_size,\n                    num_workers,\n                    device):\n    if device=='cuda':\n        num_workers=0\n    #sampler = dgl.dataloading.MultiLayerNeighborSampler([pos_neigh_sample, pos_neigh_sample])\n    sampler = dgl.dataloading.MultiLayerFullNeighborSampler(n_layers)\n\n    nodeloader = dgl.dataloading.DataLoader(\n        graph=g,\n        indices={'customer': torch.tensor(customer_ids, device=device), 'product': torch.tensor(all_product_ids, device=device), 'prod_group': torch.tensor(prod_gp_ids, device=device)},\n        graph_sampler=sampler,\n        batch_size=batch_size,\n        device=device,\n        shuffle=False,\n        drop_last=False,\n        num_workers=num_workers\n    )\n\n    return nodeloader\n\n\ndef get_embeddings(g,\n                   out_dim,\n                   trained_model,\n                   nodeloader,\n                   device):\n\n    y = {ntype: torch.empty(g.number_of_nodes(ntype), out_dim, device=device) for ntype in g.ntypes}\n    trained_model = trained_model.to(device)\n    trained_model.eval()\n\n    with tqdm.tqdm(nodeloader) as tq, torch.no_grad():\n        for step, (input_nodes, output_nodes, blocks) in enumerate(tq):\n            blocks = [b.to(device) for b in blocks]\n\n            input_features = blocks[0].srcdata['feats']\n            input_features['customer'] = trained_model.customer_embd(input_features['customer']).to(device)\n            input_features['product'] = trained_model.product_embd(input_features['product']).to(device)\n            input_features['prod_group'] = trained_model.prod_group_embd(input_features['prod_group']).to(device)\n\n            h = trained_model.get_node_representations(blocks, input_features)\n            for ntype in h.keys():\n                if ntype in output_nodes.keys():\n                    y[ntype][output_nodes[ntype]] = h[ntype]\n\n    return y\n\n\ndef get_preds_parallel(h, user_ids, model, embd_dim, device, k=100):\n\n    input_node = [i for i in user_ids]\n\n    #print(input_node)\n    with torch.no_grad():\n        model = model.to(device)\n        model.eval()\n\n        user_embd_rpt = torch.repeat_interleave(h['customer'][input_node], h['product'].shape[0], dim=0)\n        prd_embd_rpt = torch.cat([h['product']] * len(input_node)).reshape(-1, embd_dim)\n        cat_embd = torch.cat((user_embd_rpt, prd_embd_rpt), 1).to(device)\n        #print(cat_embd.shape)\n        scores = model.pred_fn.layer_nn(cat_embd.to(device))\n        #print(scores.shape)\n        formatted_score = scores.reshape(len(input_node), h['product'].shape[0])\n        prod_node_id = torch.topk(formatted_score, k=k).indices\n        temp = pd.concat([pd.DataFrame(input_node), pd.DataFrame(prod_node_id.cpu().numpy())], axis=1)\n        #print(temp)\n        temp.to_csv('./temp.csv', index=False, mode='a', header=None)\n\n\ndef batch(iterable, n=1):\n    l = len(iterable)\n    for ndx in range(0, l, n):\n        yield iterable[ndx:min(ndx+n, l)]\n\ndef get_scores(g,\n               h,\n               model,\n               user_ids,\n               embd_dim,\n               device,\n               batch_size,\n               k: int =100):\n\n    batch_size = batch_size\n    recs = []\n    if os.path.exists(\"./temp.csv\"):\n        os.remove(\"./temp.csv\")\n\n    #predictdataset = PredictNodeLoaderDataset(user_ids, h)\n    #user_ids = torch.utils.data.DataLoader(predictdataset, batch_size=32, num_workers=2)\n\n    uids = batch(user_ids, batch_size)\n    total_batches = int(len(user_ids)/batch_size) + 1\n    \n    with tqdm.tqdm(uids, total=total_batches) as tq:\n        for step, i in enumerate(tq):\n            get_preds_parallel(h, i, model, embd_dim, device, k=k)\n        #    if step % 50 == 0:\n        #        print(f\"batch {step+1} of {total_batches} done...\")\n\n\n    recs = pd.read_csv('./temp.csv', header=None)\n    \n    cols = ['cnid']\n    for i in range(100):\n        cols.append('p'+str(i))\n\n    recs.columns = cols\n\n    customer_df = pd.read_parquet(\"./user.parquet\")\n    product_df = pd.read_parquet(\"./product.parquet\")\n\n    pmap = product_df.set_index('product_node_id').to_dict()['article_id']\n    cmap = customer_df.set_index('customer_node_id').to_dict()['customer_id']\n\n    for i in cols[1:]:\n        recs[i] = recs[i].map(pmap)\n\n    recs['cnid'] = recs['cnid'].map(cmap)\n\n    temp = pd.DataFrame(recs.drop('cnid', axis=1).apply(lambda x: ','.join(x.astype(str)), axis=1))\n    temp.columns = ['prediction']\n    temp['customer_id'] = recs['cnid']\n    temp.to_csv(\"./predictions.csv\", index=False)\n\n    return temp\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.993225Z","iopub.status.idle":"2022-05-25T11:46:40.993761Z","shell.execute_reply.started":"2022-05-25T11:46:40.99353Z","shell.execute_reply":"2022-05-25T11:46:40.993556Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Training model","metadata":{}},{"cell_type":"code","source":"import dgl.dataloading\nfrom dgl.data.utils import load_graphs\n\n\nimport torch.nn.utils\nimport tqdm","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.994817Z","iopub.status.idle":"2022-05-25T11:46:40.995369Z","shell.execute_reply.started":"2022-05-25T11:46:40.99513Z","shell.execute_reply":"2022-05-25T11:46:40.995169Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if torch.cuda.is_available():\n    device = 'cuda'\nelse:\n    device = 'cpu'\n    \nuse_amp = False\n\nprint(device)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.996425Z","iopub.status.idle":"2022-05-25T11:46:40.996973Z","shell.execute_reply.started":"2022-05-25T11:46:40.99673Z","shell.execute_reply":"2022-05-25T11:46:40.996757Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def get_pred_scores(g, config, model, customer_ids):\n\n    product_ids = list(np.arange(g.number_of_nodes('product')).astype('int64'))\n    prod_gp_ids = list(np.arange(g.number_of_nodes('prod_group')).astype('int64'))\n\n    print(f\"getting node loader for users:{len(customer_ids)} and products: {len(product_ids)} and product groups: {len(prod_gp_ids)}\")\n    nodeloader = get_node_loader(g, customer_ids=customer_ids,\n                                 all_product_ids=product_ids,\n                                 prod_gp_ids=prod_gp_ids,\n                                 pos_neigh_sample=config[\"sample_neigh\"],\n                                 n_layers=config[\"n_layers\"],\n                                 batch_size=config['batch_size'],\n                                 num_workers=2,\n                                 device='cuda')\n    print(f\"getting node embeddings...\")\n    node_embds = get_embeddings(g,\n                                out_dim=config['embd_dim'],\n                                trained_model=model,\n                                nodeloader=nodeloader,\n                                device='cuda')\n    \n    #node_embds = {'customer': node_embds['customer'].detach(),\n    #              'product': node_embds['product'].detach(),\n    #              'prod_group': node_embds['prod_group'].detach()}\n\n    print(f\"getting scores...\")\n    scores = get_scores(g,\n                        h=node_embds,\n                        model=model,\n                        user_ids=customer_ids,\n                        embd_dim=config['embd_dim'],\n                        device='cuda',\n                        batch_size=32,\n                        k=100)\n    return scores\n\ndef evaluate(pred_df):\n    prch_df = pd.read_csv(\"../input/h-and-m-personalized-fashion-recommendations/transactions_train.csv\")\n    prch_df = prch_df[prch_df['t_dat']>='2020-09-01']\n    prch_df = prch_df.groupby('customer_id').apply(lambda x: str(x['article_id'].values).replace('\\n', '')[1:-1]).reset_index()\n    prch_df.columns = ['customer_id', 'purchases']\n    pred_df = pred_df.merge(prch_df, on='customer_id')\n\n    print(f\"MAP@12: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=12)}\")\n    print(f\"MAP@24: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=24)}\")\n    print(f\"MAP@36: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=36)}\")\n    print(f\"MAP@48: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=48)}\")\n    print(f\"MAP@60: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=60)}\")\n    print(f\"MAP@72: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=72)}\")\n    print(f\"MAP@100: {mean_avg_precision_k(pred_df['purchases'], pred_df['prediction'], k=100)}\")\n\ndef get_graphs():\n    print(\"loading training graph\")\n    glist, _ = load_graphs(\"./h&m_train.bin\")\n    g = glist[0].to(device)\n\n    g.ndata['feats'] = {'prod_group': torch.randn(g.number_of_nodes(\"prod_group\"), 16, device=device)}\n\n    for ntype in g.ntypes:\n        print(f\"{ntype}: {g.num_nodes(ntype)}\")\n        print(f\"{ntype} : {g.node_attr_schemes(ntype)}\")\n        print(\"\")\n\n    for etype in g.etypes:\n        print(f\"{etype}: {g.num_edges(etype)}\")\n        print(f\"{etype} : {g.edge_attr_schemes(etype)}\")\n        print(\"\")\n\n    print(\"loading validation graph\")\n    glist, _ = load_graphs(\"./h&m_test.bin\")\n    test_g = glist[0].to(device)\n\n    test_g.ndata['feats'] = {'prod_group': g.ndata['feats']['prod_group']}\n\n    for ntype in test_g.ntypes:\n        print(f\"{ntype}: {test_g.number_of_nodes(ntype)}\")\n        print(f\"{ntype} : {test_g.node_attr_schemes(ntype)}\")\n        print(\"\")\n\n    for etype in test_g.etypes:\n        print(f\"{etype}: {test_g.num_edges(etype)}\")\n        print(f\"{etype} : {test_g.edge_attr_schemes(etype)}\")\n        print(\"\")\n    \n    return g, test_g\n\n\ndef get_data_loaders(config, g, test_g):\n    train_eid_dict = {etype: g.edges(etype=etype, form='eid') for etype in g.canonical_etypes}\n    test_eid_dict = {etype: test_g.edges(etype=etype, form='eid') for etype in test_g.canonical_etypes}\n\n    neg_neigh_sample = config[\"sample_neigh\"]\n    pos_neigh_sample = config[\"sample_neigh\"]\n\n    negative_sampler = dgl.dataloading.negative_sampler.Uniform(neg_neigh_sample)\n\n    sampler = dgl.dataloading.MultiLayerNeighborSampler(config[\"n_layers\"]*[pos_neigh_sample])\n    sampler = dgl.dataloading.as_edge_prediction_sampler(sampler=sampler, negative_sampler=negative_sampler,\n                                                         exclude='reverse_types',\n                                                         reverse_etypes={'purchased': 'rev_purchased', 'rev_purchased': 'purchased',\n                                                                         'rev_same_product_group':'same_product_group', 'same_product_group':'rev_same_product_group'},\n                                                         )\n\n    train_dataloader = dgl.dataloading.DataLoader(\n        # The following arguments are specific to NodeDataLoader.\n        g,  # The graph\n        train_eid_dict,  # The edges to iterate over\n        sampler,  # The neighbor sampler\n        device='cuda',  # Put the MFGs on CPU or cuda\n        # The following arguments are inherited from PyTorch DataLoader.\n        batch_size=config[\"batch_size\"],  # Batch size\n        shuffle=True,  # Whether to shuffle the nodes for every epoch\n        drop_last=False,  # Whether to drop the last incomplete batch\n        num_workers=0  # Number of sampler processes\n    )\n\n    test_dataloader = dgl.dataloading.DataLoader(\n        # The following arguments are specific to NodeDataLoader.\n        test_g,  # The graph\n        test_eid_dict,  # The edges to iterate over\n        sampler,  # The neighbor sampler\n        device='cuda',  # Put the MFGs on CPU or GPU\n        # The following arguments are inherited from PyTorch DataLoader.\n        batch_size=config[\"batch_size\"],  # Batch size\n        shuffle=True,  # Whether to shuffle the nodes for every epoch\n        drop_last=False,  # Whether to drop the last incomplete batch\n        num_workers=0  # Number of sampler processes\n    )\n    return train_dataloader, test_dataloader\n\n\ndef train_model(config, model, checkpoint, train_dataloader, test_dataloader):\n    epochs = config[\"epochs\"]\n\n    print(f\"model parameters: {sum(p.numel() for p in model.parameters())}\")\n    model.to(device)\n    try:\n        opt = torch.optim.Adam(list(model.parameters()), lr=config[\"lr\"])\n        opt.load_state_dict(checkpoint['optimizer'])\n        scaler = torch.cuda.amp.GradScaler()\n        scaler.load_state_dict(checkpoint['scaler'])\n        print('couldnt load checkpoint')\n    except:\n        opt = torch.optim.Adam(list(model.parameters()), lr=config[\"lr\"])\n        scaler = torch.cuda.amp.GradScaler()\n\n    for epoch in range(epochs):\n        print(f'Epoch: {epoch}')\n        roc_Score = []\n        with tqdm.tqdm(train_dataloader) as tq:\n            for step, (input_nodes, pos_graph, neg_graph, mfgs) in enumerate(tq):\n                with torch.cuda.amp.autocast(enabled=use_amp):\n                    mfgs = [b.to(torch.device(device)) for b in mfgs]\n                    pos_graph = pos_graph.to(torch.device(device))\n                    neg_graph = neg_graph.to(torch.device(device))\n\n                    inputs = mfgs[0].srcdata['feats']\n                    _, pos_score, neg_score = model(mfgs, inputs, pos_graph, neg_graph)\n                    \n                    loss = compute_loss_bcelogits(pos_score['purchased'], neg_score['purchased'])\n                    roc_score = compute_auc(pos_score['purchased'], neg_score['purchased'])\n                roc_Score.append(roc_score)\n                scaler.scale(loss).backward()\n\n                scaler.unscale_(opt)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), config[\"clip_grad_value\"])\n\n                scaler.step(opt)\n                scaler.update()\n                opt.zero_grad(set_to_none=True)\n\n                tq.set_postfix({'loss': '%.03f' % loss, 'roc':'%.03f' % roc_score}, refresh=False)\n        \n        troc_Score = []\n        with tqdm.tqdm(test_dataloader) as test_tq:\n            for step, (input_nodes, pos_graph, neg_graph, mfgs) in enumerate(test_tq):\n                with torch.cuda.amp.autocast(enabled=use_amp):\n                    mfgs = [b.to(torch.device(device)) for b in mfgs]\n                    pos_graph = pos_graph.to(torch.device(device))\n                    neg_graph = neg_graph.to(torch.device(device))\n\n                    inputs = mfgs[0].srcdata['feats']\n                    _, pos_score, neg_score = model(mfgs, inputs, pos_graph, neg_graph)\n                    \n                    loss = compute_loss_bcelogits(pos_score['purchased'], neg_score['purchased'])\n                    troc_score = compute_auc(pos_score['purchased'], neg_score['purchased'])\n                troc_Score.append(troc_score)\n                scaler.scale(loss).backward()\n\n                scaler.unscale_(opt)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), config[\"clip_grad_value\"])\n\n                scaler.step(opt)\n                scaler.update()\n                opt.zero_grad(set_to_none=True)\n\n                tq.set_postfix({'loss': '%.03f' % loss, 'roc':'%.03f' % troc_score}, refresh=False)\n        \n        print(f\"epoch {epoch}, Train ROC: {np.nanmean(roc_Score)}, Test ROC: {np.nanmean(troc_Score)}\")\n\n        checkpoint = {'model': model.state_dict(),\n                      'optimizer': opt.state_dict(),\n                      'scaler': scaler.state_dict()}\n        \n        torch.save(checkpoint, './model_checkpoint_'+str(epoch)+'.pt')\n        \n    torch.save(checkpoint, './best_model_checkpoint.pt')\n\n    return model\n","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.998205Z","iopub.status.idle":"2022-05-25T11:46:40.998738Z","shell.execute_reply.started":"2022-05-25T11:46:40.998507Z","shell.execute_reply":"2022-05-25T11:46:40.998533Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def main(epochs,\n         lr,\n         sample_n,\n         batch_size,\n         hidden_units,\n         embd_dim,\n         train_new_model,\n         n_layers: int = 2):\n    \n    config = {\n        \"lr\": lr,\n        \"epochs\": epochs,\n        \"sample_neigh\": sample_n,\n        \"batch_size\": batch_size,\n        \"hidden_units\": hidden_units,\n        \"embd_dim\": embd_dim,\n        \"clip_grad_value\": 5,\n        \"n_layers\": n_layers\n    }\n\n    g, test_g = get_graphs()\n\n    train_dataloader, test_dataloader = get_data_loaders(config, g, test_g)\n\n    dim_dict = {\"customer\": g.ndata['feats']['customer'].shape[1],\n                \"product\": g.ndata['feats']['product'].shape[1],\n                \"prod_group\": g.ndata['feats']['prod_group'].shape[1],\n                \"hidden\": config[\"hidden_units\"],\n                \"out\": config[\"embd_dim\"]}\n\n    model = GNNModel(g, dim_dict, n_layers=n_layers)\n    model = model.to(torch.device(device))\n    print(model)\n    if train_new_model:\n        checkpoint = {}\n        model = train_model(config=config,\n                            model=model,\n                            checkpoint=checkpoint,\n                            train_dataloader=train_dataloader,\n                            test_dataloader=test_dataloader)\n\n    try:\n        model_checkpoint = \"./best_model_checkpoint.pt\"\n        print(f\"loading model from {model_checkpoint}\")\n        checkpoint = torch.load(model_checkpoint)\n        model.load_state_dict(checkpoint['model'])\n        print(f\"loaded saved model!\")\n    except:\n        print(f\"No saved model found...training new\")\n        checkpoint = {}\n        model = train_model(config=config,\n                            model=model,\n                            checkpoint=checkpoint,\n                            train_dataloader=train_dataloader, test_dataloader=test_dataloader)\n\n    #print(model)\n    customer_ids = list(np.arange(g.number_of_nodes('customer')).astype('int64'))\n    #customer_ids = list(np.arange(100).astype('int64'))\n\n    scores = get_pred_scores(g, config=config, model=model, customer_ids=customer_ids)\n\n    evaluate(scores)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T11:46:40.999832Z","iopub.status.idle":"2022-05-25T11:46:41.000392Z","shell.execute_reply.started":"2022-05-25T11:46:41.000143Z","shell.execute_reply":"2022-05-25T11:46:41.000183Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"main(epochs=2,\n     lr=0.021,\n     sample_n=10,\n     batch_size=4096,\n     hidden_units=32,\n     embd_dim=64,\n     train_new_model=True,\n     n_layers= 2)","metadata":{"execution":{"iopub.status.busy":"2022-05-25T04:07:38.845889Z","iopub.execute_input":"2022-05-25T04:07:38.846589Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# For evaluation\n'''\nmain(epochs=25,\n     lr=0.021,\n     sample_n=10,\n     batch_size=4096,\n     hidden_units=32,\n     embd_dim=64,\n     train_new_model=False,\n     n_layers= 2)\n'''","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"recs = pd.read_csv('./temp.csv', header=None)\n\n\ncols = ['cnid']\nfor i in range(100):\n    cols.append('p'+str(i))\n\nrecs.columns = cols\n\ncustomer_df = pd.read_parquet(\"./user.parquet\")\nproduct_df = pd.read_parquet(\"./product.parquet\")\n\npmap = product_df.set_index('product_node_id').to_dict()['article_id']\ncmap = customer_df.set_index('customer_node_id').to_dict()['customer_id']\n\nfor i in cols[1:]:\n    recs[i] = recs[i].map(pmap)\n\nrecs['cnid'] = recs['cnid'].map(cmap)\n\ntemp = pd.DataFrame(recs.drop('cnid', axis=1).apply(lambda x: ','.join(x.astype(str)), axis=1))\ntemp.columns = ['prediction']\ntemp['customer_id'] = recs['cnid']\ntemp.to_csv(\"./predictions.csv\", index=False)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"scores = pd.read_csv('./predictions.csv')\nevaluate(scores)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"scores\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}