replay-rec 0.20.3__py3-none-any.whl → 0.20.3rc0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- replay/__init__.py +1 -1
- replay/experimental/__init__.py +0 -0
- replay/experimental/metrics/__init__.py +62 -0
- replay/experimental/metrics/base_metric.py +603 -0
- replay/experimental/metrics/coverage.py +97 -0
- replay/experimental/metrics/experiment.py +175 -0
- replay/experimental/metrics/hitrate.py +26 -0
- replay/experimental/metrics/map.py +30 -0
- replay/experimental/metrics/mrr.py +18 -0
- replay/experimental/metrics/ncis_precision.py +31 -0
- replay/experimental/metrics/ndcg.py +49 -0
- replay/experimental/metrics/precision.py +22 -0
- replay/experimental/metrics/recall.py +25 -0
- replay/experimental/metrics/rocauc.py +49 -0
- replay/experimental/metrics/surprisal.py +90 -0
- replay/experimental/metrics/unexpectedness.py +76 -0
- replay/experimental/models/__init__.py +50 -0
- replay/experimental/models/admm_slim.py +257 -0
- replay/experimental/models/base_neighbour_rec.py +200 -0
- replay/experimental/models/base_rec.py +1386 -0
- replay/experimental/models/base_torch_rec.py +234 -0
- replay/experimental/models/cql.py +454 -0
- replay/experimental/models/ddpg.py +932 -0
- replay/experimental/models/dt4rec/__init__.py +0 -0
- replay/experimental/models/dt4rec/dt4rec.py +189 -0
- replay/experimental/models/dt4rec/gpt1.py +401 -0
- replay/experimental/models/dt4rec/trainer.py +127 -0
- replay/experimental/models/dt4rec/utils.py +264 -0
- replay/experimental/models/extensions/spark_custom_models/__init__.py +0 -0
- replay/experimental/models/extensions/spark_custom_models/als_extension.py +792 -0
- replay/experimental/models/hierarchical_recommender.py +331 -0
- replay/experimental/models/implicit_wrap.py +131 -0
- replay/experimental/models/lightfm_wrap.py +303 -0
- replay/experimental/models/mult_vae.py +332 -0
- replay/experimental/models/neural_ts.py +986 -0
- replay/experimental/models/neuromf.py +406 -0
- replay/experimental/models/scala_als.py +293 -0
- replay/experimental/models/u_lin_ucb.py +115 -0
- replay/experimental/nn/data/__init__.py +1 -0
- replay/experimental/nn/data/schema_builder.py +102 -0
- replay/experimental/preprocessing/__init__.py +3 -0
- replay/experimental/preprocessing/data_preparator.py +839 -0
- replay/experimental/preprocessing/padder.py +229 -0
- replay/experimental/preprocessing/sequence_generator.py +208 -0
- replay/experimental/scenarios/__init__.py +1 -0
- replay/experimental/scenarios/obp_wrapper/__init__.py +8 -0
- replay/experimental/scenarios/obp_wrapper/obp_optuna_objective.py +74 -0
- replay/experimental/scenarios/obp_wrapper/replay_offline.py +261 -0
- replay/experimental/scenarios/obp_wrapper/utils.py +85 -0
- replay/experimental/scenarios/two_stages/__init__.py +0 -0
- replay/experimental/scenarios/two_stages/reranker.py +117 -0
- replay/experimental/scenarios/two_stages/two_stages_scenario.py +757 -0
- replay/experimental/utils/__init__.py +0 -0
- replay/experimental/utils/logger.py +24 -0
- replay/experimental/utils/model_handler.py +186 -0
- replay/experimental/utils/session_handler.py +44 -0
- {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/METADATA +11 -17
- {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/RECORD +61 -6
- {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/WHEEL +0 -0
- {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/licenses/LICENSE +0 -0
- {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
from replay.utils import TORCH_AVAILABLE
|
|
7
|
+
|
|
8
|
+
from .utils import matrix2df
|
|
9
|
+
|
|
10
|
+
if TORCH_AVAILABLE:
|
|
11
|
+
import torch
|
|
12
|
+
from torch.nn import functional as func
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TrainerConfig:
|
|
19
|
+
"""
|
|
20
|
+
Config holder for trainer
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
epochs = 1
|
|
24
|
+
lr_scheduler = None
|
|
25
|
+
|
|
26
|
+
def __init__(self, **kwargs):
|
|
27
|
+
for key, value in kwargs.items():
|
|
28
|
+
setattr(self, key, value)
|
|
29
|
+
|
|
30
|
+
def update(self, **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Arguments setter
|
|
33
|
+
"""
|
|
34
|
+
for key, value in kwargs.items():
|
|
35
|
+
setattr(self, key, value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Trainer:
|
|
39
|
+
"""
|
|
40
|
+
Trainer for DT4Rec
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
grad_norm_clip = 1.0
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
model,
|
|
48
|
+
train_dataloader,
|
|
49
|
+
tconf,
|
|
50
|
+
val_dataloader=None,
|
|
51
|
+
experiment=None,
|
|
52
|
+
use_cuda=True,
|
|
53
|
+
):
|
|
54
|
+
self.model = model
|
|
55
|
+
self.train_dataloader = train_dataloader
|
|
56
|
+
self.optimizer = tconf.optimizer
|
|
57
|
+
self.epochs = tconf.epochs
|
|
58
|
+
self.lr_scheduler = tconf.lr_scheduler
|
|
59
|
+
assert (val_dataloader is None) == (experiment is None)
|
|
60
|
+
self.val_dataloader = val_dataloader
|
|
61
|
+
self.experiment = experiment
|
|
62
|
+
|
|
63
|
+
# take over whatever gpus are on the system
|
|
64
|
+
self.device = "cpu"
|
|
65
|
+
if use_cuda and torch.cuda.is_available():
|
|
66
|
+
self.device = torch.cuda.current_device()
|
|
67
|
+
self.model = torch.nn.DataParallel(self.model).to(self.device)
|
|
68
|
+
|
|
69
|
+
def _move_batch(self, batch):
|
|
70
|
+
return [elem.to(self.device) for elem in batch]
|
|
71
|
+
|
|
72
|
+
def _train_epoch(self, epoch):
|
|
73
|
+
self.model.train()
|
|
74
|
+
|
|
75
|
+
losses = []
|
|
76
|
+
pbar = tqdm(
|
|
77
|
+
enumerate(self.train_dataloader),
|
|
78
|
+
total=len(self.train_dataloader),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
for iter_, batch in pbar:
|
|
82
|
+
# place data on the correct device
|
|
83
|
+
states, actions, rtgs, timesteps, users = self._move_batch(batch)
|
|
84
|
+
targets = actions
|
|
85
|
+
|
|
86
|
+
# forward the model
|
|
87
|
+
logits = self.model(states, actions, rtgs, timesteps, users)
|
|
88
|
+
|
|
89
|
+
loss = func.cross_entropy(logits.reshape(-1, logits.size(-1)), targets.reshape(-1)).mean()
|
|
90
|
+
losses.append(loss.item())
|
|
91
|
+
|
|
92
|
+
# backprop and update the parametersx
|
|
93
|
+
self.model.zero_grad()
|
|
94
|
+
loss.backward()
|
|
95
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_norm_clip)
|
|
96
|
+
self.optimizer.step()
|
|
97
|
+
if self.lr_scheduler is not None:
|
|
98
|
+
self.lr_scheduler.step()
|
|
99
|
+
|
|
100
|
+
# report progress
|
|
101
|
+
if self.lr_scheduler is not None:
|
|
102
|
+
current_lr = self.lr_scheduler.get_lr()
|
|
103
|
+
else:
|
|
104
|
+
current_lr = self.optimizer.param_groups[-1]["lr"]
|
|
105
|
+
pbar.set_description(f"epoch {epoch+1} iter {iter_}: train loss {loss.item():.5f}, lr {current_lr}")
|
|
106
|
+
|
|
107
|
+
def _evaluation_epoch(self, epoch):
|
|
108
|
+
self.model.eval()
|
|
109
|
+
ans_df = pd.DataFrame(columns=["user_idx", "item_idx", "relevance"])
|
|
110
|
+
val_items = self.val_dataloader.dataset.val_items
|
|
111
|
+
with torch.no_grad():
|
|
112
|
+
for batch in tqdm(self.val_dataloader):
|
|
113
|
+
states, actions, rtgs, timesteps, users = self._move_batch(batch)
|
|
114
|
+
logits = self.model(states, actions, rtgs, timesteps, users)
|
|
115
|
+
items_relevances = logits[:, -1, :][:, val_items]
|
|
116
|
+
ans_df = ans_df.append(matrix2df(items_relevances, users.squeeze(), val_items))
|
|
117
|
+
self.experiment.add_result(f"epoch: {epoch}", ans_df)
|
|
118
|
+
self.experiment.results.to_csv("results.csv")
|
|
119
|
+
|
|
120
|
+
def train(self):
|
|
121
|
+
"""
|
|
122
|
+
Run training loop
|
|
123
|
+
"""
|
|
124
|
+
for epoch in range(self.epochs):
|
|
125
|
+
self._train_epoch(epoch)
|
|
126
|
+
if self.experiment is not None:
|
|
127
|
+
self._evaluation_epoch(epoch)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import bisect
|
|
2
|
+
import random
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
9
|
+
from replay.utils import TORCH_AVAILABLE
|
|
10
|
+
|
|
11
|
+
if TORCH_AVAILABLE:
|
|
12
|
+
import torch
|
|
13
|
+
from torch.optim import Optimizer
|
|
14
|
+
from torch.optim.lr_scheduler import _LRScheduler
|
|
15
|
+
from torch.utils.data import Dataset
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_seed(seed):
|
|
19
|
+
"""
|
|
20
|
+
Set random seed in all dependicies
|
|
21
|
+
"""
|
|
22
|
+
random.seed(seed)
|
|
23
|
+
np.random.seed(seed)
|
|
24
|
+
torch.manual_seed(seed)
|
|
25
|
+
torch.cuda.manual_seed_all(seed)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StateActionReturnDataset(Dataset):
|
|
29
|
+
"""
|
|
30
|
+
Create Dataset from user trajectories
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, user_trajectory, trajectory_len):
|
|
34
|
+
self.user_trajectory = user_trajectory
|
|
35
|
+
self.trajectory_len = trajectory_len
|
|
36
|
+
|
|
37
|
+
self.len = 0
|
|
38
|
+
self.prefix_lens = [0]
|
|
39
|
+
for trajectory in self.user_trajectory:
|
|
40
|
+
self.len += max(1, len(trajectory["actions"]) - 30 + 1)
|
|
41
|
+
self.prefix_lens.append(self.len)
|
|
42
|
+
|
|
43
|
+
def __len__(self):
|
|
44
|
+
return self.len
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, idx):
|
|
47
|
+
user_num = bisect.bisect_right(self.prefix_lens, idx) - 1
|
|
48
|
+
start = idx - self.prefix_lens[user_num]
|
|
49
|
+
|
|
50
|
+
user = self.user_trajectory[user_num]
|
|
51
|
+
end = min(len(user["actions"]), start + self.trajectory_len)
|
|
52
|
+
states = torch.tensor(np.array(user["states"][start:end]), dtype=torch.float32)
|
|
53
|
+
actions = torch.tensor(user["actions"][start:end], dtype=torch.long)
|
|
54
|
+
rtgs = torch.tensor(user["rtgs"][start:end], dtype=torch.float32)
|
|
55
|
+
# strange logic but work
|
|
56
|
+
timesteps = start
|
|
57
|
+
|
|
58
|
+
return states, actions, rtgs, timesteps, user_num
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ValidateDataset(Dataset):
|
|
62
|
+
"""
|
|
63
|
+
Dataset for Validation
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, user_trajectory, max_context_len, val_users, val_items):
|
|
67
|
+
self.user_trajectory = user_trajectory
|
|
68
|
+
self.max_context_len = max_context_len
|
|
69
|
+
self.val_users = val_users
|
|
70
|
+
self.val_items = val_items
|
|
71
|
+
|
|
72
|
+
def __len__(self):
|
|
73
|
+
return len(self.val_users)
|
|
74
|
+
|
|
75
|
+
def __getitem__(self, idx):
|
|
76
|
+
user_idx = self.val_users[idx]
|
|
77
|
+
user = self.user_trajectory[user_idx]
|
|
78
|
+
if len(user["actions"]) <= self.max_context_len:
|
|
79
|
+
start = 0
|
|
80
|
+
end = -1
|
|
81
|
+
else:
|
|
82
|
+
end = -1
|
|
83
|
+
start = end - self.max_context_len
|
|
84
|
+
|
|
85
|
+
states = torch.tensor(
|
|
86
|
+
np.array(user["states"][start - (start < 0) : end]),
|
|
87
|
+
dtype=torch.float32,
|
|
88
|
+
)
|
|
89
|
+
actions = torch.tensor(user["actions"][start:end], dtype=torch.long)
|
|
90
|
+
rtgs = torch.zeros(end - start + 1 if start < 0 else len(user["actions"]))
|
|
91
|
+
rtgs[start:end] = torch.tensor(user["rtgs"][start:end], dtype=torch.float32)
|
|
92
|
+
rtgs[end] = 10
|
|
93
|
+
timesteps = len(user["actions"]) + start if start < 0 else 0
|
|
94
|
+
|
|
95
|
+
return states, actions, rtgs, timesteps, user_idx
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def pad_sequence(
|
|
99
|
+
sequences: Union[torch.Tensor, list[torch.Tensor]],
|
|
100
|
+
batch_first: bool = False,
|
|
101
|
+
padding_value: float = 0.0,
|
|
102
|
+
pos: str = "right",
|
|
103
|
+
) -> torch.Tensor:
|
|
104
|
+
"""
|
|
105
|
+
Pad sequence
|
|
106
|
+
"""
|
|
107
|
+
if pos == "right":
|
|
108
|
+
padded_sequence = torch.nn.utils.rnn.pad_sequence(sequences, batch_first, padding_value)
|
|
109
|
+
elif pos == "left":
|
|
110
|
+
sequences = tuple(s.flip(0) for s in sequences)
|
|
111
|
+
padded_sequence = torch.nn.utils.rnn.pad_sequence(sequences, batch_first, padding_value)
|
|
112
|
+
_seq_dim = padded_sequence.dim()
|
|
113
|
+
padded_sequence = padded_sequence.flip(-_seq_dim + batch_first)
|
|
114
|
+
else:
|
|
115
|
+
msg = f"pos should be either 'right' or 'left', but got {pos}"
|
|
116
|
+
raise ValueError(msg)
|
|
117
|
+
return padded_sequence
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Collator:
|
|
121
|
+
"""
|
|
122
|
+
Callable class to merge several items to one batch
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, item_pad):
|
|
126
|
+
self.item_pad = item_pad
|
|
127
|
+
|
|
128
|
+
def __call__(self, batch):
|
|
129
|
+
states, actions, rtgs, timesteps, users_num = zip(*batch)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
pad_sequence(
|
|
133
|
+
states,
|
|
134
|
+
batch_first=True,
|
|
135
|
+
padding_value=self.item_pad,
|
|
136
|
+
pos="left",
|
|
137
|
+
),
|
|
138
|
+
pad_sequence(
|
|
139
|
+
actions,
|
|
140
|
+
batch_first=True,
|
|
141
|
+
padding_value=self.item_pad,
|
|
142
|
+
pos="left",
|
|
143
|
+
).unsqueeze(-1),
|
|
144
|
+
pad_sequence(rtgs, batch_first=True, padding_value=0, pos="left").unsqueeze(-1),
|
|
145
|
+
torch.tensor(timesteps).unsqueeze(-1).unsqueeze(-1),
|
|
146
|
+
torch.tensor(users_num).unsqueeze(-1),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def matrix2df(matrix, users=None, items=None):
|
|
151
|
+
"""
|
|
152
|
+
Creata DataFrame from matrix
|
|
153
|
+
"""
|
|
154
|
+
users = np.arange(matrix.shape[0]) if users is None else np.array(users.cpu())
|
|
155
|
+
if items is None:
|
|
156
|
+
items = np.arange(matrix.shape[1])
|
|
157
|
+
x1 = np.repeat(users, len(items))
|
|
158
|
+
x2 = np.tile(items, len(users))
|
|
159
|
+
x3 = np.array(matrix.cpu()).flatten()
|
|
160
|
+
|
|
161
|
+
return pd.DataFrame(np.array([x1, x2, x3]).T, columns=["user_idx", "item_idx", "relevance"])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class WarmUpScheduler(_LRScheduler):
|
|
165
|
+
"""
|
|
166
|
+
Implementation of WarmUp
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
optimizer: Optimizer,
|
|
172
|
+
dim_embed: int,
|
|
173
|
+
warmup_steps: int,
|
|
174
|
+
last_epoch: int = -1,
|
|
175
|
+
) -> None:
|
|
176
|
+
self.dim_embed = dim_embed
|
|
177
|
+
self.warmup_steps = warmup_steps
|
|
178
|
+
self.num_param_groups = len(optimizer.param_groups)
|
|
179
|
+
|
|
180
|
+
super().__init__(optimizer, last_epoch)
|
|
181
|
+
|
|
182
|
+
def get_lr(self) -> float:
|
|
183
|
+
lr = calc_lr(self._step_count, self.dim_embed, self.warmup_steps)
|
|
184
|
+
return [lr] * self.num_param_groups
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def calc_lr(step, dim_embed, warmup_steps):
|
|
188
|
+
"""
|
|
189
|
+
Learning rate calculation
|
|
190
|
+
"""
|
|
191
|
+
return dim_embed ** (-0.5) * min(step ** (-0.5), step * warmup_steps ** (-1.5))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_dataset(
|
|
195
|
+
df, user_num, item_pad, time_col="timestamp", user_col="user_idx", item_col="item_idx", relevance_col="relevance"
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Create dataset from DataFrame
|
|
199
|
+
"""
|
|
200
|
+
user_trajectory = [{} for _ in range(user_num)]
|
|
201
|
+
df = df.sort_values(by=time_col)
|
|
202
|
+
for user_idx in tqdm(range(user_num)):
|
|
203
|
+
user_trajectory[user_idx]["states"] = [[item_pad, item_pad, item_pad]]
|
|
204
|
+
user_trajectory[user_idx]["actions"] = []
|
|
205
|
+
user_trajectory[user_idx]["rewards"] = []
|
|
206
|
+
|
|
207
|
+
user = user_trajectory[user_idx]
|
|
208
|
+
user_df = df[df[user_col] == user_idx]
|
|
209
|
+
for _, row in user_df.iterrows():
|
|
210
|
+
action = row[item_col]
|
|
211
|
+
user["actions"].append(action)
|
|
212
|
+
if row[relevance_col] > 3:
|
|
213
|
+
user["rewards"].append(1)
|
|
214
|
+
user["states"].append([user["states"][-1][1], user["states"][-1][2], action])
|
|
215
|
+
else:
|
|
216
|
+
user["rewards"].append(0)
|
|
217
|
+
user["states"].append(user["states"][-1])
|
|
218
|
+
|
|
219
|
+
user["rtgs"] = np.cumsum(user["rewards"][::-1])[::-1]
|
|
220
|
+
for key in user:
|
|
221
|
+
user[key] = np.array(user[key])
|
|
222
|
+
|
|
223
|
+
return user_trajectory
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# For debug
|
|
227
|
+
def fast_create_dataset(
|
|
228
|
+
df,
|
|
229
|
+
user_num,
|
|
230
|
+
item_pad,
|
|
231
|
+
time_field="timestamp",
|
|
232
|
+
user_field="user_idx",
|
|
233
|
+
item_field="item_idx",
|
|
234
|
+
relevance_field="relevance",
|
|
235
|
+
):
|
|
236
|
+
"""
|
|
237
|
+
Create dataset from DataFrame
|
|
238
|
+
"""
|
|
239
|
+
user_trajectory = [{} for _ in range(user_num)]
|
|
240
|
+
df = df.sort_values(by=time_field)
|
|
241
|
+
for user_idx in tqdm(range(user_num)):
|
|
242
|
+
user_trajectory[user_idx]["states"] = [[item_pad, item_pad, item_pad]]
|
|
243
|
+
user_trajectory[user_idx]["actions"] = []
|
|
244
|
+
user_trajectory[user_idx]["rewards"] = []
|
|
245
|
+
|
|
246
|
+
user = user_trajectory[user_idx]
|
|
247
|
+
user_df = df[df[user_field] == user_idx]
|
|
248
|
+
for idx, (_, row) in enumerate(user_df.iterrows()):
|
|
249
|
+
if idx >= 35:
|
|
250
|
+
break
|
|
251
|
+
action = row[item_field]
|
|
252
|
+
user["actions"].append(action)
|
|
253
|
+
if row[relevance_field] > 3:
|
|
254
|
+
user["rewards"].append(1)
|
|
255
|
+
user["states"].append([user["states"][-1][1], user["states"][-1][2], action])
|
|
256
|
+
else:
|
|
257
|
+
user["rewards"].append(0)
|
|
258
|
+
user["states"].append(user["states"][-1])
|
|
259
|
+
|
|
260
|
+
user["rtgs"] = np.cumsum(user["rewards"][::-1])[::-1]
|
|
261
|
+
for key in user:
|
|
262
|
+
user[key] = np.array(user[key])
|
|
263
|
+
|
|
264
|
+
return user_trajectory
|
|
File without changes
|