Coverage for hopwise/model/sequential_recommender/caser.py: 92%

102 statements  

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

1# @Time : 2020/9/21 

2# @Author : Jingsen Zhang 

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

4 

5# UPDATE 

6# @Time : 2020/10/2 

7# @Author : Jingsen Zhang 

8# @Email : zhangjingsen@ruc.edu.cn 

9 

10r"""Caser 

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

12 

13Reference: 

14 Jiaxi Tang et al., "Personalized Top-N Sequential Recommendation via Convolutional Sequence Embedding" in WSDM 2018. 

15 

16Reference code: 

17 https://github.com/graytowne/caser_pytorch 

18 

19""" # noqa: E501 

20 

21import torch 

22from torch import nn 

23from torch.nn import functional as F 

24from torch.nn.init import constant_, normal_, xavier_normal_ 

25 

26from hopwise.model.abstract_recommender import SequentialRecommender 

27from hopwise.model.loss import BPRLoss, RegLoss 

28 

29 

30class Caser(SequentialRecommender): 

31 r"""Caser is a model that incorporate CNN for recommendation. 

32 

33 Note: 

34 We did not use the sliding window to generate training instances as in the paper, in order that 

35 the generation method we used is common to other sequential models. 

36 For comparison with other models, we set the parameter T in the paper as 1. 

37 In addition, to prevent excessive CNN layers (ValueError: Training loss is nan), please make sure the parameters MAX_ITEM_LIST_LENGTH small, such as 10. 

38 """ # noqa: E501 

39 

40 def __init__(self, config, dataset): 

41 super().__init__(config, dataset) 

42 

43 # load parameters info 

44 self.embedding_size = config["embedding_size"] 

45 self.loss_type = config["loss_type"] 

46 self.n_h = config["nh"] 

47 self.n_v = config["nv"] 

48 self.dropout_prob = config["dropout_prob"] 

49 self.reg_weight = config["reg_weight"] 

50 

51 # load dataset info 

52 self.n_users = dataset.user_num 

53 

54 # define layers and loss 

55 self.user_embedding = nn.Embedding(self.n_users, self.embedding_size, padding_idx=0) 

56 self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) 

57 

58 # vertical conv layer 

59 self.conv_v = nn.Conv2d(in_channels=1, out_channels=self.n_v, kernel_size=(self.max_seq_length, 1)) 

60 

61 # horizontal conv layer 

62 lengths = [i + 1 for i in range(self.max_seq_length)] 

63 self.conv_h = nn.ModuleList( 

64 [ 

65 nn.Conv2d( 

66 in_channels=1, 

67 out_channels=self.n_h, 

68 kernel_size=(i, self.embedding_size), 

69 ) 

70 for i in lengths 

71 ] 

72 ) 

73 

74 # fully-connected layer 

75 self.fc1_dim_v = self.n_v * self.embedding_size 

76 self.fc1_dim_h = self.n_h * len(lengths) 

77 fc1_dim_in = self.fc1_dim_v + self.fc1_dim_h 

78 self.fc1 = nn.Linear(fc1_dim_in, self.embedding_size) 

79 self.fc2 = nn.Linear(self.embedding_size + self.embedding_size, self.embedding_size) 

80 

81 self.dropout = nn.Dropout(self.dropout_prob) 

82 self.ac_conv = nn.ReLU() 

83 self.ac_fc = nn.ReLU() 

84 self.reg_loss = RegLoss() 

85 

86 if self.loss_type == "BPR": 

87 self.loss_fct = BPRLoss() 

88 elif self.loss_type == "CE": 

89 self.loss_fct = nn.CrossEntropyLoss() 

90 else: 

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

92 

93 # parameters initialization 

94 self.apply(self._init_weights) 

95 

96 def _init_weights(self, module): 

97 if isinstance(module, nn.Embedding): 

98 normal_(module.weight.data, 0, 1.0 / module.embedding_dim) 

99 elif isinstance(module, nn.Linear): 

100 xavier_normal_(module.weight.data) 

101 if module.bias is not None: 

102 constant_(module.bias.data, 0) 

103 

104 def forward(self, user, item_seq): 

105 # Embedding Look-up 

106 # use unsqueeze() to get a 4-D input for convolution layers. (batch_size * 1 * max_length * embedding_size) 

107 item_seq_emb = self.item_embedding(item_seq).unsqueeze(1) 

108 user_emb = self.user_embedding(user).squeeze(1) 

109 

110 # Convolutional Layers 

111 out, out_h, out_v = None, None, None 

112 # vertical conv layer 

113 if self.n_v: 

114 out_v = self.conv_v(item_seq_emb) 

115 out_v = out_v.view(-1, self.fc1_dim_v) # prepare for fully connect 

116 

117 # horizontal conv layer 

118 out_hs = list() 

119 if self.n_h: 

120 for conv in self.conv_h: 

121 conv_out = self.ac_conv(conv(item_seq_emb).squeeze(3)) 

122 pool_out = F.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) 

123 out_hs.append(pool_out) 

124 out_h = torch.cat(out_hs, 1) # prepare for fully connect 

125 

126 # Fully-connected Layers 

127 out = torch.cat([out_v, out_h], 1) 

128 # apply dropout 

129 out = self.dropout(out) 

130 # fully-connected layer 

131 z = self.ac_fc(self.fc1(out)) 

132 x = torch.cat([z, user_emb], 1) 

133 seq_output = self.ac_fc(self.fc2(x)) 

134 # the hidden_state of the predicted item, size:(batch_size * hidden_size) 

135 return seq_output 

136 

137 def reg_loss_conv_h(self): 

138 r"""L2 loss on conv_h""" 

139 loss_conv_h = 0 

140 for name, parm in self.conv_h.named_parameters(): 

141 if name.endswith("weight"): 

142 loss_conv_h = loss_conv_h + parm.norm(2) 

143 return self.reg_weight * loss_conv_h 

144 

145 def calculate_loss(self, interaction): 

146 item_seq = interaction[self.ITEM_SEQ] 

147 user = interaction[self.USER_ID] 

148 seq_output = self.forward(user, item_seq) 

149 pos_items = interaction[self.POS_ITEM_ID] 

150 if self.loss_type == "BPR": 

151 neg_items = interaction[self.NEG_ITEM_ID] 

152 pos_items_emb = self.item_embedding(pos_items) 

153 neg_items_emb = self.item_embedding(neg_items) 

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

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

156 loss = self.loss_fct(pos_score, neg_score) 

157 

158 else: # self.loss_type = 'CE' 

159 test_item_emb = self.item_embedding.weight 

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

161 loss = self.loss_fct(logits, pos_items) 

162 

163 reg_loss = self.reg_loss( 

164 [ 

165 self.user_embedding.weight, 

166 self.item_embedding.weight, 

167 self.conv_v.weight, 

168 self.fc1.weight, 

169 self.fc2.weight, 

170 ] 

171 ) 

172 loss = loss + self.reg_weight * reg_loss + self.reg_loss_conv_h() 

173 return loss 

174 

175 def predict(self, interaction): 

176 item_seq = interaction[self.ITEM_SEQ] 

177 user = interaction[self.USER_ID] 

178 test_item = interaction[self.ITEM_ID] 

179 seq_output = self.forward(user, item_seq) 

180 test_item_emb = self.item_embedding(test_item) 

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

182 return scores 

183 

184 def full_sort_predict(self, interaction): 

185 item_seq = interaction[self.ITEM_SEQ] 

186 user = interaction[self.USER_ID] 

187 seq_output = self.forward(user, item_seq) 

188 test_items_emb = self.item_embedding.weight 

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

190 return scores