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

1# @Time : 2020/9/22 10:57 

2# @Author : Zihan Lin 

3# @Email : zhlin@ruc.edu.cn 

4# @File : pnn.py 

5 

6r"""PNN 

7################################################ 

8Reference: 

9 Qu Y et al. "Product-based neural networks for user response prediction." in ICDM 2016 

10 

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 

14 

15""" 

16 

17import torch 

18from torch import nn 

19from torch.nn.init import constant_, xavier_normal_ 

20 

21from hopwise.model.abstract_recommender import ContextRecommender 

22from hopwise.model.layers import MLPLayers 

23 

24 

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 

28 

29 """ 

30 

31 def __init__(self, config, dataset): 

32 super().__init__(config, dataset) 

33 

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"] 

40 

41 self.num_pair = int(self.num_feature_field * (self.num_feature_field - 1) / 2) 

42 

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) 

48 

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() 

58 

59 # parameters initialization 

60 self.apply(self._init_weights) 

61 

62 def reg_loss(self): 

63 """Calculate the L2 normalization loss of model parameters. 

64 Including weight matrices of mlp layers. 

65 

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 

74 

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) 

82 

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] 

97 

98 output = self.predict_layer(self.mlp_layers(output)) # [batch_size,1] 

99 return output.squeeze(-1) 

100 

101 def calculate_loss(self, interaction): 

102 label = interaction[self.LABEL] 

103 output = self.forward(interaction) 

104 

105 return self.loss(output, label) + self.reg_loss() 

106 

107 def predict(self, interaction): 

108 return self.sigmoid(self.forward(interaction)) 

109 

110 

111class InnerProductLayer(nn.Module): 

112 """InnerProduct Layer used in PNN that compute the element-wise 

113 product or inner product between feature vectors. 

114 

115 """ 

116 

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) 

125 

126 def forward(self, feat_emb): 

127 """Args: 

128 feat_emb(torch.FloatTensor) :3D tensor with shape: [batch_size,num_pairs,embedding_size]. 

129 

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] 

142 

143 inner_product = p * q 

144 

145 return inner_product.sum(dim=-1) # [batch_size, num_pairs] 

146 

147 

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 """ 

152 

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__() 

160 

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 

164 

165 self.kernel = nn.Parameter(torch.rand(embed_size, num_pairs, embed_size), requires_grad=True) 

166 nn.init.xavier_uniform_(self.kernel) 

167 

168 self.to(device) 

169 

170 def forward(self, feat_emb): 

171 """Args: 

172 feat_emb(torch.FloatTensor) :3D tensor with shape: [batch_size,num_pairs,embedding_size]. 

173 

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] 

185 

186 # ------------------------- 

187 

188 p.unsqueeze_(dim=1) # [batch_size, 1, num_pairs, emb_dim] 

189 

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] 

193 

194 outer_product = p * q 

195 return outer_product.sum(dim=-1) # [batch_size,num_pairs]