Coverage for hopwise/model/sequential_recommender/fdsa.py: 85%

111 statements  

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

1# @Time : 2020/9/18 11:27 

2# @Author : Hui Wang 

3# @Email : hui.wang@ruc.edu.cn 

4 

5r"""FDSA 

6################################################ 

7 

8Reference: 

9 Tingting Zhang et al. "Feature-level Deeper Self-Attention Network for Sequential Recommendation." 

10 In IJCAI 2019 

11 

12""" 

13 

14import torch 

15from torch import nn 

16 

17from hopwise.model.abstract_recommender import SequentialRecommender 

18from hopwise.model.layers import ( 

19 FeatureSeqEmbLayer, 

20 TransformerEncoder, 

21 VanillaAttention, 

22) 

23from hopwise.model.loss import BPRLoss 

24 

25 

26class FDSA(SequentialRecommender): 

27 r"""FDSA is similar with the GRU4RecF implemented in hopwise, which uses two different Transformer encoders to 

28 encode items and features respectively and concatenates the two subparts' outputs as the final output. 

29 

30 """ 

31 

32 def __init__(self, config, dataset): 

33 super().__init__(config, dataset) 

34 

35 # load parameters info 

36 self.n_layers = config["n_layers"] 

37 self.n_heads = config["n_heads"] 

38 self.hidden_size = config["hidden_size"] # same as embedding_size 

39 self.inner_size = config["inner_size"] # the dimensionality in feed-forward layer 

40 self.hidden_dropout_prob = config["hidden_dropout_prob"] 

41 self.attn_dropout_prob = config["attn_dropout_prob"] 

42 self.hidden_act = config["hidden_act"] 

43 self.layer_norm_eps = config["layer_norm_eps"] 

44 

45 self.selected_features = config["selected_features"] 

46 self.pooling_mode = config["pooling_mode"] 

47 self.device = config["device"] 

48 self.num_feature_field = len(config["selected_features"]) 

49 

50 self.initializer_range = config["initializer_range"] 

51 self.loss_type = config["loss_type"] 

52 

53 # define layers and loss 

54 self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) 

55 self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) 

56 

57 self.feature_embed_layer = FeatureSeqEmbLayer( 

58 dataset, 

59 self.hidden_size, 

60 self.selected_features, 

61 self.pooling_mode, 

62 self.device, 

63 ) 

64 

65 self.item_trm_encoder = TransformerEncoder( 

66 n_layers=self.n_layers, 

67 n_heads=self.n_heads, 

68 hidden_size=self.hidden_size, 

69 inner_size=self.inner_size, 

70 hidden_dropout_prob=self.hidden_dropout_prob, 

71 attn_dropout_prob=self.attn_dropout_prob, 

72 hidden_act=self.hidden_act, 

73 layer_norm_eps=self.layer_norm_eps, 

74 ) 

75 

76 self.feature_att_layer = VanillaAttention(self.hidden_size, self.hidden_size) 

77 # For simplicity, we use same architecture for item_trm and feature_trm 

78 self.feature_trm_encoder = TransformerEncoder( 

79 n_layers=self.n_layers, 

80 n_heads=self.n_heads, 

81 hidden_size=self.hidden_size, 

82 inner_size=self.inner_size, 

83 hidden_dropout_prob=self.hidden_dropout_prob, 

84 attn_dropout_prob=self.attn_dropout_prob, 

85 hidden_act=self.hidden_act, 

86 layer_norm_eps=self.layer_norm_eps, 

87 ) 

88 

89 self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) 

90 self.dropout = nn.Dropout(self.hidden_dropout_prob) 

91 self.concat_layer = nn.Linear(self.hidden_size * 2, self.hidden_size) 

92 if self.loss_type == "BPR": 

93 self.loss_fct = BPRLoss() 

94 elif self.loss_type == "CE": 

95 self.loss_fct = nn.CrossEntropyLoss() 

96 else: 

97 raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") 

98 

99 # parameters initialization 

100 self.apply(self._init_weights) 

101 self.other_parameter_name = ["feature_embed_layer"] 

102 

103 def _init_weights(self, module): 

104 """Initialize the weights""" 

105 if isinstance(module, (nn.Linear, nn.Embedding)): 

106 # Slightly different from the TF version which uses truncated_normal for initialization 

107 # cf https://github.com/pytorch/pytorch/pull/5617 

108 module.weight.data.normal_(mean=0.0, std=self.initializer_range) 

109 elif isinstance(module, nn.LayerNorm): 

