Coverage for hopwise/model/general_recommender/convncf.py: 92%
75 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
1# @Time : 2020/10/6
2# @Author : Yingqian Min
3# @Email : eliver_min@foxmail.com
5r"""ConvNCF
6################################################
7Reference:
8 Xiangnan He et al. "Outer Product-based Neural Collaborative Filtering." in IJCAI 2018.
10Reference code:
11 https://github.com/duxy-me/ConvNCF
12"""
14import copy
16import torch
17from torch import nn
19from hopwise.model.abstract_recommender import GeneralRecommender
20from hopwise.model.general_recommender.bpr import BPR
21from hopwise.model.layers import CNNLayers, MLPLayers
22from hopwise.utils import InputType
25class ConvNCFBPRLoss(nn.Module):
26 """ConvNCFBPRLoss, based on Bayesian Personalized Ranking,
28 Shape:
29 - Pos_score: (N)
30 - Neg_score: (N), same shape as the Pos_score
31 - Output: scalar.
33 Examples::
35 >>> loss = ConvNCFBPRLoss()
36 >>> pos_score = torch.randn(3, requires_grad=True)
37 >>> neg_score = torch.randn(3, requires_grad=True)
38 >>> output = loss(pos_score, neg_score)
39 >>> output.backward()
40 """
42 def __init__(self):
43 super().__init__()
45 def forward(self, pos_score, neg_score):
46 distance = pos_score - neg_score
47 loss = torch.sum(torch.log(1 + torch.exp(-distance)))
48 return loss
51class ConvNCF(GeneralRecommender):
52 r"""ConvNCF is a a new neural network framework for collaborative filtering based on NCF.
53 It uses an outer product operation above the embedding layer,
54 which results in a semantic-rich interaction map that encodes pairwise correlations between embedding dimensions.
55 We carefully design the data interface and use sparse tensor to train and test efficiently.
56 We implement the model following the original author with a pairwise training mode.
57 """
59 input_type = InputType.PAIRWISE
61 def __init__(self, config, dataset):
62 super().__init__(config, dataset)
64 # load dataset info
65 self.LABEL = config["LABEL_FIELD"]
67 # load parameters info
68 self.embedding_size = config["embedding_size"]
69 self.cnn_channels = config["cnn_channels"]
70 self.cnn_kernels = config["cnn_kernels"]
71 self.cnn_strides = config["cnn_strides"]
72 self.dropout_prob = config["dropout_prob"]
73 self.regs = config["reg_weights"]
74 self.train_method = config["train_method"]
75 self.pre_model_path = config["pre_model_path"]
77 # define layers and loss
78 assert self.train_method in ["after_pretrain", "no_pretrain"]
79 if self.train_method == "after_pretrain":
80 assert self.pre_model_path != ""
81 pretrain_state = torch.load(self.pre_model_path)["state_dict"]
82 bpr = BPR(config=config, dataset=dataset)
83 bpr.load_state_dict(pretrain_state)
84 self.user_embedding = copy.deepcopy(bpr.user_embedding)
85 self.item_embedding = copy.deepcopy(bpr.item_embedding)
86 else:
87 self.user_embedding = nn.Embedding(self.n_users, self.embedding_size)
88 self.item_embedding = nn.Embedding(self.n_items, self.embedding_size)
90 self.cnn_layers = CNNLayers(self.cnn_channels, self.cnn_kernels, self.cnn_strides, activation="relu")
91 self.predict_layers = MLPLayers([self.cnn_channels[-1], 1], self.dropout_prob, activation="none")
92 self.loss = ConvNCFBPRLoss()
94 def forward(self, user, item):
95 user_e = self.user_embedding(user)
96 item_e = self.item_embedding(item)
98 interaction_map = torch.bmm(user_e.unsqueeze(2), item_e.unsqueeze(1))
99 interaction_map = interaction_map.unsqueeze(1)
101 cnn_output = self.cnn_layers(interaction_map)
102 cnn_output = cnn_output.sum(axis=(2, 3))
104 prediction = self.predict_layers(cnn_output)
105 prediction = prediction.squeeze(-1)
107 return prediction
109 def reg_loss(self):
110 r"""Calculate the L2 normalization loss of model parameters.
111 Including embedding matrices and weight matrices of model.
113 Returns:
114 loss(torch.FloatTensor): The L2 Loss tensor. shape of [1,]
115 """
116 reg_1, reg_2 = self.regs[:2]
117 loss_1 = reg_1 * self.user_embedding.weight.norm(2)
118 loss_2 = reg_1 * self.item_embedding.weight.norm(2)
119 loss_3 = 0
120 for name, parm in self.cnn_layers.named_parameters():
121 if name.endswith("weight"):
122 loss_3 = loss_3 + reg_2 * parm.norm(2)
123 for name, parm in self.predict_layers.named_parameters():
124 if name.endswith("weight"):
125 loss_3 = loss_3 + reg_2 * parm.norm(2)
126 return loss_1 + loss_2 + loss_3
128 def calculate_loss(self, interaction):
129 user = interaction[self.USER_ID]
130 pos_item = interaction[self.ITEM_ID]
131 neg_item = interaction[self.NEG_ITEM_ID]
133 pos_item_score = self.forward(user, pos_item)
134 neg_item_score = self.forward(user, neg_item)
136 loss = self.loss(pos_item_score, neg_item_score)
137 opt_loss = loss + self.reg_loss()
139 return opt_loss
141 def predict(self, interaction):
142 user = interaction[self.USER_ID]
143 item = interaction[self.ITEM_ID]
144 return self.forward(user, item)