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.
Files changed (43) hide show
  1. nextrec/__version__.py +1 -1
  2. nextrec/basic/activation.py +10 -18
  3. nextrec/basic/asserts.py +1 -22
  4. nextrec/basic/callback.py +2 -2
  5. nextrec/basic/features.py +6 -37
  6. nextrec/basic/heads.py +13 -1
  7. nextrec/basic/layers.py +33 -123
  8. nextrec/basic/loggers.py +3 -2
  9. nextrec/basic/metrics.py +85 -4
  10. nextrec/basic/model.py +518 -7
  11. nextrec/basic/summary.py +88 -42
  12. nextrec/cli.py +117 -30
  13. nextrec/data/data_processing.py +8 -13
  14. nextrec/data/preprocessor.py +449 -844
  15. nextrec/loss/grad_norm.py +78 -76
  16. nextrec/models/multi_task/ple.py +1 -0
  17. nextrec/models/multi_task/share_bottom.py +1 -0
  18. nextrec/models/ranking/afm.py +4 -9
  19. nextrec/models/ranking/dien.py +7 -8
  20. nextrec/models/ranking/ffm.py +2 -2
  21. nextrec/models/retrieval/sdm.py +1 -2
  22. nextrec/models/sequential/hstu.py +0 -2
  23. nextrec/models/tree_base/base.py +1 -1
  24. nextrec/utils/__init__.py +2 -1
  25. nextrec/utils/config.py +1 -1
  26. nextrec/utils/console.py +1 -1
  27. nextrec/utils/onnx_utils.py +252 -0
  28. nextrec/utils/torch_utils.py +63 -56
  29. nextrec/utils/types.py +43 -0
  30. {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/METADATA +10 -4
  31. {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/RECORD +34 -42
  32. nextrec/models/multi_task/[pre]star.py +0 -192
  33. nextrec/models/representation/autorec.py +0 -0
  34. nextrec/models/representation/bpr.py +0 -0
  35. nextrec/models/representation/cl4srec.py +0 -0
  36. nextrec/models/representation/lightgcn.py +0 -0
  37. nextrec/models/representation/mf.py +0 -0
  38. nextrec/models/representation/s3rec.py +0 -0
  39. nextrec/models/sequential/sasrec.py +0 -0
  40. nextrec/utils/feature.py +0 -29
  41. {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/WHEEL +0 -0
  42. {nextrec-0.4.33.dist-info → nextrec-0.5.0.dist-info}/entry_points.txt +0 -0
  43. {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)
@@ -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 27/12/2025
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
- from typing import Any, Dict, Iterable, Literal
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 OptimizerName, SchedulerName
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: Literal[
60
- "xavier_uniform",
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(f"Unsupported optimizer: {optimizer}")
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(f"Unsupported scheduler: {scheduler}")
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.4.33
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
  ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
70
74
  ![PyTorch](https://img.shields.io/badge/PyTorch-1.10+-ee4c2c.svg)
71
75
  ![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)
72
- ![Version](https://img.shields.io/badge/Version-0.4.33-orange.svg)
76
+ ![Version](https://img.shields.io/badge/Version-0.5.0-orange.svg)
73
77
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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.4.33,NextRec CLI支持单机训练,分布式训练相关功能尚在开发中。
263
+ > 截止当前版本0.5.0,NextRec CLI支持单机训练,分布式训练相关功能尚在开发中。
258
264
 
259
265
  ## 兼容平台
260
266
 
261
- 当前最新版本为0.4.33,所有模型和测试代码均已在以下平台通过验证,如果开发者在使用中遇到兼容问题,请在issue区提出错误报告及系统版本:
267
+ 当前最新版本为0.5.0,所有模型和测试代码均已在以下平台通过验证,如果开发者在使用中遇到兼容问题,请在issue区提出错误报告及系统版本:
262
268
 
263
269
  | 平台 | 配置 |
264
270
  |------|------|