書いてる理由
- レコメンド * 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があり、まず[1]を確認。前回、movielensのデータの前処理をやったので今回はDataloader。
コード
コード解説
前回は、movielensのデータにuserIdとitemIdを0~の連番で以下のように整形するところまでやったので、今度はこれを学習に利用できるようにDataloaderにしていく。
userId | itemId | rating | ts |
---|---|---|---|
0 | 0 | 5 | 978300760 |
0 | 1 | 3 | 978302109 |
0 | 2 | 3 | 978301968 |
0 | 3 | 4 | 978300275 |
0 | 4 | 5 | 978824291 |
データローダー作成
データローダーはclass SampleGenerator(object):
で作成する。
一個一個の動きを見ていく。
class SampleGenerator(object): def __init__(self, ratings): self.ratings = ratings self.preprocess_ratings = self._binarize(ratings) # ratingの二値化 self.user_pool = set(self.ratings['userId'].unique()) # ユーザーの集合体の作成 self.item_pool = set(self.ratings['itemId'].unique()) # アイテムの集合体の作成 self.negatives = self._sample_negative(ratings) # 反応していないitemIdと99個のランダムに抽出されたitemIdをくっつける self.train_ratings, self.test_ratings = self._split_train_test(self.preprocess_ratings) # 最新のレビューがテスト/それ以外がtrain
レーティングの二値化
self.preprocess_ratings = self._binarize(ratings) # ratingを全て1に変換 def _binarize(self, ratings): ratings = deepcopy(ratings) ratings['rating'][ratings['rating'] > 0] = 1.0 return ratings
今のデータは、以下のようにratingに1~5の数字が入っている。
userId | itemId | rating | ts |
---|---|---|---|
0 | 0 | 5 | 978300760 |
0 | 1 | 3 | 978302109 |
0 | 2 | 3 | 978301968 |
0 | 3 | 4 | 978300275 |
0 | 4 | 5 | 978824291 |
これを、ratings['rating'][ratings['rating'] > 0] ==1
にすることで、0より大きい数字を全て1に置き換える。
userId | itemId | rating | ts |
---|---|---|---|
0 | 0 | 1 | 978300760 |
0 | 1 | 1 | 978302109 |
0 | 2 | 1 | 978301968 |
0 | 3 | 1 | 978300275 |
0 | 4 | 1 | 978824291 |
ユーザーとアイテムの集合体の作成
self.user_pool = set(self.ratings['userId'].unique()) # ユーザーの集合体の作成 self.item_pool = set(self.ratings['itemId'].unique()) # アイテムの集合体の作成
特に解説なし。set()でユニークな集合体が作れる。
レーティングしていないアイテム情報の追加
def _sample_negative(self, ratings): interact_status = ratings.groupby('userId')['itemId'].apply(set).reset_index().rename( columns={'itemId': 'interacted_items'}) # interacted_itemsにレビューしたitemIdの集合体が入る interact_status['negative_items'] = interact_status['interacted_items'].apply(lambda x: self.item_pool - x) interact_status['negative_samples'] = interact_status['negative_items'].apply(lambda x: random.sample(x, 99)) return interact_status[['userId', 'negative_items', 'negative_samples']] self.negatives = self._sample_negative(ratings) # 反応していないitemIdと99個のランダムに抽出されたitemIdをくっつける
レビューデータの特性上、レビューをした情報しか残っておらず、レビューしていないという情報がない。
そのためレビューしていない他のアイテムからランダムにレビューしていない情報をくっつける。
interact_status = ratings.groupby('userId')['itemId'].apply(set).reset_index().rename(columns={'itemId': 'interacted_items'})
で、以下のようにユーザーごとにレビューしたアイテムの集合体を作る。
userId | interacted_items |
---|---|
0 | {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... |
1 | {0, 18, 20, 42, 47, 48, 52, 53, 54, 55, 56, 57... |
2 | {128, 4, 5, 22, 166, 168, 41, 44, 175, 176, 17... |
3 | {139, 26, 156, 43, 44, 48, 63, 64, 208, 209, 2... |
4 | {3, 4, 9, 18, 27, 38, 39, 43, 48, 51, 59, 62, ... |
interact_status['negative_items'] = interact_status['interacted_items'].apply(lambda x: self.item_pool - x)
でアイテム全体の集合から、レビューしたアイテムの集合を引いてレビューしていないアイテムの集合を作る。
interact_status['negative_samples'] = interact_status['negative_items'].apply(lambda x: random.sample(x, 99))
でその中から99個ランダムにサンプリングする。
この二つのカラムをくっつけて、レビューしていない全体のアイテムとランダムに99個サンプリングしたデータセットが出来上がる。
userId | negative_items | negative_samples |
---|---|---|
0 | {53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 6... | [2562, 3103, 2665, 959, 744, 3274, 1986, 1644,... |
1 | {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14... | [2248, 491, 1770, 473, 2819, 3539, 3532, 3352,... |
2 | {0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1... | [1172, 1474, 1877, 457, 2621, 3420, 1932, 2147... |
3 | {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | [1495, 905, 3473, 345, 828, 2121, 591, 3442, 1... |
4 | {0, 1, 2, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, ... | [1264, 1112, 2071, 80, 2295, 3159, 1122, 424, ... |
学習用とテスト用に分割
def _split_train_test(self, ratings): ratings['rank_latest'] = ratings.groupby(['userId'])['ts'].rank(method='first', ascending=False) test = ratings[ratings['rank_latest'] == 1] train = ratings[ratings['rank_latest'] > 1] return train[['userId', 'itemId', 'rating']], test[['userId', 'itemId', 'rating']] self.train_ratings, self.test_ratings = self._split_train_test(self.preprocess_ratings) # 最新のレビューがテスト/それ以外がtrain
ratings['rank_latest'] = ratings.groupby(['userId'])['ts'].rank(method='first', ascending=False)
でratingsにタイムスタンプ順に連番を降る。
test = ratings[ratings['rank_latest'] == 1]
とtrain = ratings[ratings['rank_latest'] > 1]
で最新の一つをtestにそれ以外をtrainにする。
userIdとitemIdとratingを持ったtrainとtestを返却。
以上で、データセットの前処理完了。
次は学習中の検証用データの準備。
検証用データの準備
class SampleGenerator(object): [中略] @property def evaluate_data(self): test_ratings = pd.merge(self.test_ratings, self.negatives[['userId', 'negative_samples']], on='userId') test_users, test_items, negative_users, negative_items = [], [], [], [] for row in test_ratings.itertuples(): test_users.append(int(row.userId)) test_items.append(int(row.itemId)) for i in range(len(row.negative_samples)): negative_users.append(int(row.userId)) negative_items.append(int(row.negative_samples[i])) return [torch.LongTensor(test_users), torch.LongTensor(test_items), torch.LongTensor(negative_users), torch.LongTensor(negative_items)] eval_data = sample_generator.evaluate_data # テストデータの反応あり、なしデータをTensor型で取得
test_ratings = pd.merge(self.test_ratings, self.negatives[['userId', 'negative_samples']], on='userId')
で先ほど作成した、テスト用のデータセットと、レビューしていないデータを結合する。
結合したデータは以下の形。
userId | itemId | rating | negative_samples |
---|---|---|---|
0 | 25 | 1 | [2562, 3103, 2665, 959, 744, 3274, 1986, 1644,... |
1 | 66 | 1 | [2248, 491, 1770, 473, 2819, 3539, 3532, 3352,... |
2 | 207 | 1 | [1172, 1474, 1877, 457, 2621, 3420, 1932, 2147... |
3 | 208 | 1 | [1495, 905, 3473, 345, 828, 2121, 591, 3442, 1... |
4 | 222 | 1 | [1264, 1112, 2071, 80, 2295, 3159, 1122, 424, ... |
これをfor文を回して、それぞれuser_list, item_list, negative_user_list, negative_item_listに格納していく。
user_listとitem_listはレビューしたuserIdとitemIdが対になっていて、ユーザーがレビューした最新の1アイテムだけなので、長さは6040。
negativeは、1ユーザーにつき99個のサンプリングなので6040 * 99 = 597960個ずつが入る。
この4つをpytorchで扱えるように、torch.LongTensor()
でreturn。
ここまでで、データの準備と整形が完了したので、次はネットワークの準備。
[補足]データ準備と検証データ用意のコード全体
class SampleGenerator(object): def __init__(self, ratings): self.ratings = ratings self.preprocess_ratings = self._binarize(ratings) # ratingを全て1に変換 self.user_pool = set(self.ratings['userId'].unique()) # ユーザーの集合体の作成 self.item_pool = set(self.ratings['itemId'].unique()) # アイテムの集合体の作成 self.negatives = self._sample_negative(ratings) # 反応していないitemIdと99個のランダムに抽出されたitemIdをくっつける self.train_ratings, self.test_ratings = self._split_train_test(self.preprocess_ratings) # 最新のレビューがテスト/それ以外がtrain def _binarize(self, ratings): ratings = deepcopy(ratings) ratings['rating'][ratings['rating'] > 0] = 1.0 return ratings def _sample_negative(self, ratings): interact_status = ratings.groupby('userId')['itemId'].apply(set).reset_index().rename( columns={'itemId': 'interacted_items'}) # interacted_itemsにレビューしたitemIdの集合体が入る interact_status['negative_items'] = interact_status['interacted_items'].apply(lambda x: self.item_pool - x) interact_status['negative_samples'] = interact_status['negative_items'].apply(lambda x: random.sample(x, 99)) return interact_status[['userId', 'negative_items', 'negative_samples']] def _split_train_test(self, ratings): ratings['rank_latest'] = ratings.groupby(['userId'])['ts'].rank(method='first', ascending=False) test = ratings[ratings['rank_latest'] == 1] train = ratings[ratings['rank_latest'] > 1] return train[['userId', 'itemId', 'rating']], test[['userId', 'itemId', 'rating']] def instance_a_train_loader(self, num_negatives, batch_size): users, items, ratings = [], [], [] train_ratings = pd.merge(self.train_ratings, self.negatives[['userId', 'negative_items']], on='userId') train_ratings['negatives'] = train_ratings['negative_items'].apply(lambda x: random.sample(x, num_negatives)) for row in train_ratings.itertuples(): users.append(int(row.userId)) items.append(int(row.itemId)) ratings.append(float(row.rating)) for i in range(num_negatives): users.append(int(row.userId)) items.append(int(row.negatives[i])) ratings.append(float(0)) # negative samples get 0 rating dataset = UserItemRatingDataset(user_tensor=torch.LongTensor(users), item_tensor=torch.LongTensor(items), target_tensor=torch.FloatTensor(ratings)) return DataLoader(dataset, batch_size=batch_size, shuffle=True) @property def evaluate_data(self): test_ratings = pd.merge(self.test_ratings, self.negatives[['userId', 'negative_samples']], on='userId') test_users, test_items, negative_users, negative_items = [], [], [], [] for row in test_ratings.itertuples(): test_users.append(int(row.userId)) test_items.append(int(row.itemId)) for i in range(len(row.negative_samples)): negative_users.append(int(row.userId)) negative_items.append(int(row.negative_samples[i])) return [torch.LongTensor(test_users), torch.LongTensor(test_items), torch.LongTensor(negative_users), torch.LongTensor(negative_items)] def main (): # make dataloader sample_generator = SampleGenerator(ratings=ml1m_rating) eval_data = sample_generator.evaluate_data # テストデータの反応あり、なしデータをTensor型で取得