4
2

More than 1 year has passed since last update.

推薦システム知識0の筆者がPytorchでMatrix Factorizationを実装するまで

Last updated at Posted at 2023-01-03

はじめに

この記事は推薦システム知識0の筆者が、推薦システムについて調べつつ、パーソナライズありの推薦アルゴリズムとしてメジャーな手法であるMatrix Factorizationを実装し、動かしてみるまでを記録したものです。
性能評価や実際の推薦システムにおける使い方までは踏み込めていませんが、参考になれば幸いです。

やったこと

  • 推薦システムについて知る
  • 推薦するためのメジャーな手法を知る
  • PytorchでMatrix Factorizationを実装する
  • MovieLens Datasetで動かしてみる

推薦システム(Recommender system)とは

ユーザーに意思決定支援を行うシステムのことです。
具体例としては、ECサイトを訪れたユーザーにおすすめ商品を表示したりするといったシステムが考えられます。
推薦システムにおけるKPIは、クリック率、売上、購入者数など、適用先によって選択されるようです。
推薦システムと類似の言葉として「推薦エンジン」が使われている文献もありましたが、同義で使われている印象でした。

推薦システムの種類

パーソナライズなし

ユーザーによらず共通の推薦を提示するものです。
例えば、新着順、人気順といったものが挙げられます。

パーソナライズあり

ユーザーによって異なる推薦を提示するものです。
ユーザーの閲覧履歴、購入履歴、個人の情報(性別・年齢など)を活用して、ユーザーに応じて推薦を可変にします。ユーザーを似たようなグループ(segment)単位に分けて推薦するといった方法もあります。
パーソナライズありの推薦には、下記のようなものがあります。
※ここでは深層学習を用いた手法は除いています。

  • コンテンツベースフィルタリング(Content-based filtering)
  • 協調フィルタリング(Collaborative filtering)
  • Matrix Factarization

