text_html_output_1.png text_plain_output_1.png
from collections import Counter

from torch.utils.data import Dataset, DataLoader

import numpy as np # linear algebra

import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

import pytorch_lightning as pl

import torch

import torch.nn as nn

import torch.nn.functional as F

raw_behaviour = pd.read_csv('/kaggle/input/mind-news-dataset/MINDsmall_train/behaviors.tsv', sep='\t', names=['impressionId', 'userId', 'timestamp', 'click_history', 'impressions'])

news = pd.read_csv('/kaggle/input/mind-news-dataset/MINDsmall_train/news.tsv', sep='\t', names=['itemId', 'category', 'subcategory', 'title', 'abstract', 'url', 'title_entities', 'abstract_entities'])

def process_impression(impression_list):
    list_of_strings = impression_list.split()
    click = [x.split('-')[0] for x in list_of_strings if x.split('-')[1] == '1']
    non_click = [x.split('-')[0] for x in list_of_strings if x.split('-')[1] == '0']
    return (click, non_click)
raw_behaviour['click'], raw_behaviour['noclicks'] = zip(*raw_behaviour['impressions'].map(process_impression))

raw_behaviour['epochhrs'] = pd.to_datetime(raw_behaviour['timestamp']).values.astype(np.int64) / 1000000.0 / 1000 / 3600
raw_behaviour['epochhrs'] = raw_behaviour['epochhrs'].round()

raw_behaviour = raw_behaviour.explode('click').reset_index(drop=True)
click_history = raw_behaviour[['userId', 'click_history']].drop_duplicates().dropna()
click_history['click_history'] = click_history.click_history.map(lambda x: x.split())
click_history = click_history.explode('click_history').rename(columns={'click_history': 'click'})
click_history['epochhrs'] = raw_behaviour.epochhrs.min()
click_history['noclicks'] = pd.Series([[] for _ in range(len(click_history.index))])
raw_behaviour = pd.concat([raw_behaviour, click_history], axis=0).reset_index(drop=True)

min_click_cutoff = 100

raw_behaviour = raw_behaviour[raw_behaviour.groupby('click')['userId'].transform('size') >= min_click_cutoff].reset_index(drop=True)
click_set = set(raw_behaviour['click'].unique())
raw_behaviour['noclicks'] = raw_behaviour['noclicks'].apply(lambda impressions: [impression for impression in impressions if impression in click_set])

behaviour = raw_behaviour[['epochhrs', 'userId', 'click', 'noclicks']].copy()

test_time_th = behaviour['epochhrs'].quantile(0.9)
train = behaviour[behaviour['epochhrs'] < test_time_th].copy()
ind2item = {idx + 1: itemid for idx, itemid in enumerate(train.click.unique())}
item2ind = {itemid: idx for idx, itemid in ind2item.items()}
train['noclicks'] = train['noclicks'].map(lambda list_of_items: [item2ind.get(l, 0) for l in list_of_items])
train['click'] = train['click'].map(lambda item: item2ind.get(item, 0))
ind2user = {idx + 1: userid for idx, userid in enumerate(train['userId'].unique())}
user2ind = {userid: idx for idx, userid in ind2user.items()}
train['userIdx'] = train['userId'].map(lambda x: user2ind.get(x, 0))
valid = behaviour[behaviour['epochhrs'] >= test_time_th].copy()
valid['click'] = valid['click'].map(lambda item: item2ind.get(item, 0))
valid['noclicks'] = valid['noclicks'].map(lambda list_of_items: [item2ind.get(l, 0) for l in list_of_items])
valid['userIdx'] = valid['userId'].map(lambda x: user2ind.get(x, 0))

class MindDataset(Dataset):
    def __init__(self, df):
        self.data = {'userIdx': torch.tensor(df.userIdx.values.astype(np.int64)), 'click': torch.tensor(df.click.values.astype(np.int64))}
    def __len__(self):
        return len(self.data['userIdx'])
    def __getitem__(self, idx):
        return {key: val[idx] for key, val in self.data.items()}

bs = 1024
ds_train = MindDataset(train)
train_loader = DataLoader(ds_train, batch_size=bs, shuffle=True)
ds_valid = MindDataset(valid)
valid_loader = DataLoader(ds_valid, batch_size=bs, shuffle=False)
batch = next(iter(train_loader))

class NewsMF(pl.LightningModule):
    def __init__(self, num_users, num_items, dim=10):
        super().__init__()
        self.dim = dim
        self.useremb = nn.Embedding(num_embeddings=num_users, embedding_dim=dim)
        self.itememb = nn.Embedding(num_embeddings=num_items, embedding_dim=dim)
        self.num_users = num_users
        self.num_items = num_items
    def step(self, batch, batch_idx, phase='train'):
        batch_size = batch['userIdx'].size(0)
        uservec = self.useremb(batch['userIdx'])
        itemvec_click = self.itememb(batch['click'])
        neg_sample = torch.randint_like(batch['click'], 1, self.num_items)
        itemvec_noclick = self.itememb(neg_sample)
        score_click = torch.sigmoid((uservec * itemvec_click).sum(-1).unsqueeze(-1))
        score_noclick = torch.sigmoid((uservec * itemvec_noclick).sum(-1).unsqueeze(-1))
        scores_all = torch.concat((score_click, score_noclick), dim=1)
        target_all = torch.concat((torch.ones_like(score_click), torch.zeros_like(score_noclick)), dim=1)
        loss = F.binary_cross_entropy(scores_all, target_all)
        return loss
    def training_step(self, batch, batch_idx):
        return self.step(batch, batch_idx, 'train')
    def validation_step(self, batch, batch_idx):
        return self.step(batch, batch_idx, 'val')
    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=0.001)
        return optimizer

mf_model = NewsMF(num_users=len(ind2user) + 1, num_items=len(ind2item) + 1, dim=50)
trainer = pl.Trainer(max_epochs=50, accelerator='gpu')
trainer.fit(model=mf_model, train_dataloaders=train_loader)

itememb = mf_model.itememb.weight.detach()
news['ind'] = news['itemId'].map(item2ind)
news = news.sort_values('ind').reset_index(drop=True)
news['n_click_training'] = news['ind'].map(dict(Counter(train.click)))
news.sort_values('n_click_training', ascending=False).head(15)