TLDR:
Проблема:
задан набор данных о пользователях, фильмах и рейтингах. Можем ли мы создать модель, которая предсказывает рейтинги фильмов для пользователей?
Набор данных: ml-latest-small.zip из https://grouplens.org/datasets/movielens/
Решение (2 части):
1) Базовая матричная факторизация
2) Расширенная матричная факторизация (термины смещения, смещение, инициализация весов, сигмовидный_диапазон) https://medium. com/@datadote/matrix-factorization-advanced-pictures-code-part-2-3072450879c1
Код: 01_matrix_fact_simple.ipynb https://github.com/Datadote/ матрица-факторизация-pytorch

Шаги:
1) Опишите проблему и изучите набор данных
2) Предварительно обработайте набор данных для обучения и проверки
3) Создайте модель матричной факторизации
4) Обучите модель
5) Проверить результаты
6) Дальнейшие действия

1) Опишите проблему и изучите набор данных

Проблема. Дан набор данных о пользователях, фильмах и оценках. Можем ли мы создать модель, которая предсказывает рейтинги фильмов для пользователей?
Набор данных: ml-latest-small.zip из https://grouplens.org/datasets/movielens/
Данные состоят из пользователей, фильмов, рейтингов, меток времени, заголовков и жанров.

DATA_DIR = './data/ml-latest-small/'
dfm = pd.read_csv(DATA_DIR+'movies.csv')
df = pd.read_csv(DATA_DIR+'ratings.csv')
df = df.merge(dfm, on='movieId', how='left')
df = df.sort_values(['userId', 'timestamp'], ascending=[True, True]).reset_index(drop=True)
df.head(3)

2) Предварительно обработать набор данных для обучения и проверки

i) Преобразование столбцов в категориальные с помощью defaultdict(LabelEncoder)
Это переназначает диапазон столбцов на [0, len(unique(column))]. Например, необработанный максимальный идентификатор фильма равен 193609, но существует только 9723 уникальных идентификатора фильма. Это переназначение (193609 -> 9723) важно для уменьшения использования памяти во встраиваниях модели.

d = defaultdict(LabelEncoder)
cols_cat = ['userId', 'movieId']
for c in cols_cat:
    d[c].fit(df[c].unique())
    df[c] = d[c].transform(df[c])
df.head(3)

ii) Разделить данные на обучение/проверку. Создайте наборы данных MovieDatasets и загрузчики данных
Каждый пользователь имеет не менее 20 оценок. Используйте последние 5 оценок для проверки. Используйте оставшиеся данные для поезда.

df_train = df.groupby('userId').head(-5).reset_index(drop=True)
df_val = df.groupby('userId').tail(5).reset_index(drop=True)

class MovieDataset(Dataset):
    def __init__(self, df):
        super().__init__()
        self.df = df[['userId', 'movieId', 'rating']]
        self.x_user_movie = list(zip(df.userId.values, df.movieId.values))
        self.y_rating = self.df.rating.values
    def __len__(self):
        return len(self.df)
    def __getitem__(self, idx):
        return self.x_user_movie[idx], self.y_rating[idx]

BS = 8192
ds_train = MovieDataset(df_train)
ds_val = MovieDataset(df_val)
dl_train = DataLoader(ds_train, BS, shuffle=True, num_workers=4)
dl_val = DataLoader(ds_val, BS, shuffle=True, num_workers=4)

xb, yb = next(iter(dl_train))
print(xb)
print(yb)

3) Создайте матричную модель факторизации

i) идея совместной фильтрации
Набор данных может быть представлен в виде таблицы пользователей и фильмов. Можем ли мы найти похожих пользователей и предсказать рейтинги непросмотренных фильмов?

ii) Реализация PyTorch
Каждый пользователь и фильм (элемент) проходят через слой nn.Embedding. Этот слой создает векторное представление. Затем векторы user_emb и item_emb перемножаются и суммируются (эквивалентно скалярному произведению).

class MF(nn.Module):
    """ Matrix factorization model simple """
    def __init__(self, num_users, num_items, emb_dim):
        super().__init__()
        self.user_emb = nn.Embedding(num_embeddings=num_users, embedding_dim=emb_dim)
        self.item_emb = nn.Embedding(num_embeddings=num_items, embedding_dim=emb_dim)
    def forward(self, user, item):
        user_emb = self.user_emb(user)
        item_emb = self.item_emb(item)
        element_product = (user_emb*item_emb).sum(1)
        return element_product

n_users = len(df.userId.unique())
n_items = len(df.movieId.unique())
mdl = MF(n_users, n_items, emb_dim=32)
mdl.to(device)
print(mdl)

4) Модель поезда

Оптимизатор AdamW и среднеквадратичные потери (MSE) используются для обучения модели.

LR = 0.2
NUM_EPOCHS = 10

opt = optim.AdamW(mdl.parameters(), lr=LR)
loss_fn = nn.MSELoss()
epoch_train_losses, epoch_val_losses = [], []

for i in range(NUM_EPOCHS):
    train_losses, val_losses = [], []
    mdl.train()
    for xb,yb in dl_train:
        xUser = xb[0].to(device, dtype=torch.long)
        xItem = xb[1].to(device, dtype=torch.long)
        yRatings = yb.to(device, dtype=torch.float)
        preds = mdl(xUser, xItem)
        loss = loss_fn(preds, yRatings)
        train_losses.append(loss.item())
        opt.zero_grad()
        loss.backward()
        opt.step()
    mdl.eval()
    for xb,yb in dl_val:
        xUser = xb[0].to(device, dtype=torch.long)
        xItem = xb[1].to(device, dtype=torch.long)
        yRatings = yb.to(device, dtype=torch.float)
        preds = mdl(xUser, xItem)
        loss = loss_fn(preds, yRatings)
        val_losses.append(loss.item())
    # Start logging
    epoch_train_loss = np.mean(train_losses)
    epoch_val_loss = np.mean(val_losses)
    epoch_train_losses.append(epoch_train_loss)
    epoch_val_losses.append(epoch_val_loss)
    print(f'Epoch: {i}, Train Loss: {epoch_train_loss:0.1f}, Val Loss:{epoch_val_loss:0.1f}')

5) Проверить результаты

Давайте проведем некоторые проверки на вменяемость. Диапазон оценок модели составляет [-8,3, 9,8], что выходит за пределы фактического диапазона оценок [0,5, 5]. Некоторые рейтинги прогнозов выглядят близкими к фактическим рейтингам.

user_emb_min_w = mdl.user_emb.weight.min().item()
user_emb_max_w = mdl.user_emb.weight.max().item()
item_emb_min_w = mdl.item_emb.weight.min().item()
item_emb_max_w = mdl.item_emb.weight.max().item()
print(f'Emb user min/max w: {user_emb_min_w:0.3f} / {user_emb_max_w:0.3f}')
print(f'Emb item min/max w: {item_emb_min_w:0.3f} / {item_emb_max_w:0.3f}')
print(f'Preds min/max: {preds.min().item():0.2f} / {preds.max().item():0.2f}')
print(f'Rating min/max: {yRatings.min().item():0.2f} / {yRatings.max().item():0.2f}')
print(preds.detach().cpu().numpy()[:6])
print(yRatings.detach().cpu().numpy()[:6])

6) Следующие шаги

Во второй части мы улучшим эту матричную модель факторизации, добавив термины смещения пользователя и элемента, смещение, инициализацию веса и sigmoid_range. Для части 2 https://medium.com/@datadote/matrix-factorization-advanced-pictures-code-part-2-3072450879c1