nextrec 0.4.33__py3-none-any.whl → 0.5.0__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.
- nextrec/__version__.py +1 -1
- nextrec/basic/activation.py +10 -18
- nextrec/basic/asserts.py +1 -22
- nextrec/basic/callback.py +2 -2
- nextrec/basic/features.py +6 -37
- nextrec/basic/heads.py +13 -1
- nextrec/basic/layers.py +33 -123
- nextrec/basic/loggers.py +3 -2
- nextrec/basic/metrics.py +85 -4
- nextrec/basic/model.py +518 -7
- nextrec/basic/summary.py +88 -42
- nextrec/cli.py +117 -30
- nextrec/data/data_processing.py +8 -13
- nextrec/data/preprocessor.py +449 -844
- nextrec/loss/grad_norm.py +78 -76
- nextrec/models/multi_task/ple.py +1 -0
- nextrec/models/multi_task/share_bottom.py +1 -0
- nextrec/models/ranking/afm.py +4 -9
- nextrec/models/ranking/dien.py +7 -8
- nextrec/models/ranking/ffm.py +2 -2
- nextrec/models/retrieval/sdm.py +1 -2
- nextrec/models/sequential/hstu.py +0 -2
- nextrec/models/tree_base/base.py +1 -1
- nextrec/utils/__init__.py +2 -1
- nextrec/utils/config.py +1 -1
- nextrec/utils/console.py +1 -1
- nextrec/utils/onnx_utils.py +252 -0
- nextrec/utils/torch_utils.py +63 -56
- nextrec/utils/types.py +43 -0
- {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/METADATA +10 -4
- {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/RECORD +34 -42
- nextrec/models/multi_task/[pre]star.py +0 -192
- nextrec/models/representation/autorec.py +0 -0
- nextrec/models/representation/bpr.py +0 -0
- nextrec/models/representation/cl4srec.py +0 -0
- nextrec/models/representation/lightgcn.py +0 -0
- nextrec/models/representation/mf.py +0 -0
- nextrec/models/representation/s3rec.py +0 -0
- nextrec/models/sequential/sasrec.py +0 -0
- nextrec/utils/feature.py +0 -29
- {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/WHEEL +0 -0
- {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/entry_points.txt +0 -0
- {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ONNX utilities for NextRec.
|
|
3
|
+
|
|
4
|
+
Date: create on 25/01/2026
|
|
5
|
+
Author: Yang Zhou, zyaztec@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
import numpy as np
|
|
15
|
+
import onnxruntime as ort
|
|
16
|
+
|
|
17
|
+
from nextrec.basic.features import DenseFeature, SequenceFeature, SparseFeature
|
|
18
|
+
from nextrec.utils.torch_utils import to_numpy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OnnxModelWrapper(torch.nn.Module):
|
|
22
|
+
"""Wrap a NextRec model to accept positional ONNX inputs."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, model: torch.nn.Module, feature_names: Sequence[str]):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.model = model
|
|
27
|
+
self.feature_names = list(feature_names)
|
|
28
|
+
|
|
29
|
+
def forward(self, *inputs: torch.Tensor):
|
|
30
|
+
if len(inputs) != len(self.feature_names):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"[OnnxWrapper Error] Number of inputs does not match feature names."
|
|
33
|
+
)
|
|
34
|
+
x = {name: tensor for name, tensor in zip(self.feature_names, inputs)}
|
|
35
|
+
output = self.model(x)
|
|
36
|
+
if isinstance(output, list):
|
|
37
|
+
return tuple(output)
|
|
38
|
+
return output
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_dummy_inputs(
|
|
42
|
+
features: Iterable[object],
|
|
43
|
+
batch_size: int,
|
|
44
|
+
device: torch.device,
|
|
45
|
+
default_seq_len: int = 10,
|
|
46
|
+
seq_len_map: dict[str, int] | None = None,
|
|
47
|
+
) -> list[torch.Tensor]:
|
|
48
|
+
tensors: list[torch.Tensor] = []
|
|
49
|
+
for feature in features:
|
|
50
|
+
if isinstance(feature, DenseFeature):
|
|
51
|
+
input_dim = max(int(feature.input_dim), 1)
|
|
52
|
+
tensors.append(
|
|
53
|
+
torch.zeros((batch_size, input_dim), dtype=torch.float32, device=device)
|
|
54
|
+
)
|
|
55
|
+
elif isinstance(feature, SequenceFeature):
|
|
56
|
+
seq_len = None
|
|
57
|
+
if seq_len_map:
|
|
58
|
+
seq_len = seq_len_map.get(feature.name)
|
|
59
|
+
if seq_len is None:
|
|
60
|
+
seq_len = (
|
|
61
|
+
feature.max_len if feature.max_len is not None else default_seq_len
|
|
62
|
+
)
|
|
63
|
+
seq_len = max(int(seq_len), 1)
|
|
64
|
+
tensors.append(
|
|
65
|
+
torch.zeros((batch_size, seq_len), dtype=torch.long, device=device)
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
tensors.append(torch.zeros((batch_size,), dtype=torch.long, device=device))
|
|
69
|
+
return tensors
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_dense(feature: DenseFeature, array: object) -> np.ndarray:
|
|
73
|
+
arr = np.asarray(array, dtype=np.float32)
|
|
74
|
+
if arr.ndim == 1:
|
|
75
|
+
arr = arr.reshape(-1, 1)
|
|
76
|
+
else:
|
|
77
|
+
arr = arr.reshape(arr.shape[0], -1)
|
|
78
|
+
expected = max(int(feature.input_dim), 1)
|
|
79
|
+
if arr.shape[1] != expected:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"[ONNX Input Error] Dense feature '{feature.name}' expects {expected} dims but got {arr.shape[1]}."
|
|
82
|
+
)
|
|
83
|
+
return arr
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalize_sparse(feature: SparseFeature, array: object) -> np.ndarray:
|
|
87
|
+
arr = np.asarray(array, dtype=np.int64)
|
|
88
|
+
if arr.ndim == 2 and arr.shape[1] == 1:
|
|
89
|
+
arr = arr.reshape(-1)
|
|
90
|
+
elif arr.ndim != 1:
|
|
91
|
+
arr = arr.reshape(arr.shape[0], -1)
|
|
92
|
+
if arr.shape[1] != 1:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"[ONNX Input Error] Sparse feature '{feature.name}' expects 1 dim but got {arr.shape}."
|
|
95
|
+
)
|
|
96
|
+
arr = arr.reshape(-1)
|
|
97
|
+
return arr
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def normalize_sequence(feature: SequenceFeature, array: object) -> np.ndarray:
|
|
101
|
+
arr = np.asarray(array, dtype=np.int64)
|
|
102
|
+
if arr.ndim == 1:
|
|
103
|
+
arr = arr.reshape(1, -1)
|
|
104
|
+
elif arr.ndim > 2:
|
|
105
|
+
arr = arr.reshape(arr.shape[0], -1)
|
|
106
|
+
max_len = feature.max_len if feature.max_len is not None else arr.shape[1]
|
|
107
|
+
max_len = max(int(max_len), 1)
|
|
108
|
+
if arr.shape[1] > max_len:
|
|
109
|
+
arr = arr[:, :max_len]
|
|
110
|
+
elif arr.shape[1] < max_len:
|
|
111
|
+
pad_value = feature.padding_idx if feature.padding_idx is not None else 0
|
|
112
|
+
pad_width = max_len - arr.shape[1]
|
|
113
|
+
arr = np.pad(arr, ((0, 0), (0, pad_width)), constant_values=pad_value)
|
|
114
|
+
return arr
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_onnx_input_feed(
|
|
118
|
+
features: Iterable[object],
|
|
119
|
+
feature_batch: dict[str, object],
|
|
120
|
+
input_names: Sequence[str] | None = None,
|
|
121
|
+
) -> dict[str, np.ndarray]:
|
|
122
|
+
feed: dict[str, np.ndarray] = {}
|
|
123
|
+
for feature in features:
|
|
124
|
+
if input_names is not None and feature.name not in input_names:
|
|
125
|
+
continue
|
|
126
|
+
if feature.name not in feature_batch:
|
|
127
|
+
raise KeyError(
|
|
128
|
+
f"[ONNX Input Error] Feature '{feature.name}' missing from batch data."
|
|
129
|
+
)
|
|
130
|
+
value = to_numpy(feature_batch[feature.name])
|
|
131
|
+
if isinstance(feature, DenseFeature):
|
|
132
|
+
value = normalize_dense(feature, value)
|
|
133
|
+
elif isinstance(feature, SequenceFeature):
|
|
134
|
+
value = normalize_sequence(feature, value)
|
|
135
|
+
else:
|
|
136
|
+
value = normalize_sparse(feature, value)
|
|
137
|
+
feed[feature.name] = value
|
|
138
|
+
return feed
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def pad_tensor(
|
|
142
|
+
value: torch.Tensor, pad_rows: int, pad_value: int | float
|
|
143
|
+
) -> torch.Tensor:
|
|
144
|
+
if pad_rows <= 0:
|
|
145
|
+
return value
|
|
146
|
+
pad_shape = (pad_rows, *value.shape[1:])
|
|
147
|
+
pad = torch.full(pad_shape, pad_value, dtype=value.dtype, device=value.device)
|
|
148
|
+
return torch.cat([value, pad], dim=0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def pad_array(value: np.ndarray, pad_rows: int, pad_value: int | float) -> np.ndarray:
|
|
152
|
+
if pad_rows <= 0:
|
|
153
|
+
return value
|
|
154
|
+
pad_shape = (pad_rows, *value.shape[1:])
|
|
155
|
+
pad = np.full(pad_shape, pad_value, dtype=value.dtype)
|
|
156
|
+
return np.concatenate([value, pad], axis=0)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def pad_onnx_inputs(
|
|
160
|
+
features: Iterable[object],
|
|
161
|
+
feature_batch: dict[str, object],
|
|
162
|
+
target_batch: int,
|
|
163
|
+
) -> tuple[dict[str, object], int]:
|
|
164
|
+
if target_batch <= 0:
|
|
165
|
+
return feature_batch, 0
|
|
166
|
+
padded: dict[str, object] = {}
|
|
167
|
+
orig_batch = None
|
|
168
|
+
for feature in features:
|
|
169
|
+
if feature.name not in feature_batch:
|
|
170
|
+
continue
|
|
171
|
+
value = feature_batch[feature.name]
|
|
172
|
+
if isinstance(value, torch.Tensor):
|
|
173
|
+
batch = value.shape[0] if value.dim() > 0 else 1
|
|
174
|
+
else:
|
|
175
|
+
arr = np.asarray(value)
|
|
176
|
+
batch = arr.shape[0] if arr.ndim > 0 else 1
|
|
177
|
+
if orig_batch is None:
|
|
178
|
+
orig_batch = int(batch)
|
|
179
|
+
pad_rows = max(int(target_batch) - int(batch), 0)
|
|
180
|
+
if isinstance(feature, DenseFeature):
|
|
181
|
+
pad_value: int | float = 0.0
|
|
182
|
+
elif isinstance(feature, SequenceFeature):
|
|
183
|
+
pad_value = feature.padding_idx if feature.padding_idx is not None else 0
|
|
184
|
+
else:
|
|
185
|
+
pad_value = 0
|
|
186
|
+
if isinstance(value, torch.Tensor):
|
|
187
|
+
padded[feature.name] = pad_tensor(value, pad_rows, pad_value)
|
|
188
|
+
else:
|
|
189
|
+
padded[feature.name] = pad_array(np.asarray(value), pad_rows, pad_value)
|
|
190
|
+
if orig_batch is None:
|
|
191
|
+
orig_batch = 0
|
|
192
|
+
return padded, orig_batch
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def pad_id_batch(
|
|
196
|
+
id_batch: dict[str, object],
|
|
197
|
+
target_batch: int,
|
|
198
|
+
pad_value: int | float = 0,
|
|
199
|
+
) -> tuple[dict[str, object], int]:
|
|
200
|
+
if target_batch <= 0:
|
|
201
|
+
return id_batch, 0
|
|
202
|
+
padded: dict[str, object] = {}
|
|
203
|
+
orig_batch = None
|
|
204
|
+
for name, value in id_batch.items():
|
|
205
|
+
if isinstance(value, torch.Tensor):
|
|
206
|
+
batch = value.shape[0] if value.dim() > 0 else 1
|
|
207
|
+
else:
|
|
208
|
+
arr = np.asarray(value)
|
|
209
|
+
batch = arr.shape[0] if arr.ndim > 0 else 1
|
|
210
|
+
if orig_batch is None:
|
|
211
|
+
orig_batch = int(batch)
|
|
212
|
+
pad_rows = max(int(target_batch) - int(batch), 0)
|
|
213
|
+
if isinstance(value, torch.Tensor):
|
|
214
|
+
padded[name] = pad_tensor(value, pad_rows, pad_value)
|
|
215
|
+
else:
|
|
216
|
+
padded[name] = pad_array(np.asarray(value), pad_rows, pad_value)
|
|
217
|
+
if orig_batch is None:
|
|
218
|
+
orig_batch = 0
|
|
219
|
+
return padded, orig_batch
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def merge_onnx_outputs(outputs: Sequence[np.ndarray]) -> np.ndarray:
|
|
223
|
+
if not outputs:
|
|
224
|
+
raise ValueError("[ONNX Output Error] Empty ONNX output list.")
|
|
225
|
+
if len(outputs) == 1:
|
|
226
|
+
return outputs[0]
|
|
227
|
+
normalized: list[np.ndarray] = []
|
|
228
|
+
batch = outputs[0].shape[0] if outputs[0].ndim > 0 else None
|
|
229
|
+
for out in outputs:
|
|
230
|
+
arr = np.asarray(out)
|
|
231
|
+
if arr.ndim == 0:
|
|
232
|
+
arr = arr.reshape(1, 1)
|
|
233
|
+
elif arr.ndim == 1:
|
|
234
|
+
arr = arr.reshape(-1, 1)
|
|
235
|
+
if batch is not None and arr.shape[0] != batch:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"[ONNX Output Error] Output batch size mismatch across ONNX outputs."
|
|
238
|
+
)
|
|
239
|
+
normalized.append(arr)
|
|
240
|
+
return np.concatenate(normalized, axis=1)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_onnx_session(
|
|
244
|
+
onnx_path: str | Path,
|
|
245
|
+
):
|
|
246
|
+
available = ort.get_available_providers()
|
|
247
|
+
preferred = ["CUDAExecutionProvider", "CPUExecutionProvider"]
|
|
248
|
+
selected = [p for p in preferred if p in available]
|
|
249
|
+
if not selected:
|
|
250
|
+
selected = available
|
|
251
|
+
|
|
252
|
+
return ort.InferenceSession(str(onnx_path), providers=selected)
|
nextrec/utils/torch_utils.py
CHANGED
|
@@ -5,14 +5,15 @@ This module groups device setup, distributed helpers, optimizers/schedulers,
|
|
|
5
5
|
initialization, and tensor helpers.
|
|
6
6
|
|
|
7
7
|
Date: create on 27/10/2025
|
|
8
|
-
Checkpoint: edit on
|
|
8
|
+
Checkpoint: edit on 22/01/2026
|
|
9
9
|
Author: Yang Zhou, zyaztec@gmail.com
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
|
|
15
|
+
import numbers
|
|
16
|
+
from typing import Any, Dict, Iterable
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
import torch
|
|
@@ -22,7 +23,55 @@ from torch.utils.data import DataLoader, IterableDataset
|
|
|
22
23
|
from torch.utils.data.distributed import DistributedSampler
|
|
23
24
|
|
|
24
25
|
from nextrec.basic.loggers import colorize
|
|
25
|
-
from nextrec.utils.types import
|
|
26
|
+
from nextrec.utils.types import (
|
|
27
|
+
EmbeddingInitType,
|
|
28
|
+
InitializerActivationType,
|
|
29
|
+
OptimizerName,
|
|
30
|
+
SchedulerName,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def to_list(value: str | list[str] | None) -> list[str]:
|
|
35
|
+
if value is None:
|
|
36
|
+
return []
|
|
37
|
+
if isinstance(value, str):
|
|
38
|
+
return [value]
|
|
39
|
+
return list(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def as_float(value: Any) -> float | None:
|
|
43
|
+
if isinstance(value, numbers.Number):
|
|
44
|
+
return float(value)
|
|
45
|
+
if hasattr(value, "item"):
|
|
46
|
+
try:
|
|
47
|
+
return float(value.item())
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def to_numpy(values: Any) -> np.ndarray:
|
|
54
|
+
if isinstance(values, torch.Tensor):
|
|
55
|
+
return values.detach().cpu().numpy()
|
|
56
|
+
return np.asarray(values)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def to_tensor(
|
|
60
|
+
value: Any, dtype: torch.dtype, device: torch.device | str | None = None
|
|
61
|
+
) -> torch.Tensor:
|
|
62
|
+
if value is None:
|
|
63
|
+
raise ValueError("[Tensor Utils Error] Cannot convert None to tensor.")
|
|
64
|
+
tensor = value if isinstance(value, torch.Tensor) else torch.as_tensor(value)
|
|
65
|
+
if tensor.dtype != dtype:
|
|
66
|
+
tensor = tensor.to(dtype=dtype)
|
|
67
|
+
|
|
68
|
+
if device is not None:
|
|
69
|
+
target_device = (
|
|
70
|
+
device if isinstance(device, torch.device) else torch.device(device)
|
|
71
|
+
)
|
|
72
|
+
if tensor.device != target_device:
|
|
73
|
+
tensor = tensor.to(target_device)
|
|
74
|
+
return tensor
|
|
26
75
|
|
|
27
76
|
|
|
28
77
|
def resolve_nonlinearity(activation: str) -> str:
|
|
@@ -56,30 +105,8 @@ def resolve_gain(activation: str, param: Dict[str, Any]) -> float:
|
|
|
56
105
|
|
|
57
106
|
|
|
58
107
|
def get_initializer(
|
|
59
|
-
init_type:
|
|
60
|
-
|
|
61
|
-
"xavier_normal",
|
|
62
|
-
"kaiming_uniform",
|
|
63
|
-
"kaiming_normal",
|
|
64
|
-
"orthogonal",
|
|
65
|
-
"normal",
|
|
66
|
-
"uniform",
|
|
67
|
-
] = "normal",
|
|
68
|
-
activation: Literal[
|
|
69
|
-
"linear",
|
|
70
|
-
"conv1d",
|
|
71
|
-
"conv2d",
|
|
72
|
-
"conv3d",
|
|
73
|
-
"conv_transpose1d",
|
|
74
|
-
"conv_transpose2d",
|
|
75
|
-
"conv_transpose3d",
|
|
76
|
-
"sigmoid",
|
|
77
|
-
"tanh",
|
|
78
|
-
"relu",
|
|
79
|
-
"leaky_relu",
|
|
80
|
-
"selu",
|
|
81
|
-
"gelu",
|
|
82
|
-
] = "linear",
|
|
108
|
+
init_type: EmbeddingInitType = "normal",
|
|
109
|
+
activation: InitializerActivationType = "linear",
|
|
83
110
|
param: Dict[str, Any] | None = None,
|
|
84
111
|
):
|
|
85
112
|
param = param or {}
|
|
@@ -108,7 +135,7 @@ def get_initializer(
|
|
|
108
135
|
elif init_type == "uniform":
|
|
109
136
|
nn.init.uniform_(tensor, a=param.get("a", -0.05), b=param.get("b", 0.05))
|
|
110
137
|
else:
|
|
111
|
-
raise ValueError(f"Unknown init_type: {init_type}")
|
|
138
|
+
raise ValueError(f"[Initializer Error] Unknown init_type: {init_type}")
|
|
112
139
|
return tensor
|
|
113
140
|
|
|
114
141
|
return initializer_fn
|
|
@@ -172,12 +199,14 @@ def get_optimizer(
|
|
|
172
199
|
elif opt_name == "rmsprop":
|
|
173
200
|
opt_class = torch.optim.RMSprop
|
|
174
201
|
else:
|
|
175
|
-
raise NotImplementedError(
|
|
202
|
+
raise NotImplementedError(
|
|
203
|
+
f"[Optimizer Error] Unsupported optimizer: {optimizer}"
|
|
204
|
+
)
|
|
176
205
|
optimizer_fn = opt_class(params=params, **optimizer_params)
|
|
177
206
|
elif isinstance(optimizer, torch.optim.Optimizer):
|
|
178
207
|
optimizer_fn = optimizer
|
|
179
208
|
else:
|
|
180
|
-
raise TypeError(f"Invalid optimizer type: {type(optimizer)}")
|
|
209
|
+
raise TypeError(f"[Optimizer Error] Invalid optimizer type: {type(optimizer)}")
|
|
181
210
|
return optimizer_fn
|
|
182
211
|
|
|
183
212
|
|
|
@@ -203,7 +232,9 @@ def get_scheduler(
|
|
|
203
232
|
optimizer, **scheduler_params
|
|
204
233
|
)
|
|
205
234
|
else:
|
|
206
|
-
raise NotImplementedError(
|
|
235
|
+
raise NotImplementedError(
|
|
236
|
+
f"[Scheduler Error] Unsupported scheduler: {scheduler}"
|
|
237
|
+
)
|
|
207
238
|
elif isinstance(scheduler, type) and issubclass(
|
|
208
239
|
scheduler,
|
|
209
240
|
(torch.optim.lr_scheduler._LRScheduler, torch.optim.lr_scheduler.LRScheduler),
|
|
@@ -215,35 +246,11 @@ def get_scheduler(
|
|
|
215
246
|
):
|
|
216
247
|
scheduler_fn = scheduler
|
|
217
248
|
else:
|
|
218
|
-
raise TypeError(f"Invalid scheduler type: {type(scheduler)}")
|
|
249
|
+
raise TypeError(f"[Scheduler Error] Invalid scheduler type: {type(scheduler)}")
|
|
219
250
|
|
|
220
251
|
return scheduler_fn
|
|
221
252
|
|
|
222
253
|
|
|
223
|
-
def to_numpy(values: Any) -> np.ndarray:
|
|
224
|
-
if isinstance(values, torch.Tensor):
|
|
225
|
-
return values.detach().cpu().numpy()
|
|
226
|
-
return np.asarray(values)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def to_tensor(
|
|
230
|
-
value: Any, dtype: torch.dtype, device: torch.device | str | None = None
|
|
231
|
-
) -> torch.Tensor:
|
|
232
|
-
if value is None:
|
|
233
|
-
raise ValueError("[Tensor Utils Error] Cannot convert None to tensor.")
|
|
234
|
-
tensor = value if isinstance(value, torch.Tensor) else torch.as_tensor(value)
|
|
235
|
-
if tensor.dtype != dtype:
|
|
236
|
-
tensor = tensor.to(dtype=dtype)
|
|
237
|
-
|
|
238
|
-
if device is not None:
|
|
239
|
-
target_device = (
|
|
240
|
-
device if isinstance(device, torch.device) else torch.device(device)
|
|
241
|
-
)
|
|
242
|
-
if tensor.device != target_device:
|
|
243
|
-
tensor = tensor.to(target_device)
|
|
244
|
-
return tensor
|
|
245
|
-
|
|
246
|
-
|
|
247
254
|
def init_process_group(
|
|
248
255
|
distributed: bool, rank: int, world_size: int, device_id: int | None = None
|
|
249
256
|
) -> None:
|
nextrec/utils/types.py
CHANGED
|
@@ -64,6 +64,40 @@ TaskTypeName = Literal["binary", "regression"]
|
|
|
64
64
|
|
|
65
65
|
TaskTypeInput = TaskTypeName | str
|
|
66
66
|
|
|
67
|
+
EmbeddingInitType = Literal[
|
|
68
|
+
"normal",
|
|
69
|
+
"uniform",
|
|
70
|
+
"xavier_uniform",
|
|
71
|
+
"xavier_normal",
|
|
72
|
+
"kaiming_uniform",
|
|
73
|
+
"kaiming_normal",
|
|
74
|
+
"orthogonal",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
SequenceCombinerType = Literal[
|
|
78
|
+
"mean",
|
|
79
|
+
"sum",
|
|
80
|
+
"concat",
|
|
81
|
+
"dot_attention",
|
|
82
|
+
"self_attention",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
InitializerActivationType = Literal[
|
|
86
|
+
"linear",
|
|
87
|
+
"conv1d",
|
|
88
|
+
"conv2d",
|
|
89
|
+
"conv3d",
|
|
90
|
+
"conv_transpose1d",
|
|
91
|
+
"conv_transpose2d",
|
|
92
|
+
"conv_transpose3d",
|
|
93
|
+
"sigmoid",
|
|
94
|
+
"tanh",
|
|
95
|
+
"relu",
|
|
96
|
+
"leaky_relu",
|
|
97
|
+
"selu",
|
|
98
|
+
"gelu",
|
|
99
|
+
]
|
|
100
|
+
|
|
67
101
|
MetricsName = Literal[
|
|
68
102
|
"auc",
|
|
69
103
|
"gauc",
|
|
@@ -97,4 +131,13 @@ MetricsName = Literal[
|
|
|
97
131
|
"mrr@5",
|
|
98
132
|
"mrr@10",
|
|
99
133
|
"mrr@20",
|
|
134
|
+
"topk_recall@5",
|
|
135
|
+
"topk_recall@10",
|
|
136
|
+
"topk_recall@20",
|
|
137
|
+
"topk_precision@5",
|
|
138
|
+
"topk_precision@10",
|
|
139
|
+
"topk_precision@20",
|
|
140
|
+
"lift@5",
|
|
141
|
+
"lift@10",
|
|
142
|
+
"lift@20",
|
|
100
143
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextrec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A comprehensive recommendation library with match, ranking, and multi-task learning models
|
|
5
5
|
Project-URL: Homepage, https://github.com/zerolovesea/NextRec
|
|
6
6
|
Project-URL: Repository, https://github.com/zerolovesea/NextRec
|
|
@@ -24,10 +24,14 @@ Requires-Dist: numpy<2.0,>=1.21; sys_platform == 'linux' and python_version < '3
|
|
|
24
24
|
Requires-Dist: numpy<3.0,>=1.26; sys_platform == 'linux' and python_version >= '3.12'
|
|
25
25
|
Requires-Dist: numpy>=1.23.0; sys_platform == 'win32'
|
|
26
26
|
Requires-Dist: numpy>=1.24.0; sys_platform == 'darwin'
|
|
27
|
+
Requires-Dist: onnx>=1.16.0
|
|
28
|
+
Requires-Dist: onnxruntime>=1.18.0
|
|
29
|
+
Requires-Dist: onnxscript>=0.1.1
|
|
27
30
|
Requires-Dist: pandas<2.0,>=1.5; sys_platform == 'linux' and python_version < '3.12'
|
|
28
31
|
Requires-Dist: pandas<2.3.0,>=2.1.0; sys_platform == 'win32'
|
|
29
32
|
Requires-Dist: pandas>=2.0.0; sys_platform == 'darwin'
|
|
30
33
|
Requires-Dist: pandas>=2.1.0; sys_platform == 'linux' and python_version >= '3.12'
|
|
34
|
+
Requires-Dist: polars>=0.20.0
|
|
31
35
|
Requires-Dist: pyarrow<13.0.0,>=10.0.0; sys_platform == 'linux' and python_version < '3.12'
|
|
32
36
|
Requires-Dist: pyarrow<15.0.0,>=12.0.0; sys_platform == 'win32'
|
|
33
37
|
Requires-Dist: pyarrow>=12.0.0; sys_platform == 'darwin'
|
|
@@ -69,7 +73,7 @@ Description-Content-Type: text/markdown
|
|
|
69
73
|

|
|
70
74
|

|
|
71
75
|

|
|
72
|
-

|
|
73
77
|
[](https://deepwiki.com/zerolovesea/NextRec)
|
|
74
78
|
|
|
75
79
|
中文文档 | [English Version](README_en.md)
|
|
@@ -102,6 +106,7 @@ NextRec是一个基于PyTorch的现代推荐系统框架,旨在为研究工程
|
|
|
102
106
|
- **高效训练与评估**:内置多种优化器、学习率调度、早停、模型检查点与详细的日志管理,开箱即用。
|
|
103
107
|
|
|
104
108
|
## NextRec近期进展
|
|
109
|
+
- **28/01/2026** 在v0.4.39中加入了对onnx导出和加载的支持,并大大加速了数据预处理速度(最高9x加速)
|
|
105
110
|
- **01/01/2026** 新年好,在v0.4.27中加入了多个多目标模型的支持:[APG](nextrec/models/multi_task/apg.py), [ESCM](nextrec/models/multi_task/escm.py), [HMoE](nextrec/models/multi_task/hmoe.py), [Cross Stitch](nextrec/models/multi_task/cross_stitch.py)
|
|
106
111
|
- **28/12/2025** 在v0.4.21中加入了对SwanLab和Wandb的支持,通过model的`fit`方法进行配置:`use_swanlab=True, swanlab_kwargs={"project": "NextRec","name":"tutorial_movielens_deepfm"},`
|
|
107
112
|
- **21/12/2025** 在v0.4.16中加入了对[GradNorm](/nextrec/loss/grad_norm.py)的支持,通过compile的`loss_weight='grad_norm'`进行配置
|
|
@@ -136,6 +141,7 @@ pip install nextrec # or pip install -e .
|
|
|
136
141
|
- [example_multitask.py](/tutorials/example_multitask.py) - 电商数据集上的ESMM多任务学习训练示例
|
|
137
142
|
- [movielen_match_dssm.py](/tutorials/movielen_match_dssm.py) - 基于movielen 100k数据集训练的 DSSM 召回模型示例
|
|
138
143
|
|
|
144
|
+
- [example_onnx.py](/tutorials/example_onnx.py) - 使用NextRec训练和导出onnx模型
|
|
139
145
|
- [example_distributed_training.py](/tutorials/distributed/example_distributed_training.py) - 使用NextRec进行单机多卡训练的代码示例
|
|
140
146
|
|
|
141
147
|
- [run_all_ranking_models.py](/tutorials/run_all_ranking_models.py) - 快速校验所有排序模型的可用性
|
|
@@ -254,11 +260,11 @@ nextrec --mode=predict --predict_config=path/to/predict_config.yaml
|
|
|
254
260
|
|
|
255
261
|
预测结果固定保存到 `{checkpoint_path}/predictions/{name}.{save_data_format}`。
|
|
256
262
|
|
|
257
|
-
> 截止当前版本0.
|
|
263
|
+
> 截止当前版本0.5.0,NextRec CLI支持单机训练,分布式训练相关功能尚在开发中。
|
|
258
264
|
|
|
259
265
|
## 兼容平台
|
|
260
266
|
|
|
261
|
-
当前最新版本为0.
|
|
267
|
+
当前最新版本为0.5.0,所有模型和测试代码均已在以下平台通过验证,如果开发者在使用中遇到兼容问题,请在issue区提出错误报告及系统版本:
|
|
262
268
|
|
|
263
269
|
| 平台 | 配置 |
|
|
264
270
|
|------|------|
|