tabpackage 0.0.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.
- tabpackage/__init__.py +18 -0
- tabpackage/_training.py +3380 -0
- tabpackage/ensemble.py +263 -0
- tabpackage/metrics.py +253 -0
- tabpackage/nn.py +2110 -0
- tabpackage/optim.py +943 -0
- tabpackage/sklearn/__init__.py +18 -0
- tabpackage/sklearn/_config.py +561 -0
- tabpackage/sklearn/_data.py +307 -0
- tabpackage/sklearn/_estimators.py +1210 -0
- tabpackage/utils/__init__.py +1 -0
- tabpackage/utils/torch.py +166 -0
- tabpackage-0.0.1.dist-info/METADATA +398 -0
- tabpackage-0.0.1.dist-info/RECORD +15 -0
- tabpackage-0.0.1.dist-info/WHEEL +4 -0
tabpackage/ensemble.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""PyTorch ensembling utilities."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TypeAlias
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
|
|
9
|
+
_ScoreFn: TypeAlias = Callable[
|
|
10
|
+
[Tensor], # Shape: (n_predictions, *)
|
|
11
|
+
Tensor, # Shape: (n_predictions,)
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_predictions(predictions: Tensor) -> None:
|
|
16
|
+
if predictions.ndim < 2:
|
|
17
|
+
raise ValueError(
|
|
18
|
+
'Predictions must have a dimension for ensemble members and at least one'
|
|
19
|
+
" dimension for each member's predictions"
|
|
20
|
+
)
|
|
21
|
+
if predictions.numel() == 0:
|
|
22
|
+
raise ValueError('Predictions must not be empty')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compute_unsanitized_ensemble_predictions(
|
|
26
|
+
member_predictions: Tensor, member_weights: Tensor | None
|
|
27
|
+
) -> Tensor:
|
|
28
|
+
"""Aggregate predictions of ensemble members into one _RAW_ prediction.
|
|
29
|
+
|
|
30
|
+
Warning:
|
|
31
|
+
Due to the floating-point arithmetic limitations:
|
|
32
|
+
|
|
33
|
+
- The computed raw predictions can contain values slightly beyond the range of
|
|
34
|
+
`member_predictions`, i.e. below/above their minimum/maximum values.
|
|
35
|
+
Transform the result as needed, e.g. with `.clamp(max=1.0)` for
|
|
36
|
+
probabilities.
|
|
37
|
+
- If `member_predictions` are probabilities, the computed distributions may
|
|
38
|
+
not sum to `1.0`.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
member_predictions: the predictions of the ensemble members.
|
|
42
|
+
Shape: `(n_members, *)`.
|
|
43
|
+
member_weights: weights to aggregate the predictions with, normalized by their
|
|
44
|
+
sum. Pass `None` for equal weights. Shape: `(n_members,)`.
|
|
45
|
+
Returns:
|
|
46
|
+
The aggregated prediction. Shape: `(*)`.
|
|
47
|
+
"""
|
|
48
|
+
_validate_predictions(member_predictions)
|
|
49
|
+
|
|
50
|
+
if member_weights is None:
|
|
51
|
+
return member_predictions.mean(0)
|
|
52
|
+
else:
|
|
53
|
+
if member_weights.ndim != 1:
|
|
54
|
+
raise ValueError('`member_weights` must be a one-dimensional tensor')
|
|
55
|
+
if len(member_weights) != len(member_predictions):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
'The number of member weights must match the number of ensemble members'
|
|
58
|
+
)
|
|
59
|
+
return (
|
|
60
|
+
(member_weights / member_weights.sum()) @ member_predictions.flatten(1)
|
|
61
|
+
).view(member_predictions.shape[1:])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def topk_ensemble(predictions: Tensor, *, score_fn: _ScoreFn, k: int) -> Tensor:
|
|
65
|
+
"""Build an equally weighted ensemble of the top-k individual performers.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
predictions: the predictions to build the ensemble from.
|
|
69
|
+
score_fn: the scoring function taking multiple stacked predictions as input and
|
|
70
|
+
returning their performance scores (one per prediction). Higher scores are
|
|
71
|
+
better. Ties are resolved by selecting the earliest predictions.
|
|
72
|
+
k: the number of predictions to select, capped by the number available.
|
|
73
|
+
Returns:
|
|
74
|
+
The selected prediction indices.
|
|
75
|
+
"""
|
|
76
|
+
_validate_predictions(predictions)
|
|
77
|
+
if k <= 0:
|
|
78
|
+
raise ValueError('`k` must be positive to select at least one ensemble member')
|
|
79
|
+
|
|
80
|
+
prediction_scores = score_fn(predictions)
|
|
81
|
+
if prediction_scores.ndim != 1:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
'The scoring function must return a one-dimensional tensor of scores'
|
|
84
|
+
)
|
|
85
|
+
if len(prediction_scores) != len(predictions):
|
|
86
|
+
raise ValueError('The scoring function must return one score per prediction')
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
torch.argmax(prediction_scores)[None]
|
|
90
|
+
if k == 1
|
|
91
|
+
else torch.argsort(prediction_scores, descending=True, stable=True)[:k]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def greedy_ensemble(
|
|
96
|
+
predictions: Tensor,
|
|
97
|
+
*,
|
|
98
|
+
score_fn: _ScoreFn,
|
|
99
|
+
init_top_k: int = 1,
|
|
100
|
+
max_ensemble_size: int | None = None,
|
|
101
|
+
with_replacement: bool = False,
|
|
102
|
+
) -> tuple[Tensor, Tensor | None]:
|
|
103
|
+
"""Build an ensemble by greedily adding the best candidate one at a time.
|
|
104
|
+
|
|
105
|
+
The algorithm initializes the ensemble by unconditionally selecting the top-k
|
|
106
|
+
predictions based on their individual performance, and then greedily adds more
|
|
107
|
+
predictions one at a time, stopping when reaching the allowed ensemble size or when
|
|
108
|
+
adding more predictions does not improve performance.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
predictions: the predictions to build the ensemble from.
|
|
112
|
+
score_fn: the scoring function taking multiple stacked predictions as input and
|
|
113
|
+
returning their performance scores (one per prediction). Higher scores are
|
|
114
|
+
better. Usually, it is based on the main task metric (e.g., accuracy,
|
|
115
|
+
_negative_ RMSE, etc.). The algorithm stops when the ensemble performance
|
|
116
|
+
stops improving according to this score.
|
|
117
|
+
init_top_k: the number of the best individual performers to unconditionally add
|
|
118
|
+
to the ensemble on initialization.
|
|
119
|
+
max_ensemble_size: the maximum allowed ensemble size. When
|
|
120
|
+
`with_replacement=False`, it is the maximum number of distinct ensemble
|
|
121
|
+
members. Otherwise, it is the maximum number of member selections that the
|
|
122
|
+
algorithm performs, including the unconditional selections in the beginning.
|
|
123
|
+
Must be specified when `with_replacement=True`.
|
|
124
|
+
with_replacement: if `True`, each prediction can be added to the ensemble any
|
|
125
|
+
number of times.
|
|
126
|
+
Returns:
|
|
127
|
+
The selected indices in original prediction order, and their selection counts
|
|
128
|
+
when `with_replacement=True`; otherwise, the second item is `None`.
|
|
129
|
+
"""
|
|
130
|
+
_validate_predictions(predictions)
|
|
131
|
+
if not torch.is_floating_point(predictions):
|
|
132
|
+
raise TypeError('Predictions must be floating-point values')
|
|
133
|
+
if init_top_k <= 0:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
'`init_top_k` must be positive to initialize the ensemble with at least one'
|
|
136
|
+
' member'
|
|
137
|
+
)
|
|
138
|
+
if with_replacement and max_ensemble_size is None:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
'Set `max_ensemble_size` to limit the number of ensemble members when'
|
|
141
|
+
' `with_replacement=True`'
|
|
142
|
+
)
|
|
143
|
+
if max_ensemble_size is not None and init_top_k > max_ensemble_size:
|
|
144
|
+
raise ValueError('`init_top_k` must not exceed `max_ensemble_size`')
|
|
145
|
+
|
|
146
|
+
device = predictions.device
|
|
147
|
+
|
|
148
|
+
# Individual candidate scores.
|
|
149
|
+
prediction_scores = score_fn(predictions)
|
|
150
|
+
|
|
151
|
+
# Initial ensemble members.
|
|
152
|
+
init_idx = topk_ensemble(
|
|
153
|
+
predictions, score_fn=lambda _: prediction_scores, k=init_top_k
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Prediction-related constants.
|
|
157
|
+
n_predictions = len(predictions)
|
|
158
|
+
prediction_range_idx = torch.arange(n_predictions, device=device)
|
|
159
|
+
|
|
160
|
+
# The mask indicating if a given prediction is selected as an ensemble member.
|
|
161
|
+
ensemble_mask = torch.zeros(n_predictions, dtype=torch.bool, device=device)
|
|
162
|
+
ensemble_mask[init_idx] = True
|
|
163
|
+
|
|
164
|
+
if with_replacement:
|
|
165
|
+
# Each prediction's weight is the number of times it was selected.
|
|
166
|
+
ensemble_weights = torch.zeros(
|
|
167
|
+
n_predictions, dtype=predictions.dtype, device=device
|
|
168
|
+
)
|
|
169
|
+
ensemble_weights[init_idx] = 1.0
|
|
170
|
+
else:
|
|
171
|
+
ensemble_weights = None
|
|
172
|
+
|
|
173
|
+
# Current ensemble prediction.
|
|
174
|
+
ensemble_prediction = predictions[init_idx].mean(0)
|
|
175
|
+
|
|
176
|
+
# Current ensemble score.
|
|
177
|
+
ensemble_score = score_fn(ensemble_prediction[None])[0]
|
|
178
|
+
|
|
179
|
+
ensemble_size = len(init_idx) # The number of distinct ensemble members.
|
|
180
|
+
ensemble_weight_sum = ensemble_size
|
|
181
|
+
|
|
182
|
+
# Without replacement, the weight sum equals the number of distinct members.
|
|
183
|
+
while ensemble_weight_sum < (
|
|
184
|
+
n_predictions
|
|
185
|
+
if max_ensemble_size is None # True only when `with_replacement=False`.
|
|
186
|
+
else max_ensemble_size
|
|
187
|
+
if with_replacement
|
|
188
|
+
else min(n_predictions, max_ensemble_size)
|
|
189
|
+
):
|
|
190
|
+
# Get the indices of predictions available for selection.
|
|
191
|
+
candidate_idx = (
|
|
192
|
+
prediction_range_idx
|
|
193
|
+
if with_replacement
|
|
194
|
+
else torch.nonzero_static(
|
|
195
|
+
~ensemble_mask, size=n_predictions - ensemble_size
|
|
196
|
+
)[:, 0]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Compute the ensemble predictions for all possible ensembles formed by adding
|
|
200
|
+
# one of the candidate predictions.
|
|
201
|
+
candidate_predictions = (
|
|
202
|
+
predictions.clone()
|
|
203
|
+
if with_replacement
|
|
204
|
+
else predictions.index_select(0, candidate_idx)
|
|
205
|
+
)
|
|
206
|
+
candidate_predictions.lerp_(
|
|
207
|
+
ensemble_prediction[None], ensemble_weight_sum / (ensemble_weight_sum + 1)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Compute ensemble scores of all possible ensembles.
|
|
211
|
+
candidates_scores = score_fn(candidate_predictions)
|
|
212
|
+
|
|
213
|
+
# Get the best possible ensemble score.
|
|
214
|
+
best_candidate_score = candidates_scores.max()
|
|
215
|
+
# If the obtained score is no better than that of the current ensemble, stop the
|
|
216
|
+
# algorithm.
|
|
217
|
+
if best_candidate_score <= ensemble_score:
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
# Find all candidates with the best score.
|
|
221
|
+
candidate_local_mask = candidates_scores == best_candidate_score
|
|
222
|
+
best_candidate_local_idx = torch.nonzero(candidate_local_mask)[:, 0]
|
|
223
|
+
best_candidate_idx = candidate_idx[candidate_local_mask]
|
|
224
|
+
if len(best_candidate_idx) == 0:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
'Cannot select the best ensemble candidate because the scoring function'
|
|
227
|
+
' returned a NaN score'
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if len(best_candidate_idx) == 1:
|
|
231
|
+
# Select the only candidate with the best score.
|
|
232
|
+
best_candidate_local_index = best_candidate_local_idx[0]
|
|
233
|
+
best_candidate_index = best_candidate_idx[0]
|
|
234
|
+
else:
|
|
235
|
+
# Break ties by individual score, then by the original prediction order.
|
|
236
|
+
best_candidate_local_index = best_candidate_local_idx[
|
|
237
|
+
torch.argmax(prediction_scores[best_candidate_idx])
|
|
238
|
+
]
|
|
239
|
+
best_candidate_index = candidate_idx[best_candidate_local_index]
|
|
240
|
+
|
|
241
|
+
# Update the ensemble state.
|
|
242
|
+
ensemble_mask[best_candidate_index] = True
|
|
243
|
+
if ensemble_weights is not None:
|
|
244
|
+
ensemble_weights[best_candidate_index] += 1.0
|
|
245
|
+
ensemble_score = candidates_scores[best_candidate_local_index]
|
|
246
|
+
# Clone the only needed prediction and delete the rest to leave more free memory
|
|
247
|
+
# for the next iteration.
|
|
248
|
+
ensemble_prediction = candidate_predictions[best_candidate_local_index].clone()
|
|
249
|
+
del candidate_predictions
|
|
250
|
+
if (
|
|
251
|
+
ensemble_weights is None
|
|
252
|
+
or ensemble_weights[best_candidate_index].item() == 1.0
|
|
253
|
+
):
|
|
254
|
+
ensemble_size += 1
|
|
255
|
+
ensemble_weight_sum += 1
|
|
256
|
+
|
|
257
|
+
if not with_replacement:
|
|
258
|
+
assert ensemble_size == ensemble_weight_sum
|
|
259
|
+
|
|
260
|
+
ensemble_idx = torch.nonzero_static(ensemble_mask, size=ensemble_size)[:, 0]
|
|
261
|
+
return ensemble_idx, (
|
|
262
|
+
None if ensemble_weights is None else ensemble_weights[ensemble_idx]
|
|
263
|
+
)
|
tabpackage/metrics.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Metric computation using PyTorch.
|
|
2
|
+
|
|
3
|
+
Common conventions:
|
|
4
|
+
|
|
5
|
+
- The pack dimension must be `tabpackage.nn.PACK_DIM`.
|
|
6
|
+
- The `*_pack` functions (`accuracy_pack`, `mse_pack`, etc.) compute common metrics for
|
|
7
|
+
prediction packs; their distinct property is that the pack dimension is preserved
|
|
8
|
+
during the metric aggregation.
|
|
9
|
+
- The argument naming follows that of Scikit-learn 1.9+. In particular, predicted
|
|
10
|
+
labels are accepted as `y_pred`, predicted probabilities as `y_proba` and decision
|
|
11
|
+
scores (the higher - the better) as `y_score`.
|
|
12
|
+
- Prediction tensors are expected to have exactly one pack dimension.
|
|
13
|
+
- Ground-truth labels with omitted pack and batch dimensions are broadcast along these
|
|
14
|
+
dimensions to match predictions. This in particular means that labels without the pack
|
|
15
|
+
dimension are shared between all pack members.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
import torch.nn as nn # noqa: PLR0402
|
|
20
|
+
from torch import Tensor
|
|
21
|
+
|
|
22
|
+
import tabpackage.utils.torch
|
|
23
|
+
from tabpackage.nn import PACK_DIM
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _check_y_shapes(*, y_true: Tensor, y_pred: Tensor, n_trailing_dims: int) -> None:
|
|
27
|
+
assert n_trailing_dims >= 0
|
|
28
|
+
|
|
29
|
+
if y_pred.ndim < 1 + n_trailing_dims:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
'The prediction tensor has too few dimensions to represent a pack of'
|
|
32
|
+
' outputs'
|
|
33
|
+
)
|
|
34
|
+
if y_true.ndim > y_pred.ndim - n_trailing_dims:
|
|
35
|
+
raise ValueError('Labels have too many dimensions for the provided predictions')
|
|
36
|
+
|
|
37
|
+
if y_true.ndim > 0:
|
|
38
|
+
# Only missing leading dimensions may be broadcast; existing ones must match.
|
|
39
|
+
end = y_pred.ndim - n_trailing_dims
|
|
40
|
+
if y_true.shape != y_pred.shape[end - y_true.ndim : end]:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
'The label and prediction shapes are incompatible; only missing leading'
|
|
43
|
+
' label dimensions may be broadcast'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if y_pred.numel() == 0:
|
|
47
|
+
raise ValueError('Predictions must not be empty')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mean_nonpack(x: Tensor) -> Tensor:
|
|
51
|
+
assert x.ndim >= 1 # Must have at least the pack dimension.
|
|
52
|
+
|
|
53
|
+
if x.ndim == 1:
|
|
54
|
+
# Relying on the `else` branch would be incorrect, because it would result in
|
|
55
|
+
# `x.mean(())`, which means aggregating across all dimensions in PyTorch.
|
|
56
|
+
# The only remaining dimension must be the pack dimension.
|
|
57
|
+
assert PACK_DIM == 0
|
|
58
|
+
return x # No dimensions to aggregate across.
|
|
59
|
+
else:
|
|
60
|
+
nonpack_dims = tuple(dim for dim in range(x.ndim) if dim != PACK_DIM)
|
|
61
|
+
return x.mean(nonpack_dims)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# The implementation follows `_mean_nonpack`.
|
|
65
|
+
def _sum_nonpack(x: Tensor) -> Tensor:
|
|
66
|
+
assert x.ndim >= 1
|
|
67
|
+
|
|
68
|
+
if x.ndim == 1:
|
|
69
|
+
assert PACK_DIM == 0
|
|
70
|
+
return x
|
|
71
|
+
else:
|
|
72
|
+
nonpack_dims = tuple(dim for dim in range(x.ndim) if dim != PACK_DIM)
|
|
73
|
+
return x.sum(nonpack_dims)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def accuracy_pack(*, y_true: Tensor, y_pred: Tensor) -> Tensor:
|
|
77
|
+
"""Compute the accuracy for each pack member."""
|
|
78
|
+
if not tabpackage.utils.torch.is_integer(y_true):
|
|
79
|
+
raise TypeError('Labels must have an integer data type')
|
|
80
|
+
if y_true.dtype != y_pred.dtype:
|
|
81
|
+
raise TypeError('Labels and predictions must have the same data type')
|
|
82
|
+
_check_y_shapes(y_true=y_true, y_pred=y_pred, n_trailing_dims=0)
|
|
83
|
+
|
|
84
|
+
return _mean_nonpack((y_true == y_pred).float())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _midranks_pack(x: Tensor) -> Tensor:
|
|
88
|
+
"""Compute the one-based ranks of sorted values, averaged within tied groups.
|
|
89
|
+
|
|
90
|
+
For example, the midranks of `[10, 20, 20, 20, 30]` are `[1, 3, 3, 3, 5]`: the
|
|
91
|
+
three tied values occupy the ranks 2, 3 and 4, hence the average rank 3.
|
|
92
|
+
|
|
93
|
+
The input must be sorted along the last (batch) dimension in the ascending order.
|
|
94
|
+
"""
|
|
95
|
+
assert x.ndim == 2
|
|
96
|
+
|
|
97
|
+
n_objects = x.shape[-1]
|
|
98
|
+
indices = torch.arange(n_objects, device=x.device)
|
|
99
|
+
ones = torch.ones((x.shape[PACK_DIM], 1), dtype=torch.bool, device=x.device)
|
|
100
|
+
|
|
101
|
+
changes = x[:, 1:] != x[:, :-1]
|
|
102
|
+
# Whether a position starts a tied group.
|
|
103
|
+
starts = torch.cat([ones, changes], dim=-1)
|
|
104
|
+
# Whether a position ends a tied group.
|
|
105
|
+
ends = torch.cat([changes, ones], dim=-1)
|
|
106
|
+
|
|
107
|
+
# For each position, the first and the last position of its tied group. The running
|
|
108
|
+
# extrema propagate the group boundaries to all the positions within the group.
|
|
109
|
+
first = torch.cummax(torch.where(starts, indices, -1), dim=-1).values
|
|
110
|
+
last = torch.cummin(
|
|
111
|
+
torch.where(ends, indices, n_objects).flip(-1), dim=-1
|
|
112
|
+
).values.flip(-1)
|
|
113
|
+
# `+ 1` turns the zero-based positions into one-based ranks.
|
|
114
|
+
return (first + last).to(x.dtype) / 2 + 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def binary_roc_auc_pack(*, y_true: Tensor, y_score: Tensor) -> Tensor:
|
|
118
|
+
"""Compute the ROC AUC for each pack member."""
|
|
119
|
+
# Because of the implementation complexity, the supported input data types and
|
|
120
|
+
# shapes are limited to those that were actually tested.
|
|
121
|
+
if y_true.dtype != torch.int64:
|
|
122
|
+
raise TypeError('Labels must have the data type `torch.int64`')
|
|
123
|
+
if y_score.dtype not in (torch.float32, torch.float64):
|
|
124
|
+
raise TypeError(
|
|
125
|
+
'Scores must have the data type `torch.float32` or `torch.float64`'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if y_true.ndim != 1:
|
|
129
|
+
raise ValueError('The label tensor must have one dimension')
|
|
130
|
+
if y_score.ndim != 2:
|
|
131
|
+
raise ValueError('The score tensor must have two dimensions')
|
|
132
|
+
if y_true.shape[-1] != y_score.shape[-1]:
|
|
133
|
+
raise ValueError('Labels and scores must have the same batch size')
|
|
134
|
+
if y_score.numel() == 0:
|
|
135
|
+
raise ValueError('Scores must not be empty')
|
|
136
|
+
|
|
137
|
+
sorted_indices = torch.argsort(y_score, dim=-1, stable=True)
|
|
138
|
+
y_score_sorted = torch.take_along_dim(y_score, sorted_indices, dim=-1)
|
|
139
|
+
y_true_sorted = y_true[sorted_indices]
|
|
140
|
+
|
|
141
|
+
n_positives = y_true.sum()
|
|
142
|
+
n_negatives = len(y_true) - n_positives
|
|
143
|
+
n_pairs = n_positives * n_negatives
|
|
144
|
+
|
|
145
|
+
# The U statistic is the number of the (positive, negative) pairs ranked correctly,
|
|
146
|
+
# with the ties counted as one half.
|
|
147
|
+
positive_rank_sum = (_midranks_pack(y_score_sorted) * y_true_sorted).sum(-1)
|
|
148
|
+
u_statistic = positive_rank_sum - n_positives * (n_positives + 1) / 2
|
|
149
|
+
|
|
150
|
+
return torch.where(n_pairs == 0, torch.nan, u_statistic / n_pairs).to(y_score.dtype)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def binary_cross_entropy_pack(*, y_true: Tensor, y_proba: Tensor) -> Tensor:
|
|
154
|
+
"""Compute the binary cross-entropy for each pack member."""
|
|
155
|
+
if not tabpackage.utils.torch.is_integer(y_true):
|
|
156
|
+
raise TypeError('Labels must have an integer data type')
|
|
157
|
+
if not torch.is_floating_point(y_proba):
|
|
158
|
+
raise TypeError('Probabilities must be floating-point values')
|
|
159
|
+
_check_y_shapes(y_true=y_true, y_pred=y_proba, n_trailing_dims=0)
|
|
160
|
+
|
|
161
|
+
return _mean_nonpack(
|
|
162
|
+
nn.functional.binary_cross_entropy(
|
|
163
|
+
input=y_proba,
|
|
164
|
+
target=y_true.to(y_proba.dtype).expand_as(y_proba),
|
|
165
|
+
reduction='none',
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def multiclass_cross_entropy_pack(*, y_true: Tensor, y_proba: Tensor) -> Tensor:
|
|
171
|
+
"""Compute the multiclass cross-entropy for each pack member."""
|
|
172
|
+
# Only class labels are supported as `y_true`; plus, this tensor is used to index
|
|
173
|
+
# another tensor. Hence the `torch.int64` requirement.
|
|
174
|
+
if y_true.dtype != torch.int64:
|
|
175
|
+
raise TypeError('Labels must have the int64 data type')
|
|
176
|
+
if not torch.is_floating_point(y_proba):
|
|
177
|
+
raise TypeError('Probabilities must be floating-point values')
|
|
178
|
+
_check_y_shapes(y_true=y_true, y_pred=y_proba, n_trailing_dims=1)
|
|
179
|
+
|
|
180
|
+
correct_class_probs = y_proba.gather(
|
|
181
|
+
-1, y_true.expand(y_proba.shape[:-1]).unsqueeze(-1)
|
|
182
|
+
).squeeze(-1)
|
|
183
|
+
return _mean_nonpack(
|
|
184
|
+
torch.log(correct_class_probs)
|
|
185
|
+
.clamp_min(-100.0) # The clamping follows `torch.nn.BCELoss`.
|
|
186
|
+
.neg()
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def mse_pack(*, y_true: Tensor, y_pred: Tensor) -> Tensor:
|
|
191
|
+
"""Compute the mean squared error for each pack member."""
|
|
192
|
+
if not torch.is_floating_point(y_true):
|
|
193
|
+
raise TypeError('Labels must be floating-point values')
|
|
194
|
+
if y_true.dtype != y_pred.dtype:
|
|
195
|
+
raise TypeError('Labels and predictions must have the same data type')
|
|
196
|
+
_check_y_shapes(y_true=y_true, y_pred=y_pred, n_trailing_dims=0)
|
|
197
|
+
|
|
198
|
+
return _mean_nonpack(
|
|
199
|
+
nn.functional.mse_loss(y_pred, y_true.expand_as(y_pred), reduction='none')
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def mae_pack(*, y_true: Tensor, y_pred: Tensor) -> Tensor:
|
|
204
|
+
"""Compute the mean absolute error for each pack member."""
|
|
205
|
+
if not torch.is_floating_point(y_true):
|
|
206
|
+
raise TypeError('Labels must be floating-point values')
|
|
207
|
+
if y_true.dtype != y_pred.dtype:
|
|
208
|
+
raise TypeError('Labels and predictions must have the same data type')
|
|
209
|
+
_check_y_shapes(y_true=y_true, y_pred=y_pred, n_trailing_dims=0)
|
|
210
|
+
|
|
211
|
+
return _mean_nonpack(
|
|
212
|
+
nn.functional.l1_loss(y_pred, y_true.expand_as(y_pred), reduction='none')
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def r2_pack(*, y_true: Tensor, y_pred: Tensor, force_finite: bool = True) -> Tensor:
|
|
217
|
+
"""Compute the R2 score for each pack member."""
|
|
218
|
+
if not torch.is_floating_point(y_true):
|
|
219
|
+
raise TypeError('Labels must be floating-point values')
|
|
220
|
+
if y_true.dtype != y_pred.dtype:
|
|
221
|
+
raise TypeError('Labels and predictions must have the same data type')
|
|
222
|
+
_check_y_shapes(y_true=y_true, y_pred=y_pred, n_trailing_dims=0)
|
|
223
|
+
|
|
224
|
+
numerator = _sum_nonpack((y_pred - y_true).square())
|
|
225
|
+
if y_true.ndim < y_pred.ndim:
|
|
226
|
+
shape_without_pack_dim = torch.Size(
|
|
227
|
+
[x for i, x in enumerate(y_pred.shape) if i != PACK_DIM]
|
|
228
|
+
)
|
|
229
|
+
denominator = torch.sum(
|
|
230
|
+
torch.broadcast_to(
|
|
231
|
+
(y_true - y_true.mean()).square(), shape_without_pack_dim
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
denominator = _sum_nonpack(
|
|
236
|
+
(
|
|
237
|
+
y_true
|
|
238
|
+
- _mean_nonpack(y_true).reshape(
|
|
239
|
+
[-1 if dim == PACK_DIM else 1 for dim in range(y_true.ndim)]
|
|
240
|
+
)
|
|
241
|
+
).square()
|
|
242
|
+
)
|
|
243
|
+
ratio = numerator / denominator
|
|
244
|
+
|
|
245
|
+
if force_finite:
|
|
246
|
+
# Map constant-target cases to scores of 1 (perfect) or 0 before checking
|
|
247
|
+
# the sample count below.
|
|
248
|
+
ratio = torch.where(
|
|
249
|
+
numerator == 0.0, 0.0, torch.where(denominator == 0.0, 1.0, ratio)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
n_objects = y_pred.numel() // y_pred.shape[PACK_DIM]
|
|
253
|
+
return torch.full_like(ratio, torch.nan) if n_objects < 2 else 1.0 - ratio
|