anchorboosting 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/CHANGELOG.rst +5 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/PKG-INFO +1 -1
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/models.py +104 -19
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/PKG-INFO +1 -1
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/SOURCES.txt +2 -3
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/environment.yml +0 -1
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/pyproject.toml +1 -1
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_anchor_boosting.py +29 -9
- anchorboosting-0.2.1/tests/test_proj.py +67 -0
- anchorboosting-0.2.0/anchorboosting/utils.py +0 -84
- anchorboosting-0.2.0/tests/test_utils.py +0 -74
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.github/workflows/build.yaml +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.github/workflows/ci.yml +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.gitignore +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.pre-commit-config.yaml +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/LICENSE +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/README.md +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/__init__.py +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/simulate.py +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/dependency_links.txt +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/requires.txt +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/top_level.txt +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/Makefile +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/bib.bib +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/changelog.rst +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/conf.py +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/index.rst +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/readthedocs.yaml +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/setup.cfg +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_derivatives.py +0 -0
- {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_refit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anchorboosting
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Efficient tree-based nonlinear anchor regression and classification
|
|
5
5
|
Author-email: Malte Londschien <malte@londschien.ch>
|
|
6
6
|
Project-URL: homepage, https://github.com/mlondschien/anchorboosting/
|
|
@@ -161,9 +161,6 @@ class AnchorBooster:
|
|
|
161
161
|
if self.objective != "regression" and not np.isin(y, [0, 1]).all():
|
|
162
162
|
raise ValueError("For binary classification, y values must be in {0, 1}.")
|
|
163
163
|
|
|
164
|
-
if Z is None:
|
|
165
|
-
Z = np.zeros(shape=(len(y), 0))
|
|
166
|
-
|
|
167
164
|
y = y.flatten()
|
|
168
165
|
|
|
169
166
|
if self.objective == "regression":
|
|
@@ -198,7 +195,7 @@ class AnchorBooster:
|
|
|
198
195
|
f = np.ones(len(y), dtype=np.float64) * self.init_score_
|
|
199
196
|
booster_preds = f.copy()
|
|
200
197
|
|
|
201
|
-
|
|
198
|
+
proj = Proj(Z)
|
|
202
199
|
|
|
203
200
|
if self.objective == "binary":
|
|
204
201
|
y_tilde = np.where(y == 1, 1, -1).astype(np.float64)
|
|
@@ -231,7 +228,7 @@ class AnchorBooster:
|
|
|
231
228
|
dr = -f * r + r**2 # d^2/df^2 loss(f, y)
|
|
232
229
|
ddr = (f**2 - 1) * r - 3 * f * r**2 + 2 * r**3 # d^3/df^3 loss
|
|
233
230
|
|
|
234
|
-
r_proj =
|
|
231
|
+
r_proj = proj(r)
|
|
235
232
|
grad = r + (self.gamma - 1) * r_proj * dr
|
|
236
233
|
|
|
237
234
|
# We wish to fit one additional tree. Intuitively, one would use
|
|
@@ -348,19 +345,8 @@ class AnchorBooster:
|
|
|
348
345
|
counts += self.params.get("lambda_l2", 0)
|
|
349
346
|
counts[counts == 0] = 1 # counts==0 should never happen.
|
|
350
347
|
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
# version using a sparse matrix is faster.
|
|
354
|
-
Mdr = scipy.sparse.csr_matrix(
|
|
355
|
-
(
|
|
356
|
-
dr,
|
|
357
|
-
(np.arange(len(leaves)), leaves),
|
|
358
|
-
),
|
|
359
|
-
shape=(len(leaves), num_leaves),
|
|
360
|
-
dtype=np.float64,
|
|
361
|
-
)
|
|
362
|
-
B = Mdr.T.dot(Q)
|
|
363
|
-
H = (self.gamma - 1) * B @ B.T
|
|
348
|
+
# (dr * M)^T P_Z (dr * M)
|
|
349
|
+
H = (self.gamma - 1) * proj.sandwich(leaves, num_leaves, weights=dr)
|
|
364
350
|
H += np.diag(counts)
|
|
365
351
|
|
|
366
352
|
# Compute the 2nd order update
|
|
@@ -449,7 +435,8 @@ class AnchorBooster:
|
|
|
449
435
|
|
|
450
436
|
f = np.full(len(y), self.init_score_, dtype="float64")
|
|
451
437
|
|
|
452
|
-
|
|
438
|
+
# The model might have stopped boosting prematurely.
|
|
439
|
+
for idx in range(leaves.shape[1]):
|
|
453
440
|
n_obs = np.bincount(leaves[:, idx], minlength=num_leaves[idx])
|
|
454
441
|
|
|
455
442
|
if self.objective == "regression":
|
|
@@ -505,3 +492,101 @@ def _suppress_stderr():
|
|
|
505
492
|
os.dup2(old_stderr_fd, stderr_fd) # put the old stderr back
|
|
506
493
|
os.close(old_stderr_fd)
|
|
507
494
|
os.close(devnull_fd)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class Proj:
|
|
498
|
+
"""
|
|
499
|
+
Cache the projection onto the subspace spanned by Z.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
Z: np.ndarray of dimension (n, d_Z) or (n,), optional, default=None
|
|
504
|
+
The `Z` matrix or 1d array of integers.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
def __init__(self, Z):
|
|
508
|
+
if Z is None:
|
|
509
|
+
self._type = "none"
|
|
510
|
+
|
|
511
|
+
elif len(Z.shape) == 1 and np.issubdtype(Z.dtype, np.integer):
|
|
512
|
+
self._type = "categorical"
|
|
513
|
+
self._Z = Z
|
|
514
|
+
self._n_categories = np.max(Z) + 1
|
|
515
|
+
counts = np.bincount(Z, minlength=self._n_categories)
|
|
516
|
+
counts[counts == 0] = 1.0 # Avoid division by zero
|
|
517
|
+
self._one_over_counts = 1 / counts
|
|
518
|
+
|
|
519
|
+
elif len(Z.shape) == 2 and np.issubdtype(Z.dtype, np.floating):
|
|
520
|
+
self._type = "linear"
|
|
521
|
+
self._Q, _ = np.linalg.qr(Z, mode="reduced")
|
|
522
|
+
else:
|
|
523
|
+
raise ValueError(
|
|
524
|
+
"Z should be either a 1d array of integers or a 2d array of floats. "
|
|
525
|
+
f"Got shape {Z.shape} and dtype {Z.dtype}."
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def __call__(self, f):
|
|
529
|
+
"""
|
|
530
|
+
Project the input array `f` onto the subspace spanned by `Z`.
|
|
531
|
+
|
|
532
|
+
Parameters
|
|
533
|
+
----------
|
|
534
|
+
f: np.ndarray of shape (n,)
|
|
535
|
+
The input array to project.
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
np.ndarray of shape (n,)
|
|
540
|
+
The projected array.
|
|
541
|
+
"""
|
|
542
|
+
if self._type == "none":
|
|
543
|
+
return np.zeros_like(f)
|
|
544
|
+
|
|
545
|
+
elif self._type == "categorical":
|
|
546
|
+
sums = np.bincount(self._Z, weights=f, minlength=self._n_categories)
|
|
547
|
+
means = sums * self._one_over_counts
|
|
548
|
+
return means[self._Z]
|
|
549
|
+
|
|
550
|
+
elif self._type == "linear":
|
|
551
|
+
return self._Q @ (self._Q.T @ f)
|
|
552
|
+
|
|
553
|
+
def sandwich(self, leaves, num_leaves, weights):
|
|
554
|
+
"""
|
|
555
|
+
For M = weights * one_hot(leaves), return proj(Z, M).T @ proj(Z, M).
|
|
556
|
+
|
|
557
|
+
Parameters
|
|
558
|
+
----------
|
|
559
|
+
leaves: np.ndarray of shape (n,)
|
|
560
|
+
The leaf indices for each sample in `f`. Integers in [0, num_leaves).
|
|
561
|
+
num_leaves: int
|
|
562
|
+
The number of leaves in the decision tree.
|
|
563
|
+
weights: np.ndarray of shape (n,)
|
|
564
|
+
The input array to project.
|
|
565
|
+
|
|
566
|
+
Returns
|
|
567
|
+
-------
|
|
568
|
+
np.ndarray of shape (d, d)
|
|
569
|
+
The sandwich product.
|
|
570
|
+
"""
|
|
571
|
+
if self._type == "none":
|
|
572
|
+
return np.zeros((num_leaves, num_leaves), dtype=weights.dtype)
|
|
573
|
+
|
|
574
|
+
elif self._type == "categorical":
|
|
575
|
+
S = np.zeros((self._n_categories, num_leaves), dtype=weights.dtype)
|
|
576
|
+
np.add.at(S, (self._Z, leaves), weights)
|
|
577
|
+
return (S * self._one_over_counts[:, np.newaxis]).T @ S
|
|
578
|
+
|
|
579
|
+
elif self._type == "linear":
|
|
580
|
+
# M^T P_Z M = (M^T Q) @ (M^T Q)^T
|
|
581
|
+
# One could also compute this using bincount, but it appears this
|
|
582
|
+
# version using a sparse matrix is faster.
|
|
583
|
+
M = scipy.sparse.csr_matrix(
|
|
584
|
+
(
|
|
585
|
+
weights,
|
|
586
|
+
(np.arange(len(leaves)), leaves),
|
|
587
|
+
),
|
|
588
|
+
shape=(len(leaves), num_leaves),
|
|
589
|
+
dtype=weights.dtype,
|
|
590
|
+
)
|
|
591
|
+
B = M.T @ self._Q
|
|
592
|
+
return B @ B.T
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anchorboosting
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Efficient tree-based nonlinear anchor regression and classification
|
|
5
5
|
Author-email: Malte Londschien <malte@londschien.ch>
|
|
6
6
|
Project-URL: homepage, https://github.com/mlondschien/anchorboosting/
|
|
@@ -11,7 +11,6 @@ readthedocs.yaml
|
|
|
11
11
|
anchorboosting/__init__.py
|
|
12
12
|
anchorboosting/models.py
|
|
13
13
|
anchorboosting/simulate.py
|
|
14
|
-
anchorboosting/utils.py
|
|
15
14
|
anchorboosting.egg-info/PKG-INFO
|
|
16
15
|
anchorboosting.egg-info/SOURCES.txt
|
|
17
16
|
anchorboosting.egg-info/dependency_links.txt
|
|
@@ -24,5 +23,5 @@ docs/conf.py
|
|
|
24
23
|
docs/index.rst
|
|
25
24
|
tests/test_anchor_boosting.py
|
|
26
25
|
tests/test_derivatives.py
|
|
27
|
-
tests/
|
|
28
|
-
tests/
|
|
26
|
+
tests/test_proj.py
|
|
27
|
+
tests/test_refit.py
|
|
@@ -5,7 +5,7 @@ requires = ['setuptools', 'setuptools-scm', 'wheel']
|
|
|
5
5
|
name = "anchorboosting"
|
|
6
6
|
description = "Efficient tree-based nonlinear anchor regression and classification"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
version = "0.2.
|
|
8
|
+
version = "0.2.1"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "Malte Londschien", email = "malte@londschien.ch" }
|
|
@@ -3,19 +3,23 @@ import numpy as np
|
|
|
3
3
|
import pytest
|
|
4
4
|
import scipy
|
|
5
5
|
|
|
6
|
-
from anchorboosting import AnchorBooster
|
|
6
|
+
from anchorboosting.models import AnchorBooster, Proj
|
|
7
7
|
from anchorboosting.simulate import f1, simulate
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.mark.parametrize("gamma", [1.0, 2.0, 100])
|
|
11
11
|
@pytest.mark.parametrize("objective", ["regression", "binary"])
|
|
12
|
-
|
|
12
|
+
@pytest.mark.parametrize("categorical_z", [True, False])
|
|
13
|
+
def test_anchor_boosting_second_order(gamma, objective, categorical_z):
|
|
13
14
|
learning_rate = 0.1
|
|
14
15
|
num_leaves = 5
|
|
15
16
|
n = 200
|
|
16
17
|
num_boost_round = 10
|
|
17
18
|
|
|
18
19
|
x, y, a = simulate(f1, n=n, shift=0, seed=0)
|
|
20
|
+
if categorical_z:
|
|
21
|
+
a = np.digitize(a[:, 0], bins=[-1, 0, 1, 2, 3])
|
|
22
|
+
assert np.issubdtype(a.dtype, np.integer)
|
|
19
23
|
|
|
20
24
|
if objective == "binary":
|
|
21
25
|
y = (y > 0).astype(int)
|
|
@@ -38,9 +42,11 @@ def test_anchor_boosting_second_order(gamma, objective):
|
|
|
38
42
|
num_iteration=1,
|
|
39
43
|
).flatten()
|
|
40
44
|
|
|
45
|
+
proj = Proj(a)
|
|
46
|
+
|
|
41
47
|
def regression_loss(leaf_values):
|
|
42
48
|
residuals = y - f - leaf_values[leaves]
|
|
43
|
-
Pa_residuals =
|
|
49
|
+
Pa_residuals = proj(residuals)
|
|
44
50
|
return np.sum(np.square(residuals)) + (gamma - 1) * residuals.T @ Pa_residuals
|
|
45
51
|
|
|
46
52
|
def probit_loss(leaf_values):
|
|
@@ -49,7 +55,7 @@ def test_anchor_boosting_second_order(gamma, objective):
|
|
|
49
55
|
dp = scipy.stats.norm.pdf(scores)
|
|
50
56
|
losses = -np.log(np.where(y == 1, p, 1 - p))
|
|
51
57
|
dl = np.where(y == 1, -dp / p, dp / (1 - p))
|
|
52
|
-
Pa_dl =
|
|
58
|
+
Pa_dl = proj(dl)
|
|
53
59
|
return np.sum(losses) + (gamma - 1) / 2 * dl.T @ Pa_dl
|
|
54
60
|
|
|
55
61
|
if objective == "regression":
|
|
@@ -176,18 +182,32 @@ def test_compare_anchor_boosting_to_lgbm(parameters):
|
|
|
176
182
|
|
|
177
183
|
|
|
178
184
|
@pytest.mark.parametrize("objective", ["regression", "binary"])
|
|
179
|
-
def
|
|
185
|
+
def test_categorical_and_onehot_is_the_same(objective):
|
|
180
186
|
X, y, a = simulate(f1, shift=0, seed=0)
|
|
181
187
|
|
|
188
|
+
a = np.digitize(a[:, 0], bins=[-1, 0, 1, 2, 3])
|
|
189
|
+
|
|
182
190
|
if objective == "binary":
|
|
183
191
|
y = (y > 0).astype(int)
|
|
184
192
|
|
|
185
|
-
|
|
193
|
+
anchor_booster1 = AnchorBooster(
|
|
186
194
|
gamma=1,
|
|
187
|
-
num_boost_round=
|
|
195
|
+
num_boost_round=10,
|
|
188
196
|
objective=objective,
|
|
189
197
|
learning_rate=0.1,
|
|
190
198
|
).fit(X, y, Z=a)
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
predictions1 = anchor_booster1.predict(X)
|
|
201
|
+
|
|
202
|
+
M = scipy.sparse.csr_matrix(
|
|
203
|
+
(np.ones(len(a), dtype=np.float64), (np.arange(len(a)), a)),
|
|
204
|
+
shape=(len(a), len(np.unique(a))),
|
|
205
|
+
)
|
|
206
|
+
anchor_booster2 = AnchorBooster(
|
|
207
|
+
gamma=1,
|
|
208
|
+
num_boost_round=10,
|
|
209
|
+
objective=objective,
|
|
210
|
+
learning_rate=0.1,
|
|
211
|
+
).fit(X, y, Z=M.toarray())
|
|
212
|
+
predictions2 = anchor_booster2.predict(X)
|
|
213
|
+
np.testing.assert_allclose(predictions1, predictions2, rtol=1e-5)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
import scipy.sparse
|
|
4
|
+
|
|
5
|
+
from anchorboosting.models import Proj
|
|
6
|
+
|
|
7
|
+
cases = [
|
|
8
|
+
(
|
|
9
|
+
np.array([1, 1, 3]),
|
|
10
|
+
np.array([1.0, 2.0, 3.0]),
|
|
11
|
+
np.array([1.5, 1.5, 3.0]),
|
|
12
|
+
),
|
|
13
|
+
(
|
|
14
|
+
np.array([[0.0], [0.0], [1.0]]),
|
|
15
|
+
np.array([1.0, 2.0, 3.0]),
|
|
16
|
+
np.array([0.0, 0.0, 3.0]),
|
|
17
|
+
),
|
|
18
|
+
(
|
|
19
|
+
np.array([[0.0, 1.0], [0.0, 1.0], [1.0, 0.0]]),
|
|
20
|
+
np.array([1.0, 2.0, 3.0]),
|
|
21
|
+
np.array([1.5, 1.5, 3]),
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
np.array([2, 2, 1, 7]),
|
|
25
|
+
np.array([1.0, 2.0, 3.0, 1.0]),
|
|
26
|
+
np.array([1.5, 1.5, 3.0, 1.0]),
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
np.array([1, 1, 0]),
|
|
30
|
+
np.array([1.0, 0.0, 3.0]),
|
|
31
|
+
np.array([0.5, 0.5, 3]),
|
|
32
|
+
),
|
|
33
|
+
(np.array([0]), np.array([1.0]), np.array([1.0])),
|
|
34
|
+
(np.array([[0.0]]), np.array([0.0]), np.array([0.0])),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("Z, f, result", cases)
|
|
39
|
+
def test_cached_proj_result(Z, f, result):
|
|
40
|
+
np.testing.assert_almost_equal(Proj(Z)(f), result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize("Z, f, _", cases)
|
|
44
|
+
def test_cached_proj_dot_product(Z, f, _):
|
|
45
|
+
np.testing.assert_almost_equal(
|
|
46
|
+
np.dot(Proj(Z)(f).T, f),
|
|
47
|
+
np.dot(Proj(Z)(f).T, Proj(Z)(f)),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_proj_sandwich():
|
|
52
|
+
n = 100
|
|
53
|
+
num_categories = 10
|
|
54
|
+
num_leaves = 5
|
|
55
|
+
rng = np.random.RandomState(0)
|
|
56
|
+
|
|
57
|
+
Z = rng.choice(np.arange(num_categories), size=n, replace=True)
|
|
58
|
+
leaves = rng.choice(np.arange(num_leaves), size=n, replace=True)
|
|
59
|
+
f = rng.normal(size=n)
|
|
60
|
+
|
|
61
|
+
M = scipy.sparse.csr_matrix((f, (np.arange(n), leaves)), (n, num_leaves)).toarray()
|
|
62
|
+
proj = Proj(Z)
|
|
63
|
+
|
|
64
|
+
sandwich1 = proj.sandwich(leaves, num_leaves, f)
|
|
65
|
+
for ldx in range(num_leaves):
|
|
66
|
+
M[:, ldx] = proj(M[:, ldx])
|
|
67
|
+
np.testing.assert_almost_equal(sandwich1, M.T @ M)
|
|
@@ -1,84 +0,0 @@
|
|
|
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])
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pytest
|
|
3
|
-
from scipy.linalg import norm
|
|
4
|
-
from sklearn.preprocessing import OneHotEncoder
|
|
5
|
-
|
|
6
|
-
from anchorboosting.utils import proj
|
|
7
|
-
|
|
8
|
-
cases = [
|
|
9
|
-
(
|
|
10
|
-
np.array([[0], [0], [1]]),
|
|
11
|
-
np.array([[1.0], [2.0], [3.0]]),
|
|
12
|
-
np.array([[1.5], [1.5], [3]]),
|
|
13
|
-
),
|
|
14
|
-
(
|
|
15
|
-
np.array([[2], [2], [1], [3]]),
|
|
16
|
-
np.array([[1.0], [2.0], [3.0], [1.0]]),
|
|
17
|
-
np.array([[1.5], [1.5], [3], [1]]),
|
|
18
|
-
),
|
|
19
|
-
(
|
|
20
|
-
np.array([[1], [1], [0]]),
|
|
21
|
-
np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
|
|
22
|
-
np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
|
|
23
|
-
),
|
|
24
|
-
(
|
|
25
|
-
np.array([[1], [1], [0]]),
|
|
26
|
-
np.array([1.0, 0.0, 3.0]),
|
|
27
|
-
np.array([0.5, 0.5, 3]),
|
|
28
|
-
),
|
|
29
|
-
(
|
|
30
|
-
np.array([[1], [1], [0]]),
|
|
31
|
-
np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
|
|
32
|
-
np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
|
|
33
|
-
),
|
|
34
|
-
(
|
|
35
|
-
np.array([[2], [2], [1]]),
|
|
36
|
-
np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
|
|
37
|
-
np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
|
|
38
|
-
),
|
|
39
|
-
(np.array([[0]]), np.array([[1]]), np.array([[1]])),
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.mark.parametrize("anchor, residuals, result", cases)
|
|
44
|
-
@pytest.mark.parametrize("n_categories", [4, None])
|
|
45
|
-
def test_proj_result(n_categories, anchor, residuals, result):
|
|
46
|
-
if n_categories is None:
|
|
47
|
-
if anchor.shape[1] != 1:
|
|
48
|
-
pytest.skip("one_hot only makes sense for 1D anchors")
|
|
49
|
-
encoder = OneHotEncoder(sparse_output=False, drop=None)
|
|
50
|
-
anchor = encoder.fit_transform(anchor).astype(float)
|
|
51
|
-
else:
|
|
52
|
-
anchor = anchor.flatten()
|
|
53
|
-
|
|
54
|
-
np.testing.assert_almost_equal(
|
|
55
|
-
proj(anchor, residuals.copy(), n_categories=n_categories), result
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@pytest.mark.parametrize("anchor, residuals, _", cases)
|
|
60
|
-
def test_proj_dot_product(anchor, residuals, _):
|
|
61
|
-
np.testing.assert_almost_equal(
|
|
62
|
-
np.dot(proj(anchor, residuals).T, residuals),
|
|
63
|
-
np.dot(proj(anchor, residuals).T, proj(anchor, residuals)),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@pytest.mark.parametrize("anchor, residuals, result", cases)
|
|
68
|
-
@pytest.mark.parametrize("gamma", [0.1, 1, 2, 10, 100, 1000])
|
|
69
|
-
def test_proj_orthogonal(anchor, residuals, result, gamma):
|
|
70
|
-
np.testing.assert_almost_equal(
|
|
71
|
-
norm(residuals - proj(anchor, residuals)) ** 2
|
|
72
|
-
+ gamma * norm(proj(anchor, residuals)) ** 2,
|
|
73
|
-
norm(residuals) ** 2 + (gamma - 1) * norm(proj(anchor, residuals)) ** 2,
|
|
74
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|