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.
- vectorphile/__init__.py +154 -0
- vectorphile/amp/__init__.py +52 -0
- vectorphile/autograd/__init__.py +13 -0
- vectorphile/autograd/engine.py +208 -0
- vectorphile/core/__init__.py +21 -0
- vectorphile/core/tensor.py +696 -0
- vectorphile/data/__init__.py +23 -0
- vectorphile/data/frame.py +537 -0
- vectorphile/data/groupby.py +98 -0
- vectorphile/data/index.py +86 -0
- vectorphile/data/series.py +228 -0
- vectorphile/fft/__init__.py +34 -0
- vectorphile/linalg/__init__.py +63 -0
- vectorphile/ml/__init__.py +44 -0
- vectorphile/ml/boost/__init__.py +133 -0
- vectorphile/ml/cluster.py +218 -0
- vectorphile/ml/decomposition.py +161 -0
- vectorphile/ml/ensemble.py +209 -0
- vectorphile/ml/linear.py +250 -0
- vectorphile/ml/metrics.py +148 -0
- vectorphile/ml/model_selection.py +171 -0
- vectorphile/ml/naive_bayes.py +113 -0
- vectorphile/ml/neighbors.py +95 -0
- vectorphile/ml/preprocessing.py +202 -0
- vectorphile/ml/svm.py +165 -0
- vectorphile/ml/tree.py +155 -0
- vectorphile/nn/__init__.py +20 -0
- vectorphile/nn/activations.py +97 -0
- vectorphile/nn/layers.py +231 -0
- vectorphile/nn/losses.py +172 -0
- vectorphile/nn/module.py +97 -0
- vectorphile/nn/sequential.py +47 -0
- vectorphile/optim/__init__.py +12 -0
- vectorphile/optim/adagrad.py +31 -0
- vectorphile/optim/adam.py +71 -0
- vectorphile/optim/lr_scheduler.py +110 -0
- vectorphile/optim/optimizer.py +30 -0
- vectorphile/optim/rmsprop.py +34 -0
- vectorphile/optim/sgd.py +38 -0
- vectorphile/optimize/__init__.py +97 -0
- vectorphile/random/__init__.py +64 -0
- vectorphile/signal/__init__.py +67 -0
- vectorphile/sparse/__init__.py +129 -0
- vectorphile/stats/__init__.py +90 -0
- vectorphile/utils/__init__.py +3 -0
- vectorphile/utils/data/__init__.py +4 -0
- vectorphile/utils/data/dataloader.py +47 -0
- vectorphile/utils/data/dataset.py +29 -0
- vectorphile-0.1.0.dist-info/METADATA +208 -0
- vectorphile-0.1.0.dist-info/RECORD +53 -0
- vectorphile-0.1.0.dist-info/WHEEL +5 -0
- vectorphile-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
]
|