vectorphile 0.1.0__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.
Files changed (53) hide show
  1. vectorphile/__init__.py +154 -0
  2. vectorphile/amp/__init__.py +52 -0
  3. vectorphile/autograd/__init__.py +13 -0
  4. vectorphile/autograd/engine.py +208 -0
  5. vectorphile/core/__init__.py +21 -0
  6. vectorphile/core/tensor.py +696 -0
  7. vectorphile/data/__init__.py +23 -0
  8. vectorphile/data/frame.py +537 -0
  9. vectorphile/data/groupby.py +98 -0
  10. vectorphile/data/index.py +86 -0
  11. vectorphile/data/series.py +228 -0
  12. vectorphile/fft/__init__.py +34 -0
  13. vectorphile/linalg/__init__.py +63 -0
  14. vectorphile/ml/__init__.py +44 -0
  15. vectorphile/ml/boost/__init__.py +133 -0
  16. vectorphile/ml/cluster.py +218 -0
  17. vectorphile/ml/decomposition.py +161 -0
  18. vectorphile/ml/ensemble.py +209 -0
  19. vectorphile/ml/linear.py +250 -0
  20. vectorphile/ml/metrics.py +148 -0
  21. vectorphile/ml/model_selection.py +171 -0
  22. vectorphile/ml/naive_bayes.py +113 -0
  23. vectorphile/ml/neighbors.py +95 -0
  24. vectorphile/ml/preprocessing.py +202 -0
  25. vectorphile/ml/svm.py +165 -0
  26. vectorphile/ml/tree.py +155 -0
  27. vectorphile/nn/__init__.py +20 -0
  28. vectorphile/nn/activations.py +97 -0
  29. vectorphile/nn/layers.py +231 -0
  30. vectorphile/nn/losses.py +172 -0
  31. vectorphile/nn/module.py +97 -0
  32. vectorphile/nn/sequential.py +47 -0
  33. vectorphile/optim/__init__.py +12 -0
  34. vectorphile/optim/adagrad.py +31 -0
  35. vectorphile/optim/adam.py +71 -0
  36. vectorphile/optim/lr_scheduler.py +110 -0
  37. vectorphile/optim/optimizer.py +30 -0
  38. vectorphile/optim/rmsprop.py +34 -0
  39. vectorphile/optim/sgd.py +38 -0
  40. vectorphile/optimize/__init__.py +97 -0
  41. vectorphile/random/__init__.py +64 -0
  42. vectorphile/signal/__init__.py +67 -0
  43. vectorphile/sparse/__init__.py +129 -0
  44. vectorphile/stats/__init__.py +90 -0
  45. vectorphile/utils/__init__.py +3 -0
  46. vectorphile/utils/data/__init__.py +4 -0
  47. vectorphile/utils/data/dataloader.py +47 -0
  48. vectorphile/utils/data/dataset.py +29 -0
  49. vectorphile-0.1.0.dist-info/METADATA +208 -0
  50. vectorphile-0.1.0.dist-info/RECORD +53 -0
  51. vectorphile-0.1.0.dist-info/WHEEL +5 -0
  52. vectorphile-0.1.0.dist-info/licenses/LICENSE +201 -0
  53. vectorphile-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,218 @@
