|
|
| import torch
|
| import pandas as pd
|
| import numpy as np
|
| from torch import nn
|
| from torch.utils.data import Dataset, DataLoader
|
| from sklearn.model_selection import train_test_split
|
|
|
| class MangaDataset(Dataset):
|
| def __init__(self, ratings_df):
|
| self.users = torch.tensor(ratings_df['user_idx'].values, dtype=torch.long)
|
| self.items = torch.tensor(ratings_df['manga_idx'].values, dtype=torch.long)
|
| self.ratings = torch.tensor(ratings_df['rating'].values, dtype=torch.float)
|
|
|
| def __len__(self):
|
| return len(self.ratings)
|
|
|
| def __getitem__(self, idx):
|
| return self.users[idx], self.items[idx], self.ratings[idx]
|
|
|
| class MangaRecommender(nn.Module):
|
| def __init__(self, num_users, num_items, n_factors=50):
|
| super().__init__()
|
| self.user_factors = nn.Embedding(num_users, n_factors)
|
| self.item_factors = nn.Embedding(num_items, n_factors)
|
|
|
|
|
| nn.init.xavier_normal_(self.user_factors.weight)
|
| nn.init.xavier_normal_(self.item_factors.weight)
|
|
|
| def forward(self, user, item):
|
| user_emb = self.user_factors(user)
|
| item_emb = self.item_factors(item)
|
| return (user_emb * item_emb).sum(1)
|
|
|
| def predict(self, user_ids):
|
| user_emb = self.user_factors(user_ids)
|
| all_items = self.item_factors.weight
|
| return torch.matmul(user_emb, all_items.t())
|
|
|
| def train_model():
|
|
|
| df = pd.read_csv('manga_ratings.csv')
|
|
|
|
|
| user_mapping = {uid: idx for idx, uid in enumerate(df['user_id'].unique())}
|
| manga_mapping = {mid: idx for idx, mid in enumerate(df['manga_id'].unique())}
|
|
|
|
|
| rating_map = {'like': 1.0, 'dislike': -1.0, None: 0.0}
|
|
|
|
|
| df['user_idx'] = df['user_id'].map(user_mapping)
|
| df['manga_idx'] = df['manga_id'].map(manga_mapping)
|
| df['rating'] = df['like_status'].map(rating_map)
|
|
|
|
|
| train_df, val_df = train_test_split(df, test_size=0.2)
|
|
|
|
|
| train_dataset = MangaDataset(train_df)
|
| val_dataset = MangaDataset(val_df)
|
|
|
|
|
| train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
|
| val_loader = DataLoader(val_dataset, batch_size=64)
|
|
|
|
|
| model = MangaRecommender(
|
| num_users=len(user_mapping),
|
| num_items=len(manga_mapping)
|
| )
|
|
|
|
|
| criterion = nn.MSELoss()
|
| optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
|
|
|
|
|
| num_epochs = 20
|
| for epoch in range(num_epochs):
|
| model.train()
|
| total_loss = 0
|
| for user, item, rating in train_loader:
|
| optimizer.zero_grad()
|
| pred = model(user, item)
|
| loss = criterion(pred, rating)
|
| loss.backward()
|
| optimizer.step()
|
| total_loss += loss.item()
|
|
|
|
|
| model.eval()
|
| val_loss = 0
|
| with torch.no_grad():
|
| for user, item, rating in val_loader:
|
| pred = model(user, item)
|
| val_loss += criterion(pred, rating).item()
|
|
|
| print(f'Epoch {epoch+1}/{num_epochs}')
|
| print(f'Train Loss: {total_loss/len(train_loader):.4f}')
|
| print(f'Val Loss: {val_loss/len(val_loader):.4f}')
|
|
|
|
|
| torch.save({
|
| 'model_state_dict': model.state_dict(),
|
| 'user_mapping': user_mapping,
|
| 'manga_mapping': manga_mapping
|
| }, 'manga_recommender.pt')
|
|
|
| if __name__ == '__main__':
|
| train_model() |