Coverage for hopwise/model/sequential_recommender/nextitnet.py: 76%

131 statements  

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

1# @Time : 2020/10/2 

2# @Author : Jingsen Zhang 

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

4 

5r"""NextItNet 

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

7 

8Reference: 

9 Fajie Yuan et al., "A Simple Convolutional Generative Network for Next Item Recommendation" in WSDM 2019. 

10 

11Reference code: 

12 - https://github.com/fajieyuan/nextitnet 

13 - https://github.com/initlisk/nextitnet_pytorch 

14 

15""" 

16 

17import numpy as np 

18import torch 

19from torch import nn 

20from torch.nn import functional as F 

21from torch.nn.init import constant_, uniform_, xavier_normal_ 

22 

23from hopwise.model.abstract_recommender import SequentialRecommender 

24from hopwise.model.loss import BPRLoss, RegLoss 

25 

26 

27class NextItNet(SequentialRecommender): 

28 r"""The network architecture of the NextItNet model is formed of a stack of holed convolutional layers, which can 

29 efficiently increase the receptive fields without relying on the pooling operation. 

30 Also residual block structure is used to ease the optimization for much deeper networks. 

31 

32 Note: 

33 As paper said, for comparison purpose, we only predict the next one item in our evaluation, 

34 and then stop the generating process. Although the number of parameters in residual block (a) is less 

35 than it in residual block (b), the performance of b is better than a. 

36 So in our model, we use residual block (b). 

37 In addition, when dilations is not equal to 1, the training may be slow. To speed up the efficiency, please set the parameters "reproducibility" False. 

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.residual_channels = config["embedding_size"] 

46 self.block_num = config["block_num"] 

47 self.dilations = config["dilations"] * self.block_num 

48 self.kernel_size = config["kernel_size"] 

49 self.reg_weight = config["reg_weight"] 

50 self.loss_type = config["loss_type"] 

51 

52 # define layers and loss 

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

54 

55 # residual blocks dilations in blocks:[1,2,4,8,1,2,4,8,...] 

56 rb = [ 

57 ResidualBlock_b( 

58 self.residual_channels, 

59 self.residual_channels, 

60 kernel_size=self.kernel_size, 

61 dilation=dilation, 

62 ) 

63 for dilation in self.dilations 

64 ] 

65 self.residual_blocks = nn.Sequential(*rb) 

66 

67 # fully-connected layer 

68 self.final_layer = nn.Linear(self.residual_channels, self.embedding_size) 

69 

70 if self.loss_type == "BPR": 

71 self.loss_fct = BPRLoss() 

72 elif self.loss_type == "CE": 

73 self.loss_fct = nn.CrossEntropyLoss() 

74 else: 

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

76 self.reg_loss = RegLoss() 

77 

78 # parameters initialization 

79 self.apply(self._init_weights) 

80 

81 def _init_weights(self, module): 

82 if isinstance(module, nn.Embedding): 

83 stdv = np.sqrt(1.0 / self.n_items) 

84 uniform_(module.weight.data, -stdv, stdv) 

85 elif isinstance(module, nn.Linear): 

86 xavier_normal_(module.weight.data) 

87 if module.bias is not None: 

88 constant_(module.bias.data, 0.1) 

89 

90 def forward(self, item_seq): 

91 item_seq_emb = self.item_embedding(item_seq) # [batch_size, seq_len, embed_size] 

92 # Residual locks 

93 dilate_outputs = self.residual_blocks(item_seq_emb) 

94 hidden = dilate_outputs[:, -1, :].view(-1, self.residual_channels) # [batch_size, embed_size] 

95 seq_output = self.final_layer(hidden) # [batch_size, embedding_size] 

96 return seq_output 

97 

98 def reg_loss_rb(self): 

99 r"""L2 loss on residual blocks""" 

100 loss_rb = 0 

101 if self.reg_weight > 0.0: 

102 for name, parm in self.residual_blocks.named_parameters(): 

103 if name.endswith("weight"): 

104 loss_rb += torch.norm(parm, 2) 

105 return self.reg_weight * loss_rb 

106 

107 def calculate_loss(self, interaction): 

108 item_seq = interaction[self.ITEM_SEQ] 

109 # item_seq_len = interaction[self.ITEM_SEQ_LEN] 

110 seq_output = self.forward(item_seq) 

111 pos_items = interaction[self.POS_ITEM_ID] 

112 if self.loss_type == "BPR": 

113 neg_items = interaction[self.NEG_ITEM_ID] 

114 pos_items_emb = self.item_embedding(pos_items) 

115 neg_items_emb = self.item_embedding(neg_items) 

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

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

118 loss = self.loss_fct(pos_score, neg_score) 

119 else: # self.loss_type = 'CE' 

120 test_item_emb = self.item_embedding.weight 

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

122 loss = self.loss_fct(logits, pos_items) 

123 reg_loss = self.reg_loss([self.item_embedding.weight, self.final_layer.weight]) 

124 loss = loss + self.reg_weight * reg_loss + self.reg_loss_rb() 

125 return loss 

126 

127 def predict(self, interaction): 

128 item_seq = interaction[self.ITEM_SEQ] 

129 test_item = interaction[self.ITEM_ID] 

130 seq_output = self.forward(item_seq) 

131 test_item_emb = self.item_embedding(test_item) 

132 scores = torch.mul(seq_output, test_item_emb).sum(dim=1) 

133 return scores 

134 

135 def full_sort_predict(self, interaction): 

136 item_seq = interaction[self.ITEM_SEQ] 

137 # item_seq_len = interaction[self.ITEM_SEQ_LEN] 

138 seq_output = self.forward(item_seq) 

139 test_items_emb = self.item_embedding.weight 

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

141 return scores 

142 

143 

144class ResidualBlock_a(nn.Module): 

145 r"""Residual block (a) in the paper""" 

146 

147 def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None): 

148 super().__init__() 

149 

150 half_channel = out_channel // 2 

151 self.ln1 = nn.LayerNorm(out_channel, eps=1e-8) 

152 self.conv1 = nn.Conv2d(in_channel, half_channel, kernel_size=(1, 1), padding=0) 

153 

154 self.ln2 = nn.LayerNorm(half_channel, eps=1e-8) 

155 self.conv2 = nn.Conv2d( 

156 half_channel, 

157 half_channel, 

158 kernel_size=(1, kernel_size), 

159 padding=0, 

160 dilation=dilation, 

161 ) 

162 

163 self.ln3 = nn.LayerNorm(half_channel, eps=1e-8) 

164 self.conv3 = nn.Conv2d(half_channel, out_channel, kernel_size=(1, 1), padding=0) 

165 

166 self.dilation = dilation 

167 self.kernel_size = kernel_size 

168 

169 def forward(self, x): # x: [batch_size, seq_len, embed_size] 

170 out = F.relu(self.ln1(x)) 

171 out = out.permute(0, 2, 1).unsqueeze(2) 

172 out = self.conv1(out).squeeze(2).permute(0, 2, 1) 

173 

174 out2 = F.relu(self.ln2(out)) 

175 out2 = self.conv_pad(out2, self.dilation) 

176 out2 = self.conv2(out2).squeeze(2).permute(0, 2, 1) 

177 

178 out3 = F.relu(self.ln3(out2)) 

179 out3 = out3.permute(0, 2, 1).unsqueeze(2) 

180 out3 = self.conv3(out3).squeeze(2).permute(0, 2, 1) 

181 return out3 + x 

182 

183 def conv_pad(self, x, dilation): # x: [batch_size, seq_len, embed_size] 

184 r"""Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout 

