Coverage for hopwise/model/context_aware_recommender/xdeepfm.py: 97%

95 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-30 13:25 +0000

1# @Time : 2020/10/13 

2# @Author : Zhichao Feng 

3# @Email : fzcbupt@gmail.com 

4 

5# UPDATE 

6# @Time : 2020/10/21 

7# @Author : Zhichao Feng 

8# @email : fzcbupt@gmail.com 

9 

10r"""xDeepFM 

11################################################ 

12Reference: 

13 Jianxun Lian at al. "xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems." 

14 in SIGKDD 2018. 

15 

16Reference code: 

17 - https://github.com/Leavingseason/xDeepFM 

18 - https://github.com/shenweichen/DeepCTR-Torch 

19""" 

20 

21import torch 

22from torch import nn 

23from torch.nn.init import constant_, xavier_normal_ 

24 

25from hopwise.model.abstract_recommender import ContextRecommender 

26from hopwise.model.layers import MLPLayers, activation_layer 

27 

28 

29class xDeepFM(ContextRecommender): 

30 """xDeepFM combines a CIN (Compressed Interaction Network) with a classical DNN. 

31 The model is able to learn certain bounded-degree feature interactions explicitly; 

32 Besides, it can also learn arbitrary low- and high-order feature interactions implicitly. 

33 """ 

34 

35 def __init__(self, config, dataset): 

36 super().__init__(config, dataset) 

37 

38 # load parameters info 

39 self.mlp_hidden_size = config["mlp_hidden_size"] 

40 self.reg_weight = config["reg_weight"] 

41 self.dropout_prob = config["dropout_prob"] 

42 self.direct = config["direct"] 

43 self.cin_layer_size = temp_cin_size = list(config["cin_layer_size"]) 

44 

45 # Check whether the size of the CIN layer is legal. 

46 if not self.direct: 

