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
« 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
5r"""NextItNet
6################################################
8Reference:
9 Fajie Yuan et al., "A Simple Convolutional Generative Network for Next Item Recommendation" in WSDM 2019.
11Reference code:
12 - https://github.com/fajieyuan/nextitnet
13 - https://github.com/initlisk/nextitnet_pytorch
15"""
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_
23from hopwise.model.abstract_recommender import SequentialRecommender
24from hopwise.model.loss import BPRLoss, RegLoss
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.
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
40 def __init__(self, config, dataset):
41 super().__init__(config, dataset)
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"]
52 # define layers and loss
53 self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0)
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)
67 # fully-connected layer
68 self.final_layer = nn.Linear(self.residual_channels, self.embedding_size)
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()
78 # parameters initialization
79 self.apply(self._init_weights)
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)
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
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
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
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
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
144class ResidualBlock_a(nn.Module):
145 r"""Residual block (a) in the paper"""
147 def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None):
148 super().__init__()
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)
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 )
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)
166 self.dilation = dilation
167 self.kernel_size = kernel_size
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)
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)
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
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
196class ResidualBlock_b(nn.Module):
197 r"""Residual block (b) in the paper"""
199 def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None):
200 super().__init__()
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)
219 self.dilation = dilation
220 self.kernel_size = kernel_size
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
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