claros 0.7.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.
- claros/__init__.py +58 -0
- claros/base.py +27 -0
- claros/cluster.py +109 -0
- claros/decomposition.py +32 -0
- claros/ensemble.py +132 -0
- claros/feature_selection.py +26 -0
- claros/linear_model.py +51 -0
- claros/links.py +34 -0
- claros/losses.py +58 -0
- claros/metrics.py +42 -0
- claros/model_selection.py +57 -0
- claros/multiclass.py +36 -0
- claros/naive_bayes.py +42 -0
- claros/neighbors.py +35 -0
- claros/pipeline.py +49 -0
- claros/preprocessing.py +16 -0
- claros/svm.py +32 -0
- claros/tree.py +92 -0
- claros-0.7.0.dist-info/METADATA +318 -0
- claros-0.7.0.dist-info/RECORD +23 -0
- claros-0.7.0.dist-info/WHEEL +5 -0
- claros-0.7.0.dist-info/licenses/LICENSE +21 -0
- claros-0.7.0.dist-info/top_level.txt +1 -0
claros/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Claros — учебная ML-библиотека, написанная с нуля на чистом NumPy.
|
|
2
|
+
|
|
3
|
+
Ничего не берётся из готовых ML-библиотек — всё реализовано руками.
|
|
4
|
+
|
|
5
|
+
Модели
|
|
6
|
+
линейные : LinearRegression, LogisticRegression
|
|
7
|
+
SVM : LinearSVC
|
|
8
|
+
байес : GaussianNB
|
|
9
|
+
деревья : DecisionTreeRegressor, DecisionTreeClassifier
|
|
10
|
+
ансамбли : RandomForestRegressor/Classifier, GradientBoostingRegressor/Classifier
|
|
11
|
+
соседи : KNeighborsRegressor, KNeighborsClassifier
|
|
12
|
+
мультикласс: OneVsRestClassifier (обёртка над бинарной моделью)
|
|
13
|
+
Кластеризация: DBSCAN, KMeans
|
|
14
|
+
Снижение разм.: PCA
|
|
15
|
+
Предобработка: StandardScaler, NoiseFeatureRemover
|
|
16
|
+
Подбор/оценка: Pipeline, train_test_split, cross_val_score, GridSearchCV, claros.metrics
|
|
17
|
+
Удобство : prep(model, scale=1, clean_noise_features=1) — предобработка без Pipeline
|
|
18
|
+
|
|
19
|
+
Своё, чего нет в sklearn: DeadZoneLoss, ArctanLink, NoiseFeatureRemover.
|
|
20
|
+
"""
|
|
21
|
+
from .base import Estimator, RegressorMixin, ClassifierMixin
|
|
22
|
+
from .losses import Loss, DeadZoneLoss, LogLoss, MSE, HingeLoss
|
|
23
|
+
from .links import Link, Identity, Sigmoid, ArctanLink
|
|
24
|
+
from .linear_model import LinearRegression, LogisticRegression
|
|
25
|
+
from .svm import LinearSVC
|
|
26
|
+
from .naive_bayes import GaussianNB
|
|
27
|
+
from .tree import DecisionTreeRegressor, DecisionTreeClassifier
|
|
28
|
+
from .ensemble import (RandomForestRegressor, RandomForestClassifier,
|
|
29
|
+
GradientBoostingRegressor, GradientBoostingClassifier)
|
|
30
|
+
from .neighbors import KNeighborsRegressor, KNeighborsClassifier
|
|
31
|
+
from .multiclass import OneVsRestClassifier
|
|
32
|
+
from .preprocessing import StandardScaler
|
|
33
|
+
from .feature_selection import NoiseFeatureRemover
|
|
34
|
+
from .decomposition import PCA
|
|
35
|
+
from .cluster import DBSCAN, KMeans
|
|
36
|
+
from .pipeline import Pipeline, prep
|
|
37
|
+
from .model_selection import train_test_split, cross_val_score, GridSearchCV
|
|
38
|
+
from . import metrics
|
|
39
|
+
|
|
40
|
+
__version__ = "0.7.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Estimator", "RegressorMixin", "ClassifierMixin",
|
|
44
|
+
"Loss", "DeadZoneLoss", "LogLoss", "MSE", "HingeLoss",
|
|
45
|
+
"Link", "Identity", "Sigmoid", "ArctanLink",
|
|
46
|
+
"LinearRegression", "LogisticRegression",
|
|
47
|
+
"LinearSVC", "GaussianNB",
|
|
48
|
+
"DecisionTreeRegressor", "DecisionTreeClassifier",
|
|
49
|
+
"RandomForestRegressor", "RandomForestClassifier",
|
|
50
|
+
"GradientBoostingRegressor", "GradientBoostingClassifier",
|
|
51
|
+
"KNeighborsRegressor", "KNeighborsClassifier",
|
|
52
|
+
"OneVsRestClassifier",
|
|
53
|
+
"PCA",
|
|
54
|
+
"StandardScaler", "NoiseFeatureRemover",
|
|
55
|
+
"DBSCAN", "KMeans",
|
|
56
|
+
"Pipeline", "prep", "train_test_split", "cross_val_score", "GridSearchCV",
|
|
57
|
+
"metrics",
|
|
58
|
+
]
|
claros/base.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Estimator: # общий базовый класс: даёт get_params / set_params (клонирование, GridSearch)
|
|
6
|
+
def get_params(self):
|
|
7
|
+
params = inspect.signature(self.__init__).parameters.values()
|
|
8
|
+
return {p.name: getattr(self, p.name) for p in params
|
|
9
|
+
if p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)}
|
|
10
|
+
|
|
11
|
+
def set_params(self, **params):
|
|
12
|
+
for key, value in params.items():
|
|
13
|
+
setattr(self, key, value)
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RegressorMixin: # даёт регрессорам метод score (R²)
|
|
18
|
+
def score(self, X, y):
|
|
19
|
+
y_pred = self.predict(X)
|
|
20
|
+
ss_res = np.sum((y - y_pred) ** 2)
|
|
21
|
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
|
22
|
+
return 1 - ss_res / ss_tot
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClassifierMixin: # даёт классификаторам метод score (доля верных ответов)
|
|
26
|
+
def score(self, X, y):
|
|
27
|
+
return np.mean(self.predict(X) == y)
|
claros/cluster.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Кластеризация: DBSCAN (по плотности) и KMeans (по центрам)."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator
|
|
4
|
+
from .feature_selection import NoiseFeatureRemover
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DBSCAN(Estimator):
|
|
8
|
+
"""Плотностная кластеризация. Сгустки — кластеры, разреженные точки — шум (метка -1).
|
|
9
|
+
|
|
10
|
+
clean_noise_features=1 включает нашу доработку: перед кластеризацией убрать
|
|
11
|
+
шумовые признаки (NoiseFeatureRemover). 0 — обычный DBSCAN (совпадает со sklearn).
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, eps=0.5, min_samples=5, clean_noise_features=0, feature_threshold=0.9):
|
|
14
|
+
self.eps = eps
|
|
15
|
+
self.min_samples = min_samples
|
|
16
|
+
self.clean_noise_features = clean_noise_features
|
|
17
|
+
self.feature_threshold = feature_threshold
|
|
18
|
+
|
|
19
|
+
def _neighbors(self, X, i):
|
|
20
|
+
dist = np.sqrt(((X - X[i]) ** 2).sum(axis=1))
|
|
21
|
+
return np.where(dist <= self.eps)[0]
|
|
22
|
+
|
|
23
|
+
def fit(self, X):
|
|
24
|
+
X = np.asarray(X, float)
|
|
25
|
+
if self.clean_noise_features:
|
|
26
|
+
self.remover_ = NoiseFeatureRemover(threshold=self.feature_threshold)
|
|
27
|
+
X = self.remover_.fit_transform(X)
|
|
28
|
+
n = X.shape[0]
|
|
29
|
+
labels = np.full(n, -2) # -2 не посещён, -1 шум, >=0 номер кластера
|
|
30
|
+
cluster = 0
|
|
31
|
+
for i in range(n):
|
|
32
|
+
if labels[i] != -2:
|
|
33
|
+
continue
|
|
34
|
+
neigh = self._neighbors(X, i)
|
|
35
|
+
if len(neigh) < self.min_samples:
|
|
36
|
+
labels[i] = -1
|
|
37
|
+
continue
|
|
38
|
+
labels[i] = cluster
|
|
39
|
+
seeds = list(neigh)
|
|
40
|
+
k = 0
|
|
41
|
+
while k < len(seeds): # заливка: очередь растёт по мере находок
|
|
42
|
+
j = seeds[k]; k += 1
|
|
43
|
+
if labels[j] == -1:
|
|
44
|
+
labels[j] = cluster # бывший шум — граница кластера
|
|
45
|
+
if labels[j] != -2:
|
|
46
|
+
continue
|
|
47
|
+
labels[j] = cluster
|
|
48
|
+
j_neigh = self._neighbors(X, j)
|
|
49
|
+
if len(j_neigh) >= self.min_samples:
|
|
50
|
+
seeds.extend(j_neigh) # j тоже ядро — доливаем его соседей
|
|
51
|
+
cluster += 1
|
|
52
|
+
self.labels_ = labels
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def fit_predict(self, X):
|
|
56
|
+
return self.fit(X).labels_
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class KMeans(Estimator):
|
|
60
|
+
"""Кластеризация по центрам (Ллойд) с n_init перезапусками (лучший по inertia).
|
|
61
|
+
|
|
62
|
+
clean_noise_features=1 — убрать шумовые признаки перед кластеризацией (как у DBSCAN).
|
|
63
|
+
"""
|
|
64
|
+
def __init__(self, n_clusters=3, max_iter=100, n_init=10, random_state=None,
|
|
65
|
+
clean_noise_features=0, feature_threshold=0.9):
|
|
66
|
+
self.n_clusters = n_clusters
|
|
67
|
+
self.max_iter = max_iter
|
|
68
|
+
self.n_init = n_init
|
|
69
|
+
self.random_state = random_state
|
|
70
|
+
self.clean_noise_features = clean_noise_features
|
|
71
|
+
self.feature_threshold = feature_threshold
|
|
72
|
+
|
|
73
|
+
def _one_run(self, X, rng):
|
|
74
|
+
centers = X[rng.choice(len(X), self.n_clusters, replace=False)].copy()
|
|
75
|
+
labels = np.zeros(len(X), dtype=int)
|
|
76
|
+
for _ in range(self.max_iter):
|
|
77
|
+
dist = np.sqrt(((X[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2))
|
|
78
|
+
labels = dist.argmin(axis=1)
|
|
79
|
+
new = np.array([X[labels == c].mean(axis=0) if np.any(labels == c) else centers[c]
|
|
80
|
+
for c in range(self.n_clusters)])
|
|
81
|
+
if np.allclose(new, centers):
|
|
82
|
+
break
|
|
83
|
+
centers = new
|
|
84
|
+
inertia = ((X - centers[labels]) ** 2).sum()
|
|
85
|
+
return centers, labels, inertia
|
|
86
|
+
|
|
87
|
+
def fit(self, X, y=None):
|
|
88
|
+
X = np.asarray(X, float)
|
|
89
|
+
if self.clean_noise_features:
|
|
90
|
+
self.remover_ = NoiseFeatureRemover(threshold=self.feature_threshold)
|
|
91
|
+
X = self.remover_.fit_transform(X)
|
|
92
|
+
rng = np.random.default_rng(self.random_state)
|
|
93
|
+
best = None
|
|
94
|
+
for _ in range(self.n_init):
|
|
95
|
+
centers, labels, inertia = self._one_run(X, rng)
|
|
96
|
+
if best is None or inertia < best[2]:
|
|
97
|
+
best = (centers, labels, inertia)
|
|
98
|
+
self.cluster_centers_, self.labels_, self.inertia_ = best
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def predict(self, X):
|
|
102
|
+
X = np.asarray(X, float)
|
|
103
|
+
if self.clean_noise_features:
|
|
104
|
+
X = self.remover_.transform(X)
|
|
105
|
+
dist = np.sqrt(((X[:, None, :] - self.cluster_centers_[None, :, :]) ** 2).sum(axis=2))
|
|
106
|
+
return dist.argmin(axis=1)
|
|
107
|
+
|
|
108
|
+
def fit_predict(self, X, y=None):
|
|
109
|
+
return self.fit(X).labels_
|
claros/decomposition.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Снижение размерности: метод главных компонент (PCA)."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PCA(Estimator):
|
|
7
|
+
"""Метод главных компонент: проекция на направления максимальной дисперсии.
|
|
8
|
+
|
|
9
|
+
Центрируем данные и берём ведущие правые сингулярные векторы (через SVD) —
|
|
10
|
+
это и есть главные компоненты. transform проецирует на них.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, n_components=2):
|
|
13
|
+
self.n_components = n_components
|
|
14
|
+
|
|
15
|
+
def fit(self, X, y=None):
|
|
16
|
+
X = np.asarray(X, float)
|
|
17
|
+
self.mean_ = X.mean(axis=0)
|
|
18
|
+
U, S, Vt = np.linalg.svd(X - self.mean_, full_matrices=False)
|
|
19
|
+
var = (S ** 2) / (len(X) - 1)
|
|
20
|
+
self.components_ = Vt[:self.n_components] # (k, d) — оси
|
|
21
|
+
self.explained_variance_ = var[:self.n_components]
|
|
22
|
+
self.explained_variance_ratio_ = (var / var.sum())[:self.n_components]
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def transform(self, X):
|
|
26
|
+
return (np.asarray(X, float) - self.mean_) @ self.components_.T
|
|
27
|
+
|
|
28
|
+
def fit_transform(self, X, y=None):
|
|
29
|
+
return self.fit(X).transform(X)
|
|
30
|
+
|
|
31
|
+
def inverse_transform(self, Xt):
|
|
32
|
+
return np.asarray(Xt, float) @ self.components_ + self.mean_
|
claros/ensemble.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Ансамбли деревьев: случайный лес (бэггинг) и градиентный бустинг."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator, RegressorMixin, ClassifierMixin
|
|
4
|
+
from .tree import DecisionTreeRegressor, DecisionTreeClassifier
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _sigmoid(z):
|
|
8
|
+
return 1.0 / (1.0 + np.exp(-z))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Forest(Estimator):
|
|
12
|
+
"""Бэггинг: много деревьев на бутстрэп-выборках со случайным подмножеством признаков."""
|
|
13
|
+
def __init__(self, n_estimators=50, max_depth=10, max_features="sqrt", random_state=None):
|
|
14
|
+
self.n_estimators = n_estimators
|
|
15
|
+
self.max_depth = max_depth
|
|
16
|
+
self.max_features = max_features
|
|
17
|
+
self.random_state = random_state
|
|
18
|
+
|
|
19
|
+
def _mf(self, d):
|
|
20
|
+
if self.max_features == "sqrt":
|
|
21
|
+
return max(1, int(np.sqrt(d)))
|
|
22
|
+
if self.max_features is None:
|
|
23
|
+
return d
|
|
24
|
+
return self.max_features
|
|
25
|
+
|
|
26
|
+
def fit(self, X, y):
|
|
27
|
+
X, y = np.asarray(X, float), np.asarray(y)
|
|
28
|
+
rng = np.random.default_rng(self.random_state)
|
|
29
|
+
n, d = X.shape
|
|
30
|
+
mf = self._mf(d)
|
|
31
|
+
self.trees_ = []
|
|
32
|
+
for _ in range(self.n_estimators):
|
|
33
|
+
idx = rng.integers(0, n, n) # бутстрэп: выборка с возвращением
|
|
34
|
+
seed = int(rng.integers(0, 2 ** 31 - 1))
|
|
35
|
+
self.trees_.append(self._make_tree(mf, seed).fit(X[idx], y[idx]))
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def _all_preds(self, X):
|
|
39
|
+
return np.array([t.predict(X) for t in self.trees_]) # (деревья, объекты)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RandomForestRegressor(_Forest, RegressorMixin):
|
|
43
|
+
"""Случайный лес для регрессии: усреднение предсказаний деревьев."""
|
|
44
|
+
def _make_tree(self, mf, seed):
|
|
45
|
+
return DecisionTreeRegressor(max_depth=self.max_depth, max_features=mf, random_state=seed)
|
|
46
|
+
|
|
47
|
+
def predict(self, X):
|
|
48
|
+
return self._all_preds(np.asarray(X, float)).mean(axis=0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RandomForestClassifier(_Forest, ClassifierMixin):
|
|
52
|
+
"""Случайный лес для классификации: голосование большинством."""
|
|
53
|
+
def _make_tree(self, mf, seed):
|
|
54
|
+
return DecisionTreeClassifier(max_depth=self.max_depth, max_features=mf, random_state=seed)
|
|
55
|
+
|
|
56
|
+
def predict(self, X):
|
|
57
|
+
preds = self._all_preds(np.asarray(X, float))
|
|
58
|
+
return np.array([self._vote(preds[:, j]) for j in range(preds.shape[1])])
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _vote(col):
|
|
62
|
+
vals, counts = np.unique(col, return_counts=True)
|
|
63
|
+
return vals[np.argmax(counts)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GradientBoostingRegressor(Estimator, RegressorMixin):
|
|
67
|
+
"""Градиентный бустинг (CatBoost-стиль) для регрессии.
|
|
68
|
+
|
|
69
|
+
Стартуем со среднего и последовательно добавляем неглубокие деревья;
|
|
70
|
+
каждое предсказывает остаток y − F (антиградиент квадратичной потери).
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
|
|
73
|
+
self.n_estimators = n_estimators
|
|
74
|
+
self.learning_rate = learning_rate
|
|
75
|
+
self.max_depth = max_depth
|
|
76
|
+
|
|
77
|
+
def fit(self, X, y):
|
|
78
|
+
X, y = np.asarray(X, float), np.asarray(y, float)
|
|
79
|
+
self.init_ = float(np.mean(y))
|
|
80
|
+
F = np.full(len(y), self.init_)
|
|
81
|
+
self.trees_ = []
|
|
82
|
+
for _ in range(self.n_estimators):
|
|
83
|
+
residual = y - F
|
|
84
|
+
tree = DecisionTreeRegressor(max_depth=self.max_depth).fit(X, residual)
|
|
85
|
+
F += self.learning_rate * tree.predict(X)
|
|
86
|
+
self.trees_.append(tree)
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def predict(self, X):
|
|
90
|
+
X = np.asarray(X, float)
|
|
91
|
+
F = np.full(X.shape[0], self.init_)
|
|
92
|
+
for tree in self.trees_:
|
|
93
|
+
F += self.learning_rate * tree.predict(X)
|
|
94
|
+
return F
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GradientBoostingClassifier(Estimator, ClassifierMixin):
|
|
98
|
+
"""Градиентный бустинг для бинарной классификации (log-loss).
|
|
99
|
+
|
|
100
|
+
Бустим в пространстве log-odds: каждое дерево учится на остатке y − p,
|
|
101
|
+
где p = sigmoid(текущей суммы) — это антиградиент log-loss по сумме.
|
|
102
|
+
"""
|
|
103
|
+
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
|
|
104
|
+
self.n_estimators = n_estimators
|
|
105
|
+
self.learning_rate = learning_rate
|
|
106
|
+
self.max_depth = max_depth
|
|
107
|
+
|
|
108
|
+
def fit(self, X, y):
|
|
109
|
+
X, y = np.asarray(X, float), np.asarray(y, float)
|
|
110
|
+
p0 = np.clip(np.mean(y), 1e-6, 1 - 1e-6)
|
|
111
|
+
self.init_ = float(np.log(p0 / (1 - p0))) # базовые log-odds
|
|
112
|
+
F = np.full(len(y), self.init_)
|
|
113
|
+
self.trees_ = []
|
|
114
|
+
for _ in range(self.n_estimators):
|
|
115
|
+
residual = y - _sigmoid(F)
|
|
116
|
+
tree = DecisionTreeRegressor(max_depth=self.max_depth).fit(X, residual)
|
|
117
|
+
F += self.learning_rate * tree.predict(X)
|
|
118
|
+
self.trees_.append(tree)
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def _decision(self, X):
|
|
122
|
+
X = np.asarray(X, float)
|
|
123
|
+
F = np.full(X.shape[0], self.init_)
|
|
124
|
+
for tree in self.trees_:
|
|
125
|
+
F += self.learning_rate * tree.predict(X)
|
|
126
|
+
return F
|
|
127
|
+
|
|
128
|
+
def predict_proba(self, X):
|
|
129
|
+
return _sigmoid(self._decision(X))
|
|
130
|
+
|
|
131
|
+
def predict(self, X):
|
|
132
|
+
return (self.predict_proba(X) > 0.5).astype(int)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .base import Estimator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NoiseFeatureRemover(Estimator): # выкидывает признаки-шум: их гистограмма почти равномерна
|
|
6
|
+
def __init__(self, bins=20, threshold=0.9):
|
|
7
|
+
self.bins = bins
|
|
8
|
+
self.threshold = threshold
|
|
9
|
+
|
|
10
|
+
def _uniformity(self, col): # 1.0 = идеально равномерно (максимум энтропии), ниже = есть структура
|
|
11
|
+
counts, _ = np.histogram(col, bins=self.bins)
|
|
12
|
+
p = counts / counts.sum()
|
|
13
|
+
p = p[p > 0]
|
|
14
|
+
entropy = -np.sum(p * np.log(p))
|
|
15
|
+
return entropy / np.log(self.bins)
|
|
16
|
+
|
|
17
|
+
def fit(self, X, y=None):
|
|
18
|
+
self.uniformity_ = np.array([self._uniformity(X[:, j]) for j in range(X.shape[1])])
|
|
19
|
+
self.keep_mask_ = self.uniformity_ < self.threshold # держим НЕ-равномерные признаки
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def transform(self, X):
|
|
23
|
+
return X[:, self.keep_mask_]
|
|
24
|
+
|
|
25
|
+
def fit_transform(self, X, y=None):
|
|
26
|
+
return self.fit(X).transform(X)
|
claros/linear_model.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .base import Estimator, RegressorMixin, ClassifierMixin
|
|
3
|
+
from .losses import MSE, LogLoss
|
|
4
|
+
from .links import Identity, Sigmoid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _LinearModel(Estimator): # общий движок: линейный счёт z=Xw+b, спуск с loss, link и L2
|
|
8
|
+
def __init__(self, loss, link, lr=0.1, n_iters=300, l2=0.0):
|
|
9
|
+
self.loss = loss
|
|
10
|
+
self.link = link
|
|
11
|
+
self.lr = lr
|
|
12
|
+
self.n_iters = n_iters
|
|
13
|
+
self.l2 = l2
|
|
14
|
+
|
|
15
|
+
def fit(self, X, y):
|
|
16
|
+
n, d = X.shape
|
|
17
|
+
self.coef_ = np.zeros(d)
|
|
18
|
+
self.intercept_ = 0.0
|
|
19
|
+
self.loss_history_ = []
|
|
20
|
+
for _ in range(self.n_iters):
|
|
21
|
+
z = X @ self.coef_ + self.intercept_
|
|
22
|
+
p = self.link.forward(z)
|
|
23
|
+
g = self.loss.gradient(y, p) * self.link.derivative(z)
|
|
24
|
+
grad_w = (X.T @ g) / n + self.l2 * self.coef_ # L2 штрафует веса, но не сдвиг
|
|
25
|
+
grad_b = g.sum() / n
|
|
26
|
+
self.coef_ -= self.lr * grad_w
|
|
27
|
+
self.intercept_ -= self.lr * grad_b
|
|
28
|
+
self.loss_history_.append(self.loss.value(y, p) + 0.5 * self.l2 * np.sum(self.coef_ ** 2))
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def _output(self, X):
|
|
32
|
+
return self.link.forward(X @ self.coef_ + self.intercept_)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LinearRegression(_LinearModel, RegressorMixin): # регрессия: по умолчанию Identity-link + MSE
|
|
36
|
+
def __init__(self, loss=None, link=None, lr=0.1, n_iters=300, l2=0.0):
|
|
37
|
+
super().__init__(loss or MSE(), link or Identity(), lr, n_iters, l2)
|
|
38
|
+
|
|
39
|
+
def predict(self, X):
|
|
40
|
+
return self._output(X)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LogisticRegression(_LinearModel, ClassifierMixin): # классификация: по умолчанию Sigmoid + log-loss
|
|
44
|
+
def __init__(self, loss=None, link=None, lr=0.1, n_iters=300, l2=0.0):
|
|
45
|
+
super().__init__(loss or LogLoss(), link or Sigmoid(), lr, n_iters, l2)
|
|
46
|
+
|
|
47
|
+
def predict_proba(self, X):
|
|
48
|
+
return self._output(X)
|
|
49
|
+
|
|
50
|
+
def predict(self, X):
|
|
51
|
+
return (self.predict_proba(X) > 0.5).astype(int)
|
claros/links.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Link: # контракт сжимающей функции: счёт z -> p и её производная dp/dz
|
|
5
|
+
def forward(self, z):
|
|
6
|
+
raise NotImplementedError
|
|
7
|
+
|
|
8
|
+
def derivative(self, z):
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Identity(Link): # без сжатия: p = z (для регрессии)
|
|
13
|
+
def forward(self, z):
|
|
14
|
+
return z
|
|
15
|
+
|
|
16
|
+
def derivative(self, z):
|
|
17
|
+
return np.ones_like(z)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Sigmoid(Link): # классическая сигмоида
|
|
21
|
+
def forward(self, z):
|
|
22
|
+
return 1 / (1 + np.exp(-z))
|
|
23
|
+
|
|
24
|
+
def derivative(self, z):
|
|
25
|
+
p = self.forward(z)
|
|
26
|
+
return p * (1 - p)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ArctanLink(Link): # твоя сжимающая функция (Коши / арктангенс)
|
|
30
|
+
def forward(self, z):
|
|
31
|
+
return 0.5 + np.arctan(z) / np.pi
|
|
32
|
+
|
|
33
|
+
def derivative(self, z):
|
|
34
|
+
return 1 / (np.pi * (1 + z ** 2))
|
claros/losses.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Loss: # контракт потери: значение, градиент по своему входу, опц. гессиан
|
|
5
|
+
def value(self, y, pred):
|
|
6
|
+
raise NotImplementedError
|
|
7
|
+
|
|
8
|
+
def gradient(self, y, pred):
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
|
|
11
|
+
def hessian(self, y, pred):
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeadZoneLoss(Loss): # твоя робастная регрессионная потеря с плоским дном
|
|
16
|
+
def __init__(self, delta=1.0):
|
|
17
|
+
self.delta = delta
|
|
18
|
+
|
|
19
|
+
def value(self, y, pred):
|
|
20
|
+
u = (pred - y) / self.delta
|
|
21
|
+
return np.mean(self.delta * ((1 + u ** 4) ** 0.25 - 1))
|
|
22
|
+
|
|
23
|
+
def gradient(self, y, pred):
|
|
24
|
+
u = (pred - y) / self.delta
|
|
25
|
+
return u ** 3 / (1 + u ** 4) ** 0.75
|
|
26
|
+
|
|
27
|
+
def hessian(self, y, pred):
|
|
28
|
+
u = (pred - y) / self.delta
|
|
29
|
+
return 3 * u ** 2 / (self.delta * (1 + u ** 4) ** 1.75)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LogLoss(Loss): # бинарная кросс-энтропия; gradient — это ∂L/∂p (по вероятности)
|
|
33
|
+
def value(self, y, p):
|
|
34
|
+
p = np.clip(p, 1e-6, 1 - 1e-6)
|
|
35
|
+
return np.mean(-(y * np.log(p) + (1 - y) * np.log(1 - p)))
|
|
36
|
+
|
|
37
|
+
def gradient(self, y, p):
|
|
38
|
+
p = np.clip(p, 1e-6, 1 - 1e-6)
|
|
39
|
+
return (p - y) / (p * (1 - p))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MSE(Loss): # стандартная средняя квадратичная потеря
|
|
43
|
+
def value(self, y, pred):
|
|
44
|
+
return np.mean((pred - y) ** 2)
|
|
45
|
+
|
|
46
|
+
def gradient(self, y, pred):
|
|
47
|
+
return 2 * (pred - y)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HingeLoss(Loss): # потеря SVM: max(0, 1 − y·z), метки y ∈ {−1, +1}, z — линейный счёт
|
|
51
|
+
def value(self, y, z):
|
|
52
|
+
return np.mean(np.maximum(0, 1 - y * z))
|
|
53
|
+
|
|
54
|
+
def gradient(self, y, z):
|
|
55
|
+
return np.where(y * z < 1, -y, 0.0) # субградиент: −y там, где зазор нарушен, иначе 0
|
|
56
|
+
|
|
57
|
+
def hessian(self, y, z):
|
|
58
|
+
return np.zeros_like(z)
|
claros/metrics.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# --- регрессия ---
|
|
5
|
+
def mse(y_true, y_pred):
|
|
6
|
+
return np.mean((y_true - y_pred) ** 2)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def rmse(y_true, y_pred):
|
|
10
|
+
return np.sqrt(mse(y_true, y_pred))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mae(y_true, y_pred):
|
|
14
|
+
return np.mean(np.abs(y_true - y_pred))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def r2_score(y_true, y_pred):
|
|
18
|
+
ss_res = np.sum((y_true - y_pred) ** 2)
|
|
19
|
+
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
|
|
20
|
+
return 1 - ss_res / ss_tot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- бинарная классификация (положительный класс = 1) ---
|
|
24
|
+
def accuracy(y_true, y_pred):
|
|
25
|
+
return np.mean(y_true == y_pred)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def precision(y_true, y_pred):
|
|
29
|
+
tp = np.sum((y_pred == 1) & (y_true == 1))
|
|
30
|
+
fp = np.sum((y_pred == 1) & (y_true == 0))
|
|
31
|
+
return tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def recall(y_true, y_pred):
|
|
35
|
+
tp = np.sum((y_pred == 1) & (y_true == 1))
|
|
36
|
+
fn = np.sum((y_pred == 0) & (y_true == 1))
|
|
37
|
+
return tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def f1_score(y_true, y_pred):
|
|
41
|
+
p, r = precision(y_true, y_pred), recall(y_true, y_pred)
|
|
42
|
+
return 2 * p * r / (p + r) if (p + r) > 0 else 0.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from itertools import product
|
|
3
|
+
from .base import Estimator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def train_test_split(X, y, test_size=0.25, random_state=None, shuffle=True): # делит данные на train и test
|
|
7
|
+
n = X.shape[0]
|
|
8
|
+
idx = np.arange(n)
|
|
9
|
+
if shuffle:
|
|
10
|
+
np.random.default_rng(random_state).shuffle(idx)
|
|
11
|
+
n_test = int(n * test_size)
|
|
12
|
+
test_idx, train_idx = idx[:n_test], idx[n_test:]
|
|
13
|
+
return X[train_idx], X[test_idx], y[train_idx], y[test_idx]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cross_val_score(model, X, y, cv=5, shuffle=True, random_state=None): # k-fold: учим на k-1 частях, оцениваем на оставшейся
|
|
17
|
+
X, y = np.asarray(X), np.asarray(y)
|
|
18
|
+
idx = np.arange(X.shape[0])
|
|
19
|
+
if shuffle:
|
|
20
|
+
np.random.default_rng(random_state).shuffle(idx)
|
|
21
|
+
folds = np.array_split(idx, cv)
|
|
22
|
+
scores = []
|
|
23
|
+
for i in range(cv):
|
|
24
|
+
test_idx = folds[i]
|
|
25
|
+
train_idx = np.concatenate([folds[j] for j in range(cv) if j != i])
|
|
26
|
+
fresh = type(model)(**model.get_params()) # свежая копия модели на каждый фолд
|
|
27
|
+
fresh.fit(X[train_idx], y[train_idx])
|
|
28
|
+
scores.append(fresh.score(X[test_idx], y[test_idx]))
|
|
29
|
+
return np.array(scores)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GridSearchCV(Estimator): # перебор гиперпараметров по сетке с кросс-валидацией
|
|
33
|
+
def __init__(self, estimator, param_grid, cv=5):
|
|
34
|
+
self.estimator = estimator
|
|
35
|
+
self.param_grid = param_grid
|
|
36
|
+
self.cv = cv
|
|
37
|
+
|
|
38
|
+
def fit(self, X, y):
|
|
39
|
+
names = list(self.param_grid.keys())
|
|
40
|
+
base = self.estimator.get_params()
|
|
41
|
+
self.results_ = []
|
|
42
|
+
self.best_score_, self.best_params_ = -np.inf, None
|
|
43
|
+
for combo in product(*[self.param_grid[k] for k in names]):
|
|
44
|
+
params = dict(zip(names, combo))
|
|
45
|
+
model = type(self.estimator)(**{**base, **params})
|
|
46
|
+
mean = cross_val_score(model, X, y, cv=self.cv).mean()
|
|
47
|
+
self.results_.append((params, mean))
|
|
48
|
+
if mean > self.best_score_:
|
|
49
|
+
self.best_score_, self.best_params_ = mean, params
|
|
50
|
+
self.best_estimator_ = type(self.estimator)(**{**base, **self.best_params_}).fit(X, y)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def predict(self, X):
|
|
54
|
+
return self.best_estimator_.predict(X)
|
|
55
|
+
|
|
56
|
+
def score(self, X, y):
|
|
57
|
+
return self.best_estimator_.score(X, y)
|
claros/multiclass.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Мультикласс поверх бинарных моделей: схема one-vs-rest."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator, ClassifierMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OneVsRestClassifier(Estimator, ClassifierMixin):
|
|
7
|
+
"""Один бинарный классификатор на класс («этот класс против всех остальных»).
|
|
8
|
+
|
|
9
|
+
Предсказание — класс с наибольшей уверенностью бинарной модели
|
|
10
|
+
(predict_proba, иначе decision_function, иначе predict). Превращает любой
|
|
11
|
+
бинарный классификатор (логрег, SVM, бустинг) в мультиклассовый.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, estimator):
|
|
14
|
+
self.estimator = estimator
|
|
15
|
+
|
|
16
|
+
def fit(self, X, y):
|
|
17
|
+
X, y = np.asarray(X, float), np.asarray(y)
|
|
18
|
+
self.classes_ = np.unique(y)
|
|
19
|
+
self.models_ = []
|
|
20
|
+
for c in self.classes_:
|
|
21
|
+
model = type(self.estimator)(**self.estimator.get_params())
|
|
22
|
+
model.fit(X, (y == c).astype(int))
|
|
23
|
+
self.models_.append(model)
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def _confidence(self, model, X):
|
|
27
|
+
if hasattr(model, "predict_proba"):
|
|
28
|
+
return model.predict_proba(X)
|
|
29
|
+
if hasattr(model, "decision_function"):
|
|
30
|
+
return model.decision_function(X)
|
|
31
|
+
return model.predict(X)
|
|
32
|
+
|
|
33
|
+
def predict(self, X):
|
|
34
|
+
X = np.asarray(X, float)
|
|
35
|
+
scores = np.array([self._confidence(m, X) for m in self.models_]) # (классы, объекты)
|
|
36
|
+
return self.classes_[scores.argmax(axis=0)]
|
claros/naive_bayes.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Наивный байесовский классификатор с гауссовыми признаками."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator, ClassifierMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GaussianNB(Estimator, ClassifierMixin):
|
|
7
|
+
"""Наивный Байес: каждый признак внутри класса считается гауссовым и независимым.
|
|
8
|
+
|
|
9
|
+
Класс выбирается по максимуму апостериорной вероятности
|
|
10
|
+
(log-prior + сумма log-плотностей признаков). Поддерживает любое число классов.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, var_smoothing=1e-9):
|
|
13
|
+
self.var_smoothing = var_smoothing
|
|
14
|
+
|
|
15
|
+
def fit(self, X, y):
|
|
16
|
+
X, y = np.asarray(X, float), np.asarray(y)
|
|
17
|
+
self.classes_ = np.unique(y)
|
|
18
|
+
eps = self.var_smoothing * X.var(axis=0).max()
|
|
19
|
+
self.theta_, self.var_, self.priors_ = [], [], []
|
|
20
|
+
for c in self.classes_:
|
|
21
|
+
Xc = X[y == c]
|
|
22
|
+
self.theta_.append(Xc.mean(axis=0))
|
|
23
|
+
self.var_.append(Xc.var(axis=0) + eps)
|
|
24
|
+
self.priors_.append(len(Xc) / len(X))
|
|
25
|
+
self.theta_, self.var_, self.priors_ = map(np.array, (self.theta_, self.var_, self.priors_))
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def _log_joint(self, X):
|
|
29
|
+
out = []
|
|
30
|
+
for i in range(len(self.classes_)):
|
|
31
|
+
ll = -0.5 * np.sum(np.log(2 * np.pi * self.var_[i]) + (X - self.theta_[i]) ** 2 / self.var_[i], axis=1)
|
|
32
|
+
out.append(np.log(self.priors_[i]) + ll)
|
|
33
|
+
return np.array(out).T # (объекты, классы)
|
|
34
|
+
|
|
35
|
+
def predict(self, X):
|
|
36
|
+
return self.classes_[np.argmax(self._log_joint(np.asarray(X, float)), axis=1)]
|
|
37
|
+
|
|
38
|
+
def predict_proba(self, X):
|
|
39
|
+
lj = self._log_joint(np.asarray(X, float))
|
|
40
|
+
lj = lj - lj.max(axis=1, keepdims=True)
|
|
41
|
+
p = np.exp(lj)
|
|
42
|
+
return p / p.sum(axis=1, keepdims=True)
|
claros/neighbors.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Метод k ближайших соседей (KNN) с векторным поиском соседей."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator, RegressorMixin, ClassifierMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _KNeighbors(Estimator):
|
|
7
|
+
"""Ленивая модель: запоминает обучающие точки, при предсказании считает все расстояния разом."""
|
|
8
|
+
def __init__(self, n_neighbors=5):
|
|
9
|
+
self.n_neighbors = n_neighbors
|
|
10
|
+
|
|
11
|
+
def fit(self, X, y):
|
|
12
|
+
self.X_ = np.asarray(X, float)
|
|
13
|
+
self.y_ = np.asarray(y)
|
|
14
|
+
self._sq_ = (self.X_ ** 2).sum(axis=1)
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def predict(self, X):
|
|
18
|
+
X = np.asarray(X, float)
|
|
19
|
+
k = min(self.n_neighbors, len(self.X_))
|
|
20
|
+
d2 = (X ** 2).sum(axis=1)[:, None] + self._sq_[None, :] - 2 * (X @ self.X_.T)
|
|
21
|
+
nn = np.argpartition(d2, k - 1, axis=1)[:, :k] # k ближайших на каждый запрос (векторно)
|
|
22
|
+
return np.array([self._aggregate(self.y_[row]) for row in nn])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KNeighborsClassifier(_KNeighbors, ClassifierMixin):
|
|
26
|
+
"""KNN-классификатор: класс — большинство среди соседей."""
|
|
27
|
+
def _aggregate(self, labels):
|
|
28
|
+
vals, counts = np.unique(labels, return_counts=True)
|
|
29
|
+
return vals[np.argmax(counts)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KNeighborsRegressor(_KNeighbors, RegressorMixin):
|
|
33
|
+
"""KNN-регрессор: значение — среднее по соседям."""
|
|
34
|
+
def _aggregate(self, values):
|
|
35
|
+
return float(np.mean(values))
|
claros/pipeline.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from .base import Estimator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Pipeline(Estimator): # цепочка: трансформеры по очереди, затем финальная модель
|
|
5
|
+
def __init__(self, steps):
|
|
6
|
+
self.steps = steps # [(имя, объект), ...]; последний — модель, остальные — трансформеры
|
|
7
|
+
|
|
8
|
+
def _through(self, X, fit):
|
|
9
|
+
for _, step in self.steps[:-1]:
|
|
10
|
+
X = step.fit_transform(X) if fit else step.transform(X)
|
|
11
|
+
return X
|
|
12
|
+
|
|
13
|
+
def fit(self, X, y=None):
|
|
14
|
+
final = self.steps[-1][1]
|
|
15
|
+
Xt = self._through(X, fit=True)
|
|
16
|
+
final.fit(Xt) if y is None else final.fit(Xt, y)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def fit_predict(self, X, y=None):
|
|
20
|
+
return self.steps[-1][1].fit_predict(self._through(X, fit=True))
|
|
21
|
+
|
|
22
|
+
def predict(self, X):
|
|
23
|
+
return self.steps[-1][1].predict(self._through(X, fit=False))
|
|
24
|
+
|
|
25
|
+
def transform(self, X):
|
|
26
|
+
return self._through(X, fit=False)
|
|
27
|
+
|
|
28
|
+
def score(self, X, y):
|
|
29
|
+
return self.steps[-1][1].score(self._through(X, fit=False), y)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def prep(model, scale=0, clean_noise_features=0, pca=0): # предобработка к любой модели без ручного Pipeline
|
|
33
|
+
"""scale=1 — масштабировать; clean_noise_features=1 — убрать шумовые признаки; pca=k — сжать до k компонент.
|
|
34
|
+
|
|
35
|
+
Покрывает все трансформеры библиотеки. Порядок: чистка → масштабирование → PCA → модель.
|
|
36
|
+
Возвращает объект с обычным fit/predict/score/fit_predict.
|
|
37
|
+
"""
|
|
38
|
+
from .feature_selection import NoiseFeatureRemover
|
|
39
|
+
from .preprocessing import StandardScaler
|
|
40
|
+
from .decomposition import PCA
|
|
41
|
+
steps = []
|
|
42
|
+
if clean_noise_features:
|
|
43
|
+
steps.append(("clean", NoiseFeatureRemover()))
|
|
44
|
+
if scale:
|
|
45
|
+
steps.append(("scale", StandardScaler()))
|
|
46
|
+
if pca:
|
|
47
|
+
steps.append(("pca", PCA(n_components=pca)))
|
|
48
|
+
steps.append(("model", model))
|
|
49
|
+
return Pipeline(steps)
|
claros/preprocessing.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .base import Estimator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StandardScaler(Estimator): # приводит каждый признак к среднему 0 и дисперсии 1
|
|
6
|
+
def fit(self, X, y=None):
|
|
7
|
+
self.mean_ = X.mean(axis=0)
|
|
8
|
+
self.std_ = X.std(axis=0)
|
|
9
|
+
self.std_[self.std_ == 0] = 1.0 # константный признак не делим на ноль
|
|
10
|
+
return self
|
|
11
|
+
|
|
12
|
+
def transform(self, X):
|
|
13
|
+
return (X - self.mean_) / self.std_
|
|
14
|
+
|
|
15
|
+
def fit_transform(self, X, y=None):
|
|
16
|
+
return self.fit(X).transform(X)
|
claros/svm.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Метод опорных векторов (линейный, мягкий зазор) на общем линейном движке."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import ClassifierMixin
|
|
4
|
+
from .linear_model import _LinearModel
|
|
5
|
+
from .losses import HingeLoss
|
|
6
|
+
from .links import Identity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LinearSVC(_LinearModel, ClassifierMixin):
|
|
10
|
+
"""Линейный SVM через hinge-потерю.
|
|
11
|
+
|
|
12
|
+
Это тот же движок, что у регрессий: Identity-линк + HingeLoss + L2 дают
|
|
13
|
+
субградиентный спуск по задаче мягкого зазора. То есть SVM получается из
|
|
14
|
+
уже готового _LinearModel, не добавляя нового цикла обучения.
|
|
15
|
+
C — обратная сила регуляризации (больше C — меньше регуляризация).
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, C=1.0, lr=0.01, n_iters=1000):
|
|
18
|
+
self.C = C
|
|
19
|
+
super().__init__(HingeLoss(), Identity(), lr=lr, n_iters=n_iters, l2=1.0 / C)
|
|
20
|
+
|
|
21
|
+
def fit(self, X, y):
|
|
22
|
+
y = np.asarray(y)
|
|
23
|
+
self.classes_ = np.unique(y) # ожидаются два класса
|
|
24
|
+
y_pm = np.where(y == self.classes_[1], 1.0, -1.0) # перевод в {−1, +1}
|
|
25
|
+
super().fit(np.asarray(X, float), y_pm)
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def decision_function(self, X):
|
|
29
|
+
return np.asarray(X, float) @ self.coef_ + self.intercept_
|
|
30
|
+
|
|
31
|
+
def predict(self, X):
|
|
32
|
+
return np.where(self.decision_function(X) > 0, self.classes_[1], self.classes_[0])
|
claros/tree.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Деревья решений (CART): рекурсивная нарезка пространства признаков по порогам."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .base import Estimator, RegressorMixin, ClassifierMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _Node:
|
|
7
|
+
"""Узел дерева: либо порог с двумя детьми, либо лист со значением."""
|
|
8
|
+
def __init__(self, feature=None, threshold=None, left=None, right=None, value=None):
|
|
9
|
+
self.feature = feature
|
|
10
|
+
self.threshold = threshold
|
|
11
|
+
self.left = left
|
|
12
|
+
self.right = right
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _DecisionTree(Estimator):
|
|
17
|
+
"""Общий движок CART. Подклассы задают примесь и значение листа.
|
|
18
|
+
|
|
19
|
+
max_features — сколько признаков рассматривать на каждом разрезе
|
|
20
|
+
(None = все); случайное подмножество нужно случайному лесу.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, max_depth=5, min_samples_split=2, max_features=None, random_state=None):
|
|
23
|
+
self.max_depth = max_depth
|
|
24
|
+
self.min_samples_split = min_samples_split
|
|
25
|
+
self.max_features = max_features
|
|
26
|
+
self.random_state = random_state
|
|
27
|
+
|
|
28
|
+
def _feature_subset(self, d):
|
|
29
|
+
if self.max_features is None or self.max_features >= d:
|
|
30
|
+
return range(d)
|
|
31
|
+
return self._rng.choice(d, self.max_features, replace=False)
|
|
32
|
+
|
|
33
|
+
def _best_split(self, X, y):
|
|
34
|
+
n = len(y)
|
|
35
|
+
parent = self._impurity(y)
|
|
36
|
+
best_gain, best_feat, best_thr = 0.0, None, None
|
|
37
|
+
for feat in self._feature_subset(X.shape[1]):
|
|
38
|
+
values = np.unique(X[:, feat])
|
|
39
|
+
for thr in (values[:-1] + values[1:]) / 2: # пороги — середины между значениями
|
|
40
|
+
left, right = y[X[:, feat] <= thr], y[X[:, feat] > thr]
|
|
41
|
+
child = (len(left) * self._impurity(left) + len(right) * self._impurity(right)) / n
|
|
42
|
+
gain = parent - child
|
|
43
|
+
if gain > best_gain:
|
|
44
|
+
best_gain, best_feat, best_thr = gain, feat, thr
|
|
45
|
+
return best_feat, best_thr
|
|
46
|
+
|
|
47
|
+
def _build(self, X, y, depth):
|
|
48
|
+
if depth >= self.max_depth or len(y) < self.min_samples_split or len(np.unique(y)) == 1:
|
|
49
|
+
return _Node(value=self._leaf_value(y))
|
|
50
|
+
feat, thr = self._best_split(X, y)
|
|
51
|
+
if feat is None:
|
|
52
|
+
return _Node(value=self._leaf_value(y))
|
|
53
|
+
mask = X[:, feat] <= thr
|
|
54
|
+
left = self._build(X[mask], y[mask], depth + 1)
|
|
55
|
+
right = self._build(X[~mask], y[~mask], depth + 1)
|
|
56
|
+
return _Node(feat, thr, left, right)
|
|
57
|
+
|
|
58
|
+
def fit(self, X, y):
|
|
59
|
+
self._rng = np.random.default_rng(self.random_state)
|
|
60
|
+
self.root_ = self._build(np.asarray(X, float), np.asarray(y), 0)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def _descend(self, x, node):
|
|
64
|
+
while node.value is None:
|
|
65
|
+
node = node.left if x[node.feature] <= node.threshold else node.right
|
|
66
|
+
return node.value
|
|
67
|
+
|
|
68
|
+
def predict(self, X):
|
|
69
|
+
return np.array([self._descend(x, self.root_) for x in np.asarray(X, float)])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DecisionTreeRegressor(_DecisionTree, RegressorMixin):
|
|
73
|
+
"""Дерево регрессии: примесь — дисперсия, лист — среднее."""
|
|
74
|
+
def _impurity(self, y):
|
|
75
|
+
return np.var(y) if len(y) else 0.0
|
|
76
|
+
|
|
77
|
+
def _leaf_value(self, y):
|
|
78
|
+
return float(np.mean(y))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DecisionTreeClassifier(_DecisionTree, ClassifierMixin):
|
|
82
|
+
"""Дерево классификации: примесь — Gini, лист — класс-большинство."""
|
|
83
|
+
def _impurity(self, y):
|
|
84
|
+
if len(y) == 0:
|
|
85
|
+
return 0.0
|
|
86
|
+
_, counts = np.unique(y, return_counts=True)
|
|
87
|
+
p = counts / counts.sum()
|
|
88
|
+
return 1.0 - np.sum(p ** 2)
|
|
89
|
+
|
|
90
|
+
def _leaf_value(self, y):
|
|
91
|
+
values, counts = np.unique(y, return_counts=True)
|
|
92
|
+
return values[np.argmax(counts)]
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claros
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Учебная ML-библиотека с нуля на NumPy: sklearn-подобный интерфейс и свои робастные компоненты
|
|
5
|
+
Author-email: Сафин Георгий <sikirchannel@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kuketa-tech/claros
|
|
8
|
+
Project-URL: Repository, https://github.com/kuketa-tech/claros
|
|
9
|
+
Keywords: machine-learning,numpy,from-scratch,education,scikit-learn
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.20
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: scikit-learn; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Claros
|
|
31
|
+
|
|
32
|
+
**Учебная ML-библиотека, написанная с нуля на чистом NumPy.** Sklearn-подобный интерфейс, никаких готовых ML-зависимостей — все алгоритмы реализованы руками.
|
|
33
|
+
|
|
34
|
+
`Python ≥ 3.9` · `только NumPy` · `лицензия MIT` · `32 теста зелёных`
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Содержание
|
|
39
|
+
|
|
40
|
+
- [Установка](#установка)
|
|
41
|
+
- [Документация](#документация)
|
|
42
|
+
- [Как устроено](#как-устроено)
|
|
43
|
+
- [Удобство: предобработка без Pipeline](#удобство-предобработка-без-pipeline)
|
|
44
|
+
- [Функции с примерами](#функции-с-примерами)
|
|
45
|
+
- [Валидация](#валидация)
|
|
46
|
+
- [Структура и тесты](#структура-и-тесты)
|
|
47
|
+
- [Дорожная карта](#дорожная-карта)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Установка
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e . # библиотека
|
|
55
|
+
pip install -e ".[dev]" # + pytest и scikit-learn для тестов
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Документация
|
|
59
|
+
|
|
60
|
+
Полная документация — в папке [`docs/`](docs/index.md): по каждому классу параметры (тип, дефолт, смысл), методы, что возвращают, атрибуты, примеры, плюс гайд «как писать свои компоненты».
|
|
61
|
+
|
|
62
|
+
- [**Полный справочник всех функций**](docs/reference.md) — всё одним списком
|
|
63
|
+
- [Модели](docs/models.md) · [Кластеризация и PCA](docs/clustering.md) · [Потери и сжимающие функции](docs/losses-and-links.md)
|
|
64
|
+
- [Предобработка и конвейеры](docs/preprocessing-and-pipelines.md) · [Оценка и подбор](docs/evaluation.md) · [Как писать свои компоненты](docs/extending.md)
|
|
65
|
+
|
|
66
|
+
Можно собрать в сайт: `pip install mkdocs && mkdocs serve`.
|
|
67
|
+
|
|
68
|
+
## Как устроено
|
|
69
|
+
|
|
70
|
+
Все модели наследуют `Estimator` и говорят на одном языке: `fit(X, y)` обучает (возвращает `self`), `predict(X)` предсказывает, `score(X, y)` — R² (регрессоры) или доля верных (классификаторы), `get_params` / `set_params` дают клонирование. Обученные величины оканчиваются на `_` (`coef_`, `labels_`).
|
|
71
|
+
|
|
72
|
+
Стержень дизайна — **потеря определяет модель**: весь шаг спуска это `g = loss.gradient(y, p) * link.derivative(z)`, поэтому линейная регрессия, логистическая регрессия и SVM — один движок с разной потерей и сжимающей функцией.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Удобство: предобработка без Pipeline
|
|
77
|
+
|
|
78
|
+
Чтобы не собирать `Pipeline` руками, предобработку можно подключить прямо к модели.
|
|
79
|
+
|
|
80
|
+
**`prep(model, scale=1, clean_noise_features=1, pca=2)`** оборачивает **любую** модель и возвращает объект с обычным `fit`/`predict`/`score`/`fit_predict`. Опции: `scale=1` — масштабирование, `clean_noise_features=1` — убрать шумовые признаки, `pca=k` — сжать до k главных компонент (порядок: чистка → масштабирование → PCA → модель):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from claros import prep, LogisticRegression, KNeighborsClassifier
|
|
84
|
+
|
|
85
|
+
prep(LogisticRegression(), scale=1).fit(X, y) # + масштабирование
|
|
86
|
+
prep(KNeighborsClassifier(5), scale=1, clean_noise_features=1).fit(X, y) # + чистка шумовых признаков
|
|
87
|
+
prep(LogisticRegression(), pca=2).fit(X, y) # + сжатие до 2 главных компонент
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Это все предобработчики библиотеки (`StandardScaler`, `NoiseFeatureRemover`, `PCA`) — больше ничего через Pipeline не идёт. Полноценный `Pipeline` остаётся для произвольных цепочек.
|
|
91
|
+
|
|
92
|
+
У `DBSCAN` и `KMeans` есть встроенный флаг-сокращение `clean_noise_features` (1 — включить нашу чистку шумовых признаков, 0 — обычный режим):
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from claros import DBSCAN
|
|
96
|
+
DBSCAN(eps=0.6, min_samples=6, clean_noise_features=1).fit_predict(X)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Полноценный `Pipeline` (см. ниже) тоже остаётся — для произвольных цепочек.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Функции с примерами
|
|
104
|
+
|
|
105
|
+
> В примерах `X` — матрица признаков `(n, d)`, `y` — цель, `X_new` — новые объекты.
|
|
106
|
+
|
|
107
|
+
### Линейные модели
|
|
108
|
+
|
|
109
|
+
**`LinearRegression(loss=MSE(), link=Identity(), lr=0.1, n_iters=300, l2=0.0)`** — линейная регрессия градиентным спуском. Атрибуты: `coef_`, `intercept_`.
|
|
110
|
+
```python
|
|
111
|
+
from claros import LinearRegression
|
|
112
|
+
m = LinearRegression().fit(X, y)
|
|
113
|
+
m.predict(X_new); m.score(X, y)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**`LogisticRegression(loss=LogLoss(), link=Sigmoid(), lr=0.1, n_iters=300, l2=0.0)`** — бинарная классификация.
|
|
117
|
+
```python
|
|
118
|
+
from claros import LogisticRegression
|
|
119
|
+
clf = LogisticRegression().fit(X, y)
|
|
120
|
+
clf.predict(X_new); clf.predict_proba(X_new)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### SVM
|
|
124
|
+
|
|
125
|
+
**`LinearSVC(C=1.0, lr=0.01, n_iters=1000)`** — линейный SVM (тот же движок + `HingeLoss`). `C` — обратная сила регуляризации.
|
|
126
|
+
```python
|
|
127
|
+
from claros import LinearSVC
|
|
128
|
+
clf = LinearSVC(C=1.0).fit(X, y)
|
|
129
|
+
clf.predict(X_new); clf.decision_function(X_new)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Наивный Байес
|
|
133
|
+
|
|
134
|
+
**`GaussianNB(var_smoothing=1e-9)`** — гауссов наивный Байес, мультикласс. Есть `predict_proba`.
|
|
135
|
+
```python
|
|
136
|
+
from claros import GaussianNB
|
|
137
|
+
clf = GaussianNB().fit(X, y)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Деревья решений
|
|
141
|
+
|
|
142
|
+
**`DecisionTreeClassifier(max_depth=5, min_samples_split=2, max_features=None, random_state=None)`** — примесь Gini.
|
|
143
|
+
**`DecisionTreeRegressor(...)`** — примесь = дисперсия.
|
|
144
|
+
```python
|
|
145
|
+
from claros import DecisionTreeClassifier
|
|
146
|
+
clf = DecisionTreeClassifier(max_depth=5).fit(X, y)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Ансамбли
|
|
150
|
+
|
|
151
|
+
**`RandomForestClassifier(n_estimators=50, max_depth=10, max_features="sqrt", random_state=None)`** / **`RandomForestRegressor(...)`** — бэггинг деревьев.
|
|
152
|
+
```python
|
|
153
|
+
from claros import RandomForestClassifier
|
|
154
|
+
clf = RandomForestClassifier(n_estimators=100).fit(X, y)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**`GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3)`** / **`GradientBoostingRegressor(...)`** — бустинг по остаткам.
|
|
158
|
+
```python
|
|
159
|
+
from claros import GradientBoostingRegressor
|
|
160
|
+
m = GradientBoostingRegressor(n_estimators=200).fit(X, y)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Ближайшие соседи
|
|
164
|
+
|
|
165
|
+
**`KNeighborsClassifier(n_neighbors=5)`** / **`KNeighborsRegressor(n_neighbors=5)`** — k ближайших (векторный поиск, ×3 к прежнему).
|
|
166
|
+
```python
|
|
167
|
+
from claros import KNeighborsClassifier
|
|
168
|
+
clf = KNeighborsClassifier(5).fit(X, y)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Мультикласс
|
|
172
|
+
|
|
173
|
+
**`OneVsRestClassifier(estimator)`** — мультикласс из любой бинарной модели.
|
|
174
|
+
```python
|
|
175
|
+
from claros import OneVsRestClassifier, LogisticRegression
|
|
176
|
+
clf = OneVsRestClassifier(LogisticRegression()).fit(X, y)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Кластеризация
|
|
180
|
+
|
|
181
|
+
**`DBSCAN(eps=0.5, min_samples=5, clean_noise_features=0, feature_threshold=0.9)`** — плотностная кластеризация; метка `-1` — шум. `clean_noise_features=1` убирает шумовые признаки перед кластеризацией (наша доработка). Атрибуты: `labels_`, `remover_` (при включённом флаге).
|
|
182
|
+
```python
|
|
183
|
+
from claros import DBSCAN
|
|
184
|
+
DBSCAN(eps=0.5, min_samples=5).fit_predict(X) # обычный DBSCAN
|
|
185
|
+
DBSCAN(eps=0.6, min_samples=6, clean_noise_features=1).fit_predict(X) # + чистка шумовых признаков
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**`KMeans(n_clusters=3, max_iter=100, n_init=10, random_state=None, clean_noise_features=0, feature_threshold=0.9)`** — по центрам, `n_init` перезапусков. Атрибуты: `cluster_centers_`, `labels_`, `inertia_`.
|
|
189
|
+
```python
|
|
190
|
+
from claros import KMeans
|
|
191
|
+
KMeans(n_clusters=3).fit_predict(X)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Снижение размерности
|
|
195
|
+
|
|
196
|
+
**`PCA(n_components=2)`** — главные компоненты через SVD. Методы: `transform`, `inverse_transform`; атрибут `explained_variance_ratio_`.
|
|
197
|
+
```python
|
|
198
|
+
from claros import PCA
|
|
199
|
+
X2 = PCA(n_components=2).fit_transform(X)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Функции потерь
|
|
203
|
+
|
|
204
|
+
**`MSE()`**, **`LogLoss()`**, **`HingeLoss()`**, **`DeadZoneLoss(delta=1.0)`** — `DeadZoneLoss` робастная (плоское дно + насыщение градиента, устойчива к выбросам).
|
|
205
|
+
```python
|
|
206
|
+
from claros import LinearRegression, DeadZoneLoss
|
|
207
|
+
LinearRegression(loss=DeadZoneLoss(0.5), lr=0.02, n_iters=40000).fit(X, y)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Сжимающие функции
|
|
211
|
+
|
|
212
|
+
**`Identity()`**, **`Sigmoid()`**, **`ArctanLink()`** — `ArctanLink` осторожнее сигмоиды (тяжёлые хвосты).
|
|
213
|
+
```python
|
|
214
|
+
from claros import LogisticRegression, ArctanLink
|
|
215
|
+
LogisticRegression(link=ArctanLink()).fit(X, y)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Предобработка и отбор признаков
|
|
219
|
+
|
|
220
|
+
**`StandardScaler()`** — среднее 0, дисперсия 1.
|
|
221
|
+
```python
|
|
222
|
+
from claros import StandardScaler
|
|
223
|
+
Xs = StandardScaler().fit_transform(X)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**`NoiseFeatureRemover(bins=20, threshold=0.9)`** — убирает признаки-шум (почти равномерная гистограмма). Атрибут `keep_mask_`.
|
|
227
|
+
```python
|
|
228
|
+
from claros import NoiseFeatureRemover
|
|
229
|
+
Xc = NoiseFeatureRemover().fit_transform(X)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Конвейер
|
|
233
|
+
|
|
234
|
+
**`prep(model, scale=0, clean_noise_features=0, pca=0)`** — предобработка к любой модели без ручного Pipeline: масштабирование, чистка шумовых признаков, PCA (см. раздел выше).
|
|
235
|
+
```python
|
|
236
|
+
from claros import prep, KNeighborsClassifier
|
|
237
|
+
prep(KNeighborsClassifier(5), scale=1, clean_noise_features=1, pca=2).fit(X, y)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**`Pipeline(steps)`** — произвольная цепочка `[(имя, объект), …]`: трансформеры по очереди, затем модель.
|
|
241
|
+
```python
|
|
242
|
+
from claros import Pipeline, StandardScaler, LogisticRegression
|
|
243
|
+
Pipeline([("scale", StandardScaler()), ("model", LogisticRegression())]).fit(X, y)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Подбор и оценка
|
|
247
|
+
|
|
248
|
+
**`train_test_split(X, y, test_size=0.25, random_state=None, shuffle=True)`** → `X_train, X_test, y_train, y_test`.
|
|
249
|
+
```python
|
|
250
|
+
from claros import train_test_split
|
|
251
|
+
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**`cross_val_score(model, X, y, cv=5, shuffle=True, random_state=None)`** → массив оценок.
|
|
255
|
+
```python
|
|
256
|
+
from claros import cross_val_score, RandomForestClassifier
|
|
257
|
+
cross_val_score(RandomForestClassifier(), X, y, cv=5).mean()
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**`GridSearchCV(estimator, param_grid, cv=5)`** — перебор по сетке. Атрибуты: `best_params_`, `best_score_`, `best_estimator_`.
|
|
261
|
+
```python
|
|
262
|
+
from claros import GridSearchCV, DecisionTreeClassifier
|
|
263
|
+
gs = GridSearchCV(DecisionTreeClassifier(), {"max_depth": [3, 5, 7]}, cv=5).fit(X, y)
|
|
264
|
+
gs.best_params_; gs.best_score_
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Метрики
|
|
268
|
+
|
|
269
|
+
Модуль **`claros.metrics`**, все вида `f(y_true, y_pred)`: регрессия — `mse`, `rmse`, `mae`, `r2_score`; классификация — `accuracy`, `precision`, `recall`, `f1_score`.
|
|
270
|
+
```python
|
|
271
|
+
from claros.metrics import accuracy, r2_score
|
|
272
|
+
accuracy(y_true, y_pred); r2_score(y_true, y_pred)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Валидация
|
|
278
|
+
|
|
279
|
+
Сверено со scikit-learn на одинаковых данных:
|
|
280
|
+
|
|
281
|
+
| Модель | claros | sklearn |
|
|
282
|
+
|-------------------------------------|-------|---------|
|
|
283
|
+
| RandomForest — accuracy (круг) | 0.972 | 0.972 |
|
|
284
|
+
| KNN — accuracy (круг) | 0.983 | 0.983 |
|
|
285
|
+
| GradientBoosting — R^2 (sin) | 0.991 | 0.991 |
|
|
286
|
+
| LinearSVC — accuracy (две группы) | 1.000 | 1.000 |
|
|
287
|
+
| GaussianNB — accuracy (3 класса) | 0.972 | 0.972 |
|
|
288
|
+
|
|
289
|
+
Метки `GaussianNB` и `DBSCAN` совпадают со scikit-learn один-в-один, доли дисперсии `PCA` — до 4-го знака.
|
|
290
|
+
|
|
291
|
+
## Структура и тесты
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
claros-project/
|
|
295
|
+
├── pyproject.toml README.md LICENSE PUBLISHING.md
|
|
296
|
+
├── claros/ # base, losses, links, linear_model, svm, naive_bayes,
|
|
297
|
+
│ # tree, ensemble, neighbors, multiclass, decomposition,
|
|
298
|
+
│ # cluster, preprocessing, feature_selection, pipeline,
|
|
299
|
+
│ # model_selection, metrics
|
|
300
|
+
└── tests/ # 32 теста
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
pytest tests/ -q # 32 passed
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Дорожная карта
|
|
308
|
+
|
|
309
|
+
- [x] линейные модели, деревья, ансамбли, KNN, кластеризация, PCA
|
|
310
|
+
- [x] SVM, наивный Байес, GridSearchCV, мультикласс
|
|
311
|
+
- [x] удобство: флаг `clean_noise_features` и `prep(...)` — предобработка без Pipeline
|
|
312
|
+
- [x] ускорение: KNN векторизован (×3); DBSCAN — поточечный поиск O(n²) (для масштаба нужен C-индекс)
|
|
313
|
+
- [ ] публикация на PyPI — имя `claros` (инструкция в `PUBLISHING.md`)
|
|
314
|
+
- [ ] оформить результаты в виде научной статьи
|
|
315
|
+
|
|
316
|
+
## Лицензия
|
|
317
|
+
|
|
318
|
+
MIT.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
claros/__init__.py,sha256=fLh0mc0-C-HTqH21OzozR8HONVcZG8Goeqy66OBjQko,2763
|
|
2
|
+
claros/base.py,sha256=QAVJGo-ZFlCHc0Y551mPiRjJZ_usAW4_e9TFNRljFzk,992
|
|
3
|
+
claros/cluster.py,sha256=bmUGoH2755QRPfwQF4K5Mpe5yzhpXKGG5cpvTEWG_KA,4787
|
|
4
|
+
claros/decomposition.py,sha256=NkPLNH8sLdu2P3o5EL8GsfKVndzeFwlCfCFdmM1tK1E,1393
|
|
5
|
+
claros/ensemble.py,sha256=-brW4x9894cHCHmEmmG97B_lj2r4Jz2O90sGaqo2ack,5471
|
|
6
|
+
claros/feature_selection.py,sha256=c6lR28qXrkzKej0u-Kq3utuOJe0vbgQuQxEaDGOASik,1065
|
|
7
|
+
claros/linear_model.py,sha256=PY9d2Zz-tGctvt1FVwbH-q-oPgTGxTPjgja7byORg64,2022
|
|
8
|
+
claros/links.py,sha256=zGWaUbpVf4AYd2_Wg8sNgTpo2lBScSYRohy1tjJToI4,912
|
|
9
|
+
claros/losses.py,sha256=NEHH7Ic01yD5-1swpyabXcFS4GX1Qhg0vG5NF-m_-Aw,1955
|
|
10
|
+
claros/metrics.py,sha256=KKgCrDS2B11Tlotkt92ENpQzNG1qVQ8msrEH-GEkN18,1087
|
|
11
|
+
claros/model_selection.py,sha256=4TY8tT4NllmjXRx8YyidcwN4_K5PjUbafr9ihb1QwJY,2358
|
|
12
|
+
claros/multiclass.py,sha256=dWI7roORGSeX6XFDmGoLZ34NgWLUrSpn-ZivM1gbTo0,1609
|
|
13
|
+
claros/naive_bayes.py,sha256=kINCwbgXVRZ5VoJT8udOpu_P5UTYXIwFBaUes63FLIE,1951
|
|
14
|
+
claros/neighbors.py,sha256=BJiDyg2vUCNf94BszHnMonhntW3SmCcrNImKEL0B1bY,1564
|
|
15
|
+
claros/pipeline.py,sha256=DqmG396tGcEL7-3B_6WPWBKK-cFkfGIw-5Bs7p6BQTA,2069
|
|
16
|
+
claros/preprocessing.py,sha256=kQEtabgKPWBZkt25N1fy7KMQ1a9fiF7IPadMJSD6aYA,562
|
|
17
|
+
claros/svm.py,sha256=ISRnvov_Nm9pnzjaXHRjtNiF1g1w5YLnwsF_rtxYLuk,1600
|
|
18
|
+
claros/tree.py,sha256=n3QAKLEZfmbpxpaevKYI34n4QamZYqAnPsbbvmnEgns,3959
|
|
19
|
+
claros-0.7.0.dist-info/licenses/LICENSE,sha256=PiFis7GpFhB-Ck2BRM2eJfVqlOuhWwJ9DYqdSPdXPo8,1082
|
|
20
|
+
claros-0.7.0.dist-info/METADATA,sha256=D5gu7ilKiCe57pJ8yxpDpqwdh6fAurqoRnAb2wmJ2Ao,15623
|
|
21
|
+
claros-0.7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
22
|
+
claros-0.7.0.dist-info/top_level.txt,sha256=qibeNIu06iT56xOL2AYwbqDx_BQN2jljnIjnXGAyu70,7
|
|
23
|
+
claros-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Сафин Георгий
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claros
|