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 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_
@@ -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)
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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