185 trick for the 1D dilated convolution to prevent the network from seeing the future items. 

186 Also the One-dimensional transformation is completed in this function. 

187 """ 

188 inputs_pad = x.permute(0, 2, 1) # [batch_size, embed_size, seq_len] 

189 inputs_pad = inputs_pad.unsqueeze(2) # [batch_size, embed_size, 1, seq_len] 

190 pad = nn.ZeroPad2d(((self.kernel_size - 1) * dilation, 0, 0, 0)) 

191 # padding operation args:(left,right,top,bottom) 

192 inputs_pad = pad(inputs_pad) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] 

193 return inputs_pad 

194 

195 

196class ResidualBlock_b(nn.Module): 

197 r"""Residual block (b) in the paper""" 

198 

199 def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None): 

200 super().__init__() 

201 

202 self.conv1 = nn.Conv2d( 

203 in_channel, 

204 out_channel, 

205 kernel_size=(1, kernel_size), 

206 padding=0, 

207 dilation=dilation, 

208 ) 

209 self.ln1 = nn.LayerNorm(out_channel, eps=1e-8) 

210 self.conv2 = nn.Conv2d( 

211 out_channel, 

212 out_channel, 

213 kernel_size=(1, kernel_size), 

214 padding=0, 

215 dilation=dilation * 2, 

216 ) 

217 self.ln2 = nn.LayerNorm(out_channel, eps=1e-8) 

218 

219 self.dilation = dilation 

220 self.kernel_size = kernel_size 

221 

222 def forward(self, x): # x: [batch_size, seq_len, embed_size] 

223 x_pad = self.conv_pad(x, self.dilation) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] 

224 out = self.conv1(x_pad).squeeze(2).permute(0, 2, 1) 

225 # [batch_size, seq_len+(self.kernel_size-1)*dilations-kernel_size+1, embed_size] 

226 out = F.relu(self.ln1(out)) 

227 out_pad = self.conv_pad(out, self.dilation * 2) 

228 out2 = self.conv2(out_pad).squeeze(2).permute(0, 2, 1) 

229 out2 = F.relu(self.ln2(out2)) 

230 return out2 + x 

231 

232 def conv_pad(self, x, dilation): 

233 r"""Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout 

234 trick for the 1D dilated convolution to prevent the network from seeing the future items. 

235 Also the One-dimensional transformation is completed in this function. 

236 """ 

237 inputs_pad = x.permute(0, 2, 1) 

238 inputs_pad = inputs_pad.unsqueeze(2) 

239 pad = nn.ZeroPad2d(((self.kernel_size - 1) * dilation, 0, 0, 0)) 

240 inputs_pad = pad(inputs_pad) 

241 return inputs_pad