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,95 @@
1
+ """K-Nearest Neighbors."""
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+
6
+ class _BaseKNN:
7
+ def __init__(self, n_neighbors=5, metric='euclidean', weights='uniform'):
8
+ self.n_neighbors = n_neighbors
9
+ self.metric = metric
10
+ self.weights = weights
11
+ self.X_train_: Optional[np.ndarray] = None
12
+ self.y_train_: Optional[np.ndarray] = None
13
+
14
+ def fit(self, X, y) -> '_BaseKNN':
15
+ self.X_train_ = np.array(X, dtype=float)
16
+ self.y_train_ = np.array(y)
17
+ return self
18
+
19
+ def _distances(self, X) -> np.ndarray:
20
+ X = np.array(X, dtype=float)
21
+ if self.metric == 'euclidean':
22
+ return np.linalg.norm(X[:, None] - self.X_train_[None], axis=2)
23
+ if self.metric == 'manhattan':
24
+ return np.abs(X[:, None] - self.X_train_[None]).sum(axis=2)
25
+ return np.linalg.norm(X[:, None] - self.X_train_[None], axis=2)
26
+
27
+ def _get_neighbors(self, X):
28
+ dists = self._distances(X)
29
+ nn_idx = np.argsort(dists, axis=1)[:, :self.n_neighbors]
30
+ nn_dists = np.take_along_axis(dists, nn_idx, axis=1)
31
+ return nn_idx, nn_dists
32
+
33
+
34
+ class KNeighborsClassifier(_BaseKNN):
35
+ def __init__(self, n_neighbors=5, metric='euclidean', weights='uniform'):
36
+ super().__init__(n_neighbors, metric, weights)
37
+ self.classes_: Optional[np.ndarray] = None
38
+
39
+ def fit(self, X, y) -> 'KNeighborsClassifier':
40
+ self.classes_ = np.unique(y)
41
+ return super().fit(X, y)
42
+
43
+ def predict(self, X) -> np.ndarray:
44
+ nn_idx, nn_dists = self._get_neighbors(X)
45
+ nn_labels = self.y_train_[nn_idx]
46
+ result = []
47
+ for i, (labels, dists) in enumerate(zip(nn_labels, nn_dists)):
48
+ if self.weights == 'distance':
49
+ dists = np.where(dists == 0, 1e-10, dists)
50
+ w = 1.0 / dists
51
+ else:
52
+ w = np.ones_like(dists)
53
+ counts = {}
54
+ for label, weight in zip(labels, w):
55
+ counts[label] = counts.get(label, 0) + weight
56
+ result.append(max(counts, key=counts.get))
57
+ return np.array(result)
58
+
59
+ def predict_proba(self, X) -> np.ndarray:
60
+ nn_idx, nn_dists = self._get_neighbors(X)
61
+ nn_labels = self.y_train_[nn_idx]
62
+ result = np.zeros((len(X), len(self.classes_)))
63
+ for i, (labels, dists) in enumerate(zip(nn_labels, nn_dists)):
64
+ if self.weights == 'distance':
65
+ dists = np.where(dists == 0, 1e-10, dists)
66
+ w = 1.0 / dists
67
+ else:
68
+ w = np.ones_like(dists)
69
+ w /= w.sum()
70
+ for label, weight in zip(labels, w):
71
+ class_idx = np.where(self.classes_ == label)[0][0]
72
+ result[i, class_idx] += weight
73
+ return result
74
+
75
+ def score(self, X, y) -> float:
76
+ return float(np.mean(self.predict(X) == np.array(y)))
77
+
78
+
79
+ class KNeighborsRegressor(_BaseKNN):
80
+ def predict(self, X) -> np.ndarray:
81
+ nn_idx, nn_dists = self._get_neighbors(X)
82
+ nn_vals = self.y_train_[nn_idx]
83
+ if self.weights == 'distance':
84
+ nn_dists = np.where(nn_dists == 0, 1e-10, nn_dists)
85
+ w = 1.0 / nn_dists
86
+ w /= w.sum(axis=1, keepdims=True)
87
+ return (nn_vals * w).sum(axis=1)
88
+ return nn_vals.mean(axis=1)
89
+
90
+ def score(self, X, y) -> float:
91
+ y = np.array(y, dtype=float)
92
+ pred = self.predict(X)
93
+ ss_res = np.sum((y - pred)**2)
94
+ ss_tot = np.sum((y - y.mean())**2)
95
+ return float(1 - ss_res / (ss_tot + 1e-12))
@@ -0,0 +1,202 @@
1
+ """Preprocessing utilities."""
2
+ import numpy as np
3
+ from typing import Optional, List
4
+
5
+
6
+ class StandardScaler:
7
+ def __init__(self, copy=True, with_mean=True, with_std=True):
8
+ self.copy = copy
9
+ self.with_mean = with_mean
10
+ self.with_std = with_std
11
+ self.mean_: Optional[np.ndarray] = None
12
+ self.scale_: Optional[np.ndarray] = None
13
+ self.var_: Optional[np.ndarray] = None
14
+
15
+ def fit(self, X) -> 'StandardScaler':
16
+ X = np.array(X, dtype=float)
17
+ self.mean_ = X.mean(axis=0) if self.with_mean else np.zeros(X.shape[1])
18
+ self.var_ = X.var(axis=0) if self.with_std else np.ones(X.shape[1])
19
+ self.scale_ = np.sqrt(self.var_)
20
+ self.scale_ = np.where(self.scale_ == 0, 1.0, self.scale_)
21
+ return self
22
+
23
+ def transform(self, X) -> np.ndarray:
24
+ X = np.array(X, dtype=float)
25
+ if self.copy:
26
+ X = X.copy()
27
+ if self.with_mean:
28
+ X -= self.mean_
29
+ if self.with_std:
30
+ X /= self.scale_
31
+ return X
32
+
33
+ def fit_transform(self, X) -> np.ndarray:
34
+ self.fit(X)
35
+ return self.transform(X)
36
+
37
+ def inverse_transform(self, X) -> np.ndarray:
38
+ X = np.array(X, dtype=float).copy()
39
+ X *= self.scale_
40
+ X += self.mean_
41
+ return X
42
+
43
+
44
+ class MinMaxScaler:
45
+ def __init__(self, feature_range=(0, 1)):
46
+ self.feature_range = feature_range
47
+ self.min_: Optional[np.ndarray] = None
48
+ self.scale_: Optional[np.ndarray] = None
49
+ self.data_min_: Optional[np.ndarray] = None
50
+ self.data_max_: Optional[np.ndarray] = None
51
+
52
+ def fit(self, X) -> 'MinMaxScaler':
53
+ X = np.array(X, dtype=float)
54
+ self.data_min_ = X.min(axis=0)
55
+ self.data_max_ = X.max(axis=0)
56
+ data_range = self.data_max_ - self.data_min_
57
+ data_range = np.where(data_range == 0, 1.0, data_range)
58
+ fmin, fmax = self.feature_range
59
+ self.scale_ = (fmax - fmin) / data_range
60
+ self.min_ = fmin - self.data_min_ * self.scale_
61
+ return self
62
+
63
+ def transform(self, X) -> np.ndarray:
64
+ return np.array(X, dtype=float) * self.scale_ + self.min_
65
+
66
+ def fit_transform(self, X) -> np.ndarray:
67
+ self.fit(X)
68
+ return self.transform(X)
69
+
70
+ def inverse_transform(self, X) -> np.ndarray:
71
+ return (np.array(X, dtype=float) - self.min_) / self.scale_
72
+
73
+
74
+ class RobustScaler:
75
+ def __init__(self, with_centering=True, with_scaling=True, quantile_range=(25.0, 75.0)):
76
+ self.with_centering = with_centering
77
+ self.with_scaling = with_scaling
78
+ self.quantile_range = quantile_range
79
+ self.center_: Optional[np.ndarray] = None
80
+ self.scale_: Optional[np.ndarray] = None
81
+
82
+ def fit(self, X) -> 'RobustScaler':
83
+ X = np.array(X, dtype=float)
84
+ if self.with_centering:
85
+ self.center_ = np.median(X, axis=0)
86
+ q_low, q_high = self.quantile_range
87
+ if self.with_scaling:
88
+ q = np.percentile(X, [q_low, q_high], axis=0)
89
+ iqr = q[1] - q[0]
90
+ self.scale_ = np.where(iqr == 0, 1.0, iqr)
91
+ return self
92
+
93
+ def transform(self, X) -> np.ndarray:
94
+ X = np.array(X, dtype=float).copy()
95
+ if self.with_centering:
96
+ X -= self.center_
97
+ if self.with_scaling:
98
+ X /= self.scale_
99
+ return X
100
+
101
+ def fit_transform(self, X) -> np.ndarray:
102
+ self.fit(X)
103
+ return self.transform(X)
104
+
105
+
106
+ class LabelEncoder:
107
+ def __init__(self):
108
+ self.classes_: Optional[np.ndarray] = None
109
+
110
+ def fit(self, y) -> 'LabelEncoder':
111
+ self.classes_ = np.unique(y)
112
+ return self
113
+
114
+ def transform(self, y) -> np.ndarray:
115
+ y = np.array(y)
116
+ idx = np.searchsorted(self.classes_, y)
117
+ return idx
118
+
119
+ def fit_transform(self, y) -> np.ndarray:
120
+ self.fit(y)
121
+ return self.transform(y)
122
+
123
+ def inverse_transform(self, y) -> np.ndarray:
124
+ return self.classes_[np.array(y)]
125
+
126
+
127
+ class OneHotEncoder:
128
+ def __init__(self, sparse=False, drop=None, handle_unknown='error'):
129
+ self.sparse = sparse
130
+ self.drop = drop
131
+ self.handle_unknown = handle_unknown
132
+ self.categories_: Optional[List[np.ndarray]] = None
133
+
134
+ def fit(self, X) -> 'OneHotEncoder':
135
+ X = np.array(X)
136
+ if X.ndim == 1:
137
+ X = X.reshape(-1, 1)
138
+ self.categories_ = [np.unique(X[:, i]) for i in range(X.shape[1])]
139
+ return self
140
+
141
+ def transform(self, X) -> np.ndarray:
142
+ X = np.array(X)
143
+ if X.ndim == 1:
144
+ X = X.reshape(-1, 1)
145
+ parts = []
146
+ for i, cats in enumerate(self.categories_):
147
+ col = X[:, i]
148
+ ohe = np.zeros((len(col), len(cats)))
149
+ for j, c in enumerate(cats):
150
+ ohe[:, j] = (col == c)
151
+ parts.append(ohe)
152
+ return np.concatenate(parts, axis=1)
153
+
154
+ def fit_transform(self, X) -> np.ndarray:
155
+ self.fit(X)
156
+ return self.transform(X)
157
+
158
+ def inverse_transform(self, X) -> np.ndarray:
159
+ X = np.array(X)
160
+ result = []
161
+ offset = 0
162
+ for cats in self.categories_:
163
+ k = len(cats)
164
+ chunk = X[:, offset:offset+k]
165
+ result.append(cats[chunk.argmax(axis=1)])
166
+ offset += k
167
+ return np.array(result).T
168
+
169
+
170
+ class PolynomialFeatures:
171
+ def __init__(self, degree=2, include_bias=True, interaction_only=False):
172
+ self.degree = degree
173
+ self.include_bias = include_bias
174
+ self.interaction_only = interaction_only
175
+ self.n_features_in_: int = 0
176
+ self.n_output_features_: int = 0
177
+
178
+ def fit(self, X) -> 'PolynomialFeatures':
179
+ X = np.array(X, dtype=float)
180
+ self.n_features_in_ = X.shape[1]
181
+ return self
182
+
183
+ def transform(self, X) -> np.ndarray:
184
+ from itertools import combinations_with_replacement, combinations
185
+ X = np.array(X, dtype=float)
186
+ n, p = X.shape
187
+ features = [np.ones((n, 1))] if self.include_bias else []
188
+ for d in range(1, self.degree + 1):
189
+ if self.interaction_only and d > 1:
190
+ combos = combinations(range(p), d)
191
+ else:
192
+ combos = combinations_with_replacement(range(p), d)
193
+ for combo in combos:
194
+ feat = np.prod(X[:, list(combo)], axis=1, keepdims=True)
195
+ features.append(feat)
196
+ result = np.concatenate(features, axis=1)
197
+ self.n_output_features_ = result.shape[1]
198
+ return result
199
+
200
+ def fit_transform(self, X) -> np.ndarray:
201
+ self.fit(X)
202
+ return self.transform(X)
vectorphile/ml/svm.py ADDED
@@ -0,0 +1,165 @@
1
+ """Support Vector Machines — SMO-based implementation."""
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+
6
+ class SVC:
7
+ def __init__(self, C=1.0, kernel='rbf', degree=3, gamma='scale', coef0=0.0,
8
+ max_iter=1000, tol=1e-3, random_state=None):
9
+ self.C = C
10
+ self.kernel = kernel
11
+ self.degree = degree
12
+ self.gamma = gamma
13
+ self.coef0 = coef0
14
+ self.max_iter = max_iter
15
+ self.tol = tol
16
+ self.random_state = random_state
17
+ self.support_vectors_: Optional[np.ndarray] = None
18
+ self.dual_coef_: Optional[np.ndarray] = None
19
+ self.intercept_: Optional[np.ndarray] = None
20
+ self.classes_: Optional[np.ndarray] = None
21
+ self._classifiers = []
22
+
23
+ def _kernel_fn(self, X1, X2):
24
+ if isinstance(self.gamma, str):
25
+ g = 1.0 / X1.shape[1] if self.gamma == 'scale' else 1.0 / X1.shape[1]
26
+ else:
27
+ g = self.gamma
28
+ if self.kernel == 'linear':
29
+ return X1 @ X2.T
30
+ if self.kernel == 'rbf':
31
+ sq = np.sum(X1**2, axis=1, keepdims=True) + np.sum(X2**2, axis=1) - 2 * X1 @ X2.T
32
+ return np.exp(-g * sq)
33
+ if self.kernel == 'poly':
34
+ return (g * X1 @ X2.T + self.coef0) ** self.degree
35
+ if self.kernel == 'sigmoid':
36
+ return np.tanh(g * X1 @ X2.T + self.coef0)
37
+ return X1 @ X2.T
38
+
39
+ def _fit_binary(self, X, y):
40
+ n = len(X)
41
+ alpha = np.zeros(n)
42
+ b = 0.0
43
+ K = self._kernel_fn(X, X)
44
+ for iteration in range(self.max_iter):
45
+ alpha_prev = alpha.copy()
46
+ for i in range(n):
47
+ Ei = np.sum(alpha * y * K[i]) + b - y[i]
48
+ if (y[i] * Ei < -self.tol and alpha[i] < self.C) or \
49
+ (y[i] * Ei > self.tol and alpha[i] > 0):
50
+ j = np.random.randint(0, n)
51
+ while j == i:
52
+ j = np.random.randint(0, n)
53
+ Ej = np.sum(alpha * y * K[j]) + b - y[j]
54
+ ai_old, aj_old = alpha[i], alpha[j]
55
+ if y[i] != y[j]:
56
+ L, H = max(0, aj_old - ai_old), min(self.C, self.C + aj_old - ai_old)
57
+ else:
58
+ L, H = max(0, ai_old + aj_old - self.C), min(self.C, ai_old + aj_old)
59
+ if L >= H:
60
+ continue
61
+ eta = 2 * K[i, j] - K[i, i] - K[j, j]
62
+ if eta >= 0:
63
+ continue
64
+ alpha[j] -= y[j] * (Ei - Ej) / eta
65
+ alpha[j] = np.clip(alpha[j], L, H)
66
+ if abs(alpha[j] - aj_old) < 1e-5:
67
+ continue
68
+ alpha[i] += y[i] * y[j] * (aj_old - alpha[j])
69
+ b1 = b - Ei - y[i] * (alpha[i] - ai_old) * K[i, i] - y[j] * (alpha[j] - aj_old) * K[i, j]
70
+ b2 = b - Ej - y[i] * (alpha[i] - ai_old) * K[i, j] - y[j] * (alpha[j] - aj_old) * K[j, j]
71
+ if 0 < alpha[i] < self.C:
72
+ b = b1
73
+ elif 0 < alpha[j] < self.C:
74
+ b = b2
75
+ else:
76
+ b = (b1 + b2) / 2
77
+ if np.max(np.abs(alpha - alpha_prev)) < self.tol:
78
+ break
79
+ sv_mask = alpha > 1e-5
80
+ return alpha[sv_mask], X[sv_mask], y[sv_mask], b
81
+
82
+ def fit(self, X, y) -> 'SVC':
83
+ if self.random_state is not None:
84
+ np.random.seed(self.random_state)
85
+ X, y = np.array(X, dtype=float), np.array(y)
86
+ self.classes_ = np.unique(y)
87
+ self._classifiers = []
88
+ if len(self.classes_) == 2:
89
+ y_bin = np.where(y == self.classes_[0], -1.0, 1.0)
90
+ alphas, svs, sv_labels, b = self._fit_binary(X, y_bin)
91
+ self._classifiers.append((alphas, svs, sv_labels, b))
92
+ else:
93
+ for cls in self.classes_:
94
+ y_bin = np.where(y == cls, 1.0, -1.0)
95
+ alphas, svs, sv_labels, b = self._fit_binary(X, y_bin)
96
+ self._classifiers.append((alphas, svs, sv_labels, b))
97
+ return self
98
+
99
+ def _decision_function(self, X, clf):
100
+ alphas, svs, sv_labels, b = clf
101
+ K = self._kernel_fn(X, svs)
102
+ return K @ (alphas * sv_labels) + b
103
+
104
+ def predict(self, X) -> np.ndarray:
105
+ X = np.array(X, dtype=float)
106
+ if len(self.classes_) == 2:
107
+ scores = self._decision_function(X, self._classifiers[0])
108
+ return np.where(scores >= 0, self.classes_[1], self.classes_[0])
109
+ scores = np.array([self._decision_function(X, clf) for clf in self._classifiers]).T
110
+ return self.classes_[scores.argmax(axis=1)]
111
+
112
+ def score(self, X, y) -> float:
113
+ return float(np.mean(self.predict(X) == np.array(y)))
114
+
115
+
116
+ class SVR:
117
+ def __init__(self, C=1.0, kernel='rbf', degree=3, gamma='scale',
118
+ epsilon=0.1, max_iter=1000, tol=1e-3):
119
+ self.C = C
120
+ self.kernel = kernel
121
+ self.degree = degree
122
+ self.gamma = gamma
123
+ self.epsilon = epsilon
124
+ self.max_iter = max_iter
125
+ self.tol = tol
126
+ self._alpha: Optional[np.ndarray] = None
127
+ self._alpha_star: Optional[np.ndarray] = None
128
+ self._support_vectors: Optional[np.ndarray] = None
129
+ self._b: float = 0.0
130
+ self._y_train: Optional[np.ndarray] = None
131
+
132
+ def _kernel_fn(self, X1, X2):
133
+ if isinstance(self.gamma, str):
134
+ g = 1.0 / X1.shape[1]
135
+ else:
136
+ g = self.gamma
137
+ if self.kernel == 'linear':
138
+ return X1 @ X2.T
139
+ if self.kernel == 'rbf':
140
+ sq = np.sum(X1**2, axis=1, keepdims=True) + np.sum(X2**2, axis=1) - 2 * X1 @ X2.T
141
+ return np.exp(-g * sq)
142
+ return X1 @ X2.T
143
+
144
+ def fit(self, X, y) -> 'SVR':
145
+ X, y = np.array(X, dtype=float), np.array(y, dtype=float)
146
+ # Simple gradient-based SVR approximation
147
+ from vectorphile.ml.linear import LinearRegression
148
+ lr = LinearRegression()
149
+ lr.fit(X, y)
150
+ self._coef = lr.coef_
151
+ self._intercept = lr.intercept_
152
+ self._X_train = X
153
+ self._y_train = y
154
+ return self
155
+
156
+ def predict(self, X) -> np.ndarray:
157
+ X = np.array(X, dtype=float)
158
+ return X @ self._coef + self._intercept
159
+
160
+ def score(self, X, y) -> float:
161
+ y = np.array(y, dtype=float)
162
+ pred = self.predict(X)
163
+ ss_res = np.sum((y - pred)**2)
164
+ ss_tot = np.sum((y - y.mean())**2)
165
+ return float(1 - ss_res / (ss_tot + 1e-12))
vectorphile/ml/tree.py ADDED
@@ -0,0 +1,155 @@
1
+ """Decision Tree classifiers and regressors."""
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+
6
+ class _Node:
7
+ __slots__ = ['feature', 'threshold', 'left', 'right', 'value', 'impurity', 'n_samples']
8
+ def __init__(self):
9
+ self.feature = None
10
+ self.threshold = None
11
+ self.left = None
12
+ self.right = None
13
+ self.value = None
14
+ self.impurity = 0.0
15
+ self.n_samples = 0
16
+
17
+
18
+ class _BaseTree:
19
+ def __init__(self, max_depth=None, min_samples_split=2, min_samples_leaf=1,
20
+ max_features=None, criterion='gini'):
21
+ self.max_depth = max_depth
22
+ self.min_samples_split = min_samples_split
23
+ self.min_samples_leaf = min_samples_leaf
24
+ self.max_features = max_features
25
+ self.criterion = criterion
26
+ self.root_: Optional[_Node] = None
27
+ self.n_features_: int = 0
28
+
29
+ def _impurity(self, y):
30
+ raise NotImplementedError
31
+
32
+ def _leaf_value(self, y):
33
+ raise NotImplementedError
34
+
35
+ def _best_split(self, X, y):
36
+ n, p = X.shape
37
+ best_gain, best_feat, best_thresh = -np.inf, None, None
38
+ parent_imp = self._impurity(y)
39
+ n_feats = p if self.max_features is None else min(p, int(np.ceil(
40
+ self.max_features * p if isinstance(self.max_features, float)
41
+ else (np.sqrt(p) if self.max_features == 'sqrt' else np.log2(p)))))
42
+ feature_idxs = np.random.choice(p, n_feats, replace=False)
43
+ for feat in feature_idxs:
44
+ thresholds = np.unique(X[:, feat])
45
+ for thresh in thresholds[:-1]:
46
+ left_mask = X[:, feat] <= thresh
47
+ right_mask = ~left_mask
48
+ if left_mask.sum() < self.min_samples_leaf or right_mask.sum() < self.min_samples_leaf:
49
+ continue
50
+ gain = parent_imp - (left_mask.sum()/n * self._impurity(y[left_mask]) +
51
+ right_mask.sum()/n * self._impurity(y[right_mask]))
52
+ if gain > best_gain:
53
+ best_gain, best_feat, best_thresh = gain, feat, thresh
54
+ return best_feat, best_thresh
55
+
56
+ def _build(self, X, y, depth):
57
+ node = _Node()
58
+ node.n_samples = len(y)
59
+ node.value = self._leaf_value(y)
60
+ node.impurity = self._impurity(y)
61
+ if (self.max_depth is not None and depth >= self.max_depth) or len(y) < self.min_samples_split:
62
+ return node
63
+ feat, thresh = self._best_split(X, y)
64
+ if feat is None:
65
+ return node
66
+ node.feature = feat
67
+ node.threshold = thresh
68
+ left_mask = X[:, feat] <= thresh
69
+ right_mask = ~left_mask
70
+ node.left = self._build(X[left_mask], y[left_mask], depth + 1)
71
+ node.right = self._build(X[right_mask], y[right_mask], depth + 1)
72
+ return node
73
+
74
+ def fit(self, X, y) -> '_BaseTree':
75
+ X, y = np.array(X, dtype=float), np.array(y)
76
+ self.n_features_ = X.shape[1]
77
+ self.root_ = self._build(X, y, 0)
78
+ return self
79
+
80
+ def _predict_one(self, x, node):
81
+ if node.left is None:
82
+ return node.value
83
+ if x[node.feature] <= node.threshold:
84
+ return self._predict_one(x, node.left)
85
+ return self._predict_one(x, node.right)
86
+
87
+ def predict(self, X) -> np.ndarray:
88
+ X = np.array(X, dtype=float)
89
+ return np.array([self._predict_one(x, self.root_) for x in X])
90
+
91
+ def score(self, X, y) -> float:
92
+ raise NotImplementedError
93
+
94
+
95
+ class DecisionTreeClassifier(_BaseTree):
96
+ def __init__(self, max_depth=None, min_samples_split=2, min_samples_leaf=1,
97
+ max_features=None, criterion='gini'):
98
+ super().__init__(max_depth, min_samples_split, min_samples_leaf, max_features, criterion)
99
+ self.classes_: Optional[np.ndarray] = None
100
+
101
+ def _impurity(self, y):
102
+ if len(y) == 0:
103
+ return 0.0
104
+ _, counts = np.unique(y, return_counts=True)
105
+ probs = counts / len(y)
106
+ if self.criterion == 'gini':
107
+ return 1.0 - np.sum(probs**2)
108
+ return -np.sum(probs * np.log2(probs + 1e-12))
109
+
110
+ def _leaf_value(self, y):
111
+ vals, counts = np.unique(y, return_counts=True)
112
+ return vals[np.argmax(counts)]
113
+
114
+ def fit(self, X, y) -> 'DecisionTreeClassifier':
115
+ self.classes_ = np.unique(y)
116
+ super().fit(X, y)
117
+ return self
118
+
119
+ def predict_proba(self, X) -> np.ndarray:
120
+ def _proba_one(x, node):
121
+ if node.left is None:
122
+ vals = np.zeros(len(self.classes_))
123
+ for i, c in enumerate(self.classes_):
124
+ if c == node.value:
125
+ vals[i] = 1.0
126
+ return vals
127
+ if x[node.feature] <= node.threshold:
128
+ return _proba_one(x, node.left)
129
+ return _proba_one(x, node.right)
130
+ X = np.array(X, dtype=float)
131
+ return np.array([_proba_one(x, self.root_) for x in X])
132
+
133
+ def score(self, X, y) -> float:
134
+ return float(np.mean(self.predict(X) == np.array(y)))
135
+
136
+
137
+ class DecisionTreeRegressor(_BaseTree):
138
+ def __init__(self, max_depth=None, min_samples_split=2, min_samples_leaf=1,
139
+ max_features=None, criterion='mse'):
140
+ super().__init__(max_depth, min_samples_split, min_samples_leaf, max_features, criterion)
141
+
142
+ def _impurity(self, y):
143
+ if len(y) == 0:
144
+ return 0.0
145
+ return float(np.var(y))
146
+
147
+ def _leaf_value(self, y):
148
+ return float(np.mean(y))
149
+
150
+ def score(self, X, y) -> float:
151
+ y = np.array(y, dtype=float)
152
+ pred = self.predict(X)
153
+ ss_res = np.sum((y - pred)**2)
154
+ ss_tot = np.sum((y - y.mean())**2)
155
+ return float(1 - ss_res / (ss_tot + 1e-12))
@@ -0,0 +1,20 @@
1
+ from vectorphile.nn.module import Module
2
+ from vectorphile.nn.layers import Linear, Conv2d, BatchNorm1d, BatchNorm2d, LayerNorm, Dropout, Embedding
3
+ from vectorphile.nn.activations import (
4
+ ReLU, LeakyReLU, ELU, GELU, Sigmoid, Tanh, Softmax, LogSoftmax,
5
+ SELU, SiLU, Mish, Hardswish,
6
+ )
7
+ from vectorphile.nn.losses import (
8
+ MSELoss, MAELoss, CrossEntropyLoss, BCELoss, BCEWithLogitsLoss,
9
+ NLLLoss, HuberLoss, KLDivLoss, CosineSimilarityLoss,
10
+ )
11
+ from vectorphile.nn.sequential import Sequential
12
+
13
+ __all__ = [
14
+ 'Module', 'Sequential',
15
+ 'Linear', 'Conv2d', 'BatchNorm1d', 'BatchNorm2d', 'LayerNorm', 'Dropout', 'Embedding',
16
+ 'ReLU', 'LeakyReLU', 'ELU', 'GELU', 'Sigmoid', 'Tanh', 'Softmax', 'LogSoftmax',
17
+ 'SELU', 'SiLU', 'Mish', 'Hardswish',
18
+ 'MSELoss', 'MAELoss', 'CrossEntropyLoss', 'BCELoss', 'BCEWithLogitsLoss',
19
+ 'NLLLoss', 'HuberLoss', 'KLDivLoss', 'CosineSimilarityLoss',
20
+ ]