1
+ """Clustering algorithms."""
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+
6
+ class KMeans:
7
+ def __init__(self, n_clusters=8, max_iter=300, tol=1e-4, n_init=10, random_state=None):
8
+ self.n_clusters = n_clusters
9
+ self.max_iter = max_iter
10
+ self.tol = tol
11
+ self.n_init = n_init
12
+ self.random_state = random_state
13
+ self.cluster_centers_: Optional[np.ndarray] = None
14
+ self.labels_: Optional[np.ndarray] = None
15
+ self.inertia_: float = np.inf
16
+
17
+ def _run_once(self, X, seed):
18
+ rng = np.random.RandomState(seed)
19
+ idx = rng.choice(len(X), self.n_clusters, replace=False)
20
+ centers = X[idx].copy()
21
+ for _ in range(self.max_iter):
22
+ dists = np.linalg.norm(X[:, None] - centers[None], axis=2)
23
+ labels = dists.argmin(axis=1)
24
+ new_centers = np.array([X[labels == k].mean(axis=0) if (labels == k).any() else centers[k]
25
+ for k in range(self.n_clusters)])
26
+ if np.max(np.abs(new_centers - centers)) < self.tol:
27
+ break
28
+ centers = new_centers
29
+ inertia = sum(np.sum((X[labels == k] - centers[k])**2) for k in range(self.n_clusters))
30
+ return centers, labels, float(inertia)
31
+
32
+ def fit(self, X) -> 'KMeans':
33
+ X = np.array(X, dtype=float)
34
+ if self.random_state is not None:
35
+ np.random.seed(self.random_state)
36
+ best_inertia = np.inf
37
+ for i in range(self.n_init):
38
+ centers, labels, inertia = self._run_once(X, seed=i)
39
+ if inertia < best_inertia:
40
+ best_inertia = inertia
41
+ self.cluster_centers_ = centers
42
+ self.labels_ = labels
43
+ self.inertia_ = inertia
44
+ return self
45
+
46
+ def predict(self, X) -> np.ndarray:
47
+ X = np.array(X, dtype=float)
48
+ dists = np.linalg.norm(X[:, None] - self.cluster_centers_[None], axis=2)
49
+ return dists.argmin(axis=1)
50
+
51
+ def fit_predict(self, X) -> np.ndarray:
52
+ self.fit(X)
53
+ return self.labels_
54
+
55
+ def score(self, X) -> float:
56
+ return -self.inertia_
57
+
58
+
59
+ class DBSCAN:
60
+ def __init__(self, eps=0.5, min_samples=5, metric='euclidean'):
61
+ self.eps = eps
62
+ self.min_samples = min_samples
63
+ self.metric = metric
64
+ self.labels_: Optional[np.ndarray] = None
65
+ self.core_sample_indices_: Optional[np.ndarray] = None
66
+
67
+ def fit(self, X) -> 'DBSCAN':
68
+ X = np.array(X, dtype=float)
69
+ n = len(X)
70
+ dists = np.linalg.norm(X[:, None] - X[None], axis=2)
71
+ labels = np.full(n, -1, dtype=int)
72
+ visited = np.zeros(n, dtype=bool)
73
+ cluster_id = 0
74
+ neighbors = [np.where(dists[i] <= self.eps)[0] for i in range(n)]
75
+ core = np.array([i for i in range(n) if len(neighbors[i]) >= self.min_samples])
76
+ self.core_sample_indices_ = core
77
+
78
+ def expand(idx, cid):
79
+ queue = list(neighbors[idx])
80
+ while queue:
81
+ q = queue.pop()
82
+ if not visited[q]:
83
+ visited[q] = True
84
+ if len(neighbors[q]) >= self.min_samples:
85
+ for nb in neighbors[q]:
86
+ if not visited[nb]:
87
+ queue.append(nb)
88
+ if labels[q] == -1:
89
+ labels[q] = cid
90
+
91
+ for i in core:
92
+ if not visited[i]:
93
+ visited[i] = True
94
+ labels[i] = cluster_id
95
+ expand(i, cluster_id)
96
+ cluster_id += 1
97
+ self.labels_ = labels
98
+ return self
99
+
100
+ def fit_predict(self, X) -> np.ndarray:
101
+ self.fit(X)
102
+ return self.labels_
103
+
104
+
105
+ class AgglomerativeClustering:
106
+ def __init__(self, n_clusters=2, linkage='ward'):
107
+ self.n_clusters = n_clusters
108
+ self.linkage = linkage
109
+ self.labels_: Optional[np.ndarray] = None
110
+
111
+ def fit(self, X) -> 'AgglomerativeClustering':
112
+ X = np.array(X, dtype=float)
113
+ n = len(X)
114
+ clusters = [{i} for i in range(n)]
115
+
116
+ def dist(c1, c2):
117
+ pts1 = X[list(c1)]
118
+ pts2 = X[list(c2)]
119
+ if self.linkage == 'ward':
120
+ merged = np.vstack([pts1, pts2])
121
+ return len(merged) * np.sum((merged.mean(0) - np.vstack([
122
+ np.tile(pts1.mean(0), (len(pts1),1)),
123
+ np.tile(pts2.mean(0), (len(pts2),1))
124
+ ]))**2)
125
+ d = np.linalg.norm(pts1[:, None] - pts2[None], axis=2)
126
+ if self.linkage == 'single':
127
+ return d.min()
128
+ if self.linkage == 'complete':
129
+ return d.max()
130
+ return d.mean()
131
+
132
+ while len(clusters) > self.n_clusters:
133
+ best_d, merge_i, merge_j = np.inf, 0, 1
134
+ for i in range(len(clusters)):
135
+ for j in range(i+1, len(clusters)):
136
+ d = dist(clusters[i], clusters[j])
137
+ if d < best_d:
138
+ best_d, merge_i, merge_j = d, i, j
139
+ clusters[merge_i] = clusters[merge_i] | clusters[merge_j]
140
+ clusters.pop(merge_j)
141
+
142
+ self.labels_ = np.zeros(n, dtype=int)
143
+ for label, cluster in enumerate(clusters):
144
+ for idx in cluster:
145
+ self.labels_[idx] = label
146
+ return self
147
+
148
+ def fit_predict(self, X) -> np.ndarray:
149
+ self.fit(X)
150
+ return self.labels_
151
+
152
+
153
+ class GaussianMixture:
154
+ def __init__(self, n_components=1, max_iter=100, tol=1e-3, random_state=None, covariance_type='full'):
155
+ self.n_components = n_components
156
+ self.max_iter = max_iter
157
+ self.tol = tol
158
+ self.random_state = random_state
159
+ self.covariance_type = covariance_type
160
+ self.weights_: Optional[np.ndarray] = None
161
+ self.means_: Optional[np.ndarray] = None
162
+ self.covariances_: Optional[np.ndarray] = None
163
+
164
+ def _gaussian_pdf(self, X, mean, cov):
165
+ d = X.shape[1]
166
+ diff = X - mean
167
+ try:
168
+ cov_inv = np.linalg.inv(cov + np.eye(d) * 1e-6)
169
+ cov_det = np.linalg.det(cov + np.eye(d) * 1e-6)
170
+ except np.linalg.LinAlgError:
171
+ cov_inv = np.eye(d)
172
+ cov_det = 1.0
173
+ exponent = -0.5 * np.einsum('ni,ij,nj->n', diff, cov_inv, diff)
174
+ return np.exp(exponent) / (np.sqrt((2*np.pi)**d * np.abs(cov_det)) + 1e-300)
175
+
176
+ def fit(self, X) -> 'GaussianMixture':
177
+ if self.random_state is not None:
178
+ np.random.seed(self.random_state)
179
+ X = np.array(X, dtype=float)
180
+ n, d = X.shape
181
+ K = self.n_components
182
+ idx = np.random.choice(n, K, replace=False)
183
+ self.means_ = X[idx].copy()
184
+ self.covariances_ = np.array([np.eye(d) for _ in range(K)])
185
+ self.weights_ = np.ones(K) / K
186
+ for _ in range(self.max_iter):
187
+ # E-step
188
+ resp = np.array([self.weights_[k] * self._gaussian_pdf(X, self.means_[k], self.covariances_[k]) for k in range(K)]).T
189
+ resp_sum = resp.sum(axis=1, keepdims=True)
190
+ resp /= (resp_sum + 1e-300)
191
+ # M-step
192
+ Nk = resp.sum(axis=0)
193
+ new_means = (resp.T @ X) / (Nk[:, None] + 1e-300)
194
+ new_covs = []
195
+ for k in range(K):
196
+ diff = X - new_means[k]
197
+ new_covs.append((resp[:, k:k+1] * diff).T @ diff / (Nk[k] + 1e-300))
198
+ old_means = self.means_.copy()
199
+ self.means_ = new_means
200
+ self.covariances_ = np.array(new_covs)
201
+ self.weights_ = Nk / n
202
+ if np.max(np.abs(self.means_ - old_means)) < self.tol:
203
+ break
204
+ return self
205
+
206
+ def predict(self, X) -> np.ndarray:
207
+ return self.predict_proba(X).argmax(axis=1)
208
+
209
+ def predict_proba(self, X) -> np.ndarray:
210
+ X = np.array(X, dtype=float)
211
+ K = self.n_components
212
+ resp = np.array([self.weights_[k] * self._gaussian_pdf(X, self.means_[k], self.covariances_[k]) for k in range(K)]).T
213
+ resp_sum = resp.sum(axis=1, keepdims=True)
214
+ return resp / (resp_sum + 1e-300)
215
+
216
+ def fit_predict(self, X) -> np.ndarray:
217
+ self.fit(X)
218
+ return self.predict(X)
@@ -0,0 +1,161 @@
1
+ """Dimensionality reduction."""
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+
6
+ class PCA:
7
+ def __init__(self, n_components=None, whiten=False, random_state=None):
8
+ self.n_components = n_components
9
+ self.whiten = whiten
10
+ self.random_state = random_state
11
+ self.components_: Optional[np.ndarray] = None
12
+ self.explained_variance_: Optional[np.ndarray] = None
13
+ self.explained_variance_ratio_: Optional[np.ndarray] = None
14
+ self.mean_: Optional[np.ndarray] = None
15
+ self.singular_values_: Optional[np.ndarray] = None
16
+
17
+ def fit(self, X) -> 'PCA':
18
+ X = np.array(X, dtype=float)
19
+ self.mean_ = X.mean(axis=0)
20
+ Xc = X - self.mean_
21
+ U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
22
+ n = len(X)
23
+ k = self.n_components or Vt.shape[0]
24
+ self.components_ = Vt[:k]
25
+ self.singular_values_ = S[:k]
26
+ self.explained_variance_ = S[:k]**2 / (n - 1)
27
+ self.explained_variance_ratio_ = self.explained_variance_ / (S**2 / (n - 1)).sum()
28
+ return self
29
+
30
+ def transform(self, X) -> np.ndarray:
31
+ X = np.array(X, dtype=float)
32
+ Xc = X - self.mean_
33
+ result = Xc @ self.components_.T
34
+ if self.whiten:
35
+ result /= np.sqrt(self.explained_variance_) + 1e-12
36
+ return result
37
+
38
+ def fit_transform(self, X) -> np.ndarray:
39
+ self.fit(X)
40
+ return self.transform(X)
41
+
42
+ def inverse_transform(self, X) -> np.ndarray:
43
+ X = np.array(X, dtype=float)
44
+ if self.whiten:
45
+ X = X * np.sqrt(self.explained_variance_)
46
+ return X @ self.components_ + self.mean_
47
+
48
+
49
+ class TruncatedSVD:
50
+ def __init__(self, n_components=2, random_state=None):
51
+ self.n_components = n_components
52
+ self.random_state = random_state
53
+ self.components_: Optional[np.ndarray] = None
54
+ self.explained_variance_: Optional[np.ndarray] = None
55
+ self.explained_variance_ratio_: Optional[np.ndarray] = None
56
+ self.singular_values_: Optional[np.ndarray] = None
57
+
58
+ def fit(self, X) -> 'TruncatedSVD':
59
+ X = np.array(X, dtype=float)
60
+ U, S, Vt = np.linalg.svd(X, full_matrices=False)
61
+ k = self.n_components
62
+ self.components_ = Vt[:k]
63
+ self.singular_values_ = S[:k]
64
+ total = (S**2).sum()
65
+ self.explained_variance_ = S[:k]**2 / len(X)
66
+ self.explained_variance_ratio_ = S[:k]**2 / (total + 1e-12)
67
+ return self
68
+
69
+ def transform(self, X) -> np.ndarray:
70
+ return np.array(X, dtype=float) @ self.components_.T
71
+
72
+ def fit_transform(self, X) -> np.ndarray:
73
+ self.fit(X)
74
+ return self.transform(X)
75
+
76
+
77
+ class NMF:
78
+ def __init__(self, n_components=2, max_iter=200, tol=1e-4, random_state=None):
79
+ self.n_components = n_components
80
+ self.max_iter = max_iter
81
+ self.tol = tol
82
+ self.random_state = random_state
83
+ self.components_: Optional[np.ndarray] = None
84
+ self.reconstruction_err_: float = np.inf
85
+
86
+ def fit_transform(self, X) -> np.ndarray:
87
+ if self.random_state is not None:
88
+ np.random.seed(self.random_state)
89
+ X = np.array(X, dtype=float)
90
+ X = np.maximum(X, 0)
91
+ n, m = X.shape
92
+ k = self.n_components
93
+ W = np.random.rand(n, k) + 0.1
94
+ H = np.random.rand(k, m) + 0.1
95
+ for _ in range(self.max_iter):
96
+ WH = W @ H + 1e-10
97
+ H *= (W.T @ X) / (W.T @ WH)
98
+ WH = W @ H + 1e-10
99
+ W *= (X @ H.T) / (WH @ H.T)
100
+ self.components_ = H
101
+ self.reconstruction_err_ = float(np.linalg.norm(X - W @ H))
102
+ return W
103
+
104
+ def fit(self, X) -> 'NMF':
105
+ self.fit_transform(X)
106
+ return self
107
+
108
+ def transform(self, X) -> np.ndarray:
109
+ X = np.maximum(np.array(X, dtype=float), 0)
110
+ H = self.components_
111
+ W, _ = np.linalg.lstsq(H.T, X.T, rcond=None)
112
+ return np.maximum(W.T, 0)
113
+
114
+
115
+ class FastICA:
116
+ def __init__(self, n_components=None, max_iter=200, tol=1e-4, fun='logcosh', random_state=None):
117
+ self.n_components = n_components
118
+ self.max_iter = max_iter
119
+ self.tol = tol
120
+ self.fun = fun
121
+ self.random_state = random_state
122
+ self.components_: Optional[np.ndarray] = None
123
+ self.mean_: Optional[np.ndarray] = None
124
+
125
+ def _g(self, x):
126
+ if self.fun == 'logcosh':
127
+ return np.tanh(x), (1 - np.tanh(x)**2).mean(axis=-1)
128
+ return x * np.exp(-x**2 / 2), (1 - x**2) * np.exp(-x**2 / 2).mean(axis=-1)
129
+
130
+ def fit_transform(self, X) -> np.ndarray:
131
+ if self.random_state is not None:
132
+ np.random.seed(self.random_state)
133
+ X = np.array(X, dtype=float)
134
+ n, p = X.shape
135
+ self.mean_ = X.mean(axis=0)
136
+ X = X - self.mean_
137
+ _, S, Vt = np.linalg.svd(X, full_matrices=False)
138
+ k = self.n_components or min(n, p)
139
+ X_white = (X @ Vt[:k].T) / (S[:k] / np.sqrt(n) + 1e-10)
140
+ W = np.random.randn(k, k)
141
+ for i in range(self.max_iter):
142
+ WX = W @ X_white.T
143
+ g, g_prime = self._g(WX)
144
+ W_new = (g @ X_white) / n - g_prime[:, None] * W
145
+ # Orthogonalization
146
+ u, _, vt = np.linalg.svd(W_new)
147
+ W_new = u @ vt
148
+ if np.max(np.abs(np.abs(np.diag(W_new @ W.T)) - 1)) < self.tol:
149
+ W = W_new
150
+ break
151
+ W = W_new
152
+ self.components_ = W @ np.diag(1.0 / (S[:k] / np.sqrt(n) + 1e-10)) @ Vt[:k]
153
+ return X_white @ W.T
154
+
155
+ def fit(self, X) -> 'FastICA':
156
+ self.fit_transform(X)
157
+ return self
158
+
159
+ def transform(self, X) -> np.ndarray:
160
+ X = np.array(X, dtype=float) - self.mean_
161
+ return X @ self.components_.T
@@ -0,0 +1,209 @@
1
+ """Ensemble methods: Random Forest, Gradient Boosting."""
2
+ import numpy as np
3
+ from typing import Optional
4
+ from vectorphile.ml.tree import DecisionTreeClassifier, DecisionTreeRegressor
5
+
6
+
7
+ class RandomForestClassifier:
8
+ def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2,
9
+ min_samples_leaf=1, max_features='sqrt', bootstrap=True, random_state=None):
10
+ self.n_estimators = n_estimators
11
+ self.max_depth = max_depth
12
+ self.min_samples_split = min_samples_split
13
+ self.min_samples_leaf = min_samples_leaf
14
+ self.max_features = max_features
15
+ self.bootstrap = bootstrap
16
+ self.random_state = random_state
17
+ self.estimators_ = []
18
+ self.classes_: Optional[np.ndarray] = None
19
+
20
+ def fit(self, X, y) -> 'RandomForestClassifier':
21
+ if self.random_state is not None:
22
+ np.random.seed(self.random_state)
23
+ X, y = np.array(X, dtype=float), np.array(y)
24
+ self.classes_ = np.unique(y)
25
+ n = len(X)
26
+ self.estimators_ = []
27
+ for _ in range(self.n_estimators):
28
+ if self.bootstrap:
29
+ idx = np.random.choice(n, n, replace=True)
30
+ Xb, yb = X[idx], y[idx]
31
+ else:
32
+ Xb, yb = X, y
33
+ tree = DecisionTreeClassifier(
34
+ max_depth=self.max_depth,
35
+ min_samples_split=self.min_samples_split,
36
+ min_samples_leaf=self.min_samples_leaf,
37
+ max_features=self.max_features,
38
+ )
39
+ tree.fit(Xb, yb)
40
+ self.estimators_.append(tree)
41
+ return self
42
+
43
+ def predict_proba(self, X) -> np.ndarray:
44
+ X = np.array(X, dtype=float)
45
+ all_proba = np.array([t.predict_proba(X) for t in self.estimators_])
46
+ return all_proba.mean(axis=0)
47
+
48
+ def predict(self, X) -> np.ndarray:
49
+ proba = self.predict_proba(X)
50
+ return self.classes_[np.argmax(proba, axis=1)]
51
+
52
+ def score(self, X, y) -> float:
53
+ return float(np.mean(self.predict(X) == np.array(y)))
54
+
55
+
56
+ class RandomForestRegressor:
57
+ def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2,
58
+ min_samples_leaf=1, max_features='sqrt', bootstrap=True, random_state=None):
59
+ self.n_estimators = n_estimators
60
+ self.max_depth = max_depth
61
+ self.min_samples_split = min_samples_split
62
+ self.min_samples_leaf = min_samples_leaf
63
+ self.max_features = max_features
64
+ self.bootstrap = bootstrap
65
+ self.random_state = random_state
66
+ self.estimators_ = []
67
+
68
+ def fit(self, X, y) -> 'RandomForestRegressor':
69
+ if self.random_state is not None:
70
+ np.random.seed(self.random_state)
71
+ X, y = np.array(X, dtype=float), np.array(y, dtype=float)
72
+ n = len(X)
73
+ self.estimators_ = []
74
+ for _ in range(self.n_estimators):
75
+ if self.bootstrap:
76
+ idx = np.random.choice(n, n, replace=True)
77
+ Xb, yb = X[idx], y[idx]
78
+ else:
79
+ Xb, yb = X, y
80
+ tree = DecisionTreeRegressor(
81
+ max_depth=self.max_depth,
82
+ min_samples_split=self.min_samples_split,
83
+ min_samples_leaf=self.min_samples_leaf,
84
+ max_features=self.max_features,
85
+ )
86
+ tree.fit(Xb, yb)
87
+ self.estimators_.append(tree)
88
+ return self
89
+
90
+ def predict(self, X) -> np.ndarray:
91
+ X = np.array(X, dtype=float)
92
+ preds = np.array([t.predict(X) for t in self.estimators_])
93
+ return preds.mean(axis=0)
94
+
95
+ def score(self, X, y) -> float:
96
+ y = np.array(y, dtype=float)
97
+ pred = self.predict(X)
98
+ ss_res = np.sum((y - pred)**2)
99
+ ss_tot = np.sum((y - y.mean())**2)
100
+ return float(1 - ss_res / (ss_tot + 1e-12))
101
+
102
+
103
+ class GradientBoostingRegressor:
104
+ def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3,
105
+ min_samples_split=2, subsample=1.0, loss='mse', random_state=None):
106
+ self.n_estimators = n_estimators
107
+ self.learning_rate = learning_rate
108
+ self.max_depth = max_depth
109
+ self.min_samples_split = min_samples_split
110
+ self.subsample = subsample
111
+ self.loss = loss
112
+ self.random_state = random_state
113
+ self.estimators_ = []
114
+ self.init_pred_: float = 0.0
115
+
116
+ def fit(self, X, y) -> 'GradientBoostingRegressor':
117
+ if self.random_state is not None:
118
+ np.random.seed(self.random_state)
119
+ X, y = np.array(X, dtype=float), np.array(y, dtype=float)
120
+ self.init_pred_ = float(y.mean())
121
+ F = np.full(len(y), self.init_pred_)
122
+ self.estimators_ = []
123
+ n = len(X)
124
+ for _ in range(self.n_estimators):
125
+ residuals = y - F
126
+ if self.subsample < 1.0:
127
+ idx = np.random.choice(n, int(n * self.subsample), replace=False)
128
+ Xs, rs = X[idx], residuals[idx]
129
+ else:
130
+ Xs, rs = X, residuals
131
+ tree = DecisionTreeRegressor(max_depth=self.max_depth, min_samples_split=self.min_samples_split)
132
+ tree.fit(Xs, rs)
133
+ update = tree.predict(X)
134
+ F += self.learning_rate * update
135
+ self.estimators_.append(tree)
136
+ return self
137
+
138
+ def predict(self, X) -> np.ndarray:
139
+ X = np.array(X, dtype=float)
140
+ F = np.full(len(X), self.init_pred_)
141
+ for tree in self.estimators_:
142
+ F += self.learning_rate * tree.predict(X)
143
+ return F
144
+
145
+ def score(self, X, y) -> float:
146
+ y = np.array(y, dtype=float)
147
+ pred = self.predict(X)
148
+ ss_res = np.sum((y - pred)**2)
149
+ ss_tot = np.sum((y - y.mean())**2)
150
+ return float(1 - ss_res / (ss_tot + 1e-12))
151
+
152
+
153
+ class GradientBoostingClassifier:
154
+ def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3,
155
+ min_samples_split=2, subsample=1.0, random_state=None):
156
+ self.n_estimators = n_estimators
157
+ self.learning_rate = learning_rate
158
+ self.max_depth = max_depth
159
+ self.min_samples_split = min_samples_split
160
+ self.subsample = subsample
161
+ self.random_state = random_state
162
+ self.estimators_ = []
163
+ self.classes_: Optional[np.ndarray] = None
164
+ self.init_pred_: Optional[np.ndarray] = None
165
+
166
+ def _softmax(self, z):
167
+ z = z - z.max(axis=1, keepdims=True)
168
+ e = np.exp(z)
169
+ return e / e.sum(axis=1, keepdims=True)
170
+
171
+ def fit(self, X, y) -> 'GradientBoostingClassifier':
172
+ if self.random_state is not None:
173
+ np.random.seed(self.random_state)
174
+ X, y = np.array(X, dtype=float), np.array(y)
175
+ self.classes_ = np.unique(y)
176
+ K = len(self.classes_)
177
+ n = len(X)
178
+ y_ohe = np.zeros((n, K))
179
+ for i, c in enumerate(self.classes_):
180
+ y_ohe[:, i] = (y == c).astype(float)
181
+ F = np.zeros((n, K))
182
+ self.init_pred_ = np.log(y_ohe.mean(axis=0) + 1e-12)
183
+ F += self.init_pred_
184
+ self.estimators_ = []
185
+ for _ in range(self.n_estimators):
186
+ probs = self._softmax(F)
187
+ self.estimators_.append([])
188
+ for k in range(K):
189
+ residuals = y_ohe[:, k] - probs[:, k]
190
+ tree = DecisionTreeRegressor(max_depth=self.max_depth, min_samples_split=self.min_samples_split)
191
+ tree.fit(X, residuals)
192
+ F[:, k] += self.learning_rate * tree.predict(X)
193
+ self.estimators_[-1].append(tree)
194
+ return self
195
+
196
+ def predict_proba(self, X) -> np.ndarray:
197
+ X = np.array(X, dtype=float)
198
+ F = np.tile(self.init_pred_, (len(X), 1)).copy()
199
+ for group in self.estimators_:
200
+ for k, tree in enumerate(group):
201
+ F[:, k] += self.learning_rate * tree.predict(X)
202
+ return self._softmax(F)
203
+
204
+ def predict(self, X) -> np.ndarray:
205
+ proba = self.predict_proba(X)
206
+ return self.classes_[np.argmax(proba, axis=1)]
207
+
208
+ def score(self, X, y) -> float:
209
+ return float(np.mean(self.predict(X) == np.array(y)))