image.png
(引用:Brief on Recommender Systems

コンテンツベースフィルタリング

アイテム間の類似度を求めることで推薦アイテムを決定する手法。
類似度の評価にはcosine類似度などが用いられます。

協調フィルタリング

アイテムに対するユーザーの振る舞い(クリックや購入など)を反映し、ユーザーあるいはアイテム間の類似性を計算することで推薦アイテムを決定する手法のことです。協調フィルタリングの主なものとして下記の2種類が挙げられます。
image.png
(引用:Recommender Systems — User-Based and Item-Based Collaborative Filtering

  • User-based filtering
    アイテムに対するユーザーの振る舞いからユーザー間の類似度を求め、似ているユーザーがアクションを起こしたアイテムを推薦する手法。

  • Item-based filtering
    アイテムに対するユーザーの振る舞いからアイテム間の類似度を求め、ユーザーがアクションを起こしたアイテムと似ているアイテムを推薦する手法。

Matrix Factorization

協調フィルタリングにはアイテム、ユーザーが増えるとデータが膨大になってしまうという欠点があります。Matrix Factorizationは、アイテム、ユーザーの特徴量の次元削減を行うことによって、データの表現を維持しつつ、フィルタリングを行う手法です。
下記の図を例にすると4人のユーザー、5つのアイテムがあるため、単純に協調フィルタリングを行おうとすると4×5の行列が必要になります。これが数千万単位になるとデータサイズは膨大になります。
そこで、ユーザー、アイテムの特徴量を次元削減し、その積が近似的に元の4×5の行列を表現することを考えます(図の右側にある4×d(d=2)、上部にあるd×5の行列が削減された特徴量です)。
今回はこの手法を用いてユーザーに対して良さげな映画をおすすめすることを考えます。
image.png
(引用:https://developers.google.com/machine-learning/recommendation/collaborative/matrix)

それぞれのメリット・デメリット

Matrix Factorizationは協調フィルタリングに関連する手法のため、表から除いています。

協調フィルタリング コンテンツベースフィルタリング
メリット 情報がないアイテムもユーザーの振る舞いによって推薦可能。
例えば、過去にそのアイテムを購入したユーザーがいれば、アイテムに関する情報がなくても推薦できる。
新サービスでもアイテムの情入門報(特徴ベクトル)があれば推薦できる。
デメリット 利用者がいないシステムや新サービスでは、ユーザーの履歴データがないため推薦できない。 情報がないアイテムは推薦できない。

推薦システムのOSSデータセット

推薦システムに関するオープンソースのデータセットには以下のようなものがあります。

  • MovieLens 25M Dataset
  • Netflix Prize Dataset
  • Amazon Review Data

MovieLensは映画のレビューサイトで、MovieLens Datasetは事前学習やベンチマークで広く使われているデータセットです。現在もデータ数を増やして更新されています。
image.png
このデータセットの中には下記が含まれています。

  • 映画のタイトルとジャンル
  • ユーザーがつけたレート
  • タグ

Matrix FactorizationをPytorchで実装する

考え方

Matrix Factorizationにおける次元削減は、理論的にはSVD(特異値分解)で行うことが可能ですが、
「次元削減したユーザー、アイテムの特徴量の内積が、元のユーザー×アイテムの行列に近づくように学習する」とすれば、TensorflowやPytorchのようなDeeplearning向けのフレームワークを用いて実装することが可能です。
つまり、

  • モデルはパラメータとして次元削減されたユーザーの特徴ベクトルアイテムの特徴ベクトルを持つ
  • 次元削減されたユーザーの特徴ベクトルアイテムの特徴ベクトルの内積と、元のユーザー×アイテムの行列の差の表現をlossとする

とすれば、あとはbackpropagationによってモデルのパラメータを更新していくことで、元の行列の特徴を維持しつつ、次元削減できると言えそうです。

ライブラリのimport

import pandas as pd
import numpy as np
from tqdm import tqdm
import torch
from torch import nn
from torch import optim
from torch.utils.data import Dataset, DataLoader
from typing import Tuple
from collections import defaultdict

Matrix Factorizationクラスを実装する

実装は非常にシンプルです。
Pytorchのモデルが必ず継承するnn.Moduleを継承します。

class MatrixFactorization(nn.Module):
    def __init__(self, n_users, n_item, k=20):
        super().__init__()
        # kを小さくすればするほど次元削減
        # ユーザーの特徴ベクトル
        self.user_factors = nn.Embedding(n_users, k, sparse=True)
        # アイテムの特徴ベクトル
        self.item_factors = nn.Embedding(n_item, k, sparse=True)

    def forward(self, user, item):
        # 次元削減された特徴ベクトルの内積
        return (self.user_factors(user) * self.item_factors(item)).sum(1)

使ってみる

データを確認する

今回はMovieLens 1M Datasetを対象に、Matrix Factorizationを行います。
データはこちらからダウンロードしました。
データの中身を覗いてみます。なお、今回はTimeStampの情報は使用しないためdropしています。

df = pd.read_csv('./datasets/ml-1m/ratings.dat', delimiter='::', header=None)
df.columns = ['UserID', 'MovieID', 'Rating', 'Timestamp']
df = df.drop(columns=['Timestamp'])
df.head()

image.png

rating_matrix = df.pivot(index='UserID', columns='MovieID', values='Rating')
n_users, n_movies = rating_matrix.shape
print(f'num of users: {n_users}  num of items: {n_movies}')
rating_matrix

num of users: 6040 num of items: 3706
image.png

これまでの説明におけるユーザー=UserIDで識別できる個人、アイテム=MovieIDで識別できる映画と言えます。ユーザー×アイテムの行列の成分は、ユーザーの映画に対するRatingとなります。ユーザーは全ての映画に対してレビューをしているわけではないので、ほとんどがNaNになっています。
この膨大な行列をMatrix Factorizationにより、次元削減した表現を獲得することが目的です。
この表現を獲得できれば、「あるユーザーのRating(≒好み)は他のユーザーのRatingに似ているから、他のユーザーのRatingが高いこの映画を推薦しよう」というシステムを作ることが可能です。

前置きが長くなってしまいましたが、学習するためにDatasetクラスを実装します。

Datasetクラスを実装する

class MovieLens1mDataset(Dataset):
    USER_ID = 0
    MOVIE_ID = 1
    RATING = 2

    def __init__(self, rating_path: str) -> None:
        super().__init__()
        self.df = pd.read_csv(rating_path, delimiter='::', header=None)
        self.df.columns = ['UserID', 'MovieID', 'Rating', 'Timestamp']
        self.df = self.df.drop(columns=['Timestamp'])

    def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
        values = self.df.iloc[index].values
        # 行列へのindexに使用するので-1する
        user_id = values[self.USER_ID] - 1
        movie_id = values[self.MOVIE_ID] - 1
        target = np.float32(values[self.RATING])
        return user_id, movie_id, target

    def __len__(self) -> int:
        return len(self.df)
dataset = MovieLens1mDataset('./datasets/ml-1m/ratings.dat')
n_train = int(len(dataset)*0.7)
n_val = len(dataset) - n_train
# 今回は7:3で分ける
train_dataset, val_dataset = torch.utils.data.random_split(dataset, [n_train, n_val])

BATCH_SIZE = 64

train_dataloader = DataLoader(
    train_dataset, 
    batch_size=BATCH_SIZE, 
    shuffle=True,
    pin_memory=True
)

val_dataloader = DataLoader(
    val_dataset, 
    batch_size=1, 
    shuffle=True,
    pin_memory=True # faster read
)

dataloaders = dict(train=train_dataloader, val=val_dataloader)

train_modelを実装する

あとは学習用の関数を実装します。
ここはPytorchの通常の実装方法と同じです。

def train_model(model, dataloaders: dict, n_epoch: int, optimizer, criterion):
    loss_results = defaultdict(list)

    for epoch in range(n_epoch):
        loss_per_epoch = dict(train=0, val=0)

        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()
            else:
                model.eval()
        
            for users, items, targets in tqdm(dataloaders[phase]):
                # 勾配を初期化
                optimizer.zero_grad()
                
                # 学習時のみ勾配を計算
                with torch.set_grad_enabled(phase == 'train'):
                    preds = model(users, items)
                    loss = criterion(preds, targets)
                    loss_per_epoch[phase] += loss

                    if phase == 'train':
                        loss.backward()
                        optimizer.step()
                    
        loss_results[phase].append(loss_per_epoch[phase])
        
        print(f"[epoch {epoch+1}] train loss: {loss_per_epoch['train']}   val loss: {loss_per_epoch['val']}")

学習する

10epochで学習しました。
今回はとりあえず動かしてみることが目的なのでパラメータチューニング等はしていません。

n_users, n_items = dataset.df.UserID.max(), dataset.df.MovieID.max()
matrix_factorization = MatrixFactorization(n_users, n_items, k=20)
criterion = nn.MSELoss()
optimizer = optim.SparseAdam(matrix_factorization.parameters(), lr=1e-2)
n_epoch = 10

train_model(matrix_factorization, dataloaders, n_epoch, optimizer, criterion)

[epoch 1] train loss: 137863.203125 val loss: 730286.75
[epoch 2] train loss: 14481.7509765625 val loss: 413263.6875
[epoch 3] train loss: 10399.3056640625 val loss: 363408.40625
[epoch 4] train loss: 9458.337890625 val loss: 343843.46875
[epoch 5] train loss: 9033.9375 val loss: 333403.9375
[epoch 6] train loss: 8765.46875 val loss: 326865.03125
[epoch 7] train loss: 8561.6259765625 val loss: 321965.5
[epoch 8] train loss: 8393.1181640625 val loss: 318977.65625
[epoch 9] train loss: 8240.7080078125 val loss: 316371.90625
[epoch 10] train loss: 8115.3759765625 val loss: 314187.78125

valデータを推論して確認

matrix_factorization.eval()
users, items, targets = next(iter(dataloaders['val']))
print(f'predict: {matrix_factorization(users, items).detach().numpy()} target: {targets.numpy()}')

predict: [5.0426493] target: [5.]

まとめ

推薦システムに関する前提知識(を100倍に希釈したもの)や、Matrix Factorizationの実装についてまとめました。また、実際にMovieLens Datasetを用いてコードを動かしてみました。

References

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2