110 module.bias.data.zero_() 

111 module.weight.data.fill_(1.0) 

112 if isinstance(module, nn.Linear) and module.bias is not None: 

113 module.bias.data.zero_() 

114 

115 def forward(self, item_seq, item_seq_len): 

116 item_emb = self.item_embedding(item_seq) 

117 

118 position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) 

119 position_ids = position_ids.unsqueeze(0).expand_as(item_seq) 

120 position_embedding = self.position_embedding(position_ids) 

121 

122 # get item_trm_input 

123 # item position add position embedding 

124 item_emb = item_emb + position_embedding 

125 item_emb = self.LayerNorm(item_emb) 

126 item_trm_input = self.dropout(item_emb) 

127 

128 sparse_embedding, dense_embedding = self.feature_embed_layer(None, item_seq) 

129 sparse_embedding = sparse_embedding["item"] 

130 dense_embedding = dense_embedding["item"] 

131 

132 # concat the sparse embedding and float embedding 

133 feature_table = [] 

134 if sparse_embedding is not None: 

135 feature_table.append(sparse_embedding) 

136 if dense_embedding is not None: 

137 feature_table.append(dense_embedding) 

138 

139 # [batch len num_features hidden_size] 

140 feature_table = torch.cat(feature_table, dim=-2) 

141 

142 # feature_emb [batch len hidden] 

143 # weight [batch len num_features] 

144 # if only one feature, the weight would be 1.0 

145 feature_emb, attn_weight = self.feature_att_layer(feature_table) 

146 # feature position add position embedding 

147 feature_emb = feature_emb + position_embedding 

148 feature_emb = self.LayerNorm(feature_emb) 

149 feature_trm_input = self.dropout(feature_emb) 

150 

151 extended_attention_mask = self.get_attention_mask(item_seq) 

152 

153 item_trm_output = self.item_trm_encoder( 

154 item_trm_input, extended_attention_mask, output_all_encoded_layers=True 

155 ) 

156 item_output = item_trm_output[-1] 

157 

158 feature_trm_output = self.feature_trm_encoder( 

159 feature_trm_input, extended_attention_mask, output_all_encoded_layers=True 

160 ) # [B Len H] 

161 feature_output = feature_trm_output[-1] 

162 

163 item_output = self.gather_indexes(item_output, item_seq_len - 1) # [B H] 

164 feature_output = self.gather_indexes(feature_output, item_seq_len - 1) # [B H] 

165 

166 output_concat = torch.cat((item_output, feature_output), -1) # [B 2*H] 

167 output = self.concat_layer(output_concat) 

168 output = self.LayerNorm(output) 

169 seq_output = self.dropout(output) 

170 return seq_output # [B H] 

171 

172 def calculate_loss(self, interaction): 

173 item_seq = interaction[self.ITEM_SEQ] 

174 item_seq_len = interaction[self.ITEM_SEQ_LEN] 

175 seq_output = self.forward(item_seq, item_seq_len) 

176 pos_items = interaction[self.POS_ITEM_ID] 

177 if self.loss_type == "BPR": 

178 neg_items = interaction[self.NEG_ITEM_ID] 

179 pos_items_emb = self.item_embedding(pos_items) 

180 neg_items_emb = self.item_embedding(neg_items) 

181 pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) # [B] 

182 neg_score = torch.sum(seq_output * neg_items_emb, dim=-1) # [B] 

183 loss = self.loss_fct(pos_score, neg_score) 

184 return loss 

185 else: # self.loss_type = 'CE' 

186 test_item_emb = self.item_embedding.weight 

187 logits = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) 

188 loss = self.loss_fct(logits, pos_items) 

189 return loss 

190 

191 def predict(self, interaction): 

192 item_seq = interaction[self.ITEM_SEQ] 

193 item_seq_len = interaction[self.ITEM_SEQ_LEN] 

194 test_item = interaction[self.ITEM_ID] 

195 seq_output = self.forward(item_seq, item_seq_len) 

196 test_item_emb = self.item_embedding(test_item) 

197 scores = torch.mul(seq_output, test_item_emb).sum(dim=1) # [B] 

198 return scores 

199 

200 def full_sort_predict(self, interaction): 

201 item_seq = interaction[self.ITEM_SEQ] 

202 item_seq_len = interaction[self.ITEM_SEQ_LEN] 

203 seq_output = self.forward(item_seq, item_seq_len) 

204 test_items_emb = self.item_embedding.weight 

205 scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] 

206 return scores