nextrec 0.4.10__py3-none-any.whl → 0.4.12__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/callback.py +44 -54
- nextrec/basic/features.py +35 -22
- nextrec/basic/layers.py +64 -68
- nextrec/basic/loggers.py +2 -2
- nextrec/basic/metrics.py +9 -5
- nextrec/basic/model.py +162 -106
- nextrec/cli.py +16 -5
- nextrec/data/preprocessor.py +4 -4
- nextrec/loss/loss_utils.py +1 -1
- nextrec/models/generative/__init__.py +1 -1
- nextrec/models/ranking/eulernet.py +44 -75
- nextrec/models/ranking/ffm.py +275 -0
- nextrec/models/ranking/lr.py +1 -3
- 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/__init__.py +2 -1
- nextrec/utils/console.py +9 -1
- nextrec/utils/model.py +14 -0
- {nextrec-0.4.10.dist-info → nextrec-0.4.12.dist-info}/METADATA +32 -11
- {nextrec-0.4.10.dist-info → nextrec-0.4.12.dist-info}/RECORD +30 -23
- /nextrec/models/{generative → sequential}/hstu.py +0 -0
- {nextrec-0.4.10.dist-info → nextrec-0.4.12.dist-info}/WHEEL +0 -0
- {nextrec-0.4.10.dist-info → nextrec-0.4.12.dist-info}/entry_points.txt +0 -0
- {nextrec-0.4.10.dist-info → nextrec-0.4.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -41,7 +41,8 @@ from nextrec.basic.features import DenseFeature, SequenceFeature, SparseFeature
|
|
|
41
41
|
from nextrec.basic.layers import LR, EmbeddingLayer, PredictionLayer
|
|
42
42
|
from nextrec.basic.model import BaseModel
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
class EulerInteractionLayer(nn.Module):
|
|
45
46
|
"""
|
|
46
47
|
Paper-aligned Euler Interaction Layer.
|
|
47
48
|
|
|
@@ -102,24 +103,32 @@ class EulerInteractionLayerPaper(nn.Module):
|
|
|
102
103
|
self.bn = None
|
|
103
104
|
self.ln = None
|
|
104
105
|
|
|
105
|
-
def forward(
|
|
106
|
+
def forward(
|
|
107
|
+
self, r: torch.Tensor, p: torch.Tensor
|
|
108
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
106
109
|
"""
|
|
107
110
|
r, p: [B, m, d]
|
|
108
111
|
return r_out, p_out: [B, n, d]
|
|
109
112
|
"""
|
|
110
113
|
B, m, d = r.shape
|
|
111
|
-
assert
|
|
114
|
+
assert (
|
|
115
|
+
m == self.m and d == self.d
|
|
116
|
+
), f"Expected [B,{self.m},{self.d}] got {r.shape}"
|
|
112
117
|
|
|
113
118
|
# Euler Transformation: rectangular -> polar
|
|
114
|
-
lam = torch.sqrt(r * r + p * p + self.eps)
|
|
115
|
-
theta = torch.atan2(p, r)
|
|
116
|
-
log_lam = torch.log(lam + self.eps)
|
|
119
|
+
lam = torch.sqrt(r * r + p * p + self.eps) # [B,m,d]
|
|
120
|
+
theta = torch.atan2(p, r) # [B,m,d]
|
|
121
|
+
log_lam = torch.log(lam + self.eps) # [B,m,d]
|
|
117
122
|
|
|
118
123
|
# Generalized Multi-order Transformation
|
|
119
124
|
# psi_k = sum_j alpha_{k,j} * theta_j + delta_k
|
|
120
125
|
# l_k = exp(sum_j alpha_{k,j} * log(lam_j) + delta'_k)
|
|
121
|
-
psi =
|
|
122
|
-
|
|
126
|
+
psi = (
|
|
127
|
+
torch.einsum("bmd,nmd->bnd", theta, self.alpha) + self.delta_phase
|
|
128
|
+
) # [B,n,d]
|
|
129
|
+
log_l = (
|
|
130
|
+
torch.einsum("bmd,nmd->bnd", log_lam, self.alpha) + self.delta_logmod
|
|
131
|
+
) # [B,n,d]
|
|
123
132
|
l = torch.exp(log_l) # [B,n,d]
|
|
124
133
|
|
|
125
134
|
# Inverse Euler Transformation
|
|
@@ -153,7 +162,7 @@ class EulerInteractionLayerPaper(nn.Module):
|
|
|
153
162
|
return r_out, p_out
|
|
154
163
|
|
|
155
164
|
|
|
156
|
-
class
|
|
165
|
+
class ComplexSpaceMapping(nn.Module):
|
|
157
166
|
"""
|
|
158
167
|
Map real embeddings e_j to complex features via Euler's formula (Eq.6-7).
|
|
159
168
|
For each field j:
|
|
@@ -174,63 +183,6 @@ class ComplexSpaceMappingPaper(nn.Module):
|
|
|
174
183
|
r = mu * torch.cos(e)
|
|
175
184
|
p = mu * torch.sin(e)
|
|
176
185
|
return r, p
|
|
177
|
-
|
|
178
|
-
class EulerNetPaper(nn.Module):
|
|
179
|
-
"""
|
|
180
|
-
Paper-aligned EulerNet core (embedding -> mapping -> L Euler layers -> linear regression).
|
|
181
|
-
"""
|
|
182
|
-
|
|
183
|
-
def __init__(
|
|
184
|
-
self,
|
|
185
|
-
*,
|
|
186
|
-
embedding_dim: int,
|
|
187
|
-
num_fields: int,
|
|
188
|
-
num_layers: int = 2,
|
|
189
|
-
num_orders: int = 8, # n in paper
|
|
190
|
-
use_implicit: bool = True,
|
|
191
|
-
norm: str | None = "ln", # None | "bn" | "ln"
|
|
192
|
-
):
|
|
193
|
-
super().__init__()
|
|
194
|
-
self.d = embedding_dim
|
|
195
|
-
self.m = num_fields
|
|
196
|
-
self.L = num_layers
|
|
197
|
-
self.n = num_orders
|
|
198
|
-
|
|
199
|
-
self.mapping = ComplexSpaceMappingPaper(embedding_dim, num_fields)
|
|
200
|
-
|
|
201
|
-
self.layers = nn.ModuleList([
|
|
202
|
-
EulerInteractionLayerPaper(
|
|
203
|
-
embedding_dim=embedding_dim,
|
|
204
|
-
num_fields=(num_fields if i == 0 else num_orders), # stack: m -> n -> n ...
|
|
205
|
-
num_orders=num_orders,
|
|
206
|
-
use_implicit=use_implicit,
|
|
207
|
-
norm=norm,
|
|
208
|
-
)
|
|
209
|
-
for i in range(num_layers)
|
|
210
|
-
])
|
|
211
|
-
|
|
212
|
-
# Output regression (Eq.16-17)
|
|
213
|
-
# After last layer: r,p are [B,n,d]. Concatenate to [B, n*d] each, then regress.
|
|
214
|
-
self.w = nn.Linear(self.n * self.d, 1, bias=False) # for real
|
|
215
|
-
self.w_im = nn.Linear(self.n * self.d, 1, bias=False) # for imag
|
|
216
|
-
|
|
217
|
-
def forward(self, field_emb: torch.Tensor) -> torch.Tensor:
|
|
218
|
-
"""
|
|
219
|
-
field_emb: [B, m, d] real embeddings e_j
|
|
220
|
-
return: logits, shape [B,1]
|
|
221
|
-
"""
|
|
222
|
-
r, p = self.mapping(field_emb) # [B,m,d]
|
|
223
|
-
|
|
224
|
-
# stack Euler interaction layers
|
|
225
|
-
for layer in self.layers:
|
|
226
|
-
r, p = layer(r, p) # -> [B,n,d]
|
|
227
|
-
|
|
228
|
-
r_flat = r.reshape(r.size(0), self.n * self.d)
|
|
229
|
-
p_flat = p.reshape(p.size(0), self.n * self.d)
|
|
230
|
-
|
|
231
|
-
z_re = self.w(r_flat)
|
|
232
|
-
z_im = self.w_im(p_flat)
|
|
233
|
-
return z_re + z_im # Eq.17 logits
|
|
234
186
|
|
|
235
187
|
|
|
236
188
|
class EulerNet(BaseModel):
|
|
@@ -313,14 +265,23 @@ class EulerNet(BaseModel):
|
|
|
313
265
|
"All interaction features must share the same embedding_dim in EulerNet."
|
|
314
266
|
)
|
|
315
267
|
|
|
316
|
-
self.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
268
|
+
self.num_layers = num_layers
|
|
269
|
+
self.num_orders = num_orders
|
|
270
|
+
self.mapping = ComplexSpaceMapping(self.embedding_dim, self.num_fields)
|
|
271
|
+
self.layers = nn.ModuleList(
|
|
272
|
+
[
|
|
273
|
+
EulerInteractionLayer(
|
|
274
|
+
embedding_dim=self.embedding_dim,
|
|
275
|
+
num_fields=(self.num_fields if i == 0 else self.num_orders),
|
|
276
|
+
num_orders=self.num_orders,
|
|
277
|
+
use_implicit=use_implicit,
|
|
278
|
+
norm=norm,
|
|
279
|
+
)
|
|
280
|
+
for i in range(self.num_layers)
|
|
281
|
+
]
|
|
323
282
|
)
|
|
283
|
+
self.w = nn.Linear(self.num_orders * self.embedding_dim, 1, bias=False)
|
|
284
|
+
self.w_im = nn.Linear(self.num_orders * self.embedding_dim, 1, bias=False)
|
|
324
285
|
|
|
325
286
|
if self.use_linear:
|
|
326
287
|
if len(self.linear_features) == 0:
|
|
@@ -336,7 +297,7 @@ class EulerNet(BaseModel):
|
|
|
336
297
|
|
|
337
298
|
self.prediction_layer = PredictionLayer(task_type=self.task)
|
|
338
299
|
|
|
339
|
-
modules = ["
|
|
300
|
+
modules = ["mapping", "layers", "w", "w_im"]
|
|
340
301
|
if self.use_linear:
|
|
341
302
|
modules.append("linear")
|
|
342
303
|
self.register_regularization_weights(
|
|
@@ -354,7 +315,7 @@ class EulerNet(BaseModel):
|
|
|
354
315
|
field_emb = self.embedding(
|
|
355
316
|
x=x, features=self.interaction_features, squeeze_dim=False
|
|
356
317
|
)
|
|
357
|
-
y_euler = self.
|
|
318
|
+
y_euler = self.euler_forward(field_emb)
|
|
358
319
|
|
|
359
320
|
if self.use_linear and self.linear is not None:
|
|
360
321
|
linear_input = self.embedding(
|
|
@@ -363,3 +324,11 @@ class EulerNet(BaseModel):
|
|
|
363
324
|
y_euler = y_euler + self.linear(linear_input)
|
|
364
325
|
|
|
365
326
|
return self.prediction_layer(y_euler)
|
|
327
|
+
|
|
328
|
+
def euler_forward(self, field_emb: torch.Tensor) -> torch.Tensor:
|
|
329
|
+
r, p = self.mapping(field_emb)
|
|
330
|
+
for layer in self.layers:
|
|
331
|
+
r, p = layer(r, p)
|
|
332
|
+
r_flat = r.reshape(r.size(0), self.num_orders * self.embedding_dim)
|
|
333
|
+
p_flat = p.reshape(p.size(0), self.num_orders * self.embedding_dim)
|
|
334
|
+
return self.w(r_flat) + self.w_im(p_flat)
|
nextrec/models/ranking/ffm.py
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 19/12/2025
|
|
3
|
+
Checkpoint: edit on 19/12/2025
|
|
4
|
+
Author: Yang Zhou, zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Juan Y, Zhuang Y, Chin W-S, et al. Field-aware Factorization Machines for CTR
|
|
7
|
+
Prediction[C]//RecSys. 2016: 43-50.
|
|
8
|
+
|
|
9
|
+
Field-aware Factorization Machines (FFM) extend FM by learning a distinct
|
|
10
|
+
embedding of each feature for every target field. For a pair of fields (i, j),
|
|
11
|
+
FFM uses v_{i,f_j} · v_{j,f_i} instead of a shared embedding, enabling richer
|
|
12
|
+
context-aware interactions and stronger CTR performance on sparse categorical
|
|
13
|
+
data.
|
|
14
|
+
|
|
15
|
+
Pipeline:
|
|
16
|
+
(1) Build field-aware embeddings v_{i,f} for each feature i toward every field f
|
|
17
|
+
(2) Compute first-order linear terms for sparse/sequence (and optional dense) fields
|
|
18
|
+
(3) For each field pair (i, j), compute v_{i,f_j} · v_{j,f_i}
|
|
19
|
+
(4) Sum linear + field-aware interaction logits and output prediction
|
|
20
|
+
|
|
21
|
+
Key Advantages:
|
|
22
|
+
- Field-aware embeddings capture asymmetric interactions between fields
|
|
23
|
+
- Improves CTR accuracy on sparse categorical features
|
|
24
|
+
- Retains interpretable second-order structure
|
|
25
|
+
|
|
26
|
+
FFM 在 FM 基础上引入字段感知机制:每个特征在不同目标字段下拥有不同的 embedding。
|
|
27
|
+
对于字段对 (i, j),模型使用 v_{i,f_j} 与 v_{j,f_i} 的内积,从而更细粒度地建模
|
|
28
|
+
跨字段交互,在稀疏高维 CTR 场景中表现更优。
|
|
29
|
+
|
|
30
|
+
处理流程:
|
|
31
|
+
(1) 为每个特征 i 构造面向每个字段 f 的嵌入 v_{i,f}
|
|
32
|
+
(2) 计算一阶线性项(稀疏/序列特征,及可选的稠密特征)
|
|
33
|
+
(3) 对每一对字段 (i, j) 计算 v_{i,f_j} · v_{j,f_i}
|
|
34
|
+
(4) 将线性项与交互项相加得到最终预测
|
|
35
|
+
|
|
36
|
+
主要优点:
|
|
37
|
+
- 字段感知嵌入可捕捉非对称交互
|
|
38
|
+
- 稀疏类别特征下预测更准确
|
|
39
|
+
- 保持二阶结构的可解释性
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import torch
|
|
43
|
+
import torch.nn as nn
|
|
44
|
+
|
|
45
|
+
from nextrec.basic.features import DenseFeature, SequenceFeature, SparseFeature
|
|
46
|
+
from nextrec.basic.layers import AveragePooling, InputMask, PredictionLayer, SumPooling
|
|
47
|
+
from nextrec.basic.model import BaseModel
|
|
48
|
+
from nextrec.utils.torch_utils import get_initializer
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FFM(BaseModel):
|
|
52
|
+
@property
|
|
53
|
+
def model_name(self):
|
|
54
|
+
return "FFM"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def default_task(self):
|
|
58
|
+
return "binary"
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
dense_features: list[DenseFeature] | None = None,
|
|
63
|
+
sparse_features: list[SparseFeature] | None = None,
|
|
64
|
+
sequence_features: list[SequenceFeature] | None = None,
|
|
65
|
+
target: list[str] | str | None = None,
|
|
66
|
+
task: str | list[str] | None = None,
|
|
67
|
+
optimizer: str = "adam",
|
|
68
|
+
optimizer_params: dict | None = None,
|
|
69
|
+
loss: str | nn.Module | None = "bce",
|
|
70
|
+
loss_params: dict | list[dict] | None = None,
|
|
71
|
+
device: str = "cpu",
|
|
72
|
+
embedding_l1_reg=1e-6,
|
|
73
|
+
dense_l1_reg=1e-5,
|
|
74
|
+
embedding_l2_reg=1e-5,
|
|
75
|
+
dense_l2_reg=1e-4,
|
|
76
|
+
**kwargs,
|
|
77
|
+
):
|
|
78
|
+
dense_features = dense_features or []
|
|
79
|
+
sparse_features = sparse_features or []
|
|
80
|
+
sequence_features = sequence_features or []
|
|
81
|
+
optimizer_params = optimizer_params or {}
|
|
82
|
+
if loss is None:
|
|
83
|
+
loss = "bce"
|
|
84
|
+
|
|
85
|
+
super(FFM, self).__init__(
|
|
86
|
+
dense_features=dense_features,
|
|
87
|
+
sparse_features=sparse_features,
|
|
88
|
+
sequence_features=sequence_features,
|
|
89
|
+
target=target,
|
|
90
|
+
task=task or self.default_task,
|
|
91
|
+
device=device,
|
|
92
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
93
|
+
dense_l1_reg=dense_l1_reg,
|
|
94
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
95
|
+
dense_l2_reg=dense_l2_reg,
|
|
96
|
+
**kwargs,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.fm_features = sparse_features + sequence_features
|
|
100
|
+
if len(self.fm_features) < 2:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"FFM requires at least two sparse/sequence features to build field-aware interactions."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.embedding_dim = self.fm_features[0].embedding_dim
|
|
106
|
+
if any(f.embedding_dim != self.embedding_dim for f in self.fm_features):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"All FFM features must share the same embedding_dim for field-aware interactions."
|
|
109
|
+
)
|
|
110
|
+
for feature in self.fm_features:
|
|
111
|
+
if isinstance(feature, SequenceFeature) and feature.combiner == "concat":
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"FFM does not support SequenceFeature with combiner='concat' because it breaks shared embedding_dim."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.field_aware_embeddings = nn.ModuleDict()
|
|
117
|
+
for src_feature in self.fm_features:
|
|
118
|
+
for target_field in self.fm_features:
|
|
119
|
+
key = self.field_aware_key(src_feature, target_field)
|
|
120
|
+
if key in self.field_aware_embeddings:
|
|
121
|
+
continue
|
|
122
|
+
self.field_aware_embeddings[key] = self.build_embedding(src_feature)
|
|
123
|
+
|
|
124
|
+
# First-order terms for sparse/sequence features: one hot
|
|
125
|
+
self.first_order_embeddings = nn.ModuleDict()
|
|
126
|
+
for feature in self.fm_features:
|
|
127
|
+
if feature.embedding_name in self.first_order_embeddings:
|
|
128
|
+
continue
|
|
129
|
+
emb = nn.Embedding(
|
|
130
|
+
num_embeddings=feature.vocab_size,
|
|
131
|
+
embedding_dim=1,
|
|
132
|
+
padding_idx=feature.padding_idx,
|
|
133
|
+
)
|
|
134
|
+
self.first_order_embeddings[feature.embedding_name] = emb
|
|
135
|
+
|
|
136
|
+
# Optional dense linear term
|
|
137
|
+
self.dense_features = list(dense_features)
|
|
138
|
+
dense_input_dim = sum([f.input_dim for f in self.dense_features])
|
|
139
|
+
self.linear_dense = (
|
|
140
|
+
nn.Linear(dense_input_dim, 1, bias=True) if dense_input_dim > 0 else None
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.prediction_layer = PredictionLayer(task_type=self.task)
|
|
144
|
+
self.input_mask = InputMask()
|
|
145
|
+
self.mean_pool = AveragePooling()
|
|
146
|
+
self.sum_pool = SumPooling()
|
|
147
|
+
|
|
148
|
+
self.embedding_params.extend(
|
|
149
|
+
emb.weight for emb in self.field_aware_embeddings.values()
|
|
150
|
+
)
|
|
151
|
+
self.embedding_params.extend(
|
|
152
|
+
emb.weight for emb in self.first_order_embeddings.values()
|
|
153
|
+
)
|
|
154
|
+
self.register_regularization_weights(
|
|
155
|
+
embedding_attr="field_aware_embeddings", include_modules=["linear_dense"]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self.compile(
|
|
159
|
+
optimizer=optimizer,
|
|
160
|
+
optimizer_params=optimizer_params,
|
|
161
|
+
loss=loss,
|
|
162
|
+
loss_params=loss_params,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def field_aware_key(
|
|
166
|
+
self, src_feature: SparseFeature | SequenceFeature, target_field
|
|
167
|
+
) -> str:
|
|
168
|
+
return f"{src_feature.embedding_name}__to__{target_field.name}"
|
|
169
|
+
|
|
170
|
+
def build_embedding(self, feature: SparseFeature | SequenceFeature) -> nn.Embedding:
|
|
171
|
+
if getattr(feature, "pretrained_weight", None) is not None:
|
|
172
|
+
weight = feature.pretrained_weight
|
|
173
|
+
if weight is None:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"[FFM Error]: Pretrained weight for '{feature.embedding_name}' is None."
|
|
176
|
+
)
|
|
177
|
+
if weight.shape != (feature.vocab_size, feature.embedding_dim):
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"[FFM Error]: Pretrained weight for '{feature.embedding_name}' has shape {weight.shape}, expected ({feature.vocab_size}, {feature.embedding_dim})."
|
|
180
|
+
)
|
|
181
|
+
embedding = nn.Embedding.from_pretrained(
|
|
182
|
+
embeddings=weight,
|
|
183
|
+
freeze=feature.freeze_pretrained,
|
|
184
|
+
padding_idx=feature.padding_idx,
|
|
185
|
+
)
|
|
186
|
+
embedding.weight.requires_grad = (
|
|
187
|
+
feature.trainable and not feature.freeze_pretrained
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
embedding = nn.Embedding(
|
|
191
|
+
num_embeddings=feature.vocab_size,
|
|
192
|
+
embedding_dim=feature.embedding_dim,
|
|
193
|
+
padding_idx=feature.padding_idx,
|
|
194
|
+
)
|
|
195
|
+
embedding.weight.requires_grad = feature.trainable
|
|
196
|
+
initialization = get_initializer(
|
|
197
|
+
init_type=feature.init_type,
|
|
198
|
+
activation="linear",
|
|
199
|
+
param=feature.init_params,
|
|
200
|
+
)
|
|
201
|
+
initialization(embedding.weight)
|
|
202
|
+
return embedding
|
|
203
|
+
|
|
204
|
+
def embed_for_field(
|
|
205
|
+
self,
|
|
206
|
+
feature: SparseFeature | SequenceFeature,
|
|
207
|
+
target_field,
|
|
208
|
+
x: dict[str, torch.Tensor],
|
|
209
|
+
) -> torch.Tensor:
|
|
210
|
+
key = self.field_aware_key(feature, target_field)
|
|
211
|
+
emb = self.field_aware_embeddings[key]
|
|
212
|
+
if isinstance(feature, SparseFeature):
|
|
213
|
+
return emb(x[feature.name].long())
|
|
214
|
+
|
|
215
|
+
seq_input = x[feature.name].long()
|
|
216
|
+
if feature.max_len is not None and seq_input.size(1) > feature.max_len:
|
|
217
|
+
seq_input = seq_input[:, -feature.max_len :]
|
|
218
|
+
seq_emb = emb(seq_input) # [B, L, D]
|
|
219
|
+
mask = self.input_mask(x, feature, seq_input)
|
|
220
|
+
if feature.combiner == "mean":
|
|
221
|
+
return self.mean_pool(seq_emb, mask)
|
|
222
|
+
if feature.combiner == "sum":
|
|
223
|
+
return self.sum_pool(seq_emb, mask)
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"[FFM Error]: Unsupported combiner '{feature.combiner}' for sequence feature '{feature.name}'."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def forward(self, x):
|
|
229
|
+
batch_size = x[self.fm_features[0].name].size(0)
|
|
230
|
+
device = x[self.fm_features[0].name].device
|
|
231
|
+
y_linear = torch.zeros(batch_size, 1, device=device)
|
|
232
|
+
|
|
233
|
+
# First-order dense part
|
|
234
|
+
if self.linear_dense is not None:
|
|
235
|
+
dense_inputs = [
|
|
236
|
+
x[f.name].float().view(batch_size, -1) for f in self.dense_features
|
|
237
|
+
]
|
|
238
|
+
dense_stack = torch.cat(dense_inputs, dim=1) if dense_inputs else None
|
|
239
|
+
if dense_stack is not None:
|
|
240
|
+
y_linear = y_linear + self.linear_dense(dense_stack)
|
|
241
|
+
|
|
242
|
+
# First-order sparse/sequence part
|
|
243
|
+
first_order_terms = []
|
|
244
|
+
for feature in self.fm_features:
|
|
245
|
+
emb = self.first_order_embeddings[feature.embedding_name]
|
|
246
|
+
if isinstance(feature, SparseFeature):
|
|
247
|
+
term = emb(x[feature.name].long()) # [B, 1]
|
|
248
|
+
else:
|
|
249
|
+
seq_input = x[feature.name].long()
|
|
250
|
+
if feature.max_len is not None and seq_input.size(1) > feature.max_len:
|
|
251
|
+
seq_input = seq_input[:, -feature.max_len :]
|
|
252
|
+
mask = self.input_mask(x, feature, seq_input).squeeze(1) # [B, L]
|
|
253
|
+
seq_weight = emb(seq_input).squeeze(-1) # [B, L]
|
|
254
|
+
term = (seq_weight * mask).sum(dim=1, keepdim=True) # [B, 1]
|
|
255
|
+
first_order_terms.append(term)
|
|
256
|
+
if first_order_terms:
|
|
257
|
+
y_linear = y_linear + torch.sum(
|
|
258
|
+
torch.stack(first_order_terms, dim=1), dim=1
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Field-aware interactions
|
|
262
|
+
y_interaction = torch.zeros(batch_size, 1, device=device)
|
|
263
|
+
num_fields = len(self.fm_features)
|
|
264
|
+
for i in range(num_fields - 1):
|
|
265
|
+
feature_i = self.fm_features[i]
|
|
266
|
+
for j in range(i + 1, num_fields):
|
|
267
|
+
feature_j = self.fm_features[j]
|
|
268
|
+
v_i_fj = self.embed_for_field(feature_i, feature_j, x)
|
|
269
|
+
v_j_fi = self.embed_for_field(feature_j, feature_i, x)
|
|
270
|
+
y_interaction = y_interaction + torch.sum(
|
|
271
|
+
v_i_fj * v_j_fi, dim=1, keepdim=True
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
y = y_linear + y_interaction
|
|
275
|
+
return self.prediction_layer(y)
|
nextrec/models/ranking/lr.py
CHANGED
|
@@ -113,8 +113,6 @@ class LR(BaseModel):
|
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
def forward(self, x):
|
|
116
|
-
input_linear = self.embedding(
|
|
117
|
-
x=x, features=self.all_features, squeeze_dim=True
|
|
118
|
-
)
|
|
116
|
+
input_linear = self.embedding(x=x, features=self.all_features, squeeze_dim=True)
|
|
119
117
|
y = self.linear(input_linear)
|
|
120
118
|
return self.prediction_layer(y)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
nextrec/utils/__init__.py
CHANGED
|
@@ -36,7 +36,7 @@ from .data import (
|
|
|
36
36
|
)
|
|
37
37
|
from .embedding import get_auto_embedding_dim
|
|
38
38
|
from .feature import normalize_to_list
|
|
39
|
-
from .model import get_mlp_output_dim, merge_features
|
|
39
|
+
from .model import compute_pair_scores, get_mlp_output_dim, merge_features
|
|
40
40
|
from .torch_utils import (
|
|
41
41
|
add_distributed_sampler,
|
|
42
42
|
concat_tensors,
|
|
@@ -88,6 +88,7 @@ __all__ = [
|
|
|
88
88
|
# Model utilities
|
|
89
89
|
"merge_features",
|
|
90
90
|
"get_mlp_output_dim",
|
|
91
|
+
"compute_pair_scores",
|
|
91
92
|
# Feature utilities
|
|
92
93
|
"normalize_to_list",
|
|
93
94
|
# Config utilities
|
nextrec/utils/console.py
CHANGED
|
@@ -4,7 +4,7 @@ Console and CLI utilities for NextRec.
|
|
|
4
4
|
This module centralizes CLI logging helpers, progress display, and metric tables.
|
|
5
5
|
|
|
6
6
|
Date: create on 19/12/2025
|
|
7
|
-
Checkpoint: edit on
|
|
7
|
+
Checkpoint: edit on 20/12/2025
|
|
8
8
|
Author: Yang Zhou, zyaztec@gmail.com
|
|
9
9
|
"""
|
|
10
10
|
|
|
@@ -242,6 +242,14 @@ def display_metrics_table(
|
|
|
242
242
|
normalized_order.append(name)
|
|
243
243
|
task_order = normalized_order
|
|
244
244
|
|
|
245
|
+
if not task_order and not grouped and not metrics:
|
|
246
|
+
if isinstance(loss, numbers.Number):
|
|
247
|
+
msg = f"Epoch {epoch}/{epochs} - {split} (loss={float(loss):.4f})"
|
|
248
|
+
if colorize is not None:
|
|
249
|
+
msg = colorize(msg)
|
|
250
|
+
logging.info(msg)
|
|
251
|
+
return
|
|
252
|
+
|
|
245
253
|
if Console is None or Table is None or box is None:
|
|
246
254
|
prefix = f"Epoch {epoch}/{epochs} - {split}:"
|
|
247
255
|
segments: list[str] = []
|
nextrec/utils/model.py
CHANGED
|
@@ -7,6 +7,8 @@ Author: Yang Zhou, zyaztec@gmail.com
|
|
|
7
7
|
|
|
8
8
|
from collections import OrderedDict
|
|
9
9
|
|
|
10
|
+
import torch
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def merge_features(primary, secondary) -> list:
|
|
12
14
|
merged: OrderedDict[str, object] = OrderedDict()
|
|
@@ -42,3 +44,15 @@ def select_features(
|
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
return [feature_map[name] for name in names]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_pair_scores(model, data, batch_size: int = 512):
|
|
50
|
+
user_emb = model.encode_user(data, batch_size=batch_size)
|
|
51
|
+
item_emb = model.encode_item(data, batch_size=batch_size)
|
|
52
|
+
with torch.no_grad():
|
|
53
|
+
user_tensor = torch.as_tensor(user_emb, device=model.device)
|
|
54
|
+
item_tensor = torch.as_tensor(item_emb, device=model.device)
|
|
55
|
+
scores = model.compute_similarity(user_tensor, item_tensor)
|
|
56
|
+
if model.training_mode == "pointwise":
|
|
57
|
+
scores = torch.sigmoid(scores)
|
|
58
|
+
return scores.detach().cpu().numpy()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextrec
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.12
|
|
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
|
|
@@ -66,7 +66,7 @@ Description-Content-Type: text/markdown
|
|
|
66
66
|

|
|
67
67
|

|
|
68
68
|

|
|
69
|
-

|
|
70
70
|
|
|
71
71
|
中文文档 | [English Version](README_en.md)
|
|
72
72
|
|
|
@@ -99,7 +99,7 @@ NextRec是一个基于PyTorch的现代推荐系统框架,旨在为研究工程
|
|
|
99
99
|
|
|
100
100
|
## NextRec近期进展
|
|
101
101
|
|
|
102
|
-
- **12/12/2025** 在v0.4.
|
|
102
|
+
- **12/12/2025** 在v0.4.12中加入了[RQ-VAE](/nextrec/models/representation/rqvae.py)模块。配套的[数据集](/dataset/ecommerce_task.csv)和[代码](tutorials/notebooks/zh/使用RQ-VAE构建语义ID.ipynb)已经同步在仓库中
|
|
103
103
|
- **07/12/2025** 发布了NextRec CLI命令行工具,它允许用户根据配置文件进行一键训练和推理,我们提供了相关的[教程](/nextrec_cli_preset/NextRec-CLI_zh.md)和[教学代码](/nextrec_cli_preset)
|
|
104
104
|
- **03/12/2025** NextRec获得了100颗🌟!感谢大家的支持
|
|
105
105
|
- **06/12/2025** 在v0.4.1中支持了单机多卡的分布式DDP训练,并且提供了配套的[代码](tutorials/distributed)
|
|
@@ -240,11 +240,11 @@ nextrec --mode=train --train_config=path/to/train_config.yaml
|
|
|
240
240
|
nextrec --mode=predict --predict_config=path/to/predict_config.yaml
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
> 截止当前版本0.4.
|
|
243
|
+
> 截止当前版本0.4.12,NextRec CLI支持单机训练,分布式训练相关功能尚在开发中。
|
|
244
244
|
|
|
245
245
|
## 兼容平台
|
|
246
246
|
|
|
247
|
-
当前最新版本为0.4.
|
|
247
|
+
当前最新版本为0.4.12,所有模型和测试代码均已在以下平台通过验证,如果开发者在使用中遇到兼容问题,请在issue区提出错误报告及系统版本:
|
|
248
248
|
|
|
249
249
|
| 平台 | 配置 |
|
|
250
250
|
|------|------|
|
|
@@ -260,7 +260,9 @@ nextrec --mode=predict --predict_config=path/to/predict_config.yaml
|
|
|
260
260
|
| 模型 | 论文 | 年份 | 状态 |
|
|
261
261
|
|------|------|------|------|
|
|
262
262
|
| [FM](nextrec/models/ranking/fm.py) | Factorization Machines | ICDM 2010 | 已支持 |
|
|
263
|
+
| [LR](nextrec/models/ranking/lr.py) | Logistic Regression | - | 已支持 |
|
|
263
264
|
| [AFM](nextrec/models/ranking/afm.py) | Attentional Factorization Machines: Learning the Weight of Feature Interactions via Attention Networks | IJCAI 2017 | 已支持 |
|
|
265
|
+
| [FFM](nextrec/models/ranking/ffm.py) | Field-aware Factorization Machines | RecSys 2016 | 已支持 |
|
|
264
266
|
| [DeepFM](nextrec/models/ranking/deepfm.py) | DeepFM: A Factorization-Machine based Neural Network for CTR Prediction | IJCAI 2017 | 已支持 |
|
|
265
267
|
| [Wide&Deep](nextrec/models/ranking/widedeep.py) | Wide & Deep Learning for Recommender Systems | DLRS 2016 | 已支持 |
|
|
266
268
|
| [xDeepFM](nextrec/models/ranking/xdeepfm.py) | xDeepFM: Combining Explicit and Implicit Feature Interactions | KDD 2018 | 已支持 |
|
|
@@ -272,16 +274,24 @@ nextrec --mode=predict --predict_config=path/to/predict_config.yaml
|
|
|
272
274
|
| [DIN](nextrec/models/ranking/din.py) | Deep Interest Network for Click-Through Rate Prediction | KDD 2018 | 已支持 |
|
|
273
275
|
| [DIEN](nextrec/models/ranking/dien.py) | Deep Interest Evolution Network for Click-Through Rate Prediction | AAAI 2019 | 已支持 |
|
|
274
276
|
| [MaskNet](nextrec/models/ranking/masknet.py) | MaskNet: Introducing Feature-wise Gating Blocks for High-dimensional Sparse Recommendation Data | 2020 | 已支持 |
|
|
277
|
+
| [EulerNet](nextrec/models/ranking/eulernet.py) | EulerNet: Efficient and Effective Feature Interaction Modeling with Euler's Formula | SIGIR 2021 | 已支持 |
|
|
275
278
|
|
|
276
279
|
### 召回模型
|
|
277
280
|
|
|
278
281
|
| 模型 | 论文 | 年份 | 状态 |
|
|
279
282
|
|------|------|------|------|
|
|
280
|
-
| [DSSM](nextrec/models/
|
|
281
|
-
| [DSSM v2](nextrec/models/
|
|
282
|
-
| [YouTube DNN](nextrec/models/
|
|
283
|
-
| [MIND](nextrec/models/
|
|
284
|
-
| [SDM](nextrec/models/
|
|
283
|
+
| [DSSM](nextrec/models/retrieval/dssm.py) | Learning Deep Structured Semantic Models | CIKM 2013 | 已支持 |
|
|
284
|
+
| [DSSM v2](nextrec/models/retrieval/dssm_v2.py) | DSSM with pairwise BPR-style optimization | - | 已支持 |
|
|
285
|
+
| [YouTube DNN](nextrec/models/retrieval/youtube_dnn.py) | Deep Neural Networks for YouTube Recommendations | RecSys 2016 | 已支持 |
|
|
286
|
+
| [MIND](nextrec/models/retrieval/mind.py) | Multi-Interest Network with Dynamic Routing | CIKM 2019 | 已支持 |
|
|
287
|
+
| [SDM](nextrec/models/retrieval/sdm.py) | Sequential Deep Matching Model | - | 已支持 |
|
|
288
|
+
|
|
289
|
+
### 序列推荐模型
|
|
290
|
+
|
|
291
|
+
| 模型 | 论文 | 年份 | 状态 |
|
|
292
|
+
|------|------|------|------|
|
|
293
|
+
| [SASRec](nextrec/models/sequential/sasrec.py) | Self-Attentive Sequential Recommendation | KDD 2018 | 开发中 |
|
|
294
|
+
| [HSTU](nextrec/models/sequential/hstu.py) | Actions speak louder than words: Trillion-parameter sequential transducers for generative recommendations | arXiv 2024 | 已支持 |
|
|
285
295
|
|
|
286
296
|
### 多任务模型
|
|
287
297
|
|
|
@@ -298,7 +308,18 @@ nextrec --mode=predict --predict_config=path/to/predict_config.yaml
|
|
|
298
308
|
| 模型 | 论文 | 年份 | 状态 |
|
|
299
309
|
|------|------|------|------|
|
|
300
310
|
| [TIGER](nextrec/models/generative/tiger.py) | Recommender Systems with Generative Retrieval | NeurIPS 2023 | 开发中 |
|
|
301
|
-
|
|
311
|
+
|
|
312
|
+
### 表征模型
|
|
313
|
+
|
|
314
|
+
| 模型 | 论文 | 年份 | 状态 |
|
|
315
|
+
|------|------|------|------|
|
|
316
|
+
| [RQ-VAE](nextrec/models/representation/rqvae.py) | RQ-VAE: RQVAE for Generative Retrieval | - | 已支持 |
|
|
317
|
+
| [BPR](nextrec/models/representation/bpr.py) | Bayesian Personalized Ranking | UAI 2009 | 开发中 |
|
|
318
|
+
| [MF](nextrec/models/representation/mf.py) | Matrix Factorization Techniques for Recommender Systems | - | 开发中 |
|
|
319
|
+
| [AutoRec](nextrec/models/representation/autorec.py) | AutoRec: Autoencoders Meet Collaborative Filtering | WWW 2015 | 开发中 |
|
|
320
|
+
| [LightGCN](nextrec/models/representation/lightgcn.py) | LightGCN: Simplifying and Powering Graph Convolution Network for Recommendation | SIGIR 2020 | 开发中 |
|
|
321
|
+
| [S3Rec](nextrec/models/representation/s3rec.py) | S3-Rec: Self-Supervised Learning for Sequential Recommendation | CIKM 2020 | 开发中 |
|
|
322
|
+
| [CL4SRec](nextrec/models/representation/cl4srec.py) | CL4SRec: Contrastive Learning for Sequential Recommendation | 2021 | 开发中 |
|
|
302
323
|
|
|
303
324
|
---
|
|
304
325
|
|