Coverage for hopwise/model/context_aware_recommender/pnn.py: 100%
101 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/9/22 10:57
2# @Author : Zihan Lin
3# @Email : zhlin@ruc.edu.cn
4# @File : pnn.py
6r"""PNN
7################################################
8Reference:
9 Qu Y et al. "Product-based neural networks for user response prediction." in ICDM 2016
11Reference code:
12 - https://github.com/shenweichen/DeepCTR-Torch/blob/master/deepctr_torch/models/pnn.py
13 - https://github.com/Atomu2014/product-nets/blob/master/python/models.py
15"""
17import torch
18from torch import nn
19from torch.nn.init import constant_, xavier_normal_
21from hopwise.model.abstract_recommender import ContextRecommender
22from hopwise.model.layers import MLPLayers
25class PNN(ContextRecommender):
26 """PNN calculate inner and outer product of feature embedding.
27 You can choose the product option with the parameter of use_inner and use_outer
29 """
31 def __init__(self, config, dataset):
32 super().__init__(config, dataset)
34 # load parameters info
35 self.mlp_hidden_size = config["mlp_hidden_size"]
36 self.dropout_prob = config["dropout_prob"]
37 self.use_inner = config["use_inner"]
38 self.use_outer = config["use_outer"]
39 self.reg_weight = config["reg_weight"]
41 self.num_pair = int(self.num_feature_field * (self.num_feature_field - 1) / 2)
43 # define layers and loss
44 product_out_dim = self.num_feature_field * self.embedding_size
45 if self.use_inner:
46 product_out_dim += self.num_pair
47 self.inner_product = InnerProductLayer(self.num_feature_field, device=self.device)
49 if self.use_outer:
50 product_out_dim += self.num_pair
51 self.outer_product = OuterProductLayer(self.num_feature_field, self.embedding_size, device=self.device)
52 size_list = [product_out_dim] + self.mlp_hidden_size
53 self.mlp_layers = MLPLayers(size_list, self.dropout_prob, bn=False)
54 self.predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1)
55 self.relu = nn.ReLU()
56 self.sigmoid = nn.Sigmoid()
57 self.loss = nn.BCEWithLogitsLoss()
59 # parameters initialization
60 self.apply(self._init_weights)
62 def reg_loss(self):
63 """Calculate the L2 normalization loss of model parameters.
64 Including weight matrices of mlp layers.
66 Returns:
67 loss(torch.FloatTensor): The L2 Loss tensor. shape of [1,]
68 """
69 reg_loss = 0
70 for name, parm in self.mlp_layers.named_parameters():
71 if name.endswith("weight"):
72 reg_loss = reg_loss + self.reg_weight * parm.norm(2)
73 return reg_loss
75 def _init_weights(self, module):
76 if isinstance(module, nn.Embedding):
77 xavier_normal_(module.weight.data)
78 elif isinstance(module, nn.Linear):
79 xavier_normal_(module.weight.data)
80 if module.bias is not None:
81 constant_(module.bias.data, 0)
83 def forward(self, interaction):
84 pnn_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim]
85 batch_size = pnn_all_embeddings.shape[0]
86 # linear part
87 linear_part = pnn_all_embeddings.view(batch_size, -1) # [batch_size,num_field*embed_dim]
88 output = [linear_part]
89 # second order part
90 if self.use_inner:
91 inner_product = self.inner_product(pnn_all_embeddings).view(batch_size, -1) # [batch_size,num_pairs]
92 output.append(inner_product)
93 if self.use_outer:
94 outer_product = self.outer_product(pnn_all_embeddings).view(batch_size, -1) # [batch_size,num_pairs]
95 output.append(outer_product)
96 output = torch.cat(output, dim=1) # [batch_size,d]
98 output = self.predict_layer(self.mlp_layers(output)) # [batch_size,1]
99 return output.squeeze(-1)
101 def calculate_loss(self, interaction):
102 label = interaction[self.LABEL]
103 output = self.forward(interaction)
105 return self.loss(output, label) + self.reg_loss()
107 def predict(self, interaction):
108 return self.sigmoid(self.forward(interaction))
111class InnerProductLayer(nn.Module):
112 """InnerProduct Layer used in PNN that compute the element-wise
113 product or inner product between feature vectors.
115 """
117 def __init__(self, num_feature_field, device):
118 """Args:
119 num_feature_field(int) :number of feature fields.
120 device(torch.device) : device object of the model.
121 """
122 super().__init__()
123 self.num_feature_field = num_feature_field
124 self.to(device)
126 def forward(self, feat_emb):
127 """Args:
128 feat_emb(torch.FloatTensor) :3D tensor with shape: [batch_size,num_pairs,embedding_size].
130 Returns:
131 inner_product(torch.FloatTensor): The inner product of input tensor. shape of [batch_size, num_pairs]
132 """
133 # num_pairs = num_feature_field * (num_feature_field-1) / 2
134 row = []
135 col = []
136 for i in range(self.num_feature_field - 1):
137 for j in range(i + 1, self.num_feature_field):
138 row.append(i)
139 col.append(j)
140 p = feat_emb[:, row] # [batch_size, num_pairs, emb_dim]
141 q = feat_emb[:, col] # [batch_size, num_pairs, emb_dim]
143 inner_product = p * q
145 return inner_product.sum(dim=-1) # [batch_size, num_pairs]
148class OuterProductLayer(nn.Module):
149 """OuterProduct Layer used in PNN. This implementation is
150 adapted from code that the author of the paper published on https://github.com/Atomu2014/product-nets.
151 """
153 def __init__(self, num_feature_field, embedding_size, device):
154 """Args:
155 num_feature_field(int) :number of feature fields.
156 embedding_size(int) :number of embedding size.
157 device(torch.device) : device object of the model.
158 """
159 super().__init__()
161 self.num_feature_field = num_feature_field
162 num_pairs = int(num_feature_field * (num_feature_field - 1) / 2)
163 embed_size = embedding_size
165 self.kernel = nn.Parameter(torch.rand(embed_size, num_pairs, embed_size), requires_grad=True)
166 nn.init.xavier_uniform_(self.kernel)
168 self.to(device)
170 def forward(self, feat_emb):
171 """Args:
172 feat_emb(torch.FloatTensor) :3D tensor with shape: [batch_size,num_pairs,embedding_size].
174 Returns:
175 outer_product(torch.FloatTensor): The outer product of input tensor. shape of [batch_size, num_pairs]
176 """
177 row = []
178 col = []
179 for i in range(self.num_feature_field - 1):
180 for j in range(i + 1, self.num_feature_field):
181 row.append(i)
182 col.append(j)
183 p = feat_emb[:, row] # [batch_size, num_pairs, emb_dim]
184 q = feat_emb[:, col] # [batch_size, num_pairs, emb_dim]
186 # -------------------------
188 p.unsqueeze_(dim=1) # [batch_size, 1, num_pairs, emb_dim]
190 p = torch.mul(p, self.kernel.unsqueeze(0)) # [batch_size,emb_dim,num_pairs,emb_dim]
191 p = torch.sum(p, dim=-1) # [batch_size,emb_dim,num_pairs]
192 p = torch.transpose(p, 2, 1) # [batch_size,num_pairs,emb_dim]
194 outer_product = p * q
195 return outer_product.sum(dim=-1) # [batch_size,num_pairs]