mumpy-toolkit 0.2.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.
- mumpy/__init__.py +128 -0
- mumpy/_core.py +218 -0
- mumpy/_math.py +283 -0
- mumpy/_parallel.py +142 -0
- mumpy/db.py +289 -0
- mumpy/fft.py +132 -0
- mumpy/frame.py +428 -0
- mumpy/io.py +196 -0
- mumpy/linalg.py +62 -0
- mumpy/metrics.py +187 -0
- mumpy/ml.py +443 -0
- mumpy/nn.py +278 -0
- mumpy/preprocessing.py +259 -0
- mumpy/random.py +99 -0
- mumpy/stats.py +122 -0
- mumpy/utils.py +105 -0
- mumpy/viz.py +81 -0
- mumpy_toolkit-0.2.0.dist-info/METADATA +200 -0
- mumpy_toolkit-0.2.0.dist-info/RECORD +22 -0
- mumpy_toolkit-0.2.0.dist-info/WHEEL +5 -0
- mumpy_toolkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- mumpy_toolkit-0.2.0.dist-info/top_level.txt +1 -0
mumpy/metrics.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""mumpy.metrics: evaluation metrics for ML/DL (pure numpy)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"mse", "rmse", "mae", "mape", "r2_score", "explained_variance",
|
|
8
|
+
"accuracy", "precision", "recall", "f1", "confusion_matrix",
|
|
9
|
+
"classification_report", "log_loss", "roc_auc", "silhouette_score",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mse(y_true, y_pred):
|
|
14
|
+
y_true = np.asanyarray(y_true, dtype=float).ravel()
|
|
15
|
+
y_pred = np.asanyarray(y_pred, dtype=float).ravel()
|
|
16
|
+
return float(np.mean((y_true - y_pred) ** 2))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def rmse(y_true, y_pred):
|
|
20
|
+
return float(np.sqrt(mse(y_true, y_pred)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mae(y_true, y_pred):
|
|
24
|
+
y_true = np.asanyarray(y_true, dtype=float).ravel()
|
|
25
|
+
y_pred = np.asanyarray(y_pred, dtype=float).ravel()
|
|
26
|
+
return float(np.mean(np.abs(y_true - y_pred)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mape(y_true, y_pred):
|
|
30
|
+
y_true = np.asanyarray(y_true, dtype=float).ravel()
|
|
31
|
+
y_pred = np.asanyarray(y_pred, dtype=float).ravel()
|
|
32
|
+
mask = y_true != 0
|
|
33
|
+
return float(np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def r2_score(y_true, y_pred):
|
|
37
|
+
y_true = np.asanyarray(y_true, dtype=float).ravel()
|
|
38
|
+
y_pred = np.asanyarray(y_pred, dtype=float).ravel()
|
|
39
|
+
ss_res = np.sum((y_true - y_pred) ** 2)
|
|
40
|
+
ss_tot = np.sum((y_true - y_true.mean()) ** 2)
|
|
41
|
+
return float(1 - ss_res / ss_tot) if ss_tot != 0 else 0.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def explained_variance(y_true, y_pred):
|
|
45
|
+
y_true = np.asanyarray(y_true, dtype=float).ravel()
|
|
46
|
+
y_pred = np.asanyarray(y_pred, dtype=float).ravel()
|
|
47
|
+
return float(1 - np.var(y_true - y_pred) / np.var(y_true)) if np.var(y_true) != 0 else 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def accuracy(y_true, y_pred):
|
|
51
|
+
y_true = np.asanyarray(y_true).ravel()
|
|
52
|
+
y_pred = np.asanyarray(y_pred).ravel()
|
|
53
|
+
return float((y_true == y_pred).mean())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def confusion_matrix(y_true, y_pred, labels=None):
|
|
57
|
+
y_true = np.asanyarray(y_true).ravel()
|
|
58
|
+
y_pred = np.asanyarray(y_pred).ravel()
|
|
59
|
+
if labels is None:
|
|
60
|
+
labels = np.unique(np.concatenate([y_true, y_pred]))
|
|
61
|
+
else:
|
|
62
|
+
labels = np.asanyarray(labels)
|
|
63
|
+
idx = {v: i for i, v in enumerate(labels.tolist())}
|
|
64
|
+
m = np.zeros((len(labels), len(labels)), dtype=int)
|
|
65
|
+
for t, p in zip(y_true.tolist(), y_pred.tolist()):
|
|
66
|
+
if t in idx and p in idx:
|
|
67
|
+
m[idx[t], idx[p]] += 1
|
|
68
|
+
return m, labels
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _prf(y_true, y_pred, average="binary", pos_label=1):
|
|
72
|
+
y_true = np.asanyarray(y_true).ravel()
|
|
73
|
+
y_pred = np.asanyarray(y_pred).ravel()
|
|
74
|
+
labels = np.unique(np.concatenate([y_true, y_pred]))
|
|
75
|
+
if average == "binary":
|
|
76
|
+
tp = int(((y_pred == pos_label) & (y_true == pos_label)).sum())
|
|
77
|
+
fp = int(((y_pred == pos_label) & (y_true != pos_label)).sum())
|
|
78
|
+
fn = int(((y_pred != pos_label) & (y_true == pos_label)).sum())
|
|
79
|
+
p = tp / (tp + fp) if (tp + fp) else 0.0
|
|
80
|
+
r = tp / (tp + fn) if (tp + fn) else 0.0
|
|
81
|
+
f = 2 * p * r / (p + r) if (p + r) else 0.0
|
|
82
|
+
return p, r, f
|
|
83
|
+
ps, rs, fs = [], [], []
|
|
84
|
+
for lab in labels:
|
|
85
|
+
tp = int(((y_pred == lab) & (y_true == lab)).sum())
|
|
86
|
+
fp = int(((y_pred == lab) & (y_true != lab)).sum())
|
|
87
|
+
fn = int(((y_pred != lab) & (y_true == lab)).sum())
|
|
88
|
+
p = tp / (tp + fp) if (tp + fp) else 0.0
|
|
89
|
+
r = tp / (tp + fn) if (tp + fn) else 0.0
|
|
90
|
+
f = 2 * p * r / (p + r) if (p + r) else 0.0
|
|
91
|
+
ps.append(p)
|
|
92
|
+
rs.append(r)
|
|
93
|
+
fs.append(f)
|
|
94
|
+
if average == "macro":
|
|
95
|
+
return float(np.mean(ps)), float(np.mean(rs)), float(np.mean(fs))
|
|
96
|
+
# micro
|
|
97
|
+
tp = sum(int(((y_pred == lab) & (y_true == lab)).sum()) for lab in labels)
|
|
98
|
+
fp = sum(int(((y_pred == lab) & (y_true != lab)).sum()) for lab in labels)
|
|
99
|
+
fn = sum(int(((y_pred != lab) & (y_true == lab)).sum()) for lab in labels)
|
|
100
|
+
p = tp / (tp + fp) if (tp + fp) else 0.0
|
|
101
|
+
r = tp / (tp + fn) if (tp + fn) else 0.0
|
|
102
|
+
f = 2 * p * r / (p + r) if (p + r) else 0.0
|
|
103
|
+
return p, r, f
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def precision(y_true, y_pred, average="binary", pos_label=1):
|
|
107
|
+
return _prf(y_true, y_pred, average, pos_label)[0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def recall(y_true, y_pred, average="binary", pos_label=1):
|
|
111
|
+
return _prf(y_true, y_pred, average, pos_label)[1]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def f1(y_true, y_pred, average="binary", pos_label=1):
|
|
115
|
+
return _prf(y_true, y_pred, average, pos_label)[2]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def classification_report(y_true, y_pred):
|
|
119
|
+
y_true = np.asanyarray(y_true).ravel()
|
|
120
|
+
y_pred = np.asanyarray(y_pred).ravel()
|
|
121
|
+
labels = np.unique(np.concatenate([y_true, y_pred]))
|
|
122
|
+
lines = {}
|
|
123
|
+
for lab in labels.tolist():
|
|
124
|
+
tp = int(((y_pred == lab) & (y_true == lab)).sum())
|
|
125
|
+
fp = int(((y_pred == lab) & (y_true != lab)).sum())
|
|
126
|
+
fn = int(((y_pred != lab) & (y_true == lab)).sum())
|
|
127
|
+
sup = int((y_true == lab).sum())
|
|
128
|
+
p = tp / (tp + fp) if (tp + fp) else 0.0
|
|
129
|
+
r = tp / (tp + fn) if (tp + fn) else 0.0
|
|
130
|
+
f = 2 * p * r / (p + r) if (p + r) else 0.0
|
|
131
|
+
lines[str(lab)] = {"precision": p, "recall": r, "f1": f, "support": sup}
|
|
132
|
+
lines["accuracy"] = float((y_true == y_pred).mean())
|
|
133
|
+
return lines
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def log_loss(y_true, y_pred, eps=1e-15):
|
|
137
|
+
y_true = np.asanyarray(y_true, dtype=float)
|
|
138
|
+
y_pred = np.clip(np.asanyarray(y_pred, dtype=float), eps, 1 - eps)
|
|
139
|
+
if y_pred.ndim == 1 or y_pred.shape[1] == 1:
|
|
140
|
+
y_pred = y_pred.ravel()
|
|
141
|
+
y_true = y_true.ravel()
|
|
142
|
+
return float(-np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)))
|
|
143
|
+
# multiclass: y_true = class indices
|
|
144
|
+
n = y_true.shape[0]
|
|
145
|
+
return float(-np.mean(np.log(y_pred[np.arange(n), y_true.astype(int).ravel()])))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def roc_auc(y_true, y_score):
|
|
149
|
+
y_true = np.asanyarray(y_true).ravel()
|
|
150
|
+
y_score = np.asanyarray(y_score, dtype=float).ravel()
|
|
151
|
+
order = np.argsort(y_score)
|
|
152
|
+
y_true = y_true[order]
|
|
153
|
+
n_pos = (y_true == 1).sum()
|
|
154
|
+
n_neg = (y_true == 0).sum()
|
|
155
|
+
if n_pos == 0 or n_neg == 0:
|
|
156
|
+
return 0.5
|
|
157
|
+
ranks = np.arange(1, len(y_true) + 1)
|
|
158
|
+
sum_rank_pos = ranks[y_true == 1].sum()
|
|
159
|
+
return float((sum_rank_pos - n_pos * (n_pos + 1) / 2) / (n_pos * n_neg))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def silhouette_score(X, labels):
|
|
163
|
+
X = np.asanyarray(X, dtype=float)
|
|
164
|
+
labels = np.asanyarray(labels).ravel()
|
|
165
|
+
uniq = np.unique(labels)
|
|
166
|
+
if len(uniq) < 2:
|
|
167
|
+
return 0.0
|
|
168
|
+
# pairwise distances (ok for moderate n; sample if huge)
|
|
169
|
+
n = len(X)
|
|
170
|
+
if n > 2000:
|
|
171
|
+
idx = np.random.default_rng(0).choice(n, 2000, replace=False)
|
|
172
|
+
X, labels = X[idx], labels[idx]
|
|
173
|
+
n = 2000
|
|
174
|
+
try:
|
|
175
|
+
from scipy.spatial.distance import cdist # optional fast path
|
|
176
|
+
D = cdist(X, X)
|
|
177
|
+
except Exception:
|
|
178
|
+
D = np.sqrt(((X[:, None, :] - X[None, :, :]) ** 2).sum(-1))
|
|
179
|
+
sils = []
|
|
180
|
+
for i in range(n):
|
|
181
|
+
same = (labels == labels[i])
|
|
182
|
+
same[i] = False
|
|
183
|
+
other = labels != labels[i]
|
|
184
|
+
a = D[i][same].mean() if same.any() else 0.0
|
|
185
|
+
b = min(D[i][labels == u].mean() for u in uniq if u != labels[i])
|
|
186
|
+
sils.append((b - a) / max(a, b) if max(a, b) > 0 else 0.0)
|
|
187
|
+
return float(np.mean(sils))
|
mumpy/ml.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""mumpy.ml: classic machine learning in pure numpy (sklearn-like API).
|
|
2
|
+
|
|
3
|
+
Estimators: LinearRegression / Ridge / LogisticRegression / KNN /
|
|
4
|
+
NaiveBayes / DecisionTree / RandomForest-lite / KMeans / PCA / train_test_split
|
|
5
|
+
|
|
6
|
+
from mumpy import ml
|
|
7
|
+
model = ml.LogisticRegression().fit(X_train, y_train)
|
|
8
|
+
pred = model.predict(X_test)
|
|
9
|
+
ml.cross_val_score(model, X, y, cv=5)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"LinearRegression", "Ridge", "LogisticRegression",
|
|
17
|
+
"KNNClassifier", "KNNRegressor", "GaussianNB",
|
|
18
|
+
"DecisionTreeClassifier", "RandomForestClassifier",
|
|
19
|
+
"KMeans", "PCA", "train_test_split", "cross_val_score",
|
|
20
|
+
"kfold", "standardize_for",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def train_test_split(X, y, test_size=0.2, random_state=None, shuffle=True, stratify=None):
|
|
25
|
+
rng = np.random.default_rng(random_state)
|
|
26
|
+
X = np.asanyarray(X)
|
|
27
|
+
y = np.asanyarray(y)
|
|
28
|
+
n = len(X)
|
|
29
|
+
if stratify is not None:
|
|
30
|
+
# stratified split
|
|
31
|
+
st = np.asanyarray(stratify)
|
|
32
|
+
tr_idx, te_idx = [], []
|
|
33
|
+
for u in np.unique(st):
|
|
34
|
+
idx = np.where(st == u)[0]
|
|
35
|
+
if shuffle:
|
|
36
|
+
idx = rng.permutation(idx)
|
|
37
|
+
k = int(len(idx) * test_size)
|
|
38
|
+
te_idx += idx[:k].tolist()
|
|
39
|
+
tr_idx += idx[k:].tolist()
|
|
40
|
+
tr_idx, te_idx = np.array(tr_idx), np.array(te_idx)
|
|
41
|
+
if shuffle:
|
|
42
|
+
tr_idx = rng.permutation(tr_idx)
|
|
43
|
+
te_idx = rng.permutation(te_idx)
|
|
44
|
+
else:
|
|
45
|
+
idx = np.arange(n)
|
|
46
|
+
if shuffle:
|
|
47
|
+
idx = rng.permutation(idx)
|
|
48
|
+
k = int(n * test_size) if isinstance(test_size, float) else int(test_size)
|
|
49
|
+
te_idx, tr_idx = idx[:k], idx[k:]
|
|
50
|
+
return X[tr_idx], X[te_idx], y[tr_idx], y[te_idx]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def kfold(n, cv=5, shuffle=True, random_state=None):
|
|
54
|
+
idx = np.arange(n)
|
|
55
|
+
if shuffle:
|
|
56
|
+
idx = np.random.default_rng(random_state).permutation(idx)
|
|
57
|
+
folds = np.array_split(idx, cv)
|
|
58
|
+
for i in range(cv):
|
|
59
|
+
te = folds[i]
|
|
60
|
+
tr = np.concatenate([folds[j] for j in range(cv) if j != i])
|
|
61
|
+
yield tr, te
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cross_val_score(estimator, X, y, cv=5, scoring="accuracy"):
|
|
65
|
+
from .metrics import accuracy, r2_score
|
|
66
|
+
X = np.asanyarray(X)
|
|
67
|
+
y = np.asanyarray(y)
|
|
68
|
+
scores = []
|
|
69
|
+
for tr, te in kfold(len(X), cv=cv):
|
|
70
|
+
from copy import deepcopy
|
|
71
|
+
est = deepcopy(estimator)
|
|
72
|
+
est.fit(X[tr], y[tr])
|
|
73
|
+
p = est.predict(X[te])
|
|
74
|
+
scores.append(accuracy(y[te], p) if scoring == "accuracy" else r2_score(y[te], p))
|
|
75
|
+
return np.array(scores)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def standardize_for(X_train, X_test=None):
|
|
79
|
+
from .preprocessing import StandardScaler
|
|
80
|
+
sc = StandardScaler().fit(X_train)
|
|
81
|
+
if X_test is None:
|
|
82
|
+
return sc.transform(X_train), sc
|
|
83
|
+
return sc.transform(X_train), sc.transform(X_test), sc
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _add_bias(X):
|
|
87
|
+
return np.hstack([np.ones((X.shape[0], 1)), X])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LinearRegression:
|
|
91
|
+
"""Ordinary least squares via lstsq (stable) or normal equations."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, fit_intercept=True):
|
|
94
|
+
self.fit_intercept = fit_intercept
|
|
95
|
+
|
|
96
|
+
def fit(self, X, y):
|
|
97
|
+
X = np.asanyarray(X, dtype=float)
|
|
98
|
+
y = np.asanyarray(y, dtype=float).ravel()
|
|
99
|
+
if X.ndim == 1:
|
|
100
|
+
X = X.reshape(-1, 1)
|
|
101
|
+
self.n_features_in_ = X.shape[1]
|
|
102
|
+
A = _add_bias(X) if self.fit_intercept else X
|
|
103
|
+
coef, *_ = np.linalg.lstsq(A, y, rcond=None)
|
|
104
|
+
if self.fit_intercept:
|
|
105
|
+
self.intercept_ = float(coef[0])
|
|
106
|
+
self.coef_ = coef[1:]
|
|
107
|
+
else:
|
|
108
|
+
self.intercept_ = 0.0
|
|
109
|
+
self.coef_ = coef
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def predict(self, X):
|
|
113
|
+
X = np.asanyarray(X, dtype=float)
|
|
114
|
+
if X.ndim == 1:
|
|
115
|
+
X = X.reshape(-1, 1)
|
|
116
|
+
return X @ self.coef_ + self.intercept_
|
|
117
|
+
|
|
118
|
+
def score(self, X, y):
|
|
119
|
+
from .metrics import r2_score
|
|
120
|
+
return r2_score(y, self.predict(X))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Ridge(LinearRegression):
|
|
124
|
+
def __init__(self, alpha=1.0, fit_intercept=True):
|
|
125
|
+
super().__init__(fit_intercept)
|
|
126
|
+
self.alpha = alpha
|
|
127
|
+
|
|
128
|
+
def fit(self, X, y):
|
|
129
|
+
X = np.asanyarray(X, dtype=float)
|
|
130
|
+
y = np.asanyarray(y, dtype=float).ravel()
|
|
131
|
+
if X.ndim == 1:
|
|
132
|
+
X = X.reshape(-1, 1)
|
|
133
|
+
self.n_features_in_ = X.shape[1]
|
|
134
|
+
if self.fit_intercept:
|
|
135
|
+
mu_x, mu_y = X.mean(0), y.mean()
|
|
136
|
+
Xc, yc = X - mu_x, y - mu_y
|
|
137
|
+
A = Xc.T @ Xc + self.alpha * np.eye(X.shape[1])
|
|
138
|
+
self.coef_ = np.linalg.solve(A, Xc.T @ yc)
|
|
139
|
+
self.intercept_ = float(mu_y - mu_x @ self.coef_)
|
|
140
|
+
else:
|
|
141
|
+
A = X.T @ X + self.alpha * np.eye(X.shape[1])
|
|
142
|
+
self.coef_ = np.linalg.solve(A, X.T @ y)
|
|
143
|
+
self.intercept_ = 0.0
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _sigmoid(z):
|
|
148
|
+
out = np.empty_like(z, dtype=float)
|
|
149
|
+
pos = z >= 0
|
|
150
|
+
out[pos] = 1 / (1 + np.exp(-z[pos]))
|
|
151
|
+
e = np.exp(z[~pos])
|
|
152
|
+
out[~pos] = e / (1 + e)
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LogisticRegression:
|
|
157
|
+
"""Binary (+ softmax multinomial) logistic regression with L2."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, lr=0.1, epochs=1000, l2=1e-4, tol=1e-6, verbose=False, seed=0):
|
|
160
|
+
self.lr = lr
|
|
161
|
+
self.epochs = epochs
|
|
162
|
+
self.l2 = l2
|
|
163
|
+
self.tol = tol
|
|
164
|
+
self.verbose = verbose
|
|
165
|
+
self.seed = seed
|
|
166
|
+
|
|
167
|
+
def fit(self, X, y):
|
|
168
|
+
rng = np.random.default_rng(self.seed)
|
|
169
|
+
X = np.asanyarray(X, dtype=float)
|
|
170
|
+
y = np.asanyarray(y).ravel()
|
|
171
|
+
if X.ndim == 1:
|
|
172
|
+
X = X.reshape(-1, 1)
|
|
173
|
+
self.classes_ = np.unique(y)
|
|
174
|
+
n, d = X.shape
|
|
175
|
+
if len(self.classes_) == 2:
|
|
176
|
+
yc = (y == self.classes_[1]).astype(float)
|
|
177
|
+
w = np.zeros(d)
|
|
178
|
+
b = 0.0
|
|
179
|
+
prev = np.inf
|
|
180
|
+
for ep in range(self.epochs):
|
|
181
|
+
z = X @ w + b
|
|
182
|
+
p = _sigmoid(z)
|
|
183
|
+
err = p - yc
|
|
184
|
+
gw = X.T @ err / n + self.l2 * w
|
|
185
|
+
gb = err.mean()
|
|
186
|
+
w -= self.lr * gw
|
|
187
|
+
b -= self.lr * gb
|
|
188
|
+
loss = -(yc * np.log(p + 1e-12) + (1 - yc) * np.log(1 - p + 1e-12)).mean()
|
|
189
|
+
if abs(prev - loss) < self.tol:
|
|
190
|
+
break
|
|
191
|
+
prev = loss
|
|
192
|
+
self.coef_ = w
|
|
193
|
+
self.intercept_ = float(b)
|
|
194
|
+
else:
|
|
195
|
+
K = len(self.classes_)
|
|
196
|
+
W = rng.normal(0, 0.01, (d, K))
|
|
197
|
+
B = np.zeros(K)
|
|
198
|
+
idx = {v: i for i, v in enumerate(self.classes_.tolist())}
|
|
199
|
+
Y = np.zeros((n, K))
|
|
200
|
+
Y[np.arange(n), [idx[v] for v in y.tolist()]] = 1.0
|
|
201
|
+
for ep in range(self.epochs):
|
|
202
|
+
Z = X @ W + B
|
|
203
|
+
Z -= Z.max(1, keepdims=True)
|
|
204
|
+
P = np.exp(Z)
|
|
205
|
+
P /= P.sum(1, keepdims=True)
|
|
206
|
+
G = (P - Y) / n
|
|
207
|
+
W -= self.lr * (X.T @ G + self.l2 * W)
|
|
208
|
+
B -= self.lr * G.sum(0)
|
|
209
|
+
self.coef_ = W
|
|
210
|
+
self.intercept_ = B
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def predict_proba(self, X):
|
|
214
|
+
X = np.asanyarray(X, dtype=float)
|
|
215
|
+
if len(self.classes_) == 2:
|
|
216
|
+
p1 = _sigmoid(X @ self.coef_ + self.intercept_)
|
|
217
|
+
return np.column_stack([1 - p1, p1])
|
|
218
|
+
Z = X @ self.coef_ + self.intercept_
|
|
219
|
+
Z -= Z.max(1, keepdims=True)
|
|
220
|
+
P = np.exp(Z)
|
|
221
|
+
return P / P.sum(1, keepdims=True)
|
|
222
|
+
|
|
223
|
+
def predict(self, X):
|
|
224
|
+
P = self.predict_proba(X)
|
|
225
|
+
return self.classes_[np.argmax(P, axis=1)]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _KNN:
|
|
229
|
+
def __init__(self, k=5):
|
|
230
|
+
self.k = k
|
|
231
|
+
|
|
232
|
+
def fit(self, X, y):
|
|
233
|
+
self.X_ = np.asanyarray(X, dtype=float)
|
|
234
|
+
self.y_ = np.asanyarray(y)
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def _neighbors(self, X):
|
|
238
|
+
X = np.asanyarray(X, dtype=float)
|
|
239
|
+
# chunked distances to save memory
|
|
240
|
+
idx = []
|
|
241
|
+
bs = 512
|
|
242
|
+
for s in range(0, len(X), bs):
|
|
243
|
+
d2 = ((X[s:s + bs, None, :] - self.X_[None, :, :]) ** 2).sum(-1)
|
|
244
|
+
idx.append(np.argpartition(d2, self.k, axis=1)[:, :self.k])
|
|
245
|
+
return np.vstack(idx)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class KNNClassifier(_KNN):
|
|
249
|
+
def predict(self, X):
|
|
250
|
+
idx = self._neighbors(X)
|
|
251
|
+
out = []
|
|
252
|
+
for row in idx:
|
|
253
|
+
vals, counts = np.unique(self.y_[row], return_counts=True)
|
|
254
|
+
out.append(vals[np.argmax(counts)])
|
|
255
|
+
return np.array(out)
|
|
256
|
+
|
|
257
|
+
def predict_proba(self, X):
|
|
258
|
+
idx = self._neighbors(X)
|
|
259
|
+
classes = np.unique(self.y_)
|
|
260
|
+
P = np.zeros((len(X), len(classes)))
|
|
261
|
+
for i, row in enumerate(idx):
|
|
262
|
+
for j, c in enumerate(classes):
|
|
263
|
+
P[i, j] = (self.y_[row] == c).mean()
|
|
264
|
+
return P
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class KNNRegressor(_KNN):
|
|
268
|
+
def predict(self, X):
|
|
269
|
+
idx = self._neighbors(X)
|
|
270
|
+
return np.array([self.y_[row].astype(float).mean() for row in idx])
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class GaussianNB:
|
|
274
|
+
def fit(self, X, y):
|
|
275
|
+
X = np.asanyarray(X, dtype=float)
|
|
276
|
+
y = np.asanyarray(y).ravel()
|
|
277
|
+
self.classes_ = np.unique(y)
|
|
278
|
+
self.theta_ = np.array([X[y == c].mean(0) for c in self.classes_])
|
|
279
|
+
self.sigma_ = np.array([X[y == c].var(0) + 1e-9 for c in self.classes_])
|
|
280
|
+
self.priors_ = np.array([(y == c).mean() for c in self.classes_])
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def predict(self, X):
|
|
284
|
+
X = np.asanyarray(X, dtype=float)
|
|
285
|
+
jll = []
|
|
286
|
+
for t, s, p in zip(self.theta_, self.sigma_, self.priors_):
|
|
287
|
+
jll.append(-0.5 * np.sum(np.log(2 * np.pi * s) + (X - t) ** 2 / s, axis=1) + np.log(p))
|
|
288
|
+
return self.classes_[np.argmax(np.column_stack(jll), axis=1)]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class DecisionTreeClassifier:
|
|
292
|
+
"""CART binary tree (gini), max_depth + min_samples_split."""
|
|
293
|
+
|
|
294
|
+
def __init__(self, max_depth=8, min_samples_split=2, seed=0):
|
|
295
|
+
self.max_depth = max_depth
|
|
296
|
+
self.min_samples_split = min_samples_split
|
|
297
|
+
self.seed = seed
|
|
298
|
+
|
|
299
|
+
def fit(self, X, y):
|
|
300
|
+
self.n_classes_ = len(np.unique(y))
|
|
301
|
+
self.classes_ = np.unique(np.asanyarray(y).ravel())
|
|
302
|
+
self._map = {v: i for i, v in enumerate(self.classes_.tolist())}
|
|
303
|
+
yi = np.array([self._map[v] for v in np.asanyarray(y).ravel().tolist()])
|
|
304
|
+
self.tree_ = self._build(np.asanyarray(X, dtype=float), yi, 0)
|
|
305
|
+
return self
|
|
306
|
+
|
|
307
|
+
def _gini(self, y):
|
|
308
|
+
if len(y) == 0:
|
|
309
|
+
return 0
|
|
310
|
+
_, c = np.unique(y, return_counts=True)
|
|
311
|
+
p = c / c.sum()
|
|
312
|
+
return 1 - (p ** 2).sum()
|
|
313
|
+
|
|
314
|
+
def _build(self, X, y, depth):
|
|
315
|
+
n, d = X.shape
|
|
316
|
+
counts = np.bincount(y, minlength=len(self.classes_))
|
|
317
|
+
pred = int(np.argmax(counts))
|
|
318
|
+
if depth >= self.max_depth or n < self.min_samples_split or self._gini(y) == 0:
|
|
319
|
+
return {"leaf": True, "pred": pred}
|
|
320
|
+
best = (1e9, None, None)
|
|
321
|
+
rng = np.random.default_rng(self.seed + depth)
|
|
322
|
+
feats = rng.choice(d, min(d, max(1, int(np.sqrt(d)))), replace=False) if d > 4 else range(d)
|
|
323
|
+
for f in feats:
|
|
324
|
+
ths = np.unique(X[:, f])
|
|
325
|
+
if len(ths) > 20:
|
|
326
|
+
ths = np.percentile(ths, np.linspace(0, 100, 20))
|
|
327
|
+
for t in ths:
|
|
328
|
+
l, r = y[X[:, f] <= t], y[X[:, f] > t]
|
|
329
|
+
if len(l) == 0 or len(r) == 0:
|
|
330
|
+
continue
|
|
331
|
+
g = (len(l) * self._gini(l) + len(r) * self._gini(r)) / n
|
|
332
|
+
if g < best[0]:
|
|
333
|
+
best = (g, f, t)
|
|
334
|
+
if best[1] is None:
|
|
335
|
+
return {"leaf": True, "pred": pred}
|
|
336
|
+
_, f, t = best
|
|
337
|
+
lm = X[:, f] <= t
|
|
338
|
+
return {"leaf": False, "f": int(f), "t": float(t),
|
|
339
|
+
"l": self._build(X[lm], y[lm], depth + 1),
|
|
340
|
+
"r": self._build(X[~lm], y[~lm], depth + 1)}
|
|
341
|
+
|
|
342
|
+
def _predict_one(self, x, node):
|
|
343
|
+
while not node["leaf"]:
|
|
344
|
+
node = node["l"] if x[node["f"]] <= node["t"] else node["r"]
|
|
345
|
+
return node["pred"]
|
|
346
|
+
|
|
347
|
+
def predict(self, X):
|
|
348
|
+
X = np.asanyarray(X, dtype=float)
|
|
349
|
+
idx = np.array([self._predict_one(x, self.tree_) for x in X])
|
|
350
|
+
return self.classes_[idx]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class RandomForestClassifier:
|
|
354
|
+
def __init__(self, n_estimators=20, max_depth=8, seed=0):
|
|
355
|
+
self.n_estimators = n_estimators
|
|
356
|
+
self.max_depth = max_depth
|
|
357
|
+
self.seed = seed
|
|
358
|
+
|
|
359
|
+
def fit(self, X, y):
|
|
360
|
+
X = np.asanyarray(X)
|
|
361
|
+
y = np.asanyarray(y)
|
|
362
|
+
rng = np.random.default_rng(self.seed)
|
|
363
|
+
self.trees_ = []
|
|
364
|
+
n = len(X)
|
|
365
|
+
for i in range(self.n_estimators):
|
|
366
|
+
idx = rng.integers(0, n, n)
|
|
367
|
+
t = DecisionTreeClassifier(max_depth=self.max_depth, seed=self.seed + i)
|
|
368
|
+
t.fit(X[idx], y[idx])
|
|
369
|
+
self.trees_.append(t)
|
|
370
|
+
self.classes_ = self.trees_[0].classes_
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
def predict(self, X):
|
|
374
|
+
votes = np.column_stack([t.predict(X) for t in self.trees_])
|
|
375
|
+
out = []
|
|
376
|
+
for row in votes:
|
|
377
|
+
vals, counts = np.unique(row, return_counts=True)
|
|
378
|
+
out.append(vals[np.argmax(counts)])
|
|
379
|
+
return np.array(out)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class KMeans:
|
|
383
|
+
def __init__(self, n_clusters=3, max_iter=100, tol=1e-4, seed=0, n_init=3):
|
|
384
|
+
self.n_clusters = n_clusters
|
|
385
|
+
self.max_iter = max_iter
|
|
386
|
+
self.tol = tol
|
|
387
|
+
self.seed = seed
|
|
388
|
+
self.n_init = n_init
|
|
389
|
+
|
|
390
|
+
def fit(self, X):
|
|
391
|
+
X = np.asanyarray(X, dtype=float)
|
|
392
|
+
best_inertia, best = np.inf, None
|
|
393
|
+
for init in range(self.n_init):
|
|
394
|
+
rng = np.random.default_rng(self.seed + init)
|
|
395
|
+
C = X[rng.choice(len(X), self.n_clusters, replace=False)]
|
|
396
|
+
for _ in range(self.max_iter):
|
|
397
|
+
d2 = ((X[:, None, :] - C[None, :, :]) ** 2).sum(-1)
|
|
398
|
+
lab = d2.argmin(1)
|
|
399
|
+
Cn = np.array([X[lab == k].mean(0) if (lab == k).any() else C[k]
|
|
400
|
+
for k in range(self.n_clusters)])
|
|
401
|
+
if np.linalg.norm(Cn - C) < self.tol:
|
|
402
|
+
C = Cn
|
|
403
|
+
break
|
|
404
|
+
C = Cn
|
|
405
|
+
inertia = float(((X - C[lab]) ** 2).sum())
|
|
406
|
+
if inertia < best_inertia:
|
|
407
|
+
best_inertia, best = inertia, (C, lab)
|
|
408
|
+
self.cluster_centers_, labels = best
|
|
409
|
+
self.labels_ = labels
|
|
410
|
+
self.inertia_ = best_inertia
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
def predict(self, X):
|
|
414
|
+
X = np.asanyarray(X, dtype=float)
|
|
415
|
+
d2 = ((X[:, None, :] - self.cluster_centers_[None, :, :]) ** 2).sum(-1)
|
|
416
|
+
return d2.argmin(1)
|
|
417
|
+
|
|
418
|
+
def fit_predict(self, X):
|
|
419
|
+
return self.fit(X).labels_
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class PCA:
|
|
423
|
+
def __init__(self, n_components=2):
|
|
424
|
+
self.n_components = n_components
|
|
425
|
+
|
|
426
|
+
def fit(self, X):
|
|
427
|
+
X = np.asanyarray(X, dtype=float)
|
|
428
|
+
self.mean_ = X.mean(0)
|
|
429
|
+
Xc = X - self.mean_
|
|
430
|
+
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
|
|
431
|
+
self.components_ = Vt[:self.n_components]
|
|
432
|
+
self.explained_variance_ = (S ** 2) / (len(X) - 1)
|
|
433
|
+
self.explained_variance_ratio_ = self.explained_variance_ / self.explained_variance_.sum()
|
|
434
|
+
return self
|
|
435
|
+
|
|
436
|
+
def transform(self, X):
|
|
437
|
+
return (np.asanyarray(X, dtype=float) - self.mean_) @ self.components_.T
|
|
438
|
+
|
|
439
|
+
def fit_transform(self, X):
|
|
440
|
+
return self.fit(X).transform(X)
|
|
441
|
+
|
|
442
|
+
def inverse_transform(self, Z):
|
|
443
|
+
return np.asanyarray(Z) @ self.components_ + self.mean_
|