書いてる理由
- レコメンド * deep learningやりたい
- まずは有名どころを真似てみる
参考
Neural Collaborative Filtering github.com
概要
レコメンドをdeep learingを使ってやりたい。
Neural Collaborative Filteringの論文をベースにpytorchで組まれているコードがあったので、それを真似して書いて動作を確認する。
コードは、[1]Generalized Matrix Factorizationと[2]Malti Layer Perceptronとこれらを組み合わせた[3]Neural matrix factorizationがあり、今回は[3]を確認。
コード
コード解説
今回は前回と前々回のGeneralized Martix FactorizationとMulti Layer Perceptronを結合させる。
ハイパーパラメータ系
neumf_config = {'alias': 'pretrain_neumf_factor8neg4', 'num_epoch': 200, 'batch_size': 1024, 'optimizer': 'adam', 'adam_lr': 1e-3, 'num_users': 6040, 'num_items': 3706, 'latent_dim_mf': 8, 'latent_dim_mlp': 8, 'num_negative': 4, 'layers': [16, 32, 16, 8], 'l2_regularization': 0.01, 'use_cuda': True, 'device_id': 0, 'pretrain': False, 'pretrain_mf': 'checkpoints/{}'.format('gmf_factor8neg4-implict_Epoch74_HR0.6402_NDCG0.3685.model'), 'pretrain_mlp': 'checkpoints/{}'.format('mlp_epoch30.model'), 'model_dir': 'checkpoints/{}_Epoch{}_HR{:.4f}_NDCG{:.4f}.model' }
num_epoch
は学習回数(全学習データを何回利用するか)、batch_size
は学習時の1回あたりの利用数、optimizer
とadam_lr
は最適化関数と学習率、num_users
とnum_items
はデータ数、latent_dim
の二つはgmfとmlpの隠れ層の数。
num_negative
はレビューしていないデータを何個利用するか(ネガティブなデータの利用数)、layers
はMLPの中間層の次元数、l2_regularization
はSGDとAdamのweight_decayにセットする値、use_cuda
とdevice_id
はGPUの利用情報、model_dir
はsaveするモデルの名前。
ネットワーク定義
上のconfigを使ってネットワークを定義する。
class NeuMF(torch.nn.Module): def __init__(self, config): super(NeuMF, self).__init__() self.config = config self.num_users = config['num_users'] self.num_items = config['num_items'] self.latent_dim_mf = config['latent_dim_mf'] self.latent_dim_mlp = config['latent_dim_mlp'] self.embedding_user_mlp = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mlp) self.embedding_item_mlp = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mlp) self.embedding_user_mf = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mf) self.embedding_item_mf = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mf) self.fc_layers = torch.nn.ModuleList() for idx, (in_size, out_size) in enumerate(zip(config['layers'][:-1], config['layers'][1:])): self.fc_layers.append(torch.nn.Linear(in_size, out_size)) self.affine_output = torch.nn.Linear(in_features=config['layers'][-1] + config['latent_dim_mf'], out_features=1) self.logistic = torch.nn.Sigmoid() def forward(self, user_indices, item_indices): user_embedding_mlp = self.embedding_user_mlp(user_indices) item_embedding_mlp = self.embedding_item_mlp(item_indices) user_embedding_mf = self.embedding_user_mf(user_indices) item_embedding_mf = self.embedding_item_mf(item_indices) mlp_vector = torch.cat([user_embedding_mlp, item_embedding_mlp], dim=-1) # the concat latent vector mf_vector = torch.mul(user_embedding_mf, item_embedding_mf) for idx, _ in enumerate(range(len(self.fc_layers))): mlp_vector = self.fc_layers[idx](mlp_vector) mlp_vector = torch.nn.ReLU()(mlp_vector) vector = torch.cat([mlp_vector, mf_vector], dim=-1) logits = self.affine_output(vector) rating = self.logistic(logits) return rating def init_weight(self): pass def load_pretrain_weights(self): """Loading weights from trained MLP model & GMF model""" config = self.config config['latent_dim'] = config['latent_dim_mlp'] mlp_model = MLP(config) if config['use_cuda'] is True: mlp_model.cuda() resume_checkpoint(mlp_model, model_dir=config['pretrain_mlp'], device_id=config['device_id']) self.embedding_user_mlp.weight.data = mlp_model.embedding_user.weight.data self.embedding_item_mlp.weight.data = mlp_model.embedding_item.weight.data for idx in range(len(self.fc_layers)): self.fc_layers[idx].weight.data = mlp_model.fc_layers[idx].weight.data config['latent_dim'] = config['latent_dim_mf'] gmf_model = GMF(config) if config['use_cuda'] is True: gmf_model.cuda() resume_checkpoint(gmf_model, model_dir=config['pretrain_mf'], device_id=config['device_id']) self.embedding_user_mf.weight.data = gmf_model.embedding_user.weight.data self.embedding_item_mf.weight.data = gmf_model.embedding_item.weight.data self.affine_output.weight.data = 0.5 * torch.cat([mlp_model.affine_output.weight.data, gmf_model.affine_output.weight.data], dim=-1) self.affine_output.bias.data = 0.5 * (mlp_model.affine_output.bias.data + gmf_model.affine_output.bias.data) class NeuMFEngine(Engine): """Engine for training & evaluating GMF model""" def __init__(self, config): self.model = NeuMF(config) if config['use_cuda'] is True: use_cuda(True, config['device_id']) self.model.cuda() super(NeuMFEngine, self).__init__(config) print(self.model) if config['pretrain']: self.model.load_pretrain_weights()
GMFとMLPの合成は、上の図の真ん中の枠の一番上のconcatinateで、これはコード上でvector = torch.cat([mlp_vector, mf_vector], dim=-1)
を指す。
特筆すべきはそこくらいだけど、改めてGMFとMLPの説明。
まず、入力はユーザーとアイテムのone-hotベクター(index番号)で、それをGMFとMLP用にベクトル表現にするために、torch.nn.Embedding
で用意する。表現する時のベクトルの次元数はconfigのlatent_dim_[mf|mlp]
の数
MLPの入力はuserのベクトルとitemのベクトルをconcatinateするので、mlp_vector = torch.cat([user_embedding_mlp, item_embedding_mlp], dim=-1)
で連結。[1024, 16]次元。
GMFの入力はusetのベクトルとitemのベクトルのアダマール積なので、mf_vector = torch.mul(user_embedding_mf, item_embedding_mf)
で各要素を掛け算。
MLPは入力されたら全結合層を何度か繰り返して特徴抽出する。
GMFのアウトプットは、[1024, 8]次元で、MLPのアウトプットも[1024, 8]次元にして、この二つをvector = torch.cat([mlp_vector, mf_vector], dim=-1)
で結合する。
結合した情報で全結合層を一回通して、シグモイド関数で0~1の値に変換して反応する確率を出して終了。
意味合い的には、GMFの単純なアダマール積がCollaborative Filteringで、MLPがuserとitemの情報をより精密に混ぜ合わせる処理で、これらを合成して反応するか否かを判断するという感じに見て取れる。
こうやって見ると、Matrix Factorizationでの次元圧縮をしないで、複数の全結合層の組み合わせでitemとuserの情報を混ぜ合わせてuserとitemの関係性を表現するCFという感じを受ける。
あと、GMFとMLPをそれぞれ単体で学習して、それらの結果をpre-trainとして読み込むと、より良いモデルができるっぽい。
これでネットワークは以上。
比較的簡単だったなぁという印象。
次は学習を実際に回すところを解説する。
予測用のスクリプトがなかったので、それも作ってもいいかも。時間があれば。
あと、本論文の簡単な紹介もしたいかな。自分の理解度向上のために。時間があれば。
さらに、単純にランダムなベクトル表現をEmbeddingしているけど、ここをword2vecのベクトル表現に置き換えるとさらに良いのでは疑惑がある。