anchorboosting 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.
@@ -0,0 +1,4 @@
1
+ from .models import AnchorBooster
2
+ from .objectives.anchor_objectives import AnchorRegressionObjective
3
+
4
+ __all__ = ["AnchorRegressionObjective", "AnchorBooster"]
@@ -0,0 +1,368 @@
1
+ import copy
2
+
3
+ import lightgbm as lgb
4
+ import numpy as np
5
+ import scipy
6
+
7
+ try:
8
+ import polars as pl
9
+
10
+ _POLARS_INSTALLED = True
11
+ except ImportError:
12
+ _POLARS_INSTALLED = False
13
+
14
+
15
+ class AnchorBooster:
16
+ """
17
+ Boost the anchor loss.
18
+
19
+ Parameters
20
+ ----------
21
+ gamma: float
22
+ The gamma parameter for the anchor regression objective function. Must be non-
23
+ negative. If 1, the objective is equivalent to a standard regression objective.
24
+ dataset_params: dict or None
25
+ The parameters for the LightGBM dataset. See LightGBM documentation for details.
26
+ num_boost_round: int
27
+ The number of boosting iterations. Default is 100.
28
+ objective: str, optional, default="regression"
29
+ The objective function to use. Can be "regression" or "binary" for probit
30
+ regression. If "binary", the outcome values must be 0 or 1.
31
+ **kwargs: dict
32
+ Additional parameters for the LightGBM model. See LightGBM documentation for
33
+ details.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ gamma,
39
+ dataset_params=None,
40
+ num_boost_round=100,
41
+ objective="regression",
42
+ **kwargs,
43
+ ):
44
+ self.gamma = gamma
45
+ self.params = kwargs
46
+ self.dataset_params = dataset_params or {}
47
+ self.num_boost_round = num_boost_round
48
+ self.booster = None
49
+ self.init_score_ = None
50
+ self.objective = objective
51
+
52
+ def fit(
53
+ self,
54
+ X,
55
+ y,
56
+ Z,
57
+ categorical_feature=None,
58
+ ):
59
+ """
60
+ Fit the model.
61
+
62
+ Parameters
63
+ ----------
64
+ X : polars.DataFrame
65
+ The input data.
66
+ y : np.ndarray
67
+ The outcome.
68
+ Z : np.ndarray
69
+ Anchors.
70
+ categorical_feature : list of str or int
71
+ List of categorical feature names or indices. If None, all features are
72
+ assumed to be numerical.
73
+ """
74
+ if self.objective != "regression" and not np.isin(y, [0, 1]).all():
75
+ raise ValueError("For binary classification, y values must be in {0, 1}.")
76
+
77
+ if self.objective == "regression":
78
+ self.init_score_ = np.mean(y)
79
+ elif self.objective == "binary":
80
+ self.init_score_ = scipy.stats.norm.ppf(np.mean(y))
81
+ else:
82
+ raise ValueError(
83
+ f"Objective must be 'regression' or 'binary'. Got {self.objective}."
84
+ )
85
+
86
+ if _POLARS_INSTALLED and isinstance(X, pl.DataFrame):
87
+ feature_name = X.columns
88
+ X = X.to_arrow()
89
+ else:
90
+ feature_name = None
91
+
92
+ self._dtype = np.result_type(Z, y)
93
+
94
+ dataset_params = {
95
+ "data": X,
96
+ "label": y,
97
+ "categorical_feature": categorical_feature,
98
+ "feature_name": feature_name,
99
+ "init_score": np.ones(len(y), dtype=self._dtype) * self.init_score_,
100
+ **self.dataset_params,
101
+ }
102
+
103
+ data = lgb.Dataset(**dataset_params)
104
+
105
+ self.booster = lgb.Booster(params=self.params, train_set=data)
106
+
107
+ return self.update(
108
+ X,
109
+ y,
110
+ Z=Z,
111
+ num_iteration=self.num_boost_round,
112
+ )
113
+
114
+ def update(self, X, y, Z, num_iteration=1):
115
+ if self.booster is None or self.init_score_ is None:
116
+ raise ValueError("AnchorBoost has not yet been fitted.")
117
+
118
+ y = y.flatten()
119
+
120
+ current_iteration = self.booster.current_iteration()
121
+ if current_iteration == 0:
122
+ f = np.ones(len(y), dtype=self._dtype) * self.init_score_
123
+ else:
124
+ f = self.booster.predict(X, raw_score=True) + self.init_score_
125
+
126
+ Q, _ = np.linalg.qr(Z, mode="reduced") # P_Z f = Q @ (Q^T @ f)
127
+
128
+ for idx in range(current_iteration, current_iteration + num_iteration):
129
+ # For regression, the loss (without anchor) is
130
+ # loss(f, y) = 0.5 * || y - f ||^2
131
+ if self.objective == "regression":
132
+ r = f - y # d/df loss(f, y)
133
+ dr = np.ones(len(y), dtype=self._dtype) # d^2/df^2 loss(f, y)
134
+ ddr = np.zeros(len(y), dtype=self._dtype) # d^3/df^3 loss(f, y)
135
+ # For probit regression, the loss (without anchor) is
136
+ # loss(f, y) = - sum_i (y_i log(p_i) + (1 - y_i) log(1 - p_i))
137
+ # where p_i = scipy.stats.cdf(f_i)
138
+ else:
139
+ # We wish to compute the following:
140
+ # p = scipy.stats.norm.cdf(f)
141
+ # dp = scipy.stats.norm.pdf(f) # d/df p(f)
142
+ # r = np.where(y == 1, -dp / p, dp / (1 - p)) # d/df loss(f, y)
143
+ # The equation for r is numerically unstable. Instead, we use
144
+ # scipy.special.log_ndtr, with log_ndtr(f) = log(norm.cdf(f)).
145
+ y_tilde = np.where(y == 1, 1, -1)
146
+ log_phi = -0.5 * f**2 - 0.5 * np.log(2 * np.pi) # log(norm.pdf(f))
147
+ r = -y_tilde * np.exp(log_phi - scipy.special.log_ndtr(y_tilde * f))
148
+ dr = -f * r + r**2 # d^2/df^2 loss(f, y)
149
+ ddr = (f**2 - 1) * r - 3 * f * r**2 + 2 * r**3 # d^3/df^3 loss
150
+
151
+ r_proj = Q @ (Q.T @ r)
152
+ grad = r + (self.gamma - 1) * r_proj * dr
153
+
154
+ # We wish to fit one additional tree. Intuitively, one would use
155
+ # is_finished = self.booster.update(fobj=self.objective.objective)
156
+ # for this. This makes a call to self.__inner_predict(0) to get the current
157
+ # predictions for all existing trees. See:
158
+ # https://github.com/microsoft/LightGBM/blob/18c11f861118aa889b9d4579c2888d\
159
+ # 5c908fd250/python-package/lightgbm/basic.py#L4165
160
+ # To avoid passing data through all trees each time, this uses a cache.
161
+ # However, this cache is based on the "original" tree values, not the one
162
+ # we set below. We thus use "our own" predictions and skip __inner_predict.
163
+ # No idea what the set_objective_to_none does, but lgbm raises if we don't.
164
+ self.booster._Booster__inner_predict_buffer = None
165
+ if not self.booster._Booster__set_objective_to_none:
166
+ self.booster.reset_parameter(
167
+ {"objective": "none"}
168
+ )._Booster__set_objective_to_none = True
169
+
170
+ # is_finished is True if there we no splits satisfying the splitting
171
+ # criteria. c.f. https://github.com/microsoft/LightGBM/pull/6890
172
+ # The hessian is used only for the `min_hessian_in_leaf` parameter to
173
+ # avoid numerical instabilities.
174
+ is_finished = self.booster._Booster__boost(grad, dr)
175
+
176
+ if is_finished:
177
+ print(f"Finished training after {idx} iterations.")
178
+ break
179
+
180
+ leaves = self.booster.predict(
181
+ X, start_iteration=idx, num_iteration=1, pred_leaf=True
182
+ ).flatten()
183
+ num_leaves = np.max(leaves) + 1
184
+
185
+ # We wish to do 2nd order updates in the leaves. Since the anchor regression
186
+ # objective is quadratic, for regression a 2nd order update is equal to the
187
+ # global minimizer.
188
+ # Let M be the one-hot encoding of the tree's leaf assignments. That is,
189
+ # M[i, j] = 1 if leaves[i] == j else 0.
190
+ # We have
191
+ # r = d/df loss(f, y)
192
+ # dr = d^2/df^2 loss(f, y)
193
+ # ddr = d^3/df^3 loss(f, y)
194
+ # The anchor loss is
195
+ # L = loss(f, y) + (gamma - 1) / 2 * || P_Z r ||^2
196
+ # d/df L = d/df loss(f, y) + (gamma - 1) P_Z r * dr
197
+ # d^2/df^2 L = diag(d^2/df^2 loss(f, y)) + (gamma - 1) diag(P_Z r * ddr)
198
+ # + (gamma - 1) diag(dr) P_Z diag(dr)
199
+ #
200
+ # We do the 2nd order update
201
+ # beta = - (M^T [d^2/df^2 L] M)^{-1} M^T [d/df L]
202
+
203
+ # M^T x = bincount(leaves_masked, weights=x)
204
+ g = np.bincount(leaves, weights=grad, minlength=num_leaves)
205
+
206
+ # M^T diag(x) M = diag(np.bincount(leaves, weights=x))
207
+ counts = np.bincount(
208
+ leaves,
209
+ weights=dr + (self.gamma - 1) * r_proj * ddr,
210
+ minlength=num_leaves,
211
+ )
212
+ counts += self.params.get("lambda_l2", 0)
213
+
214
+ # Mdr^T P_Z Mdr = (Mdr^T Q) @ (Mdr^T Q)^T
215
+ # One could also compute this using bincount, but it appears this
216
+ # version using a sparse matrix is faster.
217
+ Mdr = scipy.sparse.csr_matrix(
218
+ (
219
+ dr,
220
+ (np.arange(len(leaves)), leaves),
221
+ ),
222
+ shape=(len(leaves), num_leaves),
223
+ dtype=self._dtype,
224
+ )
225
+ B = Mdr.T.dot(Q)
226
+ H = (self.gamma - 1) * B @ B.T
227
+ H += np.diag(counts)
228
+
229
+ # Compute the 2nd order update
230
+ leaf_values = -np.linalg.solve(H, g) * self.params.get("learning_rate", 0.1)
231
+
232
+ for ldx, val in enumerate(leaf_values):
233
+ self.booster.set_leaf_output(idx, ldx, val)
234
+
235
+ # Ensure f == self.init_score_ + self.booster.predict(X)
236
+ f += leaf_values[leaves]
237
+
238
+ return self
239
+
240
+ def predict(self, X, raw_score=False, **kwargs):
241
+ """
242
+ Predict the outcome.
243
+
244
+ Parameters
245
+ ----------
246
+ X : numpy.ndarray, polars.DataFrame, or pyarrow.Table
247
+ The input data.
248
+ num_iteration : int
249
+ Number of boosting iterations to use. If -1, all are used. Else, needs to be
250
+ in [0, num_boost_round].
251
+ raw_score : bool
252
+ If True, returns scores. If False, returns predicted probabilities.
253
+ """
254
+ if self.booster is None:
255
+ raise ValueError("AnchorBoost has not yet been fitted.")
256
+
257
+ if _POLARS_INSTALLED and isinstance(X, pl.DataFrame):
258
+ X = X.to_arrow()
259
+
260
+ scores = self.booster.predict(X, raw_score=True, **kwargs)
261
+
262
+ if self.objective == "binary" and not raw_score:
263
+ return scipy.stats.norm.cdf(scores + self.init_score_)
264
+ else:
265
+ return scores + self.init_score_
266
+
267
+ def refit(self, X, y, decay_rate=0):
268
+ """
269
+ Refit the model using new data.
270
+
271
+ Set :math:`f^0_\\mathrm{refit} =` ``init_score_``.
272
+ Starting from :math:`f^j_\\mathrm{refit}`, we drop the new data :math:`(X, y)`
273
+ down the tree :math:`\\hat t^{j+1}`.
274
+ Let :math:`\\hat \\beta_\\mathrm{new}^{j+1}` be the second order optimization
275
+ of the loss :math:`\\ell(\\hat f^j_\\mathrm{refit} + \\hat t^{j+1}(X), y)`
276
+ with respect to the leaf node values :math:`\\beta^{j+1}``of
277
+ :math:`\\hat t^{j+1}(X)`.
278
+ We set
279
+ :math:`\\hat \\beta^{j+1}_\\mathrm{refit} = \\mathrm{decay rate} \\hat \\beta^{j+1}_\\mathrm{old} + (1 - \\mathrm{decay rate}) \\hat \\beta^{j+1}_\\mathrm{new}`.
280
+ Refitting updates the tree's leaf values, but not their structure.
281
+ ``AnchorBooster.refit`` differs from ``lgbm.Booster.refit`` in that it supports
282
+ probit regression and leaf nodes with no samples from the new data are not,
283
+ updated, instead of being shrunk towards zero (as in LightGBM).
284
+
285
+ Refit is not in-place, but returns a new instance of ``AnchorBooster``.
286
+
287
+ Parameters
288
+ ----------
289
+ X : numpy.ndarray, polars.DataFrame, or pyarrow.Table
290
+ The new data.
291
+ y : np.ndarray
292
+ The new outcomes.
293
+ decay_rate : float
294
+ The decay rate for the leaf values. Must be in [0, 1]. Default is 0. If 0,
295
+ the leaf values are set to the new values. If 1, the leaf values are not
296
+ updated.
297
+
298
+ Returns
299
+ -------
300
+ AnchorBooster
301
+ A new instance of AnchorBooster with the updated leaf values.
302
+ """ # noqa: E501
303
+ self_copied = copy.deepcopy(self)
304
+
305
+ # For some reason, the model params are not copied over.
306
+ # https://github.com/microsoft/LightGBM/issues/6821
307
+ self_copied.booster.params = self.booster.params
308
+
309
+ if self.objective == "binary" and not np.isin(y, [0, 1]).all():
310
+ raise ValueError("For binary classification, y values must be in {0, 1}.")
311
+
312
+ if self.objective == "binary":
313
+ y_tilde = np.where(y == 1, 1, -1)
314
+
315
+ leaves = self_copied.booster.predict(X, pred_leaf=True)
316
+ f = np.full(len(y), self.init_score_, dtype="float64")
317
+
318
+ for idx in range(self.num_boost_round):
319
+ if self.objective == "regression":
320
+ grad_hess_ones = np.empty((len(y), 3), dtype="float64")
321
+ grad_hess_ones[:, 0] = f - y
322
+ grad_hess_ones[:, 1:] = 1.0
323
+ # g = f - y
324
+ # h = 1.0
325
+ elif self.objective == "binary":
326
+ log_phi = -0.5 * f**2 - 0.5 * np.log(2 * np.pi) # log(norm.pdf(f))
327
+ grad_hess_ones = np.empty((len(y), 3), dtype="float64")
328
+ grad_hess_ones[:, 0] = -y_tilde * np.exp(
329
+ log_phi - scipy.special.log_ndtr(y_tilde * f)
330
+ )
331
+ grad_hess_ones[:, 1] = (
332
+ -f * grad_hess_ones[:, 0] + grad_hess_ones[:, 0] ** 2
333
+ )
334
+ grad_hess_ones[:, 2] = 1.0
335
+ # g = -y_tilde * np.exp(log_phi - scipy.special.log_ndtr(y_tilde * f))
336
+ # h = -f * g + g**2
337
+ else:
338
+ raise ValueError("Objective must be 'regression' or 'binary'.")
339
+
340
+ num_leaves = np.max(leaves[:, idx]) + 1
341
+
342
+ # We aggregate gradients, hessians, and counts over the leaves.
343
+ # A bincount is faster than a for loop. Still this passes 3x through the
344
+ # data. We use np.add.at to do this in a single pass and avoid a copy.
345
+ # n_obs = np.bincount(leaves[:, idx], minlength=num_leaves)
346
+ # sum_grad = np.bincount(leaves[:, idx], weights=g, minlength=num_leaves)
347
+ # sum_hess = np.bincount(leaves[:, idx], weights=h, minlength=num_leaves)
348
+ arr = np.zeros((num_leaves, 3), dtype=np.float64)
349
+ np.add.at(arr, leaves[:, idx], grad_hess_ones)
350
+
351
+ sum_grad = arr[:, 0]
352
+ sum_hess = arr[:, 1]
353
+ n_obs = arr[:, 2]
354
+
355
+ values = -sum_grad / sum_hess * self_copied.params.get("learning_rate", 0.1)
356
+
357
+ new_values = np.zeros(num_leaves, dtype="float64")
358
+
359
+ for ldx in np.where(n_obs > 0)[0]:
360
+ old_value = self_copied.booster.get_leaf_output(idx, ldx)
361
+ new_values[ldx] = (
362
+ decay_rate * old_value + (1 - decay_rate) * values[ldx]
363
+ )
364
+ self_copied.booster.set_leaf_output(idx, ldx, new_values[ldx])
365
+
366
+ f += new_values[leaves[:, idx]]
367
+
368
+ return self_copied
@@ -0,0 +1,11 @@
1
+ from .anchor_objectives import (
2
+ AnchorKookClassificationObjective,
3
+ AnchorKookMultiClassificationObjective,
4
+ AnchorRegressionObjective,
5
+ )
6
+
7
+ __all__ = [
8
+ "AnchorKookClassificationObjective",
9
+ "AnchorKookMultiClassificationObjective",
10
+ "AnchorRegressionObjective",
11
+ ]
@@ -0,0 +1,123 @@
1
+ import numpy as np
2
+
3
+ from anchorboosting.objectives.mixins import (
4
+ ClassificationMixin,
5
+ LGBMMixin,
6
+ MultiClassificationMixin,
7
+ ProjMixin,
8
+ RegressionMixin,
9
+ )
10
+
11
+
12
+ class AnchorKookClassificationObjective(ClassificationMixin, LGBMMixin, ProjMixin):
13
+ def __init__(self, gamma, precompute_proj=True, n_categories=None):
14
+ super().__init__(precompute_proj=precompute_proj, n_categories=n_categories)
15
+ self.gamma = gamma
16
+ self.name = "kook anchor classification"
17
+
18
+ def __repr__(self):
19
+ return f"AnchorKookClassificationObjective(gamma={self.gamma})"
20
+
21
+ def residuals(self, f, data):
22
+ residuals = self.predictions(f) - data.get_label()
23
+ return residuals
24
+
25
+ def loss(self, f, data):
26
+ return (
27
+ super().loss(f, data)
28
+ + (self.gamma - 1)
29
+ * self.proj(
30
+ data.anchor,
31
+ self.residuals(f, data),
32
+ copy=False,
33
+ )
34
+ ** 2
35
+ )
36
+
37
+ def grad(self, f, data):
38
+ predictions = self.predictions(f)
39
+ proj_residuals = self.proj(data.anchor, self.residuals(f, data), copy=False)
40
+
41
+ return super().grad(f, data) + 2 * (
42
+ self.gamma - 1
43
+ ) * proj_residuals * predictions * (1 - predictions)
44
+
45
+
46
+ class AnchorKookMultiClassificationObjective(
47
+ MultiClassificationMixin, LGBMMixin, ProjMixin
48
+ ):
49
+ def __init__(self, gamma, n_classes, precompute_proj=True, n_categories=None):
50
+ self.gamma = gamma
51
+ super().__init__(
52
+ n_classes=n_classes,
53
+ precompute_proj=precompute_proj,
54
+ n_categories=n_categories,
55
+ )
56
+ self.name = "kook anchor multi-classification"
57
+
58
+ def __repr__(self):
59
+ return f"AnchorKookMultiClassificationObjective(gamma={self.gamma})"
60
+
61
+ def residuals(self, f, data):
62
+ predictions = self.predictions(f)
63
+ predictions[self._indices(data.get_label())] -= 1
64
+ return predictions.flatten("F")
65
+
66
+ def loss(self, f, data):
67
+ residuals = self.residuals(f, data).reshape((-1, self.n_classes), order="F")
68
+ proj_residuals = self.proj(data.anchor, residuals, copy=False)
69
+ # Multiply with self.factor to align two-class classification with
70
+ # AnchorKookClassificationObjective
71
+ return super().loss(f, data) + self.factor * (self.gamma - 1) * np.sum(
72
+ proj_residuals**2, axis=1
73
+ )
74
+
75
+ def grad(self, f, data):
76
+ residuals = self.residuals(f, data).reshape((-1, self.n_classes), order="F")
77
+ proj_residuals = self.proj(data.anchor, residuals, copy=False)
78
+
79
+ predictions = self.predictions(f)
80
+ proj_residuals -= np.sum(proj_residuals * predictions, axis=1, keepdims=True)
81
+ # Multiply with factor to align two-class classification with
82
+ # AnchorKookClassificationObjective
83
+ anchor_grad = 2 * self.factor * (self.gamma - 1) * predictions * proj_residuals
84
+ return super().grad(f, data) + anchor_grad.flatten("F")
85
+
86
+
87
+ class AnchorRegressionObjective(RegressionMixin, LGBMMixin, ProjMixin):
88
+ def __init__(self, gamma, n_categories=None, precompute_proj=True):
89
+ super().__init__(precompute_proj=precompute_proj, n_categories=n_categories)
90
+ self.gamma = gamma
91
+ self.name = "anchor regression"
92
+
93
+ def __repr__(self):
94
+ return f"AnchorRegressionObjective(gamma={self.gamma})"
95
+
96
+ def residuals(self, f, data):
97
+ return data.get_label() - f
98
+
99
+ def loss(self, f, data):
100
+ if self.gamma == 1:
101
+ return super().loss(f, data)
102
+
103
+ # For gamma <= 1, this is equivalent to kappa := (gamma - 1) / gamma and
104
+ # loss = (1 - kappa) | y - f |^2 + kappa | P_Z (y - f) |^2
105
+ return (
106
+ super().loss(f, data)
107
+ + 0.5
108
+ * (self.gamma - 1)
109
+ * self.proj(
110
+ data.anchor,
111
+ self.residuals(f, data),
112
+ copy=False,
113
+ )
114
+ ** 2
115
+ )
116
+
117
+ def grad(self, f, data):
118
+ residuals = self.residuals(f, data)
119
+ if self.gamma == 1:
120
+ return -residuals
121
+
122
+ proj_residuals = self.proj(data.anchor, residuals, copy=True)
123
+ return -residuals - (self.gamma - 1) * proj_residuals
@@ -0,0 +1,237 @@
1
+ import numpy as np
2
+
3
+ from anchorboosting.utils import proj
4
+
5
+
6
+ class ProjMixin:
7
+ def __init__(self, precompute_proj=False, n_categories=None):
8
+ self.n_categories = n_categories
9
+ self.precompute_proj = precompute_proj
10
+ self.pinvZ = None
11
+
12
+ def proj(self, Z, *args, copy=False):
13
+ if self.n_categories is not None:
14
+ return proj(Z, *args, n_categories=self.n_categories, copy=copy)
15
+ elif self.precompute_proj:
16
+ if self.pinvZ is None:
17
+ self.pinvZ = np.linalg.pinv(Z)
18
+ if len(args) == 1:
19
+ return np.dot(Z, self.pinvZ @ args[0])
20
+ else:
21
+ return (*(np.dot(Z, self.pinvZ @ f) for f in args),)
22
+ else:
23
+ return proj(Z, *args, n_categories=None, copy=copy)
24
+
25
+
26
+ class LGBMMixin:
27
+ higher_is_better = False
28
+
29
+ def objective(self, f, data):
30
+ """Objective function for LGBM."""
31
+ return self.grad(f, data), self.hess(f, data)
32
+
33
+ def score(self, f, data):
34
+ """Score function for LGBM."""
35
+ return (
36
+ f"{self.name} ({self.gamma})",
37
+ self.loss(f, data).mean(),
38
+ self.higher_is_better,
39
+ )
40
+
41
+
42
+ class RegressionMixin:
43
+ def init_score(self, y):
44
+ return np.tile(y.mean(), len(y))
45
+
46
+ def grad(self, f, data):
47
+ # Replicate LGBM behaviour
48
+ # https://github.com/microsoft/LightGBM/blob/e9fbd19d7cbaeaea1ca54a091b160868fc\
49
+ # 5c79ec/src/objective/regression_objective.hpp#L130-L131
50
+ return -(data.get_label() - f)
51
+
52
+ def hess(self, f, data):
53
+ # Replicate LGBM behaviour
54
+ # https://github.com/microsoft/LightGBM/blob/e9fbd19d7cbaeaea1ca54a091b160868fc\
55
+ # 5c79ec/src/objective/regression_objective.hpp#L130-L131
56
+ return np.ones(len(data.get_label()))
57
+
58
+ def loss(self, f, data):
59
+ return 0.5 * (data.get_label() - f) ** 2
60
+
61
+
62
+ class ClassificationMixin:
63
+ def init_score(self, y):
64
+ """Initial score for LGBM.
65
+
66
+ Parameters
67
+ ----------
68
+ y: np.ndarray of dimension (n,)
69
+ Vector with true labels in (0, 1).
70
+
71
+ Returns
72
+ -------
73
+ np.ndarray of length n
74
+ Initial scores for LGBM.
75
+ """
76
+ p = np.sum(y) / len(y)
77
+ return np.ones(len(y)) * np.log(p / (1 - p))
78
+
79
+ def loss(self, f, data):
80
+ """Two-class negative log-likelihood loss.
81
+
82
+ Parameters
83
+ ----------
84
+ f: np.ndarray of dimension (n)
85
+ Vector with scores.
86
+ data: lgbm.Dataset
87
+ LGBM dataset with labels of dimension (n,) in (0, 1).
88
+
89
+ Returns
90
+ -------
91
+ np.ndarray of dimension (n,)
92
+ Loss.
93
+ """
94
+ return np.log(1 + np.exp((-2 * data.get_label() + 1) * f))
95
+
96
+ def predictions(self, f):
97
+ """Compute probability predictions from scores via softmax.
98
+
99
+ Parameters
100
+ ----------
101
+ f: np.ndarray of dimension (n,)
102
+ Vector with scores.
103
+
104
+ Returns
105
+ -------
106
+ np.ndarray of dimension (n,)
107
+ Vector with probabilities.
108
+ """
109
+ return 1 / (1 + np.exp(-f))
110
+
111
+ def grad(self, f, data):
112
+ """
113
+ Gradient of the two-class log-likelihood loss.
114
+
115
+ Parameters
116
+ ----------
117
+ f: np.ndarray of dimension (n,)
118
+ Vector with scores.
119
+ data: lgbm.Dataset
120
+ LGBM dataset with labels of dimension (n,) in (0, 1).
121
+ """
122
+ return self.predictions(f) - data.get_label()
123
+
124
+ def hess(self, f, data):
125
+ """
126
+ Diagonal of the Hessian of the multi-class log-likelihood loss.
127
+
128
+ Parameters
129
+ ----------
130
+ f: np.ndarray of dimension (n,)
131
+ Vector with scores.
132
+ data: lgbm.Dataset
133
+ LGBM dataset with labels of dimension (n,) in (0, 1).
134
+ """
135
+ predictions = self.predictions(f)
136
+ return predictions * (1.0 - predictions)
137
+
138
+
139
+ class MultiClassificationMixin:
140
+ def __init__(self, n_classes, **kwargs):
141
+ super().__init__(**kwargs)
142
+ self.n_classes = n_classes
143
+ self.factor = (n_classes - 1) / n_classes
144
+
145
+ def init_score(self, y):
146
+ """Initial score for LGBM.
147
+
148
+ Parameters
149
+ ----------
150
+ y: np.ndarray of dimension (n,)
151
+ Vector with true labels in (0, ..., n_classes - 1).
152
+
153
+ Returns
154
+ -------
155
+ np.ndarray of dimension (n * n_classes,)
156
+ Initial scores for LGBM. Note that this is flattened.
157
+ """
158
+ unique_values, unique_counts = np.unique(y, return_counts=True)
159
+ assert len(unique_values) == self.n_classes
160
+ assert (sorted(unique_values) == unique_values).all()
161
+
162
+ odds = np.array(unique_counts) / np.sum(unique_counts)
163
+ return np.log(np.tile(odds, (len(y), 1)).flatten("F"))
164
+
165
+ def loss(self, f, data):
166
+ """Multi-class negative log-likelihood loss.
167
+
168
+ Parameters
169
+ ----------
170
+ f: np.ndarray of dimension (n * n_classes,)
171
+ Vector with scores.
172
+ data: lgbm.Dataset
173
+ LGBM dataset with labels of dimension (n,) in (0, ..., n_classes - 1).
174
+
175
+ Returns
176
+ -------
177
+ np.ndarray of dimension (n,).
178
+ Loss.
179
+ """
180
+ y = data.get_label()
181
+ f = f.reshape((-1, self.n_classes), order="F") # (n, n_classes)
182
+ f = f - np.max(f, axis=1)[:, np.newaxis] # normalize f to avoid overflow
183
+ log_divisor = np.log(np.sum(np.exp(f), axis=1))
184
+ return -f[self._indices(y)] + log_divisor
185
+
186
+ def _indices(self, y):
187
+ return (np.arange(len(y)), y.astype(int))
188
+
189
+ def predictions(self, f):
190
+ """Compute probability predictions from scores via softmax.
191
+
192
+ Parameters
193
+ ----------
194
+ f: np.ndarray of dimension (n * n_classes,).]
195
+ Vector with scores.
196
+
197
+ Returns
198
+ -------
199
+ np.ndarray of dimension (n, n_classes)
200
+ Vector with probabilities.
201
+ """
202
+ f = f.reshape((-1, self.n_classes), order="F") # (n, n_classes)
203
+ f = f - np.max(f, axis=1)[:, np.newaxis] # normalize f to avoid overflow
204
+ predictions = np.exp(f)
205
+ predictions /= np.sum(predictions, axis=1)[:, np.newaxis]
206
+ return predictions
207
+
208
+ def grad(self, f, data):
209
+ """
210
+ Gradient of the multi-class log-likelihood loss.
211
+
212
+ Parameters
213
+ ----------
214
+ f: np.ndarray of dimension (n * n_classes,)
215
+ Vector with scores.
216
+ data: lgbm.Dataset
217
+ LGBM dataset with labels of dimension (n,) in (0, ..., n_classes - 1).
218
+ """
219
+ y = data.get_label()
220
+ predictions = self.predictions(f)
221
+ predictions[self._indices(y)] -= 1
222
+
223
+ return predictions.flatten("F")
224
+
225
+ def hess(self, f, data):
226
+ """
227
+ Diagonal of the Hessian of the multi-class log-likelihood loss.
228
+
229
+ Parameters
230
+ ----------
231
+ f: np.ndarray of dimension (n * n_classes,)
232
+ Vector with scores.
233
+ data: lgbm.Dataset
234
+ LGBM dataset with labels of dimension (n,) in (0, ..., n_classes - 1).
235
+ """
236
+ predictions = self.predictions(f).flatten("F")
237
+ return 1 / self.factor * predictions * (1.0 - predictions)
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+
3
+
4
+ def simulate(f, n=100, shift=0, seed=0):
5
+ rng = np.random.RandomState(seed)
6
+
7
+ p = 2
8
+
9
+ a = rng.normal(size=(n, 2)) + shift
10
+ h = rng.normal(size=(n, 1))
11
+ x_noise = 0.5 * rng.normal(size=(n, p))
12
+ x = x_noise + np.repeat(a[:, 0] + a[:, 1] + 2 * h[:, 0], p).reshape((n, p))
13
+
14
+ y_noise = 0.25 * rng.normal(size=n)
15
+ y = f(x[:, 0], x[:, 1]) - 2 * a[:, 0] + 3 * h[:, 0] + y_noise
16
+
17
+ return x, y, a
18
+
19
+
20
+ def f1(x2, x3):
21
+ return (x2 <= 0) + (x2 <= -0.5) * (x3 <= 1)
22
+
23
+
24
+ def f2(x2, x3):
25
+ return x2 + x3 + (x2 <= 0) + (x2 <= -0.5) * (x3 <= 1)
@@ -0,0 +1,84 @@
1
+ # copy-pasted from https://github.com/mlondschien/ivmodels/blob/09f8bc5a78b3793ae5\
2
+ # 1f0bb7f0331b70a0971ebc/ivmodels/utils.py
3
+ import numpy as np
4
+ import scipy
5
+
6
+
7
+ def proj(Z, f, n_categories=None, copy=False):
8
+ """
9
+ Project f onto the subspace spanned by Z.
10
+
11
+ If `Z` is a matrix of floats, uses a copy of `ivmodel`'s `proj:
12
+ https://github.com/mlondschien/ivmodels/blob/main/ivmodels/utils.py#L12
13
+ If `Z` is a 1d array of unsigned integers, assumes that `Z` is categorical with
14
+ values 0, 1, ..., n_categories-1. In this case, the `n_categories` argument must be
15
+ be supplied. The projection is done by averaging the values of f within each
16
+ category. If `Z` is None, returns np.zeros_like(f).
17
+
18
+ Parameters
19
+ ----------
20
+ Z: np.ndarray of dimension (n, d_Z) or (n,)
21
+ The `Z` matrix or 1d array of integers. If None, returns np.zeros_like(f).
22
+ *args: np.ndarrays of dimension (n, d_f) or (n,)
23
+ Vectors or matrices to project.
24
+ n_categories: int, optional, default=None
25
+ If not None, then Z is assumed to be categorical with categories 0, 1, ...,
26
+ n_categories-1. The projection is done by averaging the values of f within each
27
+ category.
28
+ copy: bool, default=False
29
+ If False, overwrites *args.
30
+
31
+ Returns
32
+ -------
33
+ np.ndarray of dimension (n, d_f) or (n,)
34
+ Projection of args onto the subspace spanned by Z. Same number of
35
+ outputs as args. Same dimension as args
36
+ """
37
+ if Z is None:
38
+ return np.zeros_like(f)
39
+
40
+ if len(f.shape) > 2:
41
+ raise ValueError(f"*args should have shapes (n, d_f) or (n,). Got {f.shape}.")
42
+ if f.shape[0] != Z.shape[0]:
43
+ raise ValueError(f"Shape mismatch: Z.shape={Z.shape}, f.shape={f.shape}.")
44
+
45
+ if n_categories is not None:
46
+ f = f.copy() if copy else f
47
+
48
+ if len(Z.shape) != 1 or not np.issubdtype(Z.dtype, np.integer):
49
+ raise ValueError(
50
+ "If n_categories is not None, then Z should be a single column of "
51
+ f"integers. Got shape {Z.shape} and dtype {Z.dtype}."
52
+ )
53
+
54
+ if len(f.shape) == 1:
55
+ means = np.zeros((n_categories,))
56
+ counts = np.zeros(n_categories)
57
+
58
+ # np.add.at(a, indices, b) is equivalent to a[indices] += b
59
+ np.add.at(means, Z, f)
60
+ np.add.at(counts, Z, 1)
61
+
62
+ means[counts > 0] = means[counts > 0] / counts[counts > 0]
63
+
64
+ f[:] = means[Z]
65
+ else:
66
+ means = np.zeros((n_categories, f.shape[1]))
67
+ counts = np.zeros(n_categories)
68
+
69
+ # np.add.at(a, indices, b) is equivalent to a[indices] += b
70
+ np.add.at(means, Z, f)
71
+ np.add.at(counts, Z, 1)
72
+
73
+ means[counts > 0, :] = means[counts > 0, :] / counts[counts > 0, None]
74
+
75
+ f[:] = means[Z, :]
76
+
77
+ return f
78
+
79
+ # The gelsy driver raises in this case - we handle it separately
80
+ if len(f.shape) == 2 and f.shape[1] == 0:
81
+ return np.zeros_like(f)
82
+
83
+ # return np.dot(Z, scipy.linalg.pinv(Z.T @ Z) @ Z.T @ args[0])
84
+ return np.dot(Z, scipy.linalg.lstsq(Z, f, cond=None, lapack_driver="gelsy")[0])
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: anchorboosting
3
+ Version: 0.0.1
4
+ Summary: Anchor Boosting
5
+ Author-email: Malte Londschien <malte@londschien.ch>
6
+ Project-URL: homepage, https://github.com/mlondschien/anchorboosting/
7
+ Classifier: Programming Language :: Python
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: scipy
13
+ Requires-Dist: lightgbm
14
+ Dynamic: license-file
15
+
16
+ # Anchor Boosting
17
+
18
+ This repository combines ideas from [1, 2 and 3].
19
+ [1] suggests regularizing linear regression with the correlation between a so-called anchor variable and the regression's residuals.
20
+ The anchor variable is assumed to be exogenous to the system, i.e., not causally affected by covariates, the outcome, or relevant hidden variables.
21
+ [1] show that such regularization induces robustness of the linear regression model to shift interventions with limited magnitude in directions as observed in the data.
22
+
23
+ [2] proposes to generalize this to nonlinear regression by simply optimizing, e.g., via boosting, the regularized anchor regression loss, replacing linear regression with a more flexible model class.
24
+
25
+ Lastly, [3] give ideas to generalize anchor regression to classification.
26
+
27
+ This repository combines these approaches and implements non-linear anchor (multiclass-) classification using LGBM.
28
+
29
+ ## Classification via boosting
30
+
31
+ We first describe how to explicitly solve a classification problem with boosting.
32
+ This includes calculating the gradient and the diagonal of the hessian of the loss with respect to model parameters.
33
+ In each boosting step, the gradient is fit with a weak learner (CART tree).
34
+ The diagonal of the hessian is used as a stopping criterium only (`min_hessian_in_leaf`).
35
+
36
+ We separately describe two-class and multi-class classification, even though the former is a special case of the latter.
37
+ $K$-class classification involves learning $K$ scores (parameters), each fitted with a single weak learner.
38
+ I.e., the boosting algorithm will train $K$ trees in each set.
39
+
40
+ ### Two-class classification
41
+
42
+ Consider a setup with observations $x_i \in \mathbb{R}^p$ with outcomes $y_i \in \{-1, 1\}$ for $i=1, \ldots, n$.
43
+ We assign raw scores $f_i \in \mathbb{R}$ to each observation and use the expit to obtain probability predictions.
44
+ The estimated probability of observation $x_i$ with raw score $f_i$ to belong to class 1 is $p_i = \frac{e^f}{1 + e^f} = (1 + e^{-f})^{-1}$.
45
+ The log-likelihood of raw scores $`f=(f_i)_{i=1}^n`$ is
46
+
47
+ $$\ell(f, y) = -\sum_{i=1}^n \log(1 + e^{-yf}).$$
48
+
49
+ The gradient of the log-likelihood is
50
+
51
+ $$
52
+ \frac{d}{d f_i} \ell(f, y) = y \frac{e^{-yf}}{1 + e^{-yf}} =
53
+ \begin{cases}
54
+ 1 - p & y = 1 \\
55
+ -p & y = 0
56
+ \end{cases}
57
+ $$
58
+
59
+ The (diagonal of the) Hessian of the log-likelihood is
60
+
61
+ $$
62
+ \frac{d^2}{d^2f_i} \ell(f, y) = (1 - p_i) p_i.
63
+ $$
64
+
65
+ ### Multiclass classification
66
+ Consider a setup with observations $x_i \in \mathbb{R}^p$ with outcomes $y_i \in \{1, \ldots, K\}$ for $i=1, \ldots, n$.
67
+ We assign raw scores $f_i = (f_{i, 1}, \ldots, f_{i, K}) \in \mathbb{R}^K$ to each observation and use cross-entropy to obtain probability predictions.
68
+ For $k = 1, \ldots, K$ and $i=1, \ldots, n$ the estimated probability of observation $x_i$ with raw score $f_i$ to belong to class $k$ is $p_{i, k} := \exp(f_{i, k}) / \sum_{j=1}^K \exp(f_{i, j})$. The log-likelihood of raw scores $`(f_i)_{i=1}^n`$ is then
69
+ ```math
70
+ \ell\left((f_{i, k})_{i=1, \ldots, n}^{k=1, \ldots, K}, (y_i)_{i=1}^n\right) = \sum_{i=1}^n \left(f_{i, y} - \log\left(\sum_{j=1}^K \exp(f_{i,j})\right)\right).
71
+ ```
72
+
73
+ The gradient of the log-likelihood is
74
+
75
+ $$
76
+ \frac{d}{df_{i, k}} \ell(f, y) = \begin{cases}
77
+ 1 - p_{i, k} & y_i = k \\
78
+ -p_{i, k} & y_i \neq k \\
79
+ \end{cases}
80
+ $$
81
+
82
+ The (diagonal of the) Hessian of the log-likelihood is
83
+ ```math
84
+ \frac{d^2}{d^2 f_{i, k}} \ell(f, y) = (1 - p_{i, k}) p_{i, k}
85
+ ```
86
+
87
+ ## Anchorized classification
88
+
89
+ [1] suggest adding a regularization term based on an "anchor variable" $A$ to the linear least squares optimization problem to improve distributional robustness.
90
+
91
+ Say additional to features and outcomes we observe anchor values $a_i \in \mathbb{R}^q, i=1,\ldots K$.
92
+ Write $A = (a_i)_{i=1}^n \in \mathbb{R}^{n \times q}$ and $\pi_A$ for the linear projection onto the column space of $A$.
93
+ [1] show that
94
+
95
+ $$
96
+ b^\gamma = \underset{b}{\arg\min} \|(\mathrm{Id} - \pi_A)(Y - Xb)\|_2^2 + \gamma\|\pi_A(Y - Xb)\|_2^2
97
+ $$
98
+
99
+ minimizes the linear model's worst-case risk with respect to certain shift interventions as seen in the data.
100
+
101
+ [2] suggests applying anchor regression to nonlinear regression by boosting the above anchor loss with a flexible (nonlinear) learner.
102
+ [3] presents an idea to generalize anchor regression to different distributions than the Gaussian distribution, i.e., using different losses than the squared error.
103
+ One interesting case is the logistic loss for (two-class) classification.
104
+ This is mostly based on intelligently defining residuals to be used in the equivalent classification anchor loss.
105
+
106
+ [4] proposes the usage of a different type of residuals for classification outside of the use with anchor regression.
107
+ We apply their residuals in the context of anchor classification.
108
+
109
+ ### Anchorized two-class classification according to [3]
110
+
111
+ Note that again $y \in \{-1, 1\}$.
112
+ [3] suggest defining residuals as
113
+
114
+ $$r_i = \frac{d}{d f}(\ell(f, y)) = y \frac{e^{-yf}}{1 + e^{-yf}} =
115
+ \begin{cases}
116
+ 1 - p & y = 1 \\
117
+ -p & y = 0
118
+ \end{cases}
119
+ $$
120
+
121
+ For some tuning parameter $\gamma$, we add the regularization term $(\gamma - 1) \| \pi_A r \|_2^2$ to our optimization problem.
122
+
123
+ $$
124
+ \hat f = \underset{f}{\arg \min} \ \ell(f, Y) + (\gamma - 1) \|\pi_A r\|_2^2
125
+ $$
126
+
127
+ This encourages uncorrelatedness between the residuals and the anchor and, hopefully, better domain generalization. To optimize this, we also calculate the gradient of the regularization term. First, note that
128
+
129
+ $$
130
+ \frac{d}{d f_i} p_i = p_i (1 - p_i)
131
+ $$
132
+
133
+ such that
134
+
135
+ $$
136
+ \frac{d}{d f_i} \| \pi_A r \|_2^2 = 2 \pi_A r \cdot p (1 - p)
137
+ $$
138
+
139
+ ### Anchorized multiclass classification according to [3]
140
+
141
+ We combine the notions of [1, 2, 3].
142
+ Motivated by [3], define residuals
143
+ ```math
144
+ r_{i, k} =
145
+ \frac{d}{df_{i, k}} \ell(f, y) =
146
+ \begin{cases}
147
+ 1 - p_{i, k} & y_i = k \\
148
+ -p_{i, k} & y_i \neq k
149
+ \end{cases}
150
+ ```
151
+ such that for all $i$ we have $\sum_{k} r_{i, k} = 0$.
152
+
153
+ For some tuning parameter $\gamma$, we add the regularization term $(\gamma - 1) \| \pi_A r \|_2^2$ to our optimization problem.
154
+
155
+ $$
156
+ \hat f = \underset{f}{\arg \min} \ \ell(f, Y) + (\gamma - 1) \|\pi_A r\|_2^2
157
+ $$
158
+
159
+ This encourages uncorrelatedness between the residuals and the anchor and, hopefully, better domain generalization. To optimize this, we also calculate the gradient of the regularization term. First, note that
160
+
161
+ $$
162
+ \frac{d}{d f_{i, k}} p_{i, j} =
163
+ \frac{d}{d f_{i, k}} \frac{\exp(f_{i, j})}{\sum_l \exp(f_{i, l})} =
164
+ \begin{cases}
165
+ \frac{\exp(f_{i, k})}{\sum_l \exp(f_{i, l})} - \frac{\exp(f_{i, k})^2}{(\sum_l \exp(f_{i, l}))^2} & j = k \\
166
+ -\frac{\exp(f_{i, k})\exp{(f_{i, j})}}{(\sum_l \exp(f_{i, l}))^2} & j \neq k
167
+ \end{cases} =
168
+ \begin{cases}
169
+ p_{i, j} - p_{i, j}p_{i, k} & j = k \\
170
+ -p_{i, j} p_{i, k} & j \neq k
171
+ \end{cases}.
172
+ $$
173
+
174
+ Then,
175
+
176
+ ```math
177
+ \frac{d}{d f_{i, k}} \| \pi_A r\|_2^2 = 2 \pi_A r \cdot \left(\begin{cases}
178
+ p_{i, j} - p_{i, j}p_{i, k} & l=i, j = k \\
179
+ -p_{i, j} p_{i, k} & l=i, j \neq k \\
180
+ 0 & l \neq i
181
+ \end{cases}\right)_{l=1, \ldots, n}^{j=1, \ldots K}
182
+ =(\pi_A r)_{i, k} \ p_{i, k} - \sum_{l=1}^K (\pi_A r)_{i, l} \ p_{i, l}.
183
+ ```
184
+
185
+ <!---
186
+ The diagonal of the hessian is equal to
187
+
188
+ $$
189
+ \frac{d^2}{d f_{i, k}^2} \| \pi_A r\|^2 = 2 \pi_A r \cdot
190
+ \left(\begin{cases}
191
+ p_{i, j} - 3 p_{i, j}^2 + 2 p_{i, j}^3 & l=i, j = k \\
192
+ 2 p_{i, j} p_{i, k}^2 - p_{i, j} p{i, k} & l=i, j \neq k \\
193
+ 0 & l \neq i
194
+ \end{cases}\right)_{l=1, \ldots, n}^{j=1, \ldots K} +
195
+ 2 \pi_A \left(\begin{cases}
196
+ p_{i, j} - p_{i, j}p_{i, k} & l=i, j = k \\
197
+ - p_{i, j} p_{i, k} & l=i, j \neq k \\
198
+ 0 & l \neq i
199
+ \end{cases}\right)_{l=1, \ldots, n}^{j=1, \ldots K} \cdot \left(\begin{cases}
200
+ p_{i, j} - p_{i, j}p_{i, k} & l=i, j = k \\
201
+ - p_{i, j} p_{i, k} & l=i, j \neq k \\
202
+ 0 & l \neq i
203
+ \end{cases}\right)_{l=1, \ldots, n}^{j=1, \ldots K}
204
+ $$
205
+
206
+ $$
207
+ \frac{d^2}{d f_{i, k} d f_{i, l}} p_{i, j} =
208
+ \begin{cases}
209
+ (1 - 2 p_{i, j}) p_{i, j} (1 - p_{i, j}) & j = k \\
210
+ p_{i, j}^2 p_{i, k} & j \neq k
211
+ \end{cases}
212
+ $$ -->
213
+
214
+ ## Anchorized two-class classification with residuals motivated by [4]
215
+
216
+
217
+
218
+ ## References
219
+
220
+ [1] Rothenhäusler, D., N. Meinshausen, P. Bühlmann, and J. Peters (2021). Anchor regression: Heterogeneous data meet causality. Journal of the Royal Statistical Society Series B (Statistical Methodology) 83(2), 215–246.
221
+
222
+ [2] Bühlmann, P. (2020). Invariance, causality and robustness. Statistical Science 35(3), 404– 426.
223
+
224
+ [3] Kook, L., B. Sick, and P. Bühlmann (2022). Distributional anchor regression. Statistics and Computing 32(3), 1–19.
@@ -0,0 +1,12 @@
1
+ anchorboosting/__init__.py,sha256=MGNvLzDGE19NKIwN6muC6P6c-dznFfn-VoQi_kd08Yc,160
2
+ anchorboosting/models.py,sha256=7MEPp9lvSr0_FEO_Dxgf-GWLz_N5R2GRoU2cerGMULM,14787
3
+ anchorboosting/simulate.py,sha256=eMBwbOsE9U9t8l1uCNg8asLHSV5v6Rff8CHGmltlaKI,577
4
+ anchorboosting/utils.py,sha256=Bh439YcvTYQ6QBIISuL1BPSo_v7nq8VAalSONosxeEg,3152
5
+ anchorboosting/objectives/__init__.py,sha256=hbBiAd6Ng_sfc1aaBc559DaCn1oGacC5XJDeq2Eoiz8,284
6
+ anchorboosting/objectives/anchor_objectives.py,sha256=rloFuorTs-kcGvE2L1zGlJJlPj3r_fukN-PtTwo9nOo,4223
7
+ anchorboosting/objectives/mixins.py,sha256=-ceTSWUFyOBBOrFltHTYFTLoW31qlUOC683nnaLfTk0,7182
8
+ anchorboosting-0.0.1.dist-info/licenses/LICENSE,sha256=DZyvhSIAeCQbdMQXcvAU_5VZo7j6Iqh5wxwNUNIkgXY,1506
9
+ anchorboosting-0.0.1.dist-info/METADATA,sha256=FP5A3K_iLtFRn_jVDPI2IUgEWxmLqK0T2BH7ZvRAc1Y,8811
10
+ anchorboosting-0.0.1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
11
+ anchorboosting-0.0.1.dist-info/top_level.txt,sha256=MQhJ5Jg216IFXbvNuArVQZPT8NEYMbU1k-PkG_EnBXc,15
12
+ anchorboosting-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,13 @@
1
+ BSD 3-clause license
2
+ Copyright (c) 2022 Malte Londschien
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10
+
11
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ anchorboosting