nextrec 0.1.1__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/__init__.py +41 -0
- nextrec/__version__.py +1 -0
- nextrec/basic/__init__.py +0 -0
- nextrec/basic/activation.py +92 -0
- nextrec/basic/callback.py +35 -0
- nextrec/basic/dataloader.py +447 -0
- nextrec/basic/features.py +87 -0
- nextrec/basic/layers.py +985 -0
- nextrec/basic/loggers.py +124 -0
- nextrec/basic/metrics.py +557 -0
- nextrec/basic/model.py +1438 -0
- nextrec/data/__init__.py +27 -0
- nextrec/data/data_utils.py +132 -0
- nextrec/data/preprocessor.py +662 -0
- nextrec/loss/__init__.py +35 -0
- nextrec/loss/loss_utils.py +136 -0
- nextrec/loss/match_losses.py +294 -0
- nextrec/models/generative/hstu.py +0 -0
- nextrec/models/generative/tiger.py +0 -0
- nextrec/models/match/__init__.py +13 -0
- nextrec/models/match/dssm.py +200 -0
- nextrec/models/match/dssm_v2.py +162 -0
- nextrec/models/match/mind.py +210 -0
- nextrec/models/match/sdm.py +253 -0
- nextrec/models/match/youtube_dnn.py +172 -0
- nextrec/models/multi_task/esmm.py +129 -0
- nextrec/models/multi_task/mmoe.py +161 -0
- nextrec/models/multi_task/ple.py +260 -0
- nextrec/models/multi_task/share_bottom.py +126 -0
- nextrec/models/ranking/__init__.py +17 -0
- nextrec/models/ranking/afm.py +118 -0
- nextrec/models/ranking/autoint.py +140 -0
- nextrec/models/ranking/dcn.py +120 -0
- nextrec/models/ranking/deepfm.py +95 -0
- nextrec/models/ranking/dien.py +214 -0
- nextrec/models/ranking/din.py +181 -0
- nextrec/models/ranking/fibinet.py +130 -0
- nextrec/models/ranking/fm.py +87 -0
- nextrec/models/ranking/masknet.py +125 -0
- nextrec/models/ranking/pnn.py +128 -0
- nextrec/models/ranking/widedeep.py +105 -0
- nextrec/models/ranking/xdeepfm.py +117 -0
- nextrec/utils/__init__.py +18 -0
- nextrec/utils/common.py +14 -0
- nextrec/utils/embedding.py +19 -0
- nextrec/utils/initializer.py +47 -0
- nextrec/utils/optimizer.py +75 -0
- nextrec-0.1.1.dist-info/METADATA +302 -0
- nextrec-0.1.1.dist-info/RECORD +51 -0
- nextrec-0.1.1.dist-info/WHEEL +4 -0
- nextrec-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Huang T, Zhang Z, Zhang B, et al. FiBiNET: Combining feature importance and bilinear feature interaction
|
|
7
|
+
for click-through rate prediction[C]//RecSys. 2019: 169-177.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
|
|
13
|
+
from nextrec.basic.model import BaseModel
|
|
14
|
+
from nextrec.basic.layers import (
|
|
15
|
+
BiLinearInteractionLayer,
|
|
16
|
+
EmbeddingLayer,
|
|
17
|
+
LR,
|
|
18
|
+
MLP,
|
|
19
|
+
PredictionLayer,
|
|
20
|
+
SENETLayer,
|
|
21
|
+
)
|
|
22
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FiBiNET(BaseModel):
|
|
26
|
+
@property
|
|
27
|
+
def model_name(self):
|
|
28
|
+
return "FiBiNET"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def task_type(self):
|
|
32
|
+
return "binary"
|
|
33
|
+
|
|
34
|
+
def __init__(self,
|
|
35
|
+
dense_features: list[DenseFeature] | list = [],
|
|
36
|
+
sparse_features: list[SparseFeature] | list = [],
|
|
37
|
+
sequence_features: list[SequenceFeature] | list = [],
|
|
38
|
+
mlp_params: dict = {},
|
|
39
|
+
bilinear_type: str = "field_interaction",
|
|
40
|
+
senet_reduction: int = 3,
|
|
41
|
+
target: list[str] | list = [],
|
|
42
|
+
optimizer: str = "adam",
|
|
43
|
+
optimizer_params: dict = {},
|
|
44
|
+
loss: str | nn.Module | None = "bce",
|
|
45
|
+
device: str = 'cpu',
|
|
46
|
+
model_id: str = "baseline",
|
|
47
|
+
embedding_l1_reg=1e-6,
|
|
48
|
+
dense_l1_reg=1e-5,
|
|
49
|
+
embedding_l2_reg=1e-5,
|
|
50
|
+
dense_l2_reg=1e-4):
|
|
51
|
+
|
|
52
|
+
super(FiBiNET, self).__init__(
|
|
53
|
+
dense_features=dense_features,
|
|
54
|
+
sparse_features=sparse_features,
|
|
55
|
+
sequence_features=sequence_features,
|
|
56
|
+
target=target,
|
|
57
|
+
task=self.task_type,
|
|
58
|
+
device=device,
|
|
59
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
60
|
+
dense_l1_reg=dense_l1_reg,
|
|
61
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
62
|
+
dense_l2_reg=dense_l2_reg,
|
|
63
|
+
early_stop_patience=20,
|
|
64
|
+
model_id=model_id
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.loss = loss
|
|
68
|
+
if self.loss is None:
|
|
69
|
+
self.loss = "bce"
|
|
70
|
+
|
|
71
|
+
self.linear_features = sparse_features + sequence_features
|
|
72
|
+
self.deep_features = dense_features + sparse_features + sequence_features
|
|
73
|
+
self.interaction_features = sparse_features + sequence_features
|
|
74
|
+
|
|
75
|
+
if len(self.interaction_features) < 2:
|
|
76
|
+
raise ValueError("FiBiNET requires at least two sparse/sequence features for interactions.")
|
|
77
|
+
|
|
78
|
+
self.embedding = EmbeddingLayer(features=self.deep_features)
|
|
79
|
+
|
|
80
|
+
self.num_fields = len(self.interaction_features)
|
|
81
|
+
self.embedding_dim = self.interaction_features[0].embedding_dim
|
|
82
|
+
if any(f.embedding_dim != self.embedding_dim for f in self.interaction_features):
|
|
83
|
+
raise ValueError("All interaction features must share the same embedding_dim in FiBiNET.")
|
|
84
|
+
|
|
85
|
+
self.senet = SENETLayer(num_fields=self.num_fields, reduction_ratio=senet_reduction)
|
|
86
|
+
self.bilinear_standard = BiLinearInteractionLayer(
|
|
87
|
+
input_dim=self.embedding_dim,
|
|
88
|
+
num_fields=self.num_fields,
|
|
89
|
+
bilinear_type=bilinear_type,
|
|
90
|
+
)
|
|
91
|
+
self.bilinear_senet = BiLinearInteractionLayer(
|
|
92
|
+
input_dim=self.embedding_dim,
|
|
93
|
+
num_fields=self.num_fields,
|
|
94
|
+
bilinear_type=bilinear_type,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
linear_dim = sum([f.embedding_dim for f in self.linear_features])
|
|
98
|
+
self.linear = LR(linear_dim)
|
|
99
|
+
|
|
100
|
+
num_pairs = self.num_fields * (self.num_fields - 1) // 2
|
|
101
|
+
interaction_dim = num_pairs * self.embedding_dim * 2
|
|
102
|
+
self.mlp = MLP(input_dim=interaction_dim, **mlp_params)
|
|
103
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
104
|
+
|
|
105
|
+
# Register regularization weights
|
|
106
|
+
self._register_regularization_weights(
|
|
107
|
+
embedding_attr='embedding',
|
|
108
|
+
include_modules=['linear', 'senet', 'bilinear_standard', 'bilinear_senet', 'mlp']
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.compile(
|
|
112
|
+
optimizer=optimizer,
|
|
113
|
+
optimizer_params=optimizer_params,
|
|
114
|
+
loss=loss
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def forward(self, x):
|
|
118
|
+
input_linear = self.embedding(x=x, features=self.linear_features, squeeze_dim=True)
|
|
119
|
+
y_linear = self.linear(input_linear)
|
|
120
|
+
|
|
121
|
+
field_emb = self.embedding(x=x, features=self.interaction_features, squeeze_dim=False)
|
|
122
|
+
senet_emb = self.senet(field_emb)
|
|
123
|
+
|
|
124
|
+
bilinear_standard = self.bilinear_standard(field_emb).flatten(start_dim=1)
|
|
125
|
+
bilinear_senet = self.bilinear_senet(senet_emb).flatten(start_dim=1)
|
|
126
|
+
deep_input = torch.cat([bilinear_standard, bilinear_senet], dim=1)
|
|
127
|
+
y_deep = self.mlp(deep_input)
|
|
128
|
+
|
|
129
|
+
y = y_linear + y_deep
|
|
130
|
+
return self.prediction_layer(y)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Rendle S. Factorization machines[C]//ICDM. 2010: 995-1000.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import torch.nn as nn
|
|
10
|
+
|
|
11
|
+
from nextrec.basic.model import BaseModel
|
|
12
|
+
from nextrec.basic.layers import EmbeddingLayer, FM as FMInteraction, LR, PredictionLayer
|
|
13
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FM(BaseModel):
|
|
17
|
+
@property
|
|
18
|
+
def model_name(self):
|
|
19
|
+
return "FM"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def task_type(self):
|
|
23
|
+
return "binary"
|
|
24
|
+
|
|
25
|
+
def __init__(self,
|
|
26
|
+
dense_features: list[DenseFeature] | list = [],
|
|
27
|
+
sparse_features: list[SparseFeature] | list = [],
|
|
28
|
+
sequence_features: list[SequenceFeature] | list = [],
|
|
29
|
+
target: list[str] | list = [],
|
|
30
|
+
optimizer: str = "adam",
|
|
31
|
+
optimizer_params: dict = {},
|
|
32
|
+
loss: str | nn.Module | None = "bce",
|
|
33
|
+
device: str = 'cpu',
|
|
34
|
+
model_id: str = "baseline",
|
|
35
|
+
embedding_l1_reg=1e-6,
|
|
36
|
+
dense_l1_reg=1e-5,
|
|
37
|
+
embedding_l2_reg=1e-5,
|
|
38
|
+
dense_l2_reg=1e-4):
|
|
39
|
+
|
|
40
|
+
super(FM, self).__init__(
|
|
41
|
+
dense_features=dense_features,
|
|
42
|
+
sparse_features=sparse_features,
|
|
43
|
+
sequence_features=sequence_features,
|
|
44
|
+
target=target,
|
|
45
|
+
task=self.task_type,
|
|
46
|
+
device=device,
|
|
47
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
48
|
+
dense_l1_reg=dense_l1_reg,
|
|
49
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
50
|
+
dense_l2_reg=dense_l2_reg,
|
|
51
|
+
early_stop_patience=20,
|
|
52
|
+
model_id=model_id
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.loss = loss
|
|
56
|
+
if self.loss is None:
|
|
57
|
+
self.loss = "bce"
|
|
58
|
+
|
|
59
|
+
self.fm_features = sparse_features + sequence_features
|
|
60
|
+
if len(self.fm_features) == 0:
|
|
61
|
+
raise ValueError("FM requires at least one sparse or sequence feature.")
|
|
62
|
+
|
|
63
|
+
self.embedding = EmbeddingLayer(features=self.fm_features)
|
|
64
|
+
|
|
65
|
+
fm_input_dim = sum([f.embedding_dim for f in self.fm_features])
|
|
66
|
+
self.linear = LR(fm_input_dim)
|
|
67
|
+
self.fm = FMInteraction(reduce_sum=True)
|
|
68
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
69
|
+
|
|
70
|
+
# Register regularization weights
|
|
71
|
+
self._register_regularization_weights(
|
|
72
|
+
embedding_attr='embedding',
|
|
73
|
+
include_modules=['linear']
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.compile(
|
|
77
|
+
optimizer=optimizer,
|
|
78
|
+
optimizer_params=optimizer_params,
|
|
79
|
+
loss=loss
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def forward(self, x):
|
|
83
|
+
input_fm = self.embedding(x=x, features=self.fm_features, squeeze_dim=False)
|
|
84
|
+
y_linear = self.linear(input_fm.flatten(start_dim=1))
|
|
85
|
+
y_fm = self.fm(input_fm)
|
|
86
|
+
y = y_linear + y_fm
|
|
87
|
+
return self.prediction_layer(y)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Pan Z, Sun F, Liu J, et al. MaskNet: Introducing feature-wise gating blocks for high-dimensional
|
|
7
|
+
sparse recommendation data (CCF-Tencent CTR competition solution, 2020).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
|
|
13
|
+
from nextrec.basic.model import BaseModel
|
|
14
|
+
from nextrec.basic.layers import EmbeddingLayer, LR, MLP, PredictionLayer
|
|
15
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MaskNet(BaseModel):
|
|
19
|
+
@property
|
|
20
|
+
def model_name(self):
|
|
21
|
+
return "MaskNet"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def task_type(self):
|
|
25
|
+
return "binary"
|
|
26
|
+
|
|
27
|
+
def __init__(self,
|
|
28
|
+
dense_features: list[DenseFeature] | list = [],
|
|
29
|
+
sparse_features: list[SparseFeature] | list = [],
|
|
30
|
+
sequence_features: list[SequenceFeature] | list = [],
|
|
31
|
+
num_blocks: int = 3,
|
|
32
|
+
mask_hidden_dim: int = 64,
|
|
33
|
+
block_dropout: float = 0.1,
|
|
34
|
+
mlp_params: dict = {},
|
|
35
|
+
target: list[str] | list = [],
|
|
36
|
+
optimizer: str = "adam",
|
|
37
|
+
optimizer_params: dict = {},
|
|
38
|
+
loss: str | nn.Module | None = "bce",
|
|
39
|
+
device: str = 'cpu',
|
|
40
|
+
model_id: str = "baseline",
|
|
41
|
+
embedding_l1_reg=1e-6,
|
|
42
|
+
dense_l1_reg=1e-5,
|
|
43
|
+
embedding_l2_reg=1e-5,
|
|
44
|
+
dense_l2_reg=1e-4):
|
|
45
|
+
|
|
46
|
+
super(MaskNet, self).__init__(
|
|
47
|
+
dense_features=dense_features,
|
|
48
|
+
sparse_features=sparse_features,
|
|
49
|
+
sequence_features=sequence_features,
|
|
50
|
+
target=target,
|
|
51
|
+
task=self.task_type,
|
|
52
|
+
device=device,
|
|
53
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
54
|
+
dense_l1_reg=dense_l1_reg,
|
|
55
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
56
|
+
dense_l2_reg=dense_l2_reg,
|
|
57
|
+
early_stop_patience=20,
|
|
58
|
+
model_id=model_id
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.loss = loss
|
|
62
|
+
if self.loss is None:
|
|
63
|
+
self.loss = "bce"
|
|
64
|
+
|
|
65
|
+
self.mask_features = sparse_features + sequence_features
|
|
66
|
+
if len(self.mask_features) == 0:
|
|
67
|
+
raise ValueError("MaskNet requires at least one sparse/sequence feature.")
|
|
68
|
+
|
|
69
|
+
self.embedding = EmbeddingLayer(features=self.mask_features)
|
|
70
|
+
self.num_fields = len(self.mask_features)
|
|
71
|
+
self.embedding_dim = self.mask_features[0].embedding_dim
|
|
72
|
+
if any(f.embedding_dim != self.embedding_dim for f in self.mask_features):
|
|
73
|
+
raise ValueError("MaskNet expects identical embedding_dim across mask_features.")
|
|
74
|
+
|
|
75
|
+
self.num_blocks = max(1, num_blocks)
|
|
76
|
+
self.field_dim = self.num_fields * self.embedding_dim
|
|
77
|
+
|
|
78
|
+
self.linear = LR(self.field_dim)
|
|
79
|
+
self.mask_generators = nn.ModuleList()
|
|
80
|
+
for _ in range(self.num_blocks):
|
|
81
|
+
self.mask_generators.append(
|
|
82
|
+
nn.Sequential(
|
|
83
|
+
nn.Linear(self.field_dim, mask_hidden_dim),
|
|
84
|
+
nn.ReLU(),
|
|
85
|
+
nn.Linear(mask_hidden_dim, self.num_fields)
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.block_dropout = nn.Dropout(block_dropout)
|
|
90
|
+
self.final_mlp = MLP(input_dim=self.field_dim * self.num_blocks, **mlp_params)
|
|
91
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
92
|
+
|
|
93
|
+
self._register_regularization_weights(
|
|
94
|
+
embedding_attr='embedding',
|
|
95
|
+
include_modules=['linear', 'mask_generators', 'final_mlp']
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.compile(
|
|
99
|
+
optimizer=optimizer,
|
|
100
|
+
optimizer_params=optimizer_params,
|
|
101
|
+
loss=loss
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def forward(self, x):
|
|
105
|
+
field_emb = self.embedding(x=x, features=self.mask_features, squeeze_dim=False)
|
|
106
|
+
flat_input = field_emb.flatten(start_dim=1)
|
|
107
|
+
y_linear = self.linear(flat_input)
|
|
108
|
+
|
|
109
|
+
block_input = field_emb
|
|
110
|
+
mask_input = flat_input
|
|
111
|
+
block_outputs = []
|
|
112
|
+
for mask_gen in self.mask_generators:
|
|
113
|
+
mask_logits = mask_gen(mask_input)
|
|
114
|
+
mask = torch.sigmoid(mask_logits).unsqueeze(-1)
|
|
115
|
+
masked_emb = block_input * mask
|
|
116
|
+
block_output = self.block_dropout(masked_emb.flatten(start_dim=1))
|
|
117
|
+
block_outputs.append(block_output)
|
|
118
|
+
mask_input = block_output
|
|
119
|
+
block_input = masked_emb.view_as(field_emb)
|
|
120
|
+
|
|
121
|
+
stacked = torch.cat(block_outputs, dim=1)
|
|
122
|
+
y_deep = self.final_mlp(stacked)
|
|
123
|
+
|
|
124
|
+
y = y_linear + y_deep
|
|
125
|
+
return self.prediction_layer(y)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Qu Y, Cai H, Ren K, et al. Product-based neural networks for user response prediction[C]//ICDM. 2016: 1149-1154.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
import torch.nn as nn
|
|
11
|
+
|
|
12
|
+
from nextrec.basic.model import BaseModel
|
|
13
|
+
from nextrec.basic.layers import EmbeddingLayer, MLP, PredictionLayer
|
|
14
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PNN(BaseModel):
|
|
18
|
+
@property
|
|
19
|
+
def model_name(self):
|
|
20
|
+
return "PNN"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def task_type(self):
|
|
24
|
+
return "binary"
|
|
25
|
+
|
|
26
|
+
def __init__(self,
|
|
27
|
+
dense_features: list[DenseFeature] | list = [],
|
|
28
|
+
sparse_features: list[SparseFeature] | list = [],
|
|
29
|
+
sequence_features: list[SequenceFeature] | list = [],
|
|
30
|
+
mlp_params: dict = {},
|
|
31
|
+
product_type: str = "inner",
|
|
32
|
+
outer_product_dim: int | None = None,
|
|
33
|
+
target: list[str] | list = [],
|
|
34
|
+
optimizer: str = "adam",
|
|
35
|
+
optimizer_params: dict = {},
|
|
36
|
+
loss: str | nn.Module | None = "bce",
|
|
37
|
+
device: str = 'cpu',
|
|
38
|
+
model_id: str = "baseline",
|
|
39
|
+
embedding_l1_reg=1e-6,
|
|
40
|
+
dense_l1_reg=1e-5,
|
|
41
|
+
embedding_l2_reg=1e-5,
|
|
42
|
+
dense_l2_reg=1e-4):
|
|
43
|
+
|
|
44
|
+
super(PNN, self).__init__(
|
|
45
|
+
dense_features=dense_features,
|
|
46
|
+
sparse_features=sparse_features,
|
|
47
|
+
sequence_features=sequence_features,
|
|
48
|
+
target=target,
|
|
49
|
+
task=self.task_type,
|
|
50
|
+
device=device,
|
|
51
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
52
|
+
dense_l1_reg=dense_l1_reg,
|
|
53
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
54
|
+
dense_l2_reg=dense_l2_reg,
|
|
55
|
+
early_stop_patience=20,
|
|
56
|
+
model_id=model_id
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.loss = loss
|
|
60
|
+
if self.loss is None:
|
|
61
|
+
self.loss = "bce"
|
|
62
|
+
|
|
63
|
+
self.field_features = sparse_features + sequence_features
|
|
64
|
+
if len(self.field_features) < 2:
|
|
65
|
+
raise ValueError("PNN requires at least two sparse/sequence features.")
|
|
66
|
+
|
|
67
|
+
self.embedding = EmbeddingLayer(features=self.field_features)
|
|
68
|
+
self.num_fields = len(self.field_features)
|
|
69
|
+
self.embedding_dim = self.field_features[0].embedding_dim
|
|
70
|
+
if any(f.embedding_dim != self.embedding_dim for f in self.field_features):
|
|
71
|
+
raise ValueError("All field features must share the same embedding_dim for PNN.")
|
|
72
|
+
|
|
73
|
+
self.product_type = product_type.lower()
|
|
74
|
+
if self.product_type not in {"inner", "outer"}:
|
|
75
|
+
raise ValueError("product_type must be 'inner' or 'outer'.")
|
|
76
|
+
|
|
77
|
+
self.num_pairs = self.num_fields * (self.num_fields - 1) // 2
|
|
78
|
+
if self.product_type == "outer":
|
|
79
|
+
self.outer_dim = outer_product_dim or self.embedding_dim
|
|
80
|
+
self.kernel = nn.Linear(self.embedding_dim, self.outer_dim, bias=False)
|
|
81
|
+
product_dim = self.num_pairs * self.outer_dim
|
|
82
|
+
else:
|
|
83
|
+
self.outer_dim = None
|
|
84
|
+
product_dim = self.num_pairs
|
|
85
|
+
|
|
86
|
+
linear_dim = self.num_fields * self.embedding_dim
|
|
87
|
+
self.mlp = MLP(input_dim=linear_dim + product_dim, **mlp_params)
|
|
88
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
89
|
+
|
|
90
|
+
modules = ['mlp']
|
|
91
|
+
if self.product_type == "outer":
|
|
92
|
+
modules.append('kernel')
|
|
93
|
+
self._register_regularization_weights(
|
|
94
|
+
embedding_attr='embedding',
|
|
95
|
+
include_modules=modules
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.compile(
|
|
99
|
+
optimizer=optimizer,
|
|
100
|
+
optimizer_params=optimizer_params,
|
|
101
|
+
loss=loss
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def forward(self, x):
|
|
105
|
+
field_emb = self.embedding(x=x, features=self.field_features, squeeze_dim=False)
|
|
106
|
+
linear_signal = field_emb.flatten(start_dim=1)
|
|
107
|
+
|
|
108
|
+
if self.product_type == "inner":
|
|
109
|
+
interactions = []
|
|
110
|
+
for i in range(self.num_fields - 1):
|
|
111
|
+
vi = field_emb[:, i, :]
|
|
112
|
+
for j in range(i + 1, self.num_fields):
|
|
113
|
+
vj = field_emb[:, j, :]
|
|
114
|
+
interactions.append(torch.sum(vi * vj, dim=1, keepdim=True))
|
|
115
|
+
product_signal = torch.cat(interactions, dim=1)
|
|
116
|
+
else:
|
|
117
|
+
transformed = self.kernel(field_emb) # [B, F, outer_dim]
|
|
118
|
+
interactions = []
|
|
119
|
+
for i in range(self.num_fields - 1):
|
|
120
|
+
vi = transformed[:, i, :]
|
|
121
|
+
for j in range(i + 1, self.num_fields):
|
|
122
|
+
vj = transformed[:, j, :]
|
|
123
|
+
interactions.append(vi * vj)
|
|
124
|
+
product_signal = torch.stack(interactions, dim=1).flatten(start_dim=1)
|
|
125
|
+
|
|
126
|
+
deep_input = torch.cat([linear_signal, product_signal], dim=1)
|
|
127
|
+
y = self.mlp(deep_input)
|
|
128
|
+
return self.prediction_layer(y)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Cheng H T, Koc L, Harmsen J, et al. Wide & deep learning for recommender systems[C]
|
|
7
|
+
//Proceedings of the 1st workshop on deep learning for recommender systems. 2016: 7-10.
|
|
8
|
+
(https://arxiv.org/abs/1606.07792)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn as nn
|
|
13
|
+
|
|
14
|
+
from nextrec.basic.model import BaseModel
|
|
15
|
+
from nextrec.basic.layers import LR, EmbeddingLayer, MLP, PredictionLayer
|
|
16
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WideDeep(BaseModel):
|
|
20
|
+
@property
|
|
21
|
+
def model_name(self):
|
|
22
|
+
return "WideDeep"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def task_type(self):
|
|
26
|
+
return "binary"
|
|
27
|
+
|
|
28
|
+
def __init__(self,
|
|
29
|
+
dense_features: list[DenseFeature],
|
|
30
|
+
sparse_features: list[SparseFeature],
|
|
31
|
+
sequence_features: list[SequenceFeature],
|
|
32
|
+
mlp_params: dict,
|
|
33
|
+
target: list[str] = [],
|
|
34
|
+
optimizer: str = "adam",
|
|
35
|
+
optimizer_params: dict = {},
|
|
36
|
+
loss: str | nn.Module | None = "bce",
|
|
37
|
+
device: str = 'cpu',
|
|
38
|
+
model_id: str = "baseline",
|
|
39
|
+
embedding_l1_reg=1e-6,
|
|
40
|
+
dense_l1_reg=1e-5,
|
|
41
|
+
embedding_l2_reg=1e-5,
|
|
42
|
+
dense_l2_reg=1e-4):
|
|
43
|
+
|
|
44
|
+
super(WideDeep, self).__init__(
|
|
45
|
+
dense_features=dense_features,
|
|
46
|
+
sparse_features=sparse_features,
|
|
47
|
+
sequence_features=sequence_features,
|
|
48
|
+
target=target,
|
|
49
|
+
task=self.task_type,
|
|
50
|
+
device=device,
|
|
51
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
52
|
+
dense_l1_reg=dense_l1_reg,
|
|
53
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
54
|
+
dense_l2_reg=dense_l2_reg,
|
|
55
|
+
early_stop_patience=20,
|
|
56
|
+
model_id=model_id
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.loss = loss
|
|
60
|
+
if self.loss is None:
|
|
61
|
+
self.loss = "bce"
|
|
62
|
+
|
|
63
|
+
# Wide part: use all features for linear model
|
|
64
|
+
self.wide_features = sparse_features + sequence_features
|
|
65
|
+
|
|
66
|
+
# Deep part: use all features
|
|
67
|
+
self.deep_features = dense_features + sparse_features + sequence_features
|
|
68
|
+
|
|
69
|
+
# Embedding layer for deep part
|
|
70
|
+
self.embedding = EmbeddingLayer(features=self.deep_features)
|
|
71
|
+
|
|
72
|
+
# Wide part: Linear layer
|
|
73
|
+
wide_dim = sum([f.embedding_dim for f in self.wide_features])
|
|
74
|
+
self.linear = LR(wide_dim)
|
|
75
|
+
|
|
76
|
+
# Deep part: MLP
|
|
77
|
+
deep_emb_dim_total = sum([f.embedding_dim for f in self.deep_features if not isinstance(f, DenseFeature)])
|
|
78
|
+
dense_input_dim = sum([getattr(f, "embedding_dim", 1) or 1 for f in dense_features])
|
|
79
|
+
self.mlp = MLP(input_dim=deep_emb_dim_total + dense_input_dim, **mlp_params)
|
|
80
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
81
|
+
|
|
82
|
+
# Register regularization weights
|
|
83
|
+
self._register_regularization_weights(
|
|
84
|
+
embedding_attr='embedding',
|
|
85
|
+
include_modules=['linear', 'mlp']
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.compile(
|
|
89
|
+
optimizer=optimizer,
|
|
90
|
+
optimizer_params=optimizer_params,
|
|
91
|
+
loss=loss
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def forward(self, x):
|
|
95
|
+
# Deep part
|
|
96
|
+
input_deep = self.embedding(x=x, features=self.deep_features, squeeze_dim=True)
|
|
97
|
+
y_deep = self.mlp(input_deep) # [B, 1]
|
|
98
|
+
|
|
99
|
+
# Wide part
|
|
100
|
+
input_wide = self.embedding(x=x, features=self.wide_features, squeeze_dim=True)
|
|
101
|
+
y_wide = self.linear(input_wide)
|
|
102
|
+
|
|
103
|
+
# Combine wide and deep
|
|
104
|
+
y = y_wide + y_deep
|
|
105
|
+
return self.prediction_layer(y)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date: create on 09/11/2025
|
|
3
|
+
Author:
|
|
4
|
+
Yang Zhou,zyaztec@gmail.com
|
|
5
|
+
Reference:
|
|
6
|
+
[1] Lian J, Zhou X, Zhang F, et al. xdeepfm: Combining explicit and implicit feature interactions
|
|
7
|
+
for recommender systems[C]//Proceedings of the 24th ACM SIGKDD international conference on
|
|
8
|
+
knowledge discovery & data mining. 2018: 1754-1763.
|
|
9
|
+
(https://arxiv.org/abs/1803.05170)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn as nn
|
|
14
|
+
|
|
15
|
+
from nextrec.basic.model import BaseModel
|
|
16
|
+
from nextrec.basic.layers import LR, EmbeddingLayer, MLP, CIN, PredictionLayer
|
|
17
|
+
from nextrec.basic.features import DenseFeature, SparseFeature, SequenceFeature
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class xDeepFM(BaseModel):
|
|
21
|
+
@property
|
|
22
|
+
def model_name(self):
|
|
23
|
+
return "xDeepFM"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def task_type(self):
|
|
27
|
+
return "binary"
|
|
28
|
+
|
|
29
|
+
def __init__(self,
|
|
30
|
+
dense_features: list[DenseFeature],
|
|
31
|
+
sparse_features: list[SparseFeature],
|
|
32
|
+
sequence_features: list[SequenceFeature],
|
|
33
|
+
mlp_params: dict,
|
|
34
|
+
cin_size: list[int] = [128, 128],
|
|
35
|
+
split_half: bool = True,
|
|
36
|
+
target: list[str] = [],
|
|
37
|
+
optimizer: str = "adam",
|
|
38
|
+
optimizer_params: dict = {},
|
|
39
|
+
loss: str | nn.Module | None = "bce",
|
|
40
|
+
device: str = 'cpu',
|
|
41
|
+
model_id: str = "baseline",
|
|
42
|
+
embedding_l1_reg=1e-6,
|
|
43
|
+
dense_l1_reg=1e-5,
|
|
44
|
+
embedding_l2_reg=1e-5,
|
|
45
|
+
dense_l2_reg=1e-4):
|
|
46
|
+
|
|
47
|
+
super(xDeepFM, self).__init__(
|
|
48
|
+
dense_features=dense_features,
|
|
49
|
+
sparse_features=sparse_features,
|
|
50
|
+
sequence_features=sequence_features,
|
|
51
|
+
target=target,
|
|
52
|
+
task=self.task_type,
|
|
53
|
+
device=device,
|
|
54
|
+
embedding_l1_reg=embedding_l1_reg,
|
|
55
|
+
dense_l1_reg=dense_l1_reg,
|
|
56
|
+
embedding_l2_reg=embedding_l2_reg,
|
|
57
|
+
dense_l2_reg=dense_l2_reg,
|
|
58
|
+
early_stop_patience=20,
|
|
59
|
+
model_id=model_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.loss = loss
|
|
63
|
+
if self.loss is None:
|
|
64
|
+
self.loss = "bce"
|
|
65
|
+
|
|
66
|
+
# Linear part and CIN part: use sparse and sequence features
|
|
67
|
+
self.linear_features = sparse_features + sequence_features
|
|
68
|
+
|
|
69
|
+
# Deep part: use all features
|
|
70
|
+
self.deep_features = dense_features + sparse_features + sequence_features
|
|
71
|
+
|
|
72
|
+
# Embedding layer
|
|
73
|
+
self.embedding = EmbeddingLayer(features=self.deep_features)
|
|
74
|
+
|
|
75
|
+
# Linear part
|
|
76
|
+
linear_dim = sum([f.embedding_dim for f in self.linear_features])
|
|
77
|
+
self.linear = LR(linear_dim)
|
|
78
|
+
|
|
79
|
+
# CIN part: Compressed Interaction Network
|
|
80
|
+
num_fields = len(self.linear_features)
|
|
81
|
+
self.cin = CIN(input_dim=num_fields, cin_size=cin_size, split_half=split_half)
|
|
82
|
+
|
|
83
|
+
# Deep part: DNN
|
|
84
|
+
deep_emb_dim_total = sum([f.embedding_dim for f in self.deep_features if not isinstance(f, DenseFeature)])
|
|
85
|
+
dense_input_dim = sum([getattr(f, "embedding_dim", 1) or 1 for f in dense_features])
|
|
86
|
+
self.mlp = MLP(input_dim=deep_emb_dim_total + dense_input_dim, **mlp_params)
|
|
87
|
+
self.prediction_layer = PredictionLayer(task_type=self.task_type)
|
|
88
|
+
|
|
89
|
+
# Register regularization weights
|
|
90
|
+
self._register_regularization_weights(
|
|
91
|
+
embedding_attr='embedding',
|
|
92
|
+
include_modules=['linear', 'cin', 'mlp']
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.compile(
|
|
96
|
+
optimizer=optimizer,
|
|
97
|
+
optimizer_params=optimizer_params,
|
|
98
|
+
loss=loss
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def forward(self, x):
|
|
102
|
+
# Get embeddings for linear and CIN (sparse features only)
|
|
103
|
+
input_linear = self.embedding(x=x, features=self.linear_features, squeeze_dim=False)
|
|
104
|
+
|
|
105
|
+
# Linear part
|
|
106
|
+
y_linear = self.linear(input_linear.flatten(start_dim=1))
|
|
107
|
+
|
|
108
|
+
# CIN part
|
|
109
|
+
y_cin = self.cin(input_linear) # [B, 1]
|
|
110
|
+
|
|
111
|
+
# Deep part
|
|
112
|
+
input_deep = self.embedding(x=x, features=self.deep_features, squeeze_dim=True)
|
|
113
|
+
y_deep = self.mlp(input_deep) # [B, 1]
|
|
114
|
+
|
|
115
|
+
# Combine all parts
|
|
116
|
+
y = y_linear + y_cin + y_deep
|
|
117
|
+
return self.prediction_layer(y)
|