nextrec 0.4.22__py3-none-any.whl → 0.4.24__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/layers.py +96 -46
- nextrec/basic/metrics.py +128 -114
- nextrec/basic/model.py +94 -91
- nextrec/basic/summary.py +36 -2
- nextrec/data/dataloader.py +2 -0
- nextrec/data/preprocessor.py +137 -5
- nextrec/loss/listwise.py +19 -6
- nextrec/loss/pairwise.py +6 -4
- nextrec/loss/pointwise.py +8 -6
- nextrec/models/multi_task/aitm.py +0 -0
- nextrec/models/multi_task/apg.py +0 -0
- nextrec/models/multi_task/cross_stitch.py +0 -0
- nextrec/models/multi_task/esmm.py +5 -28
- nextrec/models/multi_task/mmoe.py +6 -28
- nextrec/models/multi_task/pepnet.py +335 -0
- nextrec/models/multi_task/ple.py +21 -40
- nextrec/models/multi_task/poso.py +17 -39
- nextrec/models/multi_task/share_bottom.py +5 -28
- nextrec/models/multi_task/snr_trans.py +0 -0
- nextrec/models/ranking/afm.py +3 -27
- nextrec/models/ranking/autoint.py +5 -38
- nextrec/models/ranking/dcn.py +1 -26
- nextrec/models/ranking/dcn_v2.py +6 -34
- nextrec/models/ranking/deepfm.py +2 -29
- nextrec/models/ranking/dien.py +2 -28
- nextrec/models/ranking/din.py +2 -27
- nextrec/models/ranking/eulernet.py +3 -30
- nextrec/models/ranking/ffm.py +0 -26
- nextrec/models/ranking/fibinet.py +8 -32
- nextrec/models/ranking/fm.py +0 -29
- nextrec/models/ranking/lr.py +0 -30
- nextrec/models/ranking/masknet.py +4 -30
- nextrec/models/ranking/pnn.py +4 -28
- nextrec/models/ranking/widedeep.py +0 -32
- nextrec/models/ranking/xdeepfm.py +0 -30
- nextrec/models/retrieval/dssm.py +4 -28
- nextrec/models/retrieval/dssm_v2.py +4 -28
- nextrec/models/retrieval/mind.py +2 -22
- nextrec/models/retrieval/sdm.py +4 -24
- nextrec/models/retrieval/youtube_dnn.py +4 -25
- nextrec/models/sequential/hstu.py +0 -18
- nextrec/utils/model.py +91 -4
- nextrec/utils/types.py +35 -0
- {nextrec-0.4.22.dist-info → nextrec-0.4.24.dist-info}/METADATA +8 -6
- nextrec-0.4.24.dist-info/RECORD +86 -0
- nextrec-0.4.22.dist-info/RECORD +0 -81
- {nextrec-0.4.22.dist-info → nextrec-0.4.24.dist-info}/WHEEL +0 -0
- {nextrec-0.4.22.dist-info → nextrec-0.4.24.dist-info}/entry_points.txt +0 -0
- {nextrec-0.4.22.dist-info → nextrec-0.4.24.dist-info}/licenses/LICENSE +0 -0
nextrec/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
1
|
+
__version__ = "0.4.24"
|
nextrec/basic/layers.py
CHANGED
|
@@ -20,6 +20,7 @@ import torch.nn.functional as F
|
|
|
20
20
|
from nextrec.basic.activation import activation_layer
|
|
21
21
|
from nextrec.basic.features import DenseFeature, SequenceFeature, SparseFeature
|
|
22
22
|
from nextrec.utils.torch_utils import get_initializer
|
|
23
|
+
from nextrec.utils.types import ActivationName
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class PredictionLayer(nn.Module):
|
|
@@ -590,71 +591,48 @@ class MLP(nn.Module):
|
|
|
590
591
|
def __init__(
|
|
591
592
|
self,
|
|
592
593
|
input_dim: int,
|
|
593
|
-
|
|
594
|
-
|
|
594
|
+
hidden_dims: list[int] | None = None,
|
|
595
|
+
output_dim: int | None = 1,
|
|
595
596
|
dropout: float = 0.0,
|
|
596
|
-
activation:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
"relu6",
|
|
600
|
-
"elu",
|
|
601
|
-
"selu",
|
|
602
|
-
"leaky_relu",
|
|
603
|
-
"prelu",
|
|
604
|
-
"gelu",
|
|
605
|
-
"sigmoid",
|
|
606
|
-
"tanh",
|
|
607
|
-
"softplus",
|
|
608
|
-
"softsign",
|
|
609
|
-
"hardswish",
|
|
610
|
-
"mish",
|
|
611
|
-
"silu",
|
|
612
|
-
"swish",
|
|
613
|
-
"hardsigmoid",
|
|
614
|
-
"tanhshrink",
|
|
615
|
-
"softshrink",
|
|
616
|
-
"none",
|
|
617
|
-
"linear",
|
|
618
|
-
"identity",
|
|
619
|
-
] = "relu",
|
|
620
|
-
use_norm: bool = True,
|
|
621
|
-
norm_type: Literal["batch_norm", "layer_norm"] = "layer_norm",
|
|
597
|
+
activation: ActivationName = "relu",
|
|
598
|
+
norm_type: Literal["batch_norm", "layer_norm", "none"] = "none",
|
|
599
|
+
output_activation: ActivationName = "none",
|
|
622
600
|
):
|
|
623
601
|
"""
|
|
624
602
|
Multi-Layer Perceptron (MLP) module.
|
|
625
603
|
|
|
626
604
|
Args:
|
|
627
605
|
input_dim: Dimension of the input features.
|
|
628
|
-
|
|
629
|
-
|
|
606
|
+
output_dim: Output dimension of the final layer. If None, no output layer is added.
|
|
607
|
+
hidden_dims: List of hidden layer dimensions. If None, no hidden layers are added.
|
|
630
608
|
dropout: Dropout rate between layers.
|
|
631
609
|
activation: Activation function to use between layers.
|
|
632
|
-
|
|
633
|
-
|
|
610
|
+
norm_type: Type of normalization to use ("batch_norm", "layer_norm", or "none").
|
|
611
|
+
output_activation: Activation function applied after the output layer.
|
|
634
612
|
"""
|
|
635
613
|
super().__init__()
|
|
636
|
-
|
|
637
|
-
dims = []
|
|
614
|
+
hidden_dims = hidden_dims or []
|
|
638
615
|
layers = []
|
|
639
616
|
current_dim = input_dim
|
|
640
|
-
for i_dim in
|
|
617
|
+
for i_dim in hidden_dims:
|
|
641
618
|
layers.append(nn.Linear(current_dim, i_dim))
|
|
642
|
-
if
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
raise ValueError(f"Unsupported norm_type: {norm_type}")
|
|
619
|
+
if norm_type == "batch_norm":
|
|
620
|
+
# **IMPORTANT** be careful when using BatchNorm1d in distributed training, nextrec does not support sync batch norm now
|
|
621
|
+
layers.append(nn.BatchNorm1d(i_dim))
|
|
622
|
+
elif norm_type == "layer_norm":
|
|
623
|
+
layers.append(nn.LayerNorm(i_dim))
|
|
624
|
+
elif norm_type != "none":
|
|
625
|
+
raise ValueError(f"Unsupported norm_type: {norm_type}")
|
|
650
626
|
|
|
651
627
|
layers.append(activation_layer(activation))
|
|
652
628
|
layers.append(nn.Dropout(p=dropout))
|
|
653
629
|
current_dim = i_dim
|
|
654
630
|
# output layer
|
|
655
|
-
if
|
|
656
|
-
layers.append(nn.Linear(current_dim,
|
|
657
|
-
|
|
631
|
+
if output_dim is not None:
|
|
632
|
+
layers.append(nn.Linear(current_dim, output_dim))
|
|
633
|
+
if output_activation != "none":
|
|
634
|
+
layers.append(activation_layer(output_activation))
|
|
635
|
+
self.output_dim = output_dim
|
|
658
636
|
else:
|
|
659
637
|
self.output_dim = current_dim
|
|
660
638
|
self.mlp = nn.Sequential(*layers)
|
|
@@ -663,6 +641,47 @@ class MLP(nn.Module):
|
|
|
663
641
|
return self.mlp(x)
|
|
664
642
|
|
|
665
643
|
|
|
644
|
+
class GateMLP(nn.Module):
|
|
645
|
+
"""
|
|
646
|
+
Lightweight gate network: sigmoid MLP scaled by a constant factor.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
input_dim: Dimension of the input features.
|
|
650
|
+
hidden_dim: Dimension of the hidden layer. If None, defaults to output_dim.
|
|
651
|
+
output_dim: Output dimension of the gate.
|
|
652
|
+
activation: Activation function to use in the hidden layer.
|
|
653
|
+
dropout: Dropout rate between layers.
|
|
654
|
+
use_bn: Whether to use batch normalization.
|
|
655
|
+
scale_factor: Scaling factor applied to the sigmoid output.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
def __init__(
|
|
659
|
+
self,
|
|
660
|
+
input_dim: int,
|
|
661
|
+
hidden_dim: int | None,
|
|
662
|
+
output_dim: int,
|
|
663
|
+
activation: ActivationName = "relu",
|
|
664
|
+
dropout: float = 0.0,
|
|
665
|
+
use_bn: bool = False,
|
|
666
|
+
scale_factor: float = 2.0,
|
|
667
|
+
) -> None:
|
|
668
|
+
super().__init__()
|
|
669
|
+
hidden_dim = output_dim if hidden_dim is None else hidden_dim
|
|
670
|
+
self.gate = MLP(
|
|
671
|
+
input_dim=input_dim,
|
|
672
|
+
hidden_dims=[hidden_dim],
|
|
673
|
+
output_dim=output_dim,
|
|
674
|
+
activation=activation,
|
|
675
|
+
dropout=dropout,
|
|
676
|
+
norm_type="batch_norm" if use_bn else "none",
|
|
677
|
+
output_activation="sigmoid",
|
|
678
|
+
)
|
|
679
|
+
self.scale_factor = scale_factor
|
|
680
|
+
|
|
681
|
+
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
|
|
682
|
+
return self.gate(inputs) * self.scale_factor
|
|
683
|
+
|
|
684
|
+
|
|
666
685
|
class FM(nn.Module):
|
|
667
686
|
def __init__(self, reduce_sum: bool = True):
|
|
668
687
|
super().__init__()
|
|
@@ -1007,3 +1026,34 @@ class RMSNorm(torch.nn.Module):
|
|
|
1007
1026
|
variance = torch.mean(x**2, dim=-1, keepdim=True)
|
|
1008
1027
|
x_normalized = x * torch.rsqrt(variance + self.eps)
|
|
1009
1028
|
return self.weight * x_normalized
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class DomainBatchNorm(nn.Module):
|
|
1032
|
+
"""Domain-specific BatchNorm (applied per-domain with a shared interface)."""
|
|
1033
|
+
|
|
1034
|
+
def __init__(self, num_features: int, num_domains: int):
|
|
1035
|
+
super().__init__()
|
|
1036
|
+
if num_domains < 1:
|
|
1037
|
+
raise ValueError("num_domains must be >= 1")
|
|
1038
|
+
self.bns = nn.ModuleList(
|
|
1039
|
+
[nn.BatchNorm1d(num_features) for _ in range(num_domains)]
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
def forward(self, x: torch.Tensor, domain_mask: torch.Tensor) -> torch.Tensor:
|
|
1043
|
+
if x.dim() != 2:
|
|
1044
|
+
raise ValueError("DomainBatchNorm expects 2D inputs [B, D].")
|
|
1045
|
+
output = x.clone()
|
|
1046
|
+
if domain_mask.dim() == 1:
|
|
1047
|
+
domain_ids = domain_mask.long()
|
|
1048
|
+
for idx, bn in enumerate(self.bns):
|
|
1049
|
+
mask = domain_ids == idx
|
|
1050
|
+
if mask.any():
|
|
1051
|
+
output[mask] = bn(x[mask])
|
|
1052
|
+
return output
|
|
1053
|
+
if domain_mask.dim() != 2:
|
|
1054
|
+
raise ValueError("domain_mask must be 1D indices or 2D one-hot mask.")
|
|
1055
|
+
for idx, bn in enumerate(self.bns):
|
|
1056
|
+
mask = domain_mask[:, idx] > 0
|
|
1057
|
+
if mask.any():
|
|
1058
|
+
output[mask] = bn(x[mask])
|
|
1059
|
+
return output
|
nextrec/basic/metrics.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Metrics computation and configuration for model evaluation.
|
|
3
3
|
|
|
4
4
|
Date: create on 27/10/2025
|
|
5
|
-
Checkpoint: edit on
|
|
5
|
+
Checkpoint: edit on 30/12/2025
|
|
6
6
|
Author: Yang Zhou,zyaztec@gmail.com
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -21,25 +21,12 @@ from sklearn.metrics import (
|
|
|
21
21
|
recall_score,
|
|
22
22
|
roc_auc_score,
|
|
23
23
|
)
|
|
24
|
+
from nextrec.utils.types import TaskTypeName, MetricsName
|
|
25
|
+
|
|
24
26
|
|
|
25
|
-
CLASSIFICATION_METRICS = {
|
|
26
|
-
"auc",
|
|
27
|
-
"gauc",
|
|
28
|
-
"ks",
|
|
29
|
-
"logloss",
|
|
30
|
-
"accuracy",
|
|
31
|
-
"acc",
|
|
32
|
-
"precision",
|
|
33
|
-
"recall",
|
|
34
|
-
"f1",
|
|
35
|
-
"micro_f1",
|
|
36
|
-
"macro_f1",
|
|
37
|
-
}
|
|
38
|
-
REGRESSION_METRICS = {"mse", "mae", "rmse", "r2", "mape", "msle"}
|
|
39
27
|
TASK_DEFAULT_METRICS = {
|
|
40
28
|
"binary": ["auc", "gauc", "ks", "logloss", "accuracy", "precision", "recall", "f1"],
|
|
41
29
|
"regression": ["mse", "mae", "rmse", "r2", "mape"],
|
|
42
|
-
"multilabel": ["auc", "hamming_loss", "subset_accuracy", "micro_f1", "macro_f1"],
|
|
43
30
|
"matching": ["auc", "gauc", "precision@10", "hitrate@10", "map@10", "cosine"]
|
|
44
31
|
+ [f"recall@{k}" for k in (5, 10, 20)]
|
|
45
32
|
+ [f"ndcg@{k}" for k in (5, 10, 20)]
|
|
@@ -59,7 +46,7 @@ def check_user_id(*metric_sources: Any) -> bool:
|
|
|
59
46
|
stack.extend(item.values())
|
|
60
47
|
continue
|
|
61
48
|
if isinstance(item, str):
|
|
62
|
-
metric_names.add(item
|
|
49
|
+
metric_names.add(item)
|
|
63
50
|
continue
|
|
64
51
|
try:
|
|
65
52
|
stack.extend(item)
|
|
@@ -362,9 +349,9 @@ def compute_cosine_separation(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
|
362
349
|
|
|
363
350
|
|
|
364
351
|
def configure_metrics(
|
|
365
|
-
task:
|
|
352
|
+
task: TaskTypeName | list[TaskTypeName], # 'binary' or ['binary', 'regression']
|
|
366
353
|
metrics: (
|
|
367
|
-
list[
|
|
354
|
+
list[MetricsName] | dict[str, list[MetricsName]] | None
|
|
368
355
|
), # ['auc', 'logloss'] or {'task1': ['auc'], 'task2': ['mse']}
|
|
369
356
|
target_names: list[str], # ['target1', 'target2']
|
|
370
357
|
) -> tuple[list[str], dict[str, list[str]] | None, str]:
|
|
@@ -384,13 +371,12 @@ def configure_metrics(
|
|
|
384
371
|
f"[Metrics Warning] Task {task_name} not found in targets {target_names}, skipping its metrics"
|
|
385
372
|
)
|
|
386
373
|
continue
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
for metric in lowered:
|
|
374
|
+
task_specific_metrics[task_name] = task_metrics
|
|
375
|
+
for metric in task_metrics:
|
|
390
376
|
if metric not in metrics_list:
|
|
391
377
|
metrics_list.append(metric)
|
|
392
378
|
elif metrics:
|
|
393
|
-
metrics_list = [m
|
|
379
|
+
metrics_list = [m for m in metrics]
|
|
394
380
|
else:
|
|
395
381
|
# No user provided metrics, derive per task type
|
|
396
382
|
if nums_task > 1 and isinstance(task, list):
|
|
@@ -417,11 +403,10 @@ def configure_metrics(
|
|
|
417
403
|
return metrics_list, task_specific_metrics, best_metrics_mode
|
|
418
404
|
|
|
419
405
|
|
|
420
|
-
def getbest_metric_mode(first_metric:
|
|
406
|
+
def getbest_metric_mode(first_metric: MetricsName, primary_task: TaskTypeName) -> str:
|
|
421
407
|
"""Determine if metric should be maximized or minimized."""
|
|
422
|
-
first_metric_lower = first_metric.lower()
|
|
423
408
|
# Metrics that should be maximized
|
|
424
|
-
if
|
|
409
|
+
if first_metric in {
|
|
425
410
|
"auc",
|
|
426
411
|
"gauc",
|
|
427
412
|
"ks",
|
|
@@ -437,20 +422,20 @@ def getbest_metric_mode(first_metric: str, primary_task: str) -> str:
|
|
|
437
422
|
return "max"
|
|
438
423
|
# Ranking metrics that should be maximized (with @K suffix)
|
|
439
424
|
if (
|
|
440
|
-
|
|
441
|
-
or
|
|
442
|
-
or
|
|
443
|
-
or
|
|
444
|
-
or
|
|
445
|
-
or
|
|
446
|
-
or
|
|
425
|
+
first_metric.startswith("recall@")
|
|
426
|
+
or first_metric.startswith("precision@")
|
|
427
|
+
or first_metric.startswith("hitrate@")
|
|
428
|
+
or first_metric.startswith("hr@")
|
|
429
|
+
or first_metric.startswith("mrr@")
|
|
430
|
+
or first_metric.startswith("ndcg@")
|
|
431
|
+
or first_metric.startswith("map@")
|
|
447
432
|
):
|
|
448
433
|
return "max"
|
|
449
434
|
# Cosine separation should be maximized
|
|
450
|
-
if
|
|
435
|
+
if first_metric == "cosine":
|
|
451
436
|
return "max"
|
|
452
437
|
# Metrics that should be minimized
|
|
453
|
-
if
|
|
438
|
+
if first_metric in {"logloss", "mse", "mae", "rmse", "mape", "msle"}:
|
|
454
439
|
return "min"
|
|
455
440
|
# Default based on task type
|
|
456
441
|
if primary_task == "regression":
|
|
@@ -459,7 +444,7 @@ def getbest_metric_mode(first_metric: str, primary_task: str) -> str:
|
|
|
459
444
|
|
|
460
445
|
|
|
461
446
|
def compute_single_metric(
|
|
462
|
-
metric:
|
|
447
|
+
metric: MetricsName,
|
|
463
448
|
y_true: np.ndarray,
|
|
464
449
|
y_pred: np.ndarray,
|
|
465
450
|
task_type: str,
|
|
@@ -467,30 +452,32 @@ def compute_single_metric(
|
|
|
467
452
|
) -> float:
|
|
468
453
|
"""Compute a single metric given true and predicted values."""
|
|
469
454
|
|
|
455
|
+
if y_true.size == 0:
|
|
456
|
+
return 0.0
|
|
457
|
+
|
|
470
458
|
y_p_binary = (y_pred > 0.5).astype(int)
|
|
471
|
-
metric_lower = metric.lower()
|
|
472
459
|
try:
|
|
473
|
-
if
|
|
474
|
-
k = int(
|
|
460
|
+
if metric.startswith("recall@"):
|
|
461
|
+
k = int(metric.split("@")[1])
|
|
475
462
|
return compute_recall_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
476
|
-
if
|
|
477
|
-
k = int(
|
|
463
|
+
if metric.startswith("precision@"):
|
|
464
|
+
k = int(metric.split("@")[1])
|
|
478
465
|
return compute_precision_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
479
|
-
if
|
|
480
|
-
k_str =
|
|
466
|
+
if metric.startswith("hitrate@") or metric.startswith("hr@"):
|
|
467
|
+
k_str = metric.split("@")[1]
|
|
481
468
|
k = int(k_str)
|
|
482
469
|
return compute_hitrate_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
483
|
-
if
|
|
484
|
-
k = int(
|
|
470
|
+
if metric.startswith("mrr@"):
|
|
471
|
+
k = int(metric.split("@")[1])
|
|
485
472
|
return compute_mrr_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
486
|
-
if
|
|
487
|
-
k = int(
|
|
473
|
+
if metric.startswith("ndcg@"):
|
|
474
|
+
k = int(metric.split("@")[1])
|
|
488
475
|
return compute_ndcg_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
489
|
-
if
|
|
490
|
-
k = int(
|
|
476
|
+
if metric.startswith("map@"):
|
|
477
|
+
k = int(metric.split("@")[1])
|
|
491
478
|
return compute_map_at_k(y_true, y_pred, user_ids, k) # type: ignore
|
|
492
479
|
# cosine for matching task
|
|
493
|
-
if
|
|
480
|
+
if metric == "cosine":
|
|
494
481
|
return compute_cosine_separation(y_true, y_pred)
|
|
495
482
|
if metric == "auc":
|
|
496
483
|
value = float(
|
|
@@ -571,15 +558,31 @@ def compute_single_metric(
|
|
|
571
558
|
def evaluate_metrics(
|
|
572
559
|
y_true: np.ndarray | None,
|
|
573
560
|
y_pred: np.ndarray | None,
|
|
574
|
-
metrics: list[
|
|
575
|
-
task:
|
|
576
|
-
target_names: list[str],
|
|
577
|
-
task_specific_metrics:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
561
|
+
metrics: list[MetricsName],
|
|
562
|
+
task: TaskTypeName | list[TaskTypeName],
|
|
563
|
+
target_names: list[str],
|
|
564
|
+
task_specific_metrics: dict[str, list[MetricsName]] | None = None,
|
|
565
|
+
user_ids: np.ndarray | None = None,
|
|
566
|
+
ignore_label: int | float | None = None,
|
|
567
|
+
) -> dict:
|
|
568
|
+
"""
|
|
569
|
+
Evaluate specified metrics for given true and predicted values.
|
|
570
|
+
Supports single-task and multi-task evaluation.
|
|
571
|
+
Handles optional ignore_label to exclude certain samples.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
y_true: Ground truth labels.
|
|
575
|
+
y_pred: Predicted values.
|
|
576
|
+
metrics: List of metric names to compute.
|
|
577
|
+
task: Task type(s) - 'binary', 'regression', etc.
|
|
578
|
+
target_names: Names of target variables. e.g., ['target1', 'target2']
|
|
579
|
+
task_specific_metrics: Optional dict mapping target names to specific metrics. e.g., {'target1': ['auc', 'logloss'], 'target2': ['mse']}
|
|
580
|
+
user_ids: Optional user IDs for GAUC and ranking metrics. e.g., User IDs for GAUC computation
|
|
581
|
+
ignore_label: Optional label value to ignore during evaluation.
|
|
582
|
+
|
|
583
|
+
Returns: Dictionary of computed metric values. {'auc': 0.75, 'logloss': 0.45, 'mse_target2': 3.2}
|
|
584
|
+
|
|
585
|
+
"""
|
|
583
586
|
|
|
584
587
|
result = {}
|
|
585
588
|
if y_true is None or y_pred is None:
|
|
@@ -589,70 +592,81 @@ def evaluate_metrics(
|
|
|
589
592
|
nums_task = len(task) if isinstance(task, list) else 1
|
|
590
593
|
# Single task evaluation
|
|
591
594
|
if nums_task == 1:
|
|
595
|
+
if ignore_label is not None:
|
|
596
|
+
valid_mask = y_true != ignore_label
|
|
597
|
+
if np.any(valid_mask):
|
|
598
|
+
y_true = y_true[valid_mask]
|
|
599
|
+
y_pred = y_pred[valid_mask]
|
|
600
|
+
if user_ids is not None:
|
|
601
|
+
user_ids = user_ids[valid_mask]
|
|
602
|
+
else:
|
|
603
|
+
return result
|
|
592
604
|
for metric in metrics:
|
|
593
|
-
metric_lower = metric.lower()
|
|
594
605
|
value = compute_single_metric(
|
|
595
|
-
|
|
606
|
+
metric, y_true, y_pred, primary_task, user_ids
|
|
596
607
|
)
|
|
597
|
-
result[
|
|
608
|
+
result[metric] = value
|
|
598
609
|
# Multi-task evaluation
|
|
599
610
|
else:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if not
|
|
611
|
+
task_types = []
|
|
612
|
+
for task_idx in range(nums_task):
|
|
613
|
+
if isinstance(task, list) and task_idx < len(task):
|
|
614
|
+
task_types.append(task[task_idx])
|
|
615
|
+
elif isinstance(task, str):
|
|
616
|
+
task_types.append(task)
|
|
617
|
+
else:
|
|
618
|
+
task_types.append("binary")
|
|
619
|
+
metric_allowlist = {
|
|
620
|
+
"binary": {
|
|
621
|
+
"auc",
|
|
622
|
+
"gauc",
|
|
623
|
+
"ks",
|
|
624
|
+
"logloss",
|
|
625
|
+
"accuracy",
|
|
626
|
+
"acc",
|
|
627
|
+
"precision",
|
|
628
|
+
"recall",
|
|
629
|
+
"f1",
|
|
630
|
+
"micro_f1",
|
|
631
|
+
"macro_f1",
|
|
632
|
+
},
|
|
633
|
+
"regression": {
|
|
634
|
+
"mse",
|
|
635
|
+
"mae",
|
|
636
|
+
"rmse",
|
|
637
|
+
"r2",
|
|
638
|
+
"mape",
|
|
639
|
+
"msle",
|
|
640
|
+
},
|
|
641
|
+
}
|
|
642
|
+
for task_idx in range(nums_task):
|
|
643
|
+
task_type = task_types[task_idx]
|
|
644
|
+
target_name = target_names[task_idx]
|
|
645
|
+
if task_specific_metrics is not None and task_idx < len(target_names):
|
|
646
|
+
allowed_metrics = {
|
|
647
|
+
m for m in task_specific_metrics.get(target_name, [])
|
|
648
|
+
}
|
|
649
|
+
else:
|
|
650
|
+
allowed_metrics = metric_allowlist.get(task_type)
|
|
651
|
+
for metric in metrics:
|
|
652
|
+
if allowed_metrics is not None and metric not in allowed_metrics:
|
|
642
653
|
continue
|
|
643
|
-
target_name = target_names[task_idx]
|
|
644
|
-
# Get task type for specific index
|
|
645
|
-
if isinstance(task, list) and task_idx < len(task):
|
|
646
|
-
task_type = task[task_idx]
|
|
647
|
-
elif isinstance(task, str):
|
|
648
|
-
task_type = task
|
|
649
|
-
else:
|
|
650
|
-
task_type = "binary"
|
|
651
654
|
y_true_task = y_true[:, task_idx]
|
|
652
655
|
y_pred_task = y_pred[:, task_idx]
|
|
656
|
+
task_user_ids = user_ids
|
|
657
|
+
if ignore_label is not None:
|
|
658
|
+
valid_mask = y_true_task != ignore_label
|
|
659
|
+
if np.any(valid_mask):
|
|
660
|
+
y_true_task = y_true_task[valid_mask]
|
|
661
|
+
y_pred_task = y_pred_task[valid_mask]
|
|
662
|
+
if task_user_ids is not None:
|
|
663
|
+
task_user_ids = task_user_ids[valid_mask]
|
|
664
|
+
else:
|
|
665
|
+
result[f"{metric}_{target_name}"] = 0.0
|
|
666
|
+
continue
|
|
653
667
|
# Compute metric
|
|
654
668
|
value = compute_single_metric(
|
|
655
|
-
|
|
669
|
+
metric, y_true_task, y_pred_task, task_type, task_user_ids
|
|
656
670
|
)
|
|
657
|
-
result[f"{
|
|
671
|
+
result[f"{metric}_{target_name}"] = value
|
|
658
672
|
return result
|