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
vectorphile/ml/linear.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Linear models."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _BaseLinear:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.coef_: Optional[np.ndarray] = None
|
|
9
|
+
self.intercept_: float = 0.0
|
|
10
|
+
|
|
11
|
+
def predict(self, X) -> np.ndarray:
|
|
12
|
+
X = np.array(X)
|
|
13
|
+
return X @ self.coef_ + self.intercept_
|
|
14
|
+
|
|
15
|
+
def score(self, X, y) -> float:
|
|
16
|
+
y = np.array(y)
|
|
17
|
+
pred = self.predict(X)
|
|
18
|
+
ss_res = np.sum((y - pred) ** 2)
|
|
19
|
+
ss_tot = np.sum((y - y.mean()) ** 2)
|
|
20
|
+
return 1 - ss_res / (ss_tot + 1e-12)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LinearRegression(_BaseLinear):
|
|
24
|
+
def __init__(self, fit_intercept=True):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.fit_intercept = fit_intercept
|
|
27
|
+
|
|
28
|
+
def fit(self, X, y) -> 'LinearRegression':
|
|
29
|
+
X, y = np.array(X, dtype=float), np.array(y, dtype=float)
|
|
30
|
+
if self.fit_intercept:
|
|
31
|
+
X = np.c_[np.ones(len(X)), X]
|
|
32
|
+
sol, _, _, _ = np.linalg.lstsq(X, y, rcond=None)
|
|
33
|
+
if self.fit_intercept:
|
|
34
|
+
self.intercept_, self.coef_ = float(sol[0]), sol[1:]
|
|
35
|
+
else:
|
|
36
|
+
self.coef_ = sol
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Ridge(_BaseLinear):
|
|
41
|
+
def __init__(self, alpha=1.0, fit_intercept=True):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.alpha = alpha
|
|
44
|
+
self.fit_intercept = fit_intercept
|
|
45
|
+
|
|
46
|
+
def fit(self, X, y) -> 'Ridge':
|
|
47
|
+
X, y = np.array(X, dtype=float), np.array(y, dtype=float)
|
|
48
|
+
if self.fit_intercept:
|
|
49
|
+
X_mean, y_mean = X.mean(0), y.mean()
|
|
50
|
+
Xc = X - X_mean
|
|
51
|
+
sol = np.linalg.solve(Xc.T @ Xc + self.alpha * np.eye(Xc.shape[1]), Xc.T @ (y - y_mean))
|
|
52
|
+
self.coef_ = sol
|
|
53
|
+
self.intercept_ = float(y_mean - X_mean @ sol)
|
|
54
|
+
else:
|
|
55
|
+
sol = np.linalg.solve(X.T @ X + self.alpha * np.eye(X.shape[1]), X.T @ y)
|
|
56
|
+
self.coef_ = sol
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Lasso(_BaseLinear):
|
|
61
|
+
def __init__(self, alpha=1.0, max_iter=1000, tol=1e-4, fit_intercept=True):
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.alpha = alpha
|
|
64
|
+
self.max_iter = max_iter
|
|
65
|
+
self.tol = tol
|
|
66
|
+
self.fit_intercept = fit_intercept
|
|
67
|
+
|
|
68
|
+
def fit(self, X, y) -> 'Lasso':
|
|
69
|
+
X, y = np.array(X, dtype=float), np.array(y, dtype=float)
|
|
70
|
+
if self.fit_intercept:
|
|
71
|
+
X_mean, y_mean = X.mean(0), y.mean()
|
|
72
|
+
X, y = X - X_mean, y - y_mean
|
|
73
|
+
n, p = X.shape
|
|
74
|
+
coef = np.zeros(p)
|
|
75
|
+
for _ in range(self.max_iter):
|
|
76
|
+
coef_old = coef.copy()
|
|
77
|
+
for j in range(p):
|
|
78
|
+
r = y - X @ coef + X[:, j] * coef[j]
|
|
79
|
+
rho = X[:, j] @ r
|
|
80
|
+
z = X[:, j] @ X[:, j]
|
|
81
|
+
if rho < -self.alpha * n / 2:
|
|
82
|
+
coef[j] = (rho + self.alpha * n / 2) / z
|
|
83
|
+
elif rho > self.alpha * n / 2:
|
|
84
|
+
coef[j] = (rho - self.alpha * n / 2) / z
|
|
85
|
+
else:
|
|
86
|
+
coef[j] = 0.0
|
|
87
|
+
if np.max(np.abs(coef - coef_old)) < self.tol:
|
|
88
|
+
break
|
|
89
|
+
self.coef_ = coef
|
|
90
|
+
if self.fit_intercept:
|
|
91
|
+
self.intercept_ = float(y_mean - X_mean @ coef)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ElasticNet(_BaseLinear):
|
|
96
|
+
def __init__(self, alpha=1.0, l1_ratio=0.5, max_iter=1000, tol=1e-4, fit_intercept=True):
|
|
97
|
+
super().__init__()
|
|
98
|
+
self.alpha = alpha
|
|
99
|
+
self.l1_ratio = l1_ratio
|
|
100
|
+
self.max_iter = max_iter
|
|
101
|
+
self.tol = tol
|
|
102
|
+
self.fit_intercept = fit_intercept
|
|
103
|
+
|
|
104
|
+
def fit(self, X, y) -> 'ElasticNet':
|
|
105
|
+
X, y = np.array(X, dtype=float), np.array(y, dtype=float)
|
|
106
|
+
if self.fit_intercept:
|
|
107
|
+
X_mean, y_mean = X.mean(0), y.mean()
|
|
108
|
+
X, y = X - X_mean, y - y_mean
|
|
109
|
+
n, p = X.shape
|
|
110
|
+
l1 = self.alpha * self.l1_ratio
|
|
111
|
+
l2 = self.alpha * (1 - self.l1_ratio)
|
|
112
|
+
coef = np.zeros(p)
|
|
113
|
+
for _ in range(self.max_iter):
|
|
114
|
+
coef_old = coef.copy()
|
|
115
|
+
for j in range(p):
|
|
116
|
+
r = y - X @ coef + X[:, j] * coef[j]
|
|
117
|
+
rho = X[:, j] @ r
|
|
118
|
+
z = X[:, j] @ X[:, j] + l2 * n
|
|
119
|
+
thresh = l1 * n / 2
|
|
120
|
+
if rho < -thresh:
|
|
121
|
+
coef[j] = (rho + thresh) / z
|
|
122
|
+
elif rho > thresh:
|
|
123
|
+
coef[j] = (rho - thresh) / z
|
|
124
|
+
else:
|
|
125
|
+
coef[j] = 0.0
|
|
126
|
+
if np.max(np.abs(coef - coef_old)) < self.tol:
|
|
127
|
+
break
|
|
128
|
+
self.coef_ = coef
|
|
129
|
+
if self.fit_intercept:
|
|
130
|
+
self.intercept_ = float(y_mean - X_mean @ coef)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LogisticRegression:
|
|
135
|
+
def __init__(self, C=1.0, max_iter=200, tol=1e-4, fit_intercept=True, multi_class='ovr'):
|
|
136
|
+
self.C = C
|
|
137
|
+
self.max_iter = max_iter
|
|
138
|
+
self.tol = tol
|
|
139
|
+
self.fit_intercept = fit_intercept
|
|
140
|
+
self.multi_class = multi_class
|
|
141
|
+
self.coef_: Optional[np.ndarray] = None
|
|
142
|
+
self.intercept_: Optional[np.ndarray] = None
|
|
143
|
+
self.classes_: Optional[np.ndarray] = None
|
|
144
|
+
|
|
145
|
+
def _sigmoid(self, z):
|
|
146
|
+
return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
|
|
147
|
+
|
|
148
|
+
def _fit_binary(self, X, y, lr=0.1):
|
|
149
|
+
n, p = X.shape
|
|
150
|
+
w = np.zeros(p + (1 if self.fit_intercept else 0))
|
|
151
|
+
for _ in range(self.max_iter):
|
|
152
|
+
if self.fit_intercept:
|
|
153
|
+
z = X @ w[1:] + w[0]
|
|
154
|
+
else:
|
|
155
|
+
z = X @ w
|
|
156
|
+
prob = self._sigmoid(z)
|
|
157
|
+
error = prob - y
|
|
158
|
+
if self.fit_intercept:
|
|
159
|
+
dw = np.concatenate([[error.mean()], X.T @ error / n + w[1:] / (self.C * n)])
|
|
160
|
+
else:
|
|
161
|
+
dw = X.T @ error / n + w / (self.C * n)
|
|
162
|
+
w -= lr * dw
|
|
163
|
+
if np.max(np.abs(dw)) < self.tol:
|
|
164
|
+
break
|
|
165
|
+
return w
|
|
166
|
+
|
|
167
|
+
def fit(self, X, y) -> 'LogisticRegression':
|
|
168
|
+
X, y = np.array(X, dtype=float), np.array(y)
|
|
169
|
+
self.classes_ = np.unique(y)
|
|
170
|
+
if len(self.classes_) == 2:
|
|
171
|
+
y_bin = (y == self.classes_[1]).astype(float)
|
|
172
|
+
w = self._fit_binary(X, y_bin)
|
|
173
|
+
if self.fit_intercept:
|
|
174
|
+
self.coef_ = w[1:][np.newaxis]
|
|
175
|
+
self.intercept_ = np.array([w[0]])
|
|
176
|
+
else:
|
|
177
|
+
self.coef_ = w[np.newaxis]
|
|
178
|
+
self.intercept_ = np.array([0.0])
|
|
179
|
+
else:
|
|
180
|
+
self.coef_ = []
|
|
181
|
+
self.intercept_ = []
|
|
182
|
+
for cls in self.classes_:
|
|
183
|
+
y_bin = (y == cls).astype(float)
|
|
184
|
+
w = self._fit_binary(X, y_bin)
|
|
185
|
+
if self.fit_intercept:
|
|
186
|
+
self.coef_.append(w[1:])
|
|
187
|
+
self.intercept_.append(w[0])
|
|
188
|
+
else:
|
|
189
|
+
self.coef_.append(w)
|
|
190
|
+
self.intercept_.append(0.0)
|
|
191
|
+
self.coef_ = np.array(self.coef_)
|
|
192
|
+
self.intercept_ = np.array(self.intercept_)
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def predict_proba(self, X) -> np.ndarray:
|
|
196
|
+
X = np.array(X, dtype=float)
|
|
197
|
+
logits = X @ self.coef_.T + self.intercept_
|
|
198
|
+
probs = self._sigmoid(logits)
|
|
199
|
+
if probs.ndim == 1 or probs.shape[1] == 1:
|
|
200
|
+
p = probs.flatten()
|
|
201
|
+
return np.c_[1 - p, p]
|
|
202
|
+
probs = probs / probs.sum(axis=1, keepdims=True)
|
|
203
|
+
return probs
|
|
204
|
+
|
|
205
|
+
def predict(self, X) -> np.ndarray:
|
|
206
|
+
proba = self.predict_proba(X)
|
|
207
|
+
return self.classes_[np.argmax(proba, axis=1)]
|
|
208
|
+
|
|
209
|
+
def score(self, X, y) -> float:
|
|
210
|
+
return float(np.mean(self.predict(X) == np.array(y)))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Perceptron:
|
|
214
|
+
def __init__(self, max_iter=1000, tol=1e-3, eta=1.0, fit_intercept=True):
|
|
215
|
+
self.max_iter = max_iter
|
|
216
|
+
self.tol = tol
|
|
217
|
+
self.eta = eta
|
|
218
|
+
self.fit_intercept = fit_intercept
|
|
219
|
+
self.coef_: Optional[np.ndarray] = None
|
|
220
|
+
self.intercept_: float = 0.0
|
|
221
|
+
|
|
222
|
+
def fit(self, X, y) -> 'Perceptron':
|
|
223
|
+
X, y = np.array(X, dtype=float), np.array(y)
|
|
224
|
+
classes = np.unique(y)
|
|
225
|
+
y_bin = np.where(y == classes[0], -1, 1)
|
|
226
|
+
n, p = X.shape
|
|
227
|
+
w = np.zeros(p)
|
|
228
|
+
b = 0.0
|
|
229
|
+
for _ in range(self.max_iter):
|
|
230
|
+
errors = 0
|
|
231
|
+
for xi, yi in zip(X, y_bin):
|
|
232
|
+
pred = np.sign(np.dot(w, xi) + b)
|
|
233
|
+
if pred != yi:
|
|
234
|
+
w += self.eta * yi * xi
|
|
235
|
+
b += self.eta * yi
|
|
236
|
+
errors += 1
|
|
237
|
+
if errors == 0:
|
|
238
|
+
break
|
|
239
|
+
self.coef_ = w
|
|
240
|
+
self.intercept_ = b
|
|
241
|
+
self.classes_ = classes
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def predict(self, X) -> np.ndarray:
|
|
245
|
+
X = np.array(X, dtype=float)
|
|
246
|
+
raw = np.sign(X @ self.coef_ + self.intercept_)
|
|
247
|
+
return np.where(raw <= 0, self.classes_[0], self.classes_[1])
|
|
248
|
+
|
|
249
|
+
def score(self, X, y) -> float:
|
|
250
|
+
return float(np.mean(self.predict(X) == np.array(y)))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Evaluation metrics."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def accuracy_score(y_true, y_pred, normalize=True) -> float:
|
|
7
|
+
y_true, y_pred = np.array(y_true), np.array(y_pred)
|
|
8
|
+
correct = (y_true == y_pred).sum()
|
|
9
|
+
return float(correct / len(y_true)) if normalize else int(correct)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def confusion_matrix(y_true, y_pred, labels=None) -> np.ndarray:
|
|
13
|
+
y_true, y_pred = np.array(y_true), np.array(y_pred)
|
|
14
|
+
classes = labels if labels is not None else np.unique(np.concatenate([y_true, y_pred]))
|
|
15
|
+
K = len(classes)
|
|
16
|
+
cm = np.zeros((K, K), dtype=int)
|
|
17
|
+
class_map = {c: i for i, c in enumerate(classes)}
|
|
18
|
+
for t, p in zip(y_true, y_pred):
|
|
19
|
+
cm[class_map[t], class_map[p]] += 1
|
|
20
|
+
return cm
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def precision_score(y_true, y_pred, average='binary', labels=None, zero_division=0) -> float:
|
|
24
|
+
y_true, y_pred = np.array(y_true), np.array(y_pred)
|
|
25
|
+
classes = labels or np.unique(np.concatenate([y_true, y_pred]))
|
|
26
|
+
precisions = []
|
|
27
|
+
for c in classes:
|
|
28
|
+
tp = ((y_pred == c) & (y_true == c)).sum()
|
|
29
|
+
fp = ((y_pred == c) & (y_true != c)).sum()
|
|
30
|
+
denom = tp + fp
|
|
31
|
+
precisions.append(tp / denom if denom > 0 else zero_division)
|
|
32
|
+
if average == 'micro':
|
|
33
|
+
tps = sum((y_pred == c) & (y_true == c) for c in classes).sum()
|
|
34
|
+
fps = sum((y_pred == c) & (y_true != c) for c in classes).sum()
|
|
35
|
+
return float(tps / (tps + fps)) if (tps + fps) > 0 else zero_division
|
|
36
|
+
if average == 'macro':
|
|
37
|
+
return float(np.mean(precisions))
|
|
38
|
+
if average == 'weighted':
|
|
39
|
+
weights = [(y_true == c).sum() for c in classes]
|
|
40
|
+
return float(np.average(precisions, weights=weights))
|
|
41
|
+
return float(precisions[0]) if len(precisions) == 1 else float(precisions[-1])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def recall_score(y_true, y_pred, average='binary', labels=None, zero_division=0) -> float:
|
|
45
|
+
y_true, y_pred = np.array(y_true), np.array(y_pred)
|
|
46
|
+
classes = labels or np.unique(np.concatenate([y_true, y_pred]))
|
|
47
|
+
recalls = []
|
|
48
|
+
for c in classes:
|
|
49
|
+
tp = ((y_pred == c) & (y_true == c)).sum()
|
|
50
|
+
fn = ((y_pred != c) & (y_true == c)).sum()
|
|
51
|
+
denom = tp + fn
|
|
52
|
+
recalls.append(tp / denom if denom > 0 else zero_division)
|
|
53
|
+
if average == 'macro':
|
|
54
|
+
return float(np.mean(recalls))
|
|
55
|
+
if average == 'weighted':
|
|
56
|
+
weights = [(y_true == c).sum() for c in classes]
|
|
57
|
+
return float(np.average(recalls, weights=weights))
|
|
58
|
+
return float(recalls[-1])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def f1_score(y_true, y_pred, average='binary', labels=None, zero_division=0) -> float:
|
|
62
|
+
p = precision_score(y_true, y_pred, average=average, labels=labels, zero_division=zero_division)
|
|
63
|
+
r = recall_score(y_true, y_pred, average=average, labels=labels, zero_division=zero_division)
|
|
64
|
+
return float(2 * p * r / (p + r)) if (p + r) > 0 else zero_division
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def roc_auc_score(y_true, y_score, average='macro') -> float:
|
|
68
|
+
y_true, y_score = np.array(y_true), np.array(y_score)
|
|
69
|
+
if y_score.ndim == 1:
|
|
70
|
+
return _roc_auc_binary(y_true, y_score)
|
|
71
|
+
classes = np.unique(y_true)
|
|
72
|
+
aucs = [_roc_auc_binary((y_true == c).astype(int), y_score[:, i]) for i, c in enumerate(classes)]
|
|
73
|
+
return float(np.mean(aucs))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _roc_auc_binary(y_true, y_score) -> float:
|
|
77
|
+
pos = y_true.sum()
|
|
78
|
+
neg = len(y_true) - pos
|
|
79
|
+
if pos == 0 or neg == 0:
|
|
80
|
+
return 0.5
|
|
81
|
+
order = np.argsort(y_score)[::-1]
|
|
82
|
+
y_sorted = y_true[order]
|
|
83
|
+
tpr = np.cumsum(y_sorted) / pos
|
|
84
|
+
fpr = np.cumsum(1 - y_sorted) / neg
|
|
85
|
+
tpr = np.concatenate([[0], tpr])
|
|
86
|
+
fpr = np.concatenate([[0], fpr])
|
|
87
|
+
return float(np.trapz(tpr, fpr))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mse(y_true, y_pred) -> float:
|
|
91
|
+
y_true, y_pred = np.array(y_true, dtype=float), np.array(y_pred, dtype=float)
|
|
92
|
+
return float(np.mean((y_true - y_pred)**2))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rmse(y_true, y_pred) -> float:
|
|
96
|
+
return float(np.sqrt(mse(y_true, y_pred)))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def mae(y_true, y_pred) -> float:
|
|
100
|
+
y_true, y_pred = np.array(y_true, dtype=float), np.array(y_pred, dtype=float)
|
|
101
|
+
return float(np.mean(np.abs(y_true - y_pred)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def r2_score(y_true, y_pred) -> float:
|
|
105
|
+
y_true, y_pred = np.array(y_true, dtype=float), np.array(y_pred, dtype=float)
|
|
106
|
+
ss_res = np.sum((y_true - y_pred)**2)
|
|
107
|
+
ss_tot = np.sum((y_true - y_true.mean())**2)
|
|
108
|
+
return float(1 - ss_res / (ss_tot + 1e-12))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def mean_absolute_percentage_error(y_true, y_pred) -> float:
|
|
112
|
+
y_true, y_pred = np.array(y_true, dtype=float), np.array(y_pred, dtype=float)
|
|
113
|
+
mask = y_true != 0
|
|
114
|
+
return float(np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def silhouette_score(X, labels) -> float:
|
|
118
|
+
X, labels = np.array(X, dtype=float), np.array(labels)
|
|
119
|
+
n = len(X)
|
|
120
|
+
unique_labels = np.unique(labels)
|
|
121
|
+
scores = []
|
|
122
|
+
for i in range(n):
|
|
123
|
+
same = X[labels == labels[i]]
|
|
124
|
+
a = np.mean(np.linalg.norm(same - X[i], axis=1)) if len(same) > 1 else 0.0
|
|
125
|
+
b_vals = []
|
|
126
|
+
for c in unique_labels:
|
|
127
|
+
if c == labels[i]:
|
|
128
|
+
continue
|
|
129
|
+
other = X[labels == c]
|
|
130
|
+
if len(other) > 0:
|
|
131
|
+
b_vals.append(np.mean(np.linalg.norm(other - X[i], axis=1)))
|
|
132
|
+
b = min(b_vals) if b_vals else 0.0
|
|
133
|
+
s = (b - a) / max(a, b) if max(a, b) > 0 else 0.0
|
|
134
|
+
scores.append(s)
|
|
135
|
+
return float(np.mean(scores))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def log_loss(y_true, y_pred, eps=1e-15) -> float:
|
|
139
|
+
y_true = np.array(y_true)
|
|
140
|
+
y_pred = np.clip(np.array(y_pred, dtype=float), eps, 1 - eps)
|
|
141
|
+
if y_pred.ndim == 1:
|
|
142
|
+
return float(-np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)))
|
|
143
|
+
n = len(y_true)
|
|
144
|
+
classes = np.unique(y_true)
|
|
145
|
+
y_ohe = np.zeros((n, len(classes)))
|
|
146
|
+
for i, c in enumerate(classes):
|
|
147
|
+
y_ohe[:, i] = (y_true == c)
|
|
148
|
+
return float(-np.mean(np.sum(y_ohe * np.log(y_pred), axis=1)))
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Model selection and validation utilities."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def train_test_split(*arrays, test_size=0.2, random_state=None, shuffle=True, stratify=None):
|
|
7
|
+
if random_state is not None:
|
|
8
|
+
np.random.seed(random_state)
|
|
9
|
+
n = len(arrays[0])
|
|
10
|
+
n_test = int(n * test_size)
|
|
11
|
+
if shuffle:
|
|
12
|
+
idx = np.random.permutation(n)
|
|
13
|
+
else:
|
|
14
|
+
idx = np.arange(n)
|
|
15
|
+
test_idx = idx[:n_test]
|
|
16
|
+
train_idx = idx[n_test:]
|
|
17
|
+
result = []
|
|
18
|
+
for arr in arrays:
|
|
19
|
+
arr = np.array(arr)
|
|
20
|
+
result.append(arr[train_idx])
|
|
21
|
+
result.append(arr[test_idx])
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KFold:
|
|
26
|
+
def __init__(self, n_splits=5, shuffle=False, random_state=None):
|
|
27
|
+
self.n_splits = n_splits
|
|
28
|
+
self.shuffle = shuffle
|
|
29
|
+
self.random_state = random_state
|
|
30
|
+
|
|
31
|
+
def split(self, X, y=None):
|
|
32
|
+
X = np.array(X)
|
|
33
|
+
n = len(X)
|
|
34
|
+
idx = np.arange(n)
|
|
35
|
+
if self.shuffle:
|
|
36
|
+
if self.random_state is not None:
|
|
37
|
+
np.random.seed(self.random_state)
|
|
38
|
+
np.random.shuffle(idx)
|
|
39
|
+
fold_sizes = np.full(self.n_splits, n // self.n_splits, dtype=int)
|
|
40
|
+
fold_sizes[:n % self.n_splits] += 1
|
|
41
|
+
current = 0
|
|
42
|
+
for fold_size in fold_sizes:
|
|
43
|
+
start, stop = current, current + fold_size
|
|
44
|
+
test_idx = idx[start:stop]
|
|
45
|
+
train_idx = np.concatenate([idx[:start], idx[stop:]])
|
|
46
|
+
yield train_idx, test_idx
|
|
47
|
+
current = stop
|
|
48
|
+
|
|
49
|
+
def get_n_splits(self):
|
|
50
|
+
return self.n_splits
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StratifiedKFold:
|
|
54
|
+
def __init__(self, n_splits=5, shuffle=False, random_state=None):
|
|
55
|
+
self.n_splits = n_splits
|
|
56
|
+
self.shuffle = shuffle
|
|
57
|
+
self.random_state = random_state
|
|
58
|
+
|
|
59
|
+
def split(self, X, y):
|
|
60
|
+
X, y = np.array(X), np.array(y)
|
|
61
|
+
classes, y_indices = np.unique(y, return_inverse=True)
|
|
62
|
+
rng = np.random.RandomState(self.random_state) if self.random_state else np.random.RandomState()
|
|
63
|
+
folds = [[] for _ in range(self.n_splits)]
|
|
64
|
+
for cls_idx in range(len(classes)):
|
|
65
|
+
class_indices = np.where(y_indices == cls_idx)[0]
|
|
66
|
+
if self.shuffle:
|
|
67
|
+
rng.shuffle(class_indices)
|
|
68
|
+
for i, idx in enumerate(class_indices):
|
|
69
|
+
folds[i % self.n_splits].append(idx)
|
|
70
|
+
for fold_i in range(self.n_splits):
|
|
71
|
+
test_idx = np.array(folds[fold_i])
|
|
72
|
+
train_idx = np.concatenate([np.array(folds[j]) for j in range(self.n_splits) if j != fold_i])
|
|
73
|
+
yield train_idx, test_idx
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cross_val_score(estimator, X, y, cv=5, scoring='accuracy') -> np.ndarray:
|
|
77
|
+
X, y = np.array(X), np.array(y)
|
|
78
|
+
kf = KFold(n_splits=cv, shuffle=True, random_state=42)
|
|
79
|
+
scores = []
|
|
80
|
+
for train_idx, test_idx in kf.split(X):
|
|
81
|
+
X_train, X_test = X[train_idx], X[test_idx]
|
|
82
|
+
y_train, y_test = y[train_idx], y[test_idx]
|
|
83
|
+
estimator.fit(X_train, y_train)
|
|
84
|
+
if scoring == 'accuracy' or scoring == 'r2':
|
|
85
|
+
scores.append(estimator.score(X_test, y_test))
|
|
86
|
+
else:
|
|
87
|
+
scores.append(estimator.score(X_test, y_test))
|
|
88
|
+
return np.array(scores)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GridSearchCV:
|
|
92
|
+
def __init__(self, estimator, param_grid: Dict, cv=5, scoring='accuracy', refit=True, n_jobs=1):
|
|
93
|
+
self.estimator = estimator
|
|
94
|
+
self.param_grid = param_grid
|
|
95
|
+
self.cv = cv
|
|
96
|
+
self.scoring = scoring
|
|
97
|
+
self.refit = refit
|
|
98
|
+
self.n_jobs = n_jobs
|
|
99
|
+
self.best_params_: Optional[Dict] = None
|
|
100
|
+
self.best_score_: float = -np.inf
|
|
101
|
+
self.best_estimator_ = None
|
|
102
|
+
self.cv_results_: Dict = {}
|
|
103
|
+
|
|
104
|
+
def _param_combinations(self):
|
|
105
|
+
from itertools import product
|
|
106
|
+
keys = list(self.param_grid.keys())
|
|
107
|
+
values = list(self.param_grid.values())
|
|
108
|
+
for combo in product(*values):
|
|
109
|
+
yield dict(zip(keys, combo))
|
|
110
|
+
|
|
111
|
+
def fit(self, X, y) -> 'GridSearchCV':
|
|
112
|
+
X, y = np.array(X), np.array(y)
|
|
113
|
+
results = []
|
|
114
|
+
for params in self._param_combinations():
|
|
115
|
+
est = type(self.estimator)(**{**self.estimator.__dict__, **params})
|
|
116
|
+
scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
|
|
117
|
+
mean_score = scores.mean()
|
|
118
|
+
results.append({'params': params, 'mean_test_score': mean_score, 'std_test_score': scores.std()})
|
|
119
|
+
if mean_score > self.best_score_:
|
|
120
|
+
self.best_score_ = mean_score
|
|
121
|
+
self.best_params_ = params
|
|
122
|
+
self.cv_results_ = {k: [r[k] for r in results] for k in results[0]}
|
|
123
|
+
if self.refit:
|
|
124
|
+
self.best_estimator_ = type(self.estimator)(**{**self.estimator.__dict__, **self.best_params_})
|
|
125
|
+
self.best_estimator_.fit(X, y)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def predict(self, X) -> np.ndarray:
|
|
129
|
+
return self.best_estimator_.predict(X)
|
|
130
|
+
|
|
131
|
+
def score(self, X, y) -> float:
|
|
132
|
+
return self.best_estimator_.score(X, y)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class RandomizedSearchCV:
|
|
136
|
+
def __init__(self, estimator, param_distributions: Dict, n_iter=10, cv=5,
|
|
137
|
+
scoring='accuracy', refit=True, random_state=None):
|
|
138
|
+
self.estimator = estimator
|
|
139
|
+
self.param_distributions = param_distributions
|
|
140
|
+
self.n_iter = n_iter
|
|
141
|
+
self.cv = cv
|
|
142
|
+
self.scoring = scoring
|
|
143
|
+
self.refit = refit
|
|
144
|
+
self.random_state = random_state
|
|
145
|
+
self.best_params_: Optional[Dict] = None
|
|
146
|
+
self.best_score_: float = -np.inf
|
|
147
|
+
self.best_estimator_ = None
|
|
148
|
+
|
|
149
|
+
def fit(self, X, y) -> 'RandomizedSearchCV':
|
|
150
|
+
if self.random_state is not None:
|
|
151
|
+
np.random.seed(self.random_state)
|
|
152
|
+
X, y = np.array(X), np.array(y)
|
|
153
|
+
for _ in range(self.n_iter):
|
|
154
|
+
params = {k: np.random.choice(v) if isinstance(v, (list, np.ndarray))
|
|
155
|
+
else float(np.random.uniform(*v)) for k, v in self.param_distributions.items()}
|
|
156
|
+
try:
|
|
157
|
+
est = type(self.estimator)(**{**self.estimator.__dict__, **params})
|
|
158
|
+
scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
|
|
159
|
+
mean_score = scores.mean()
|
|
160
|
+
if mean_score > self.best_score_:
|
|
161
|
+
self.best_score_ = mean_score
|
|
162
|
+
self.best_params_ = params
|
|
163
|
+
except Exception:
|
|
164
|
+
continue
|
|
165
|
+
if self.refit and self.best_params_ is not None:
|
|
166
|
+
self.best_estimator_ = type(self.estimator)(**{**self.estimator.__dict__, **self.best_params_})
|
|
167
|
+
self.best_estimator_.fit(X, y)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def predict(self, X) -> np.ndarray:
|
|
171
|
+
return self.best_estimator_.predict(X)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Naive Bayes classifiers."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GaussianNB:
|
|
7
|
+
def __init__(self, var_smoothing=1e-9):
|
|
8
|
+
self.var_smoothing = var_smoothing
|
|
9
|
+
self.classes_: Optional[np.ndarray] = None
|
|
10
|
+
self.theta_: Optional[np.ndarray] = None
|
|
11
|
+
self.var_: Optional[np.ndarray] = None
|
|
12
|
+
self.class_prior_: Optional[np.ndarray] = None
|
|
13
|
+
|
|
14
|
+
def fit(self, X, y) -> 'GaussianNB':
|
|
15
|
+
X, y = np.array(X, dtype=float), np.array(y)
|
|
16
|
+
self.classes_ = np.unique(y)
|
|
17
|
+
self.theta_ = np.array([X[y == c].mean(axis=0) for c in self.classes_])
|
|
18
|
+
self.var_ = np.array([X[y == c].var(axis=0) for c in self.classes_])
|
|
19
|
+
max_var = self.var_.max()
|
|
20
|
+
self.var_ += self.var_smoothing * max_var
|
|
21
|
+
counts = np.array([(y == c).sum() for c in self.classes_])
|
|
22
|
+
self.class_prior_ = counts / len(y)
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def _log_likelihood(self, X):
|
|
26
|
+
log_probs = []
|
|
27
|
+
for k in range(len(self.classes_)):
|
|
28
|
+
log_prior = np.log(self.class_prior_[k])
|
|
29
|
+
diff = X - self.theta_[k]
|
|
30
|
+
log_lk = -0.5 * np.sum(np.log(2 * np.pi * self.var_[k]) + diff**2 / self.var_[k], axis=1)
|
|
31
|
+
log_probs.append(log_prior + log_lk)
|
|
32
|
+
return np.array(log_probs).T
|
|
33
|
+
|
|
34
|
+
def predict(self, X) -> np.ndarray:
|
|
35
|
+
return self.classes_[self._log_likelihood(np.array(X, dtype=float)).argmax(axis=1)]
|
|
36
|
+
|
|
37
|
+
def predict_proba(self, X) -> np.ndarray:
|
|
38
|
+
log_probs = self._log_likelihood(np.array(X, dtype=float))
|
|
39
|
+
log_probs -= log_probs.max(axis=1, keepdims=True)
|
|
40
|
+
probs = np.exp(log_probs)
|
|
41
|
+
return probs / probs.sum(axis=1, keepdims=True)
|
|
42
|
+
|
|
43
|
+
def score(self, X, y) -> float:
|
|
44
|
+
return float(np.mean(self.predict(X) == np.array(y)))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MultinomialNB:
|
|
48
|
+
def __init__(self, alpha=1.0):
|
|
49
|
+
self.alpha = alpha
|
|
50
|
+
self.classes_: Optional[np.ndarray] = None
|
|
51
|
+
self.feature_log_prob_: Optional[np.ndarray] = None
|
|
52
|
+
self.class_log_prior_: Optional[np.ndarray] = None
|
|
53
|
+
|
|
54
|
+
def fit(self, X, y) -> 'MultinomialNB':
|
|
55
|
+
X, y = np.array(X, dtype=float), np.array(y)
|
|
56
|
+
self.classes_ = np.unique(y)
|
|
57
|
+
K, p = len(self.classes_), X.shape[1]
|
|
58
|
+
smoothed = np.array([X[y == c].sum(axis=0) + self.alpha for c in self.classes_])
|
|
59
|
+
self.feature_log_prob_ = np.log(smoothed / smoothed.sum(axis=1, keepdims=True))
|
|
60
|
+
counts = np.array([(y == c).sum() for c in self.classes_])
|
|
61
|
+
self.class_log_prior_ = np.log(counts / len(y))
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def predict(self, X) -> np.ndarray:
|
|
65
|
+
X = np.array(X, dtype=float)
|
|
66
|
+
log_probs = X @ self.feature_log_prob_.T + self.class_log_prior_
|
|
67
|
+
return self.classes_[log_probs.argmax(axis=1)]
|
|
68
|
+
|
|
69
|
+
def predict_proba(self, X) -> np.ndarray:
|
|
70
|
+
X = np.array(X, dtype=float)
|
|
71
|
+
log_probs = X @ self.feature_log_prob_.T + self.class_log_prior_
|
|
72
|
+
log_probs -= log_probs.max(axis=1, keepdims=True)
|
|
73
|
+
probs = np.exp(log_probs)
|
|
74
|
+
return probs / probs.sum(axis=1, keepdims=True)
|
|
75
|
+
|
|
76
|
+
def score(self, X, y) -> float:
|
|
77
|
+
return float(np.mean(self.predict(X) == np.array(y)))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BernoulliNB:
|
|
81
|
+
def __init__(self, alpha=1.0, binarize=0.0):
|
|
82
|
+
self.alpha = alpha
|
|
83
|
+
self.binarize = binarize
|
|
84
|
+
self.classes_: Optional[np.ndarray] = None
|
|
85
|
+
self.feature_log_prob_: Optional[np.ndarray] = None
|
|
86
|
+
self.class_log_prior_: Optional[np.ndarray] = None
|
|
87
|
+
|
|
88
|
+
def fit(self, X, y) -> 'BernoulliNB':
|
|
89
|
+
X = np.array(X, dtype=float)
|
|
90
|
+
if self.binarize is not None:
|
|
91
|
+
X = (X > self.binarize).astype(float)
|
|
92
|
+
y = np.array(y)
|
|
93
|
+
self.classes_ = np.unique(y)
|
|
94
|
+
K = len(self.classes_)
|
|
95
|
+
smoothed_pos = np.array([X[y == c].sum(axis=0) + self.alpha for c in self.classes_])
|
|
96
|
+
smoothed_tot = np.array([(y == c).sum() + 2 * self.alpha for c in self.classes_])
|
|
97
|
+
prob = smoothed_pos / smoothed_tot[:, None]
|
|
98
|
+
self.feature_log_prob_ = np.log(prob)
|
|
99
|
+
self._feature_log_prob_neg = np.log(1 - prob)
|
|
100
|
+
counts = np.array([(y == c).sum() for c in self.classes_])
|
|
101
|
+
self.class_log_prior_ = np.log(counts / len(y))
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def predict(self, X) -> np.ndarray:
|
|
105
|
+
X = np.array(X, dtype=float)
|
|
106
|
+
if self.binarize is not None:
|
|
107
|
+
X = (X > self.binarize).astype(float)
|
|
108
|
+
log_probs = (X @ self.feature_log_prob_.T +
|
|
109
|
+
(1 - X) @ self._feature_log_prob_neg.T + self.class_log_prior_)
|
|
110
|
+
return self.classes_[log_probs.argmax(axis=1)]
|
|
111
|
+
|
|
112
|
+
def score(self, X, y) -> float:
|
|
113
|
+
return float(np.mean(self.predict(X) == np.array(y)))
|