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
« 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
5# UPDATE
6# @Time : 2020/10/21
7# @Author : Zhichao Feng
8# @email : fzcbupt@gmail.com
10r"""xDeepFM
11################################################
12Reference:
13 Jianxun Lian at al. "xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems."
14 in SIGKDD 2018.
16Reference code:
17 - https://github.com/Leavingseason/xDeepFM
18 - https://github.com/shenweichen/DeepCTR-Torch
19"""
21import torch
22from torch import nn
23from torch.nn.init import constant_, xavier_normal_
25from hopwise.model.abstract_recommender import ContextRecommender
26from hopwise.model.layers import MLPLayers, activation_layer
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 """
35 def __init__(self, config, dataset):
36 super().__init__(config, dataset)
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"])
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 )
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)
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)
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]
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)
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)
88 def reg_loss(self, parameters):
89 """Calculate the L2 normalization loss of parameters in a certain layer.
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
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.
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
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
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)
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.
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.
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.
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)
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)
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
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
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)
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))
178 # Get predicted score.
179 y_p = self.first_order_linear(interaction) + cin_output + dnn_output
181 return y_p.squeeze(1)
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
189 def predict(self, interaction):
190 return self.sigmoid(self.forward(interaction))