linearrf 1.2.0__tar.gz → 1.2.2__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.
- {linearrf-1.2.0/src/linearrf.egg-info → linearrf-1.2.2}/PKG-INFO +1 -1
- {linearrf-1.2.0 → linearrf-1.2.2}/pyproject.toml +1 -1
- {linearrf-1.2.0 → linearrf-1.2.2/src/linearrf.egg-info}/PKG-INFO +1 -1
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/_base_lrf.py +1 -1
- linearrf-1.2.2/src/lrf/_irls.py +112 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/_node.py +2 -2
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/lrf.py +2 -2
- linearrf-1.2.0/src/lrf/_irls.py +0 -66
- {linearrf-1.2.0 → linearrf-1.2.2}/LICENSE.md +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/MANIFEST.in +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/README.md +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/setup.cfg +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/linearrf.egg-info/SOURCES.txt +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/linearrf.egg-info/dependency_links.txt +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/linearrf.egg-info/requires.txt +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/linearrf.egg-info/top_level.txt +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/__init__.py +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/_criterion.py +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/_linear_models.py +0 -0
- {linearrf-1.2.0 → linearrf-1.2.2}/src/lrf/_preprocessor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: linearrf
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A python libary to build Random Forests with Linear Models at the leaves.
|
|
5
5
|
Author-email: Marian Biermann <marianbiermann@gmx.de>
|
|
6
6
|
Project-URL: homepage, https://github.com/marianbiermann/lrf
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "linearrf"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "A python libary to build Random Forests with Linear Models at the leaves."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Marian Biermann", email = "marianbiermann@gmx.de" }]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: linearrf
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A python libary to build Random Forests with Linear Models at the leaves.
|
|
5
5
|
Author-email: Marian Biermann <marianbiermann@gmx.de>
|
|
6
6
|
Project-URL: homepage, https://github.com/marianbiermann/lrf
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IRLS:
|
|
5
|
+
"""Iteratively Reweighted Least Squares for L2-regularized logistic regression.
|
|
6
|
+
|
|
7
|
+
Each iteration is a single Newton step using the closed-form Hessian for
|
|
8
|
+
the binary log-loss: H = XᵀWX + λR, with W = diag(p(1-p)). Typically
|
|
9
|
+
converges in 5-15 iterations from a zero start and 2-5 iterations from a
|
|
10
|
+
warm start, versus the previous BFGS implementation which could take
|
|
11
|
+
hundreds of function evaluations per fit.
|
|
12
|
+
|
|
13
|
+
Each Newton step is guarded by Armijo backtracking so the loss is
|
|
14
|
+
monotonically non-increasing — protects against overshoot when the warm
|
|
15
|
+
start lands in an ill-conditioned region (saturated probabilities make
|
|
16
|
+
XᵀWX nearly singular on the data side and Newton's quadratic model
|
|
17
|
+
misleads).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
n_iter: maximum Newton iterations.
|
|
21
|
+
tol: stop when ||t·step|| / max(||β||, 1) < tol.
|
|
22
|
+
intercept: whether x[:, 0] is the intercept column. The intercept is
|
|
23
|
+
excluded from L2 regularization.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Textbook Armijo constant. _backtrack_max = 20 → smallest accepted step
|
|
27
|
+
# is 2**-20 of full Newton; below that we treat the iter as a failure.
|
|
28
|
+
_armijo_c1 = 1e-4
|
|
29
|
+
_backtrack_max = 20
|
|
30
|
+
|
|
31
|
+
def __init__(self, n_iter: int = 25, tol: float = 1e-6, intercept: bool = True):
|
|
32
|
+
self.n_iter = n_iter
|
|
33
|
+
self.tol = tol
|
|
34
|
+
self.intercept = intercept
|
|
35
|
+
|
|
36
|
+
def classification(self, x: np.ndarray, y_true: np.ndarray, coef_: np.ndarray, C: float = 1.0):
|
|
37
|
+
# λ = 1/C, matching the sklearn convention. The intercept term sits at
|
|
38
|
+
# position 0 and stays unregularized.
|
|
39
|
+
lam = 1.0 / C
|
|
40
|
+
m = x.shape[1]
|
|
41
|
+
|
|
42
|
+
reg_diag = np.full(m, lam)
|
|
43
|
+
if self.intercept:
|
|
44
|
+
reg_diag[0] = 0.0
|
|
45
|
+
|
|
46
|
+
beta = coef_.astype(np.float64, copy=True)
|
|
47
|
+
y = y_true.astype(np.float64, copy=False)
|
|
48
|
+
|
|
49
|
+
loss = self._loss(x, y, beta, reg_diag)
|
|
50
|
+
|
|
51
|
+
for _ in range(self.n_iter):
|
|
52
|
+
p = self._sigmoid(x @ beta)
|
|
53
|
+
|
|
54
|
+
# Hessian weight; clipped so saturated probabilities don't collapse
|
|
55
|
+
# XᵀWX to singular.
|
|
56
|
+
w = np.clip(p * (1.0 - p), 1e-12, None)
|
|
57
|
+
|
|
58
|
+
grad = x.T @ (p - y) + reg_diag * beta
|
|
59
|
+
|
|
60
|
+
xtwx = (x * w[:, np.newaxis]).T @ x
|
|
61
|
+
# add λR on the diagonal in-place
|
|
62
|
+
xtwx.flat[::m + 1] += reg_diag
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
step = np.linalg.solve(xtwx, grad)
|
|
66
|
+
except np.linalg.LinAlgError:
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
# Search direction d = -step; ∇fᵀd = -gᵀstep. Should be negative
|
|
70
|
+
# whenever the Hessian is SPD and grad ≠ 0. A non-negative value
|
|
71
|
+
# means the solve produced garbage — bail rather than ascend.
|
|
72
|
+
directional = -(grad @ step)
|
|
73
|
+
if directional >= 0:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
# Armijo backtracking: shrink t until f(β + t·d) ≤ f(β) + c1·t·∇fᵀd.
|
|
77
|
+
t = 1.0
|
|
78
|
+
new_beta = beta
|
|
79
|
+
new_loss = loss
|
|
80
|
+
for _ in range(self._backtrack_max):
|
|
81
|
+
new_beta = beta - t * step
|
|
82
|
+
new_loss = self._loss(x, y, new_beta, reg_diag)
|
|
83
|
+
if new_loss <= loss + self._armijo_c1 * t * directional:
|
|
84
|
+
break
|
|
85
|
+
t *= 0.5
|
|
86
|
+
else:
|
|
87
|
+
# Could not find a step satisfying sufficient decrease; stop.
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
beta = new_beta
|
|
91
|
+
loss = new_loss
|
|
92
|
+
|
|
93
|
+
# use t·step (the step actually taken) so a heavily-backtracked iter
|
|
94
|
+
# is correctly recognized as small progress
|
|
95
|
+
if np.linalg.norm(t * step) / max(np.linalg.norm(beta), 1.0) < self.tol:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
return beta
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _loss(x: np.ndarray, y: np.ndarray, beta: np.ndarray, reg_diag: np.ndarray):
|
|
102
|
+
# L(β) = Σ logaddexp(0, z) - y·z + ½ Σ λ_j β_j², z = Xβ
|
|
103
|
+
# logaddexp form avoids log(p) blowups when p is near 0 or 1.
|
|
104
|
+
z = x @ beta
|
|
105
|
+
nll = np.logaddexp(0.0, z).sum() - y @ z
|
|
106
|
+
reg = 0.5 * (reg_diag * beta).dot(beta)
|
|
107
|
+
return nll + reg
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _sigmoid(z: np.ndarray):
|
|
111
|
+
# σ(z) = exp(-logaddexp(0, -z)) — overflow-safe in both tails.
|
|
112
|
+
return np.exp(-np.logaddexp(0, -z))
|
|
@@ -53,7 +53,7 @@ class LRFRegressor(_LinearRandomForest):
|
|
|
53
53
|
def _predict_tree(self, node: Node, x: np.ndarray, results: List):
|
|
54
54
|
if node.model is None:
|
|
55
55
|
if x.shape[0] > 0:
|
|
56
|
-
left_indices = x[:, node.split_col_idx + 1]
|
|
56
|
+
left_indices = x[:, node.split_col_idx + 1] <= node.threshold
|
|
57
57
|
right_indices = ~left_indices
|
|
58
58
|
|
|
59
59
|
results = self._predict_tree(node.left_node, x[left_indices], results)
|
|
@@ -184,7 +184,7 @@ class LRFClassifier(_LinearRandomForest):
|
|
|
184
184
|
|
|
185
185
|
if node.model is None:
|
|
186
186
|
if x.shape[0] > 0:
|
|
187
|
-
left_indices = x[:, node.split_col_idx + 1]
|
|
187
|
+
left_indices = x[:, node.split_col_idx + 1] <= node.threshold
|
|
188
188
|
right_indices = ~left_indices
|
|
189
189
|
|
|
190
190
|
results = self._predict_proba_tree(node.left_node, x[left_indices], results)
|
linearrf-1.2.0/src/lrf/_irls.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class IRLS:
|
|
5
|
-
"""Iteratively Reweighted Least Squares for L2-regularized logistic regression.
|
|
6
|
-
|
|
7
|
-
Each iteration is a single Newton step using the closed-form Hessian for
|
|
8
|
-
the binary log-loss: H = XᵀWX + λR, with W = diag(p(1-p)). Typically
|
|
9
|
-
converges in 5-15 iterations from a zero start and 2-5 iterations from a
|
|
10
|
-
warm start, versus the previous BFGS implementation which could take
|
|
11
|
-
hundreds of function evaluations per fit.
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
n_iter: maximum Newton iterations.
|
|
15
|
-
tol: stop when ||step|| / max(||β||, 1) < tol.
|
|
16
|
-
intercept: whether x[:, 0] is the intercept column. The intercept is
|
|
17
|
-
excluded from L2 regularization.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self, n_iter: int = 25, tol: float = 1e-6, intercept: bool = True):
|
|
21
|
-
self.n_iter = n_iter
|
|
22
|
-
self.tol = tol
|
|
23
|
-
self.intercept = intercept
|
|
24
|
-
|
|
25
|
-
def classification(self, x: np.ndarray, y_true: np.ndarray, coef_: np.ndarray, C: float = 1.0):
|
|
26
|
-
# λ = 1/C, matching the sklearn convention. The intercept term sits at
|
|
27
|
-
# position 0 and stays unregularized.
|
|
28
|
-
lam = 1.0 / C
|
|
29
|
-
m = x.shape[1]
|
|
30
|
-
|
|
31
|
-
reg_diag = np.full(m, lam)
|
|
32
|
-
if self.intercept:
|
|
33
|
-
reg_diag[0] = 0.0
|
|
34
|
-
|
|
35
|
-
beta = coef_.astype(np.float64, copy=True)
|
|
36
|
-
y = y_true.astype(np.float64, copy=False)
|
|
37
|
-
|
|
38
|
-
for _ in range(self.n_iter):
|
|
39
|
-
p = self._sigmoid(x @ beta)
|
|
40
|
-
|
|
41
|
-
# Hessian weight; clipped so saturated probabilities don't collapse
|
|
42
|
-
# XᵀWX to singular.
|
|
43
|
-
w = np.clip(p * (1.0 - p), 1e-12, None)
|
|
44
|
-
|
|
45
|
-
grad = x.T @ (p - y) + reg_diag * beta
|
|
46
|
-
|
|
47
|
-
xtwx = (x * w[:, np.newaxis]).T @ x
|
|
48
|
-
# add λR on the diagonal in-place
|
|
49
|
-
xtwx.flat[::m + 1] += reg_diag
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
step = np.linalg.solve(xtwx, grad)
|
|
53
|
-
except np.linalg.LinAlgError:
|
|
54
|
-
break
|
|
55
|
-
|
|
56
|
-
beta -= step
|
|
57
|
-
|
|
58
|
-
if np.linalg.norm(step) / max(np.linalg.norm(beta), 1.0) < self.tol:
|
|
59
|
-
break
|
|
60
|
-
|
|
61
|
-
return beta
|
|
62
|
-
|
|
63
|
-
@staticmethod
|
|
64
|
-
def _sigmoid(z: np.ndarray):
|
|
65
|
-
# σ(z) = exp(-logaddexp(0, -z)) — overflow-safe in both tails.
|
|
66
|
-
return np.exp(-np.logaddexp(0, -z))
|
|
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
|