47 self.cin_layer_size = list(map(lambda x: int(x // 2 * 2), temp_cin_size)) 

48 if self.cin_layer_size[:-1] != temp_cin_size[:-1]: 

49 self.logger.warning( 

50 "Layer size of CIN should be even except for the last layer when direct is True." 

51 f"It is changed to {self.cin_layer_size}" 

52 ) 

53 

54 # Create a convolutional layer for each CIN layer 

55 self.conv1d_list = nn.ModuleList() 

56 self.field_nums = [self.num_feature_field] 

57 for i, layer_size in enumerate(self.cin_layer_size): 

58 conv1d = nn.Conv1d(self.field_nums[-1] * self.field_nums[0], layer_size, 1) 

59 self.conv1d_list.append(conv1d) 

60 if self.direct: 

61 self.field_nums.append(layer_size) 

62 else: 

63 self.field_nums.append(layer_size // 2) 

64 

65 # Create MLP layer 

66 size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + [1] 

67 self.mlp_layers = MLPLayers(size_list, dropout=self.dropout_prob) 

68 

69 # Get the output size of CIN 

70 if self.direct: 

71 self.final_len = sum(self.cin_layer_size) 

72 else: 

73 self.final_len = sum(self.cin_layer_size[:-1]) // 2 + self.cin_layer_size[-1] 

74 

75 self.cin_linear = nn.Linear(self.final_len, 1) 

76 self.sigmoid = nn.Sigmoid() 

77 self.loss = nn.BCEWithLogitsLoss() 

78 self.apply(self._init_weights) 

79 

80 def _init_weights(self, module): 

81 if isinstance(module, nn.Embedding) or isinstance(module, nn.Conv1d): 

82 xavier_normal_(module.weight.data) 

83 elif isinstance(module, nn.Linear): 

84 xavier_normal_(module.weight.data) 

85 if module.bias is not None: 

86 constant_(module.bias.data, 0) 

87 

88 def reg_loss(self, parameters): 

89 """Calculate the L2 normalization loss of parameters in a certain layer. 

90 

91 Returns: 

92 loss(torch.FloatTensor): The L2 Loss tensor. shape of [1,] 

93 """ 

94 reg_loss = 0 

95 for name, parm in parameters: 

96 if name.endswith("weight"): 

97 reg_loss = reg_loss + parm.norm(2) 

98 return reg_loss 

99 

100 def calculate_reg_loss(self): 

101 """Calculate the final L2 normalization loss of model parameters. 

102 Including weight matrices of mlp layers, linear layer and convolutional layers. 

103 

104 Returns: 

105 loss(torch.FloatTensor): The L2 Loss tensor. shape of [1,] 

106 """ 

107 l2_reg = 0 

108 l2_reg = l2_reg + self.reg_loss(self.mlp_layers.named_parameters()) 

109 l2_reg = l2_reg + self.reg_loss(self.first_order_linear.named_parameters()) 

110 for conv1d in self.conv1d_list: 

111 l2_reg += self.reg_loss(conv1d.named_parameters()) 

112 return l2_reg 

113 

114 def compressed_interaction_network(self, input_features, activation="ReLU"): 

115 r"""For k-th CIN layer, the output :math:`X_k` is calculated via 

116 

117 .. math:: 

118 x_{h,*}^{k} = \sum_{i=1}^{H_k-1} \sum_{j=1}^{m}W_{i,j}^{k,h}(X_{i,*}^{k-1} \circ x_{j,*}^0) 

119 

120 :math:`H_k` donates the number of feature vectors in the k-th layer, 

121 :math:`1 \le h \le H_k`. 

122 :math:`\circ` donates the Hadamard product. 

123 

124 And Then, We apply sum pooling on each feature map of the hidden layer. 

125 Finally, All pooling vectors from hidden layers are concatenated. 

126 

127 Args: 

128 input_features(torch.Tensor): [batch_size, field_num, embed_dim]. Embedding vectors of all features. 

129 activation(str): name of activation function. 

130 

131 Returns: 

132 torch.Tensor: [batch_size, num_feature_field * embedding_size]. output of CIN layer. 

133 """ 

134 batch_size, _, embedding_size = input_features.shape 

135 hidden_nn_layers = [input_features] 

136 final_result = [] 

137 for i, layer_size in enumerate(self.cin_layer_size): 

138 z_i = torch.einsum("bhd,bmd->bhmd", hidden_nn_layers[-1], hidden_nn_layers[0]) 

139 z_i = z_i.view(batch_size, self.field_nums[0] * self.field_nums[i], embedding_size) 

140 z_i = self.conv1d_list[i](z_i) 

141 

142 # Pass the CIN intermediate result through the activation function. 

143 if activation.lower() == "identity": 

144 output = z_i 

145 else: 

146 activate_func = activation_layer(activation) 

147 if activate_func is None: 

148 output = z_i 

149 else: 

150 output = activate_func(z_i) 

151 

152 # Get the output of the hidden layer. 

153 if self.direct: 

154 direct_connect = output 

155 next_hidden = output 

156 elif i != len(self.cin_layer_size) - 1: 

157 next_hidden, direct_connect = torch.split(output, 2 * [layer_size // 2], 1) 

158 else: 

159 direct_connect = output 

160 next_hidden = 0 

161 

162 final_result.append(direct_connect) 

163 hidden_nn_layers.append(next_hidden) 

164 result = torch.cat(final_result, dim=1) 

165 result = torch.sum(result, -1) 

166 return result 

167 

168 def forward(self, interaction): 

169 # Get the output of CIN. 

170 xdeepfm_input = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] 

171 cin_output = self.compressed_interaction_network(xdeepfm_input) 

172 cin_output = self.cin_linear(cin_output) 

173 

174 # Get the output of MLP layer. 

175 batch_size = xdeepfm_input.shape[0] 

176 dnn_output = self.mlp_layers(xdeepfm_input.view(batch_size, -1)) 

177 

178 # Get predicted score. 

179 y_p = self.first_order_linear(interaction) + cin_output + dnn_output 

180 

181 return y_p.squeeze(1) 

182 

183 def calculate_loss(self, interaction): 

184 label = interaction[self.LABEL] 

185 output = self.forward(interaction) 

186 l2_reg = self.calculate_reg_loss() 

187 return self.loss(output, label) + self.reg_weight * l2_reg 

188 

189 def predict(self, interaction): 

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