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
« 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
5# UPDATE
6# @Time : 2020/10/2
7# @Author : Jingsen Zhang
8# @Email : zhangjingsen@ruc.edu.cn
10r"""Caser
11################################################
13Reference:
14 Jiaxi Tang et al., "Personalized Top-N Sequential Recommendation via Convolutional Sequence Embedding" in WSDM 2018.
16Reference code:
17 https://github.com/graytowne/caser_pytorch
19""" # noqa: E501
21import torch
22from torch import nn
23from torch.nn import functional as F
24from torch.nn.init import constant_, normal_, xavier_normal_
26from hopwise.model.abstract_recommender import SequentialRecommender
27from hopwise.model.loss import BPRLoss, RegLoss
30class Caser(SequentialRecommender):
31 r"""Caser is a model that incorporate CNN for recommendation.
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
40 def __init__(self, config, dataset):
41 super().__init__(config, dataset)
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"]
51 # load dataset info
52 self.n_users = dataset.user_num
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)
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))
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 )
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)
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()
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']!")
93 # parameters initialization
94 self.apply(self._init_weights)
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)
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)
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
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
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
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
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)
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)
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
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
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