saikit-learn 0.1.0__tar.gz
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.
- saikit_learn-0.1.0/LICENSE +21 -0
- saikit_learn-0.1.0/PKG-INFO +45 -0
- saikit_learn-0.1.0/README.md +29 -0
- saikit_learn-0.1.0/pyproject.toml +30 -0
- saikit_learn-0.1.0/saikit_learn/__init__.py +3 -0
- saikit_learn-0.1.0/saikit_learn/discriminant_analysis/__init__.py +4 -0
- saikit_learn-0.1.0/saikit_learn/discriminant_analysis/base.py +54 -0
- saikit_learn-0.1.0/saikit_learn/discriminant_analysis/lda.py +58 -0
- saikit_learn-0.1.0/saikit_learn/discriminant_analysis/qda.py +97 -0
- saikit_learn-0.1.0/saikit_learn/linear_model/__init__.py +3 -0
- saikit_learn-0.1.0/saikit_learn/linear_model/logistic_regression.py +322 -0
- saikit_learn-0.1.0/saikit_learn/neighbors/__init__.py +3 -0
- saikit_learn-0.1.0/saikit_learn/neighbors/knn.py +89 -0
- saikit_learn-0.1.0/saikit_learn/neural_network/__init__.py +3 -0
- saikit_learn-0.1.0/saikit_learn/neural_network/mlp_regressor.py +175 -0
- saikit_learn-0.1.0/saikit_learn/regression/__init__.py +6 -0
- saikit_learn-0.1.0/saikit_learn/regression/lasso.py +82 -0
- saikit_learn-0.1.0/saikit_learn/regression/minimos_quadrados.py +63 -0
- saikit_learn-0.1.0/saikit_learn/regression/regressao_polinomial.py +70 -0
- saikit_learn-0.1.0/saikit_learn/regression/ridge.py +79 -0
- saikit_learn-0.1.0/saikit_learn/regression/utils/__init__.py +0 -0
- saikit_learn-0.1.0/saikit_learn/regression/utils/validacao.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 saikit-learn
|
|
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
|
+
IABILITY, 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,45 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: saikit-learn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Biblioteca de aprendizado de máquina (projeto final de Aprendizado de Máquina I, IMPA Tech)
|
|
5
|
+
Author-email: André Oliveira <al.andre.oliveira@impatech.edu.br>, Davi Pestana <al.davi.campos@impatech.edu.br>, Felipe Ribeiro <al.felipe.mendonca@impatech.edu.br>, Gabriel Krauss <al.gabriel.dias@impatech.edu.br>, Huann Vicente <al.huann.silva@impatech.edu.br>, Lucas Corazza <al.lucas.corazza@impatech.edu.br>, Nicole Freire <al.nicole.freire@impatech.edu.br>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
12
|
+
Project-URL: Repository, https://github.com/feliperibeiro-impatech/saikit_learn
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Import-Name: saikit_learn
|
|
15
|
+
|
|
16
|
+
# Saikit-learn
|
|
17
|
+
|
|
18
|
+
Biblioteca com ferramentas de aprendizado de máquina, desenvolvida como projeto final da disciplina de Aprendizado de Máquina I do IMPA Tech.
|
|
19
|
+
|
|
20
|
+
## Instalação
|
|
21
|
+
|
|
22
|
+
Para instalar a biblioteca, execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install saikit-learn
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Funcionalidades
|
|
29
|
+
|
|
30
|
+
A biblioteca reúne implementações de algoritmos de aprendizado de máquina e ferramentas relacionadas, com o objetivo de facilitar sua utilização e experimentação.
|
|
31
|
+
|
|
32
|
+
## Autores
|
|
33
|
+
|
|
34
|
+
- André Oliveira
|
|
35
|
+
- Davi Pestana
|
|
36
|
+
- Felipe Ribeiro
|
|
37
|
+
- Gabriel Krauss
|
|
38
|
+
- Huann Vicente
|
|
39
|
+
- Lucas Corazza
|
|
40
|
+
- Nicole Freire
|
|
41
|
+
|
|
42
|
+
## Licença
|
|
43
|
+
|
|
44
|
+
Este projeto está licenciado sob a licença MIT. Consulte o arquivo [LICENSE](LICENSE) para mais informações.
|
|
45
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Saikit-learn
|
|
2
|
+
|
|
3
|
+
Biblioteca com ferramentas de aprendizado de máquina, desenvolvida como projeto final da disciplina de Aprendizado de Máquina I do IMPA Tech.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
Para instalar a biblioteca, execute:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install saikit-learn
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Funcionalidades
|
|
14
|
+
|
|
15
|
+
A biblioteca reúne implementações de algoritmos de aprendizado de máquina e ferramentas relacionadas, com o objetivo de facilitar sua utilização e experimentação.
|
|
16
|
+
|
|
17
|
+
## Autores
|
|
18
|
+
|
|
19
|
+
- André Oliveira
|
|
20
|
+
- Davi Pestana
|
|
21
|
+
- Felipe Ribeiro
|
|
22
|
+
- Gabriel Krauss
|
|
23
|
+
- Huann Vicente
|
|
24
|
+
- Lucas Corazza
|
|
25
|
+
- Nicole Freire
|
|
26
|
+
|
|
27
|
+
## Licença
|
|
28
|
+
|
|
29
|
+
Este projeto está licenciado sob a licença MIT. Consulte o arquivo [LICENSE](LICENSE) para mais informações.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.11,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saikit-learn"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "André Oliveira", email = "al.andre.oliveira@impatech.edu.br"},
|
|
9
|
+
{name = "Davi Pestana", email = "al.davi.campos@impatech.edu.br"},
|
|
10
|
+
{name = "Felipe Ribeiro", email = "al.felipe.mendonca@impatech.edu.br"},
|
|
11
|
+
{name = "Gabriel Krauss", email = "al.gabriel.dias@impatech.edu.br"},
|
|
12
|
+
{name = "Huann Vicente", email = "al.huann.silva@impatech.edu.br"},
|
|
13
|
+
{name = "Lucas Corazza", email = "al.lucas.corazza@impatech.edu.br"},
|
|
14
|
+
{name = "Nicole Freire", email = "al.nicole.freire@impatech.edu.br"}
|
|
15
|
+
]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = ["LICENSE"]
|
|
19
|
+
dynamic = ["version", "description"]
|
|
20
|
+
dependencies = ["numpy"]
|
|
21
|
+
requires-python = ">=3.8"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/feliperibeiro-impatech/saikit_learn"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class _BaseDiscriminant:
|
|
4
|
+
def __init__(self):
|
|
5
|
+
self._classes = None
|
|
6
|
+
self._priors = None # shape (K,)
|
|
7
|
+
self._means = None # shape (K, d)
|
|
8
|
+
self._n_features = None
|
|
9
|
+
self._covariance = None
|
|
10
|
+
|
|
11
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
12
|
+
"""Ajusta o modelo aos dados de treino.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
X (np.ndarray): Matriz de atributos
|
|
16
|
+
y (np.ndarray): Vetor com os resultados
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
self: LDA ou QDA
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if not isinstance(X, np.ndarray):
|
|
23
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
24
|
+
if not isinstance(y, np.ndarray):
|
|
25
|
+
raise TypeError(f"y deve ser np.ndarray, recebeu {type(y).__name__}")
|
|
26
|
+
|
|
27
|
+
if X.ndim != 2:
|
|
28
|
+
raise ValueError("X deve ser 2D, com shape (n_samples, n_features).")
|
|
29
|
+
if y.ndim != 1 or len(y) != X.shape[0]:
|
|
30
|
+
raise ValueError("y deve ser 1D e ter o mesmo número de amostras que X.")
|
|
31
|
+
|
|
32
|
+
if np.isnan(X).any():
|
|
33
|
+
raise ValueError("X contém NaN.")
|
|
34
|
+
|
|
35
|
+
n_samples, self._n_features = X.shape
|
|
36
|
+
self._classes = np.unique(y)
|
|
37
|
+
n_classes = len(self._classes)
|
|
38
|
+
|
|
39
|
+
if n_samples <= n_classes:
|
|
40
|
+
raise ValueError("É preciso ter mais amostras do que classes.")
|
|
41
|
+
|
|
42
|
+
self._priors = np.zeros(n_classes)
|
|
43
|
+
self._means = np.zeros((n_classes, self._n_features))
|
|
44
|
+
|
|
45
|
+
for k, c in enumerate(self._classes):
|
|
46
|
+
X_c = X[y == c]
|
|
47
|
+
self._priors[k] = len(X_c) / n_samples
|
|
48
|
+
self._means[k] = X_c.mean(axis=0)
|
|
49
|
+
|
|
50
|
+
self._fit_covariance(X, y)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def _fit_covariance(self, X: np.ndarray, y: np.ndarray):
|
|
54
|
+
raise NotImplementedError("Implemente no LDA ou QDA")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from saikit_learn.discriminant_analysis.base import _BaseDiscriminant
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class LDA(_BaseDiscriminant):
|
|
5
|
+
def _fit_covariance(self, X : np.ndarray, Y : np.ndarray) -> None:
|
|
6
|
+
"""Calcula covariância do LDA
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
X (np.ndarray): Matriz de atributos
|
|
10
|
+
Y (np.ndarray): Vetor dos valores
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
n_samples, n_features = X.shape
|
|
14
|
+
n_classes = len(self._classes)
|
|
15
|
+
|
|
16
|
+
scatter = np.zeros((n_features, n_features))
|
|
17
|
+
for k, c in enumerate(self._classes):
|
|
18
|
+
centro = X[Y == c] - self._means[k]
|
|
19
|
+
scatter += centro.T @ centro
|
|
20
|
+
|
|
21
|
+
self._covariance = scatter / (n_samples - n_classes)
|
|
22
|
+
|
|
23
|
+
def predict(self, X : np.ndarray) -> np.ndarray:
|
|
24
|
+
"""Estima valores de acordo com a covariância calculada
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
X (np.ndarray): Matriz de atributos
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
np.ndarray: Vetor dos valores calculados
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
if self._covariance is None:
|
|
34
|
+
raise RuntimeError("Chame fit antes de predict.")
|
|
35
|
+
|
|
36
|
+
X = np.asarray(X, dtype=np.float64)
|
|
37
|
+
if X.ndim == 1:
|
|
38
|
+
X = X.reshape(1, -1)
|
|
39
|
+
if X.ndim != 2 or X.shape[1] != self._n_features:
|
|
40
|
+
raise ValueError(f"X deve ter shape (n_samples, {self._n_features}).")
|
|
41
|
+
|
|
42
|
+
cov_inv = np.linalg.inv(self._covariance)
|
|
43
|
+
n_classes = len(self._classes)
|
|
44
|
+
|
|
45
|
+
predictions = []
|
|
46
|
+
|
|
47
|
+
for x in X:
|
|
48
|
+
score = np.zeros(n_classes)
|
|
49
|
+
|
|
50
|
+
for k in range(n_classes):
|
|
51
|
+
m = self._means[k]
|
|
52
|
+
p = self._priors[k]
|
|
53
|
+
score[k] = x @ cov_inv @ m - 0.5 * m @ cov_inv @ m + np.log(p)
|
|
54
|
+
|
|
55
|
+
predictions.append(self._classes[np.argmax(score)])
|
|
56
|
+
|
|
57
|
+
return np.array(predictions)
|
|
58
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from saikit_learn.discriminant_analysis.base import _BaseDiscriminant
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class QDA(_BaseDiscriminant):
|
|
5
|
+
def __init__(self, reg_param : float = 0.0):
|
|
6
|
+
"""Inicializa o QDA
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
reg_param (float): Valor somado à diagonal de cada matriz de covariância
|
|
10
|
+
para evitar matrizes singulares. Deve ser maior ou igual a 0.
|
|
11
|
+
Com 0.0 não aplica regularização.
|
|
12
|
+
"""
|
|
13
|
+
super().__init__()
|
|
14
|
+
|
|
15
|
+
if not np.isfinite(reg_param) or reg_param < 0:
|
|
16
|
+
raise ValueError("reg_param deve ser finito e não negativo.")
|
|
17
|
+
|
|
18
|
+
self.reg_param = reg_param
|
|
19
|
+
|
|
20
|
+
def _fit_covariance(self, X : np.ndarray, Y : np.ndarray) -> None:
|
|
21
|
+
"""Calcula uma covariância para cada classe do QDA
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
X (np.ndarray): Matriz de atributos
|
|
25
|
+
Y (np.ndarray): Vetor dos valores
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
self._covariance = None
|
|
29
|
+
|
|
30
|
+
n_features = X.shape[1]
|
|
31
|
+
n_classes = len(self._classes)
|
|
32
|
+
|
|
33
|
+
covariance = np.zeros((n_classes, n_features, n_features))
|
|
34
|
+
|
|
35
|
+
for k, c in enumerate(self._classes):
|
|
36
|
+
X_c = X[Y == c]
|
|
37
|
+
n_c = len(X_c)
|
|
38
|
+
|
|
39
|
+
if n_c < 2:
|
|
40
|
+
raise ValueError(f"A classe {c!r} tem {n_c} amostra(s); o QDA precisa de pelo menos 2 por classe.")
|
|
41
|
+
|
|
42
|
+
centro = X_c - self._means[k]
|
|
43
|
+
cov = centro.T @ centro / (n_c - 1) + self.reg_param * np.eye(n_features)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
np.linalg.cholesky(cov)
|
|
47
|
+
except np.linalg.LinAlgError:
|
|
48
|
+
raise ValueError(f"Covariância da classe {c!r} é singular. Use reg_param > 0 ou remova atributos colineares.")
|
|
49
|
+
|
|
50
|
+
covariance[k] = cov
|
|
51
|
+
|
|
52
|
+
self._covariance = covariance
|
|
53
|
+
|
|
54
|
+
def predict(self, X : np.ndarray) -> np.ndarray:
|
|
55
|
+
"""Estima valores de acordo com as covariâncias calculadas
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
X (np.ndarray): Matriz de atributos
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
np.ndarray: Vetor dos valores calculados
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if self._covariance is None:
|
|
65
|
+
raise RuntimeError("Chame fit antes de predict.")
|
|
66
|
+
|
|
67
|
+
if not isinstance(X, np.ndarray):
|
|
68
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
69
|
+
|
|
70
|
+
X = X.astype(np.float64, copy=False)
|
|
71
|
+
|
|
72
|
+
if X.ndim == 1:
|
|
73
|
+
X = X.reshape(1, -1)
|
|
74
|
+
if X.ndim != 2 or X.shape[1] != self._n_features:
|
|
75
|
+
raise ValueError(f"X deve ter shape (n_samples, {self._n_features}).")
|
|
76
|
+
if not np.isfinite(X).all():
|
|
77
|
+
raise ValueError("X contém valores não finitos.")
|
|
78
|
+
|
|
79
|
+
n_samples = X.shape[0]
|
|
80
|
+
n_classes = len(self._classes)
|
|
81
|
+
|
|
82
|
+
score = np.zeros((n_samples, n_classes))
|
|
83
|
+
|
|
84
|
+
for k in range(n_classes):
|
|
85
|
+
cov = self._covariance[k]
|
|
86
|
+
m = self._means[k]
|
|
87
|
+
p = self._priors[k]
|
|
88
|
+
|
|
89
|
+
_, logdet = np.linalg.slogdet(cov)
|
|
90
|
+
|
|
91
|
+
centro = X - m
|
|
92
|
+
sol = np.linalg.solve(cov, centro.T).T
|
|
93
|
+
mahalanobis = np.sum(centro * sol, axis=1)
|
|
94
|
+
|
|
95
|
+
score[:, k] = -0.5 * logdet - 0.5 * mahalanobis + np.log(p)
|
|
96
|
+
|
|
97
|
+
return self._classes[np.argmax(score, axis=1)]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LogisticRegression:
|
|
5
|
+
"""Regressão Logística treinada por Gradient Descent.
|
|
6
|
+
|
|
7
|
+
Com duas classes, treina um único classificador, que estima a probabilidade
|
|
8
|
+
da classe positiva (classes_[1]). Com mais de duas classes, treina um classificador
|
|
9
|
+
binário por classe, que distingue essa classe de todas as demais (One Vs Rest).
|
|
10
|
+
Como o Gradient Descent é sensível à escala dos atributos, padronize X antes do fit
|
|
11
|
+
|
|
12
|
+
Cada classificador minimiza a Binary Cross-Entropy média, com penalidade
|
|
13
|
+
L2 opcional nos coeficientes (o intercepto não é penalizado):
|
|
14
|
+
|
|
15
|
+
J(w, b) = BCE(w, b) + (alpha / 2) * ||w||^2
|
|
16
|
+
|
|
17
|
+
No caso de ter mais de 2 classes, as probabilidades são os scores
|
|
18
|
+
dos classificadores normalizados para somar 1.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
classes_ (np.ndarray): Classes vistas no treino, em ordem crescente.
|
|
22
|
+
coef_ (np.ndarray): Coeficientes dos classificadores, com shape
|
|
23
|
+
(n_classifiers, n_features). Cada linha é um classificador:
|
|
24
|
+
n_classifiers é 1 com duas classes e n_classes com mais de duas.
|
|
25
|
+
intercept_ (np.ndarray): Interceptos, com shape (n_classifiers,).
|
|
26
|
+
n_features_in_ (int): Número de atributos vistos no fit.
|
|
27
|
+
n_iter_ (np.ndarray): Iterações realizadas por cada classificador.
|
|
28
|
+
converged_ (np.ndarray): Indica se cada classificador atingiu o
|
|
29
|
+
critério de parada pelo gradiente antes de max_iter. Use
|
|
30
|
+
converged_.all() para saber se todos convergiram.
|
|
31
|
+
loss_history (list[list[float]]): Valor de J em cada iteração, uma
|
|
32
|
+
lista por classificador, calculado com os parâmetros anteriores
|
|
33
|
+
à atualização daquela iteração.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, learning_rate: float = 0.5, max_iter: int = 10000, tol: float = 1e-6, alpha: float = 0.0):
|
|
37
|
+
"""Inicializa a Regressão Logística.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
learning_rate (float): Taxa de aprendizado do Gradient Descent.
|
|
41
|
+
max_iter (int): Número máximo de iterações de cada classificador.
|
|
42
|
+
tol (float): Tolerância para o critério de parada. O treino de
|
|
43
|
+
cada classificador termina quando a norma do gradiente fica
|
|
44
|
+
abaixo de tol.
|
|
45
|
+
alpha (float): Força da regularização L2. Com 0.0 (padrão), não há
|
|
46
|
+
regularização. Quanto maior o alpha, menor deve ser a
|
|
47
|
+
learning_rate para o treino não divergir.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if isinstance(learning_rate, bool) or not isinstance(learning_rate, (int, float, np.integer, np.floating)):
|
|
51
|
+
raise ValueError("learning_rate deve ser um número.")
|
|
52
|
+
|
|
53
|
+
if not np.isfinite(learning_rate) or learning_rate <= 0:
|
|
54
|
+
raise ValueError("learning_rate deve ser finito e positivo.")
|
|
55
|
+
|
|
56
|
+
if isinstance(max_iter, bool) or not isinstance(max_iter, (int, np.integer)):
|
|
57
|
+
raise ValueError("max_iter deve ser um inteiro.")
|
|
58
|
+
|
|
59
|
+
if max_iter <= 0:
|
|
60
|
+
raise ValueError("max_iter deve ser um inteiro positivo.")
|
|
61
|
+
|
|
62
|
+
if isinstance(tol, bool) or not isinstance(tol, (int, float, np.integer, np.floating)):
|
|
63
|
+
raise ValueError("tol deve ser um número.")
|
|
64
|
+
|
|
65
|
+
if not np.isfinite(tol) or tol < 0:
|
|
66
|
+
raise ValueError("tol deve ser finito e não negativo.")
|
|
67
|
+
|
|
68
|
+
if isinstance(alpha, bool) or not isinstance(alpha, (int, float, np.integer, np.floating)):
|
|
69
|
+
raise ValueError("alpha deve ser um número.")
|
|
70
|
+
|
|
71
|
+
if not np.isfinite(alpha) or alpha < 0:
|
|
72
|
+
raise ValueError("alpha deve ser finito e não negativo.")
|
|
73
|
+
|
|
74
|
+
self.learning_rate = learning_rate
|
|
75
|
+
self.max_iter = int(max_iter)
|
|
76
|
+
self.tol = tol
|
|
77
|
+
self.alpha = alpha
|
|
78
|
+
|
|
79
|
+
self.classes_ = None
|
|
80
|
+
self.coef_ = None
|
|
81
|
+
self.intercept_ = None
|
|
82
|
+
self.n_features_in_ = None
|
|
83
|
+
self.n_iter_ = None
|
|
84
|
+
self.converged_ = None
|
|
85
|
+
self.loss_history = []
|
|
86
|
+
|
|
87
|
+
def _sigmoid(self, z: np.ndarray) -> np.ndarray:
|
|
88
|
+
"""Calcula a função sigmoid de forma numericamente estável."""
|
|
89
|
+
|
|
90
|
+
result = np.empty_like(z, dtype=np.float64)
|
|
91
|
+
|
|
92
|
+
positive = z >= 0
|
|
93
|
+
negative = ~positive
|
|
94
|
+
|
|
95
|
+
result[positive] = 1 / (1 + np.exp(-z[positive]))
|
|
96
|
+
|
|
97
|
+
exp_z = np.exp(z[negative])
|
|
98
|
+
result[negative] = exp_z / (1 + exp_z)
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
def _loss(self, y: np.ndarray, probabilities: np.ndarray) -> float:
|
|
103
|
+
"""Calcula a Binary Cross-Entropy média."""
|
|
104
|
+
|
|
105
|
+
probabilities = np.clip(probabilities, 1e-15, 1 - 1e-15)
|
|
106
|
+
|
|
107
|
+
return -np.mean(y * np.log(probabilities) + (1 - y) * np.log(1 - probabilities))
|
|
108
|
+
|
|
109
|
+
def _check_X(self, X: np.ndarray) -> np.ndarray:
|
|
110
|
+
"""Valida o tipo de X e o converte para float64.
|
|
111
|
+
|
|
112
|
+
Usada por fit e predict_proba para que os dois métodos aceitem e rejeitem
|
|
113
|
+
exatamente as mesmas entradas.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
X (np.ndarray): Matriz de atributos.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
np.ndarray: X convertido para dtype float64.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if not isinstance(X, np.ndarray):
|
|
123
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
124
|
+
|
|
125
|
+
if X.dtype.kind not in "biufO":
|
|
126
|
+
raise ValueError("X deve conter apenas valores numéricos.")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
return X.astype(np.float64)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
raise ValueError("X deve conter apenas valores numéricos.") from None
|
|
132
|
+
|
|
133
|
+
def _fit_binary(self, X: np.ndarray, y: np.ndarray):
|
|
134
|
+
"""Treina um classificador binário por Gradient Descent.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
X (np.ndarray): Matriz de atributos, já validada.
|
|
138
|
+
y (np.ndarray): Vetor com 0.0 e 1.0 (1.0 é a classe positiva).
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
tuple: (coef, intercept, n_iter, converged, loss_history).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
n_samples, n_features = X.shape
|
|
145
|
+
|
|
146
|
+
coef = np.zeros(n_features, dtype=np.float64)
|
|
147
|
+
intercept = 0.0
|
|
148
|
+
|
|
149
|
+
loss_history = []
|
|
150
|
+
converged = False
|
|
151
|
+
n_iter = 0
|
|
152
|
+
|
|
153
|
+
for n_iter in range(1, self.max_iter + 1):
|
|
154
|
+
z = X @ coef + intercept
|
|
155
|
+
probabilities = self._sigmoid(z)
|
|
156
|
+
|
|
157
|
+
# Função objetivo J (entropia cruzada + penalidade L2) calculada
|
|
158
|
+
# com os parâmetros anteriores à atualização desta iteração.
|
|
159
|
+
loss = self._loss(y, probabilities) + 0.5 * self.alpha * np.sum(coef ** 2)
|
|
160
|
+
loss_history.append(loss)
|
|
161
|
+
|
|
162
|
+
error = probabilities - y
|
|
163
|
+
|
|
164
|
+
# A penalidade L2 soma alpha * coef ao gradiente dos
|
|
165
|
+
# coeficientes. O intercepto não é penalizado.
|
|
166
|
+
gradient_coef = X.T @ error / n_samples + self.alpha * coef
|
|
167
|
+
gradient_intercept = np.mean(error)
|
|
168
|
+
|
|
169
|
+
# Critério de parada pela norma do gradiente: não depende da
|
|
170
|
+
# learning_rate, ao contrário da mudança nos parâmetros.
|
|
171
|
+
gradient_norm = max(np.linalg.norm(gradient_coef), abs(gradient_intercept))
|
|
172
|
+
|
|
173
|
+
if gradient_norm < self.tol:
|
|
174
|
+
converged = True
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
coef -= self.learning_rate * gradient_coef
|
|
178
|
+
intercept -= self.learning_rate * gradient_intercept
|
|
179
|
+
|
|
180
|
+
return coef, intercept, n_iter, converged, loss_history
|
|
181
|
+
|
|
182
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
183
|
+
"""Ajusta o modelo aos dados de treino.
|
|
184
|
+
|
|
185
|
+
Com duas classes, treina um classificador para a classe positiva
|
|
186
|
+
(classes_[1]). Com mais de duas, treina um por classe (One-vs-Rest).
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
X (np.ndarray): Matriz de atributos.
|
|
190
|
+
y (np.ndarray): Vetor com as classes.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
self: Modelo ajustado.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
X = self._check_X(X)
|
|
197
|
+
|
|
198
|
+
if not isinstance(y, np.ndarray):
|
|
199
|
+
raise TypeError(f"y deve ser np.ndarray, recebeu {type(y).__name__}")
|
|
200
|
+
|
|
201
|
+
if X.ndim != 2:
|
|
202
|
+
raise ValueError("X deve ser 2D, com shape (n_samples, n_features).")
|
|
203
|
+
|
|
204
|
+
if X.shape[0] == 0 or X.shape[1] == 0:
|
|
205
|
+
raise ValueError("X não pode ser vazio.")
|
|
206
|
+
|
|
207
|
+
if y.ndim != 1 or len(y) != X.shape[0]:
|
|
208
|
+
raise ValueError("y deve ser 1D e ter o mesmo número de amostras que X.")
|
|
209
|
+
|
|
210
|
+
if not np.isfinite(X).all():
|
|
211
|
+
raise ValueError("X contém valores não finitos.")
|
|
212
|
+
|
|
213
|
+
if y.dtype.kind == "f" and not np.isfinite(y).all():
|
|
214
|
+
raise ValueError("y contém valores não finitos.")
|
|
215
|
+
|
|
216
|
+
# Todo o treino usa variáveis locais. Os atributos do modelo só
|
|
217
|
+
# são atribuídos no final, para que um fit que falha não deixe
|
|
218
|
+
# o modelo num estado inconsistente.
|
|
219
|
+
n_features = X.shape[1]
|
|
220
|
+
classes = np.unique(y)
|
|
221
|
+
|
|
222
|
+
if len(classes) < 2:
|
|
223
|
+
raise ValueError("LogisticRegression requer pelo menos duas classes.")
|
|
224
|
+
|
|
225
|
+
# Com duas classes, os dois classificadores OvR seriam espelhos um do
|
|
226
|
+
# outro, então basta treinar o da classe positiva.
|
|
227
|
+
targets = classes[1:] if len(classes) == 2 else classes
|
|
228
|
+
n_classifiers = len(targets)
|
|
229
|
+
|
|
230
|
+
coef = np.zeros((n_classifiers, n_features), dtype=np.float64)
|
|
231
|
+
intercept = np.zeros(n_classifiers, dtype=np.float64)
|
|
232
|
+
n_iter = np.zeros(n_classifiers, dtype=int)
|
|
233
|
+
converged = np.zeros(n_classifiers, dtype=bool)
|
|
234
|
+
loss_history = []
|
|
235
|
+
|
|
236
|
+
for i, current_class in enumerate(targets):
|
|
237
|
+
y_binary = (y == current_class).astype(np.float64)
|
|
238
|
+
|
|
239
|
+
coef[i], intercept[i], n_iter[i], converged[i], current_loss_history = self._fit_binary(X, y_binary)
|
|
240
|
+
loss_history.append(current_loss_history)
|
|
241
|
+
|
|
242
|
+
self.classes_ = classes
|
|
243
|
+
self.n_features_in_ = n_features
|
|
244
|
+
self.coef_ = coef
|
|
245
|
+
self.intercept_ = intercept
|
|
246
|
+
self.n_iter_ = n_iter
|
|
247
|
+
self.converged_ = converged
|
|
248
|
+
self.loss_history = loss_history
|
|
249
|
+
|
|
250
|
+
return self
|
|
251
|
+
|
|
252
|
+
def predict_proba(self, X: np.ndarray) -> np.ndarray:
|
|
253
|
+
"""Calcula as probabilidades previstas para cada classe.
|
|
254
|
+
|
|
255
|
+
Com duas classes, as probabilidades vêm do classificador da classe
|
|
256
|
+
positiva. Com mais de duas, são os scores dos classificadores
|
|
257
|
+
One-vs-Rest normalizados para que cada linha some 1.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
X (np.ndarray): Matriz de atributos. Um vetor 1D é tratado como
|
|
261
|
+
uma única amostra.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
np.ndarray: Probabilidades com shape (n_samples, n_classes), com
|
|
265
|
+
as colunas na ordem de classes_.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
if self.coef_ is None:
|
|
269
|
+
raise RuntimeError("Chame fit antes de predict_proba.")
|
|
270
|
+
|
|
271
|
+
X = self._check_X(X)
|
|
272
|
+
|
|
273
|
+
if X.ndim == 1:
|
|
274
|
+
X = X.reshape(1, -1)
|
|
275
|
+
|
|
276
|
+
if X.ndim != 2 or X.shape[1] != self.n_features_in_:
|
|
277
|
+
raise ValueError(f"X deve ter shape (n_samples, {self.n_features_in_}).")
|
|
278
|
+
|
|
279
|
+
if not np.isfinite(X).all():
|
|
280
|
+
raise ValueError("X contém valores não finitos.")
|
|
281
|
+
|
|
282
|
+
z = X @ self.coef_.T + self.intercept_
|
|
283
|
+
|
|
284
|
+
if len(self.classes_) == 2:
|
|
285
|
+
probabilities_class_1 = self._sigmoid(z[:, 0])
|
|
286
|
+
|
|
287
|
+
return np.column_stack((1 - probabilities_class_1, probabilities_class_1))
|
|
288
|
+
|
|
289
|
+
# Normaliza os scores sigmoid em escala log. Dividir os sigmoids
|
|
290
|
+
# diretamente daria 0 / 0 quando todos fossem arredondados para 0.
|
|
291
|
+
log_scores = -np.logaddexp(0, -z)
|
|
292
|
+
log_scores -= log_scores.max(axis=1, keepdims=True)
|
|
293
|
+
|
|
294
|
+
probabilities = np.exp(log_scores)
|
|
295
|
+
probabilities /= probabilities.sum(axis=1, keepdims=True)
|
|
296
|
+
|
|
297
|
+
return probabilities
|
|
298
|
+
|
|
299
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
300
|
+
"""Prevê as classes dos dados fornecidos.
|
|
301
|
+
|
|
302
|
+
Com duas classes, prevê a positiva se sua probabilidade for >= 0.5.
|
|
303
|
+
Com mais de duas, prevê a de maior probabilidade.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
X (np.ndarray): Matriz de atributos.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
np.ndarray: Classes previstas.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
if self.coef_ is None:
|
|
313
|
+
raise RuntimeError("Chame fit antes de predict.")
|
|
314
|
+
|
|
315
|
+
probabilities = self.predict_proba(X)
|
|
316
|
+
|
|
317
|
+
if len(self.classes_) == 2:
|
|
318
|
+
indices = (probabilities[:, 1] >= 0.5).astype(int)
|
|
319
|
+
else:
|
|
320
|
+
indices = np.argmax(probabilities, axis=1)
|
|
321
|
+
|
|
322
|
+
return self.classes_[indices]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class KNN:
|
|
4
|
+
def __init__(self, k: int, p: int = 2) -> None:
|
|
5
|
+
"""Inicializa o classificador KNN.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
k (int): Número de base para a comparação
|
|
9
|
+
p (int, opcional): Parâmetro que define a métrica da distância. Default: 2.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
if k < 1:
|
|
13
|
+
raise ValueError("k deve ser maior ou igual a 1.")
|
|
14
|
+
if p not in [1,2]:
|
|
15
|
+
raise ValueError("p deve ser 1 ou 2.")
|
|
16
|
+
|
|
17
|
+
self._k = k
|
|
18
|
+
self._p = p
|
|
19
|
+
self._X = None
|
|
20
|
+
self._y = None
|
|
21
|
+
|
|
22
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
23
|
+
"""Treina o modelo de classificação KNN.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
X (np.ndarray): Matriz dos atributos
|
|
27
|
+
y (np.ndarray): Vetor com os resultados
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
self: KNN
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
if not isinstance(X, np.ndarray):
|
|
34
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
35
|
+
if not isinstance(y, np.ndarray):
|
|
36
|
+
raise TypeError(f"y deve ser np.ndarray, recebeu {type(y).__name__}")
|
|
37
|
+
|
|
38
|
+
if len(X) != len(y):
|
|
39
|
+
raise ValueError("X e y devem ser de mesmo tamanho")
|
|
40
|
+
|
|
41
|
+
self._X = X
|
|
42
|
+
self._y = y
|
|
43
|
+
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def _get_distances_with_labels(self, x: np.ndarray) -> np.ndarray:
|
|
47
|
+
"""Ordena os rótulos de treino pela distância até um ponto.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
x (np.ndarray): Ponto a ser comparado.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
np.ndarray: Rótulos de treino ordenados por distância crescente
|
|
54
|
+
até x.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
diff = np.abs(self._X - x)
|
|
58
|
+
distances = np.sum(diff ** self._p, axis=1) ** (1 / self._p)
|
|
59
|
+
|
|
60
|
+
result = np.argsort(distances, kind='stable')
|
|
61
|
+
|
|
62
|
+
return self._y[result]
|
|
63
|
+
|
|
64
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
65
|
+
"""Estima a classificação de cada rótulo pelos k vizinhos mais próximos.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
X (np.ndarray): Amostras pendentes de classificação.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
np.ndarray: Resultado da estimativa.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if not isinstance(X, np.ndarray):
|
|
75
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
76
|
+
|
|
77
|
+
predicted_labels = []
|
|
78
|
+
|
|
79
|
+
for x in X:
|
|
80
|
+
sorted_labels = self._get_distances_with_labels(x)
|
|
81
|
+
|
|
82
|
+
k_neighbors = sorted_labels[:self._k]
|
|
83
|
+
|
|
84
|
+
# Escolhe o primero rótulo por ser a dos pontos mais próximos
|
|
85
|
+
label, first_pos, count = np.unique(k_neighbors, return_index=True, return_counts=True)
|
|
86
|
+
candidates = count == count.max()
|
|
87
|
+
predicted_labels.append(label[np.argmin(first_pos[candidates])])
|
|
88
|
+
|
|
89
|
+
return np.array(predicted_labels)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class MLPRegressor:
|
|
4
|
+
def __init__(self, hidden_layers: tuple=(10,), learning_rate: float=0.01, n_epochs: int=1000, random_state: int = None):
|
|
5
|
+
"""Inicializa o regressor MLP.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
hidden_layers (tuple, opcional): Tamanhos das camadas ocultas. Padrão é (10,).
|
|
9
|
+
learning_rate (float, opcional): Passo do gradiente descende. Padrão é 0.01.
|
|
10
|
+
n_epochs (int, opcional): Vezes que o conjunto de treino é percorrido. Padrão é 1000.
|
|
11
|
+
random_state (int, opcional): Seed geradora dos pesos inciais. Padrão é None.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
if not isinstance(hidden_layers, (tuple, list)):
|
|
15
|
+
raise TypeError("hidden_layers deve ser uma tupla de inteiros.")
|
|
16
|
+
|
|
17
|
+
if any((not isinstance(h, (int, np.integer))) or h <= 0 for h in hidden_layers):
|
|
18
|
+
raise ValueError("Cada camada oculta deve ter um número inteiro positivo de neurônios.")
|
|
19
|
+
|
|
20
|
+
if learning_rate <= 0:
|
|
21
|
+
raise ValueError("learning_rate deve ser positivo.")
|
|
22
|
+
|
|
23
|
+
if not isinstance(n_epochs, (int, np.integer)) or n_epochs <= 0:
|
|
24
|
+
raise TypeError("n_epochs deve ser um inteiro positivo.")
|
|
25
|
+
|
|
26
|
+
self._hidden = hidden_layers
|
|
27
|
+
self._lr = learning_rate
|
|
28
|
+
self._n_epochs = n_epochs
|
|
29
|
+
self._rng = np.random.default_rng(random_state)
|
|
30
|
+
self._weights, self._biases, self._n_features = None, None, None
|
|
31
|
+
|
|
32
|
+
def _init_parameters(self, sizes: list):
|
|
33
|
+
"""Inicializa os pesos e vieses de cada camada.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
sizes (list): Matriz com o tamanho das camadas de entrada, oculta e de saída.
|
|
37
|
+
"""
|
|
38
|
+
self._weights, self._biases = [], []
|
|
39
|
+
for layer in range(len(sizes)-1):
|
|
40
|
+
n_in, n_out = sizes[layer], sizes[layer + 1]
|
|
41
|
+
|
|
42
|
+
# Desvio He para ReLU
|
|
43
|
+
W = self._rng.normal(0, np.sqrt(2/n_in), size=(n_in, n_out))
|
|
44
|
+
b = np.zeros(n_out)
|
|
45
|
+
self._weights.append(W)
|
|
46
|
+
self._biases.append(b)
|
|
47
|
+
|
|
48
|
+
def _forward(self, X: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
49
|
+
"""Propaga o X pela rede neural.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
X (np.ndarray): _description_
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
np.ndarray: Lista com L+1 arrays; activations[0] é X e activations[-1] é a saída (n, 1).
|
|
56
|
+
np.ndarray: Lista com L arrays com os valores antes da ativação de cada camada.
|
|
57
|
+
"""
|
|
58
|
+
activations, zs = [X], []
|
|
59
|
+
n_layers = len(self._weights)
|
|
60
|
+
|
|
61
|
+
for l in range(n_layers):
|
|
62
|
+
z = activations[-1] @ self._weights[l] + self._biases[l]
|
|
63
|
+
zs.append(z)
|
|
64
|
+
|
|
65
|
+
if l < n_layers- 1:
|
|
66
|
+
a = np.maximum(0, z)
|
|
67
|
+
else:
|
|
68
|
+
a = z
|
|
69
|
+
|
|
70
|
+
activations.append(a)
|
|
71
|
+
|
|
72
|
+
return activations, zs
|
|
73
|
+
|
|
74
|
+
def _backward(self, y: np.ndarray, activations: np.ndarray, z: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
75
|
+
"""Calcula os gradientes da perda em relação a pesos e vieses.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
y (np.ndarray): _description_
|
|
79
|
+
activations (np.ndarray): _description_
|
|
80
|
+
z (np.ndarray): _description_
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
np.ndarray:Gradientes dos pesos, com os mesmos tamanhos de self._weights.
|
|
84
|
+
np.ndarray: Gradientes dos vieses, com os mesmos tamanhos de self._biases.
|
|
85
|
+
"""
|
|
86
|
+
n_samples = y.shape[0]
|
|
87
|
+
n_layers = len(self._weights)
|
|
88
|
+
|
|
89
|
+
y_pred = activations[-1]
|
|
90
|
+
delta = (y_pred-y) / n_samples
|
|
91
|
+
grads_W, grads_b = [None] * n_layers, [None] * n_layers
|
|
92
|
+
|
|
93
|
+
for l in reversed(range(n_layers)):
|
|
94
|
+
grads_W[l] = activations[l].T @ delta
|
|
95
|
+
grads_b[l] = np.sum(delta, axis=0)
|
|
96
|
+
if l > 0:
|
|
97
|
+
reLU_deriv = (z[l - 1] > 0)
|
|
98
|
+
delta = delta @ self._weights[l].T * reLU_deriv
|
|
99
|
+
|
|
100
|
+
return grads_W, grads_b
|
|
101
|
+
|
|
102
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
103
|
+
"""Treina o regressor MLP por gradiente descendente.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
X (np.ndarray): Atributos de treino.
|
|
107
|
+
y (np.ndarray): Valores-alvo contínuos.
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(X, np.ndarray):
|
|
110
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}")
|
|
111
|
+
if not isinstance(y, np.ndarray):
|
|
112
|
+
raise TypeError(f"y deve ser np.ndarray, recebeu {type(y).__name__}")
|
|
113
|
+
|
|
114
|
+
if X.ndim != 2:
|
|
115
|
+
raise ValueError("X deve ser 2D")
|
|
116
|
+
if y.ndim != 1 or len(y) != X.shape[0]:
|
|
117
|
+
raise ValueError("y deve ser 1D e ter o mesmo número de amostras que X.")
|
|
118
|
+
if X.shape[0] == 0:
|
|
119
|
+
raise ValueError("X não pode ser vazio.")
|
|
120
|
+
if not np.isfinite(X).all():
|
|
121
|
+
raise ValueError("X contém NaN ou infinito.")
|
|
122
|
+
if not np.isfinite(y).all():
|
|
123
|
+
raise ValueError("y contém NaN ou infinito.")
|
|
124
|
+
|
|
125
|
+
n_samples, self._n_features = X.shape
|
|
126
|
+
y = y.reshape(-1, 1)
|
|
127
|
+
|
|
128
|
+
size = [self._n_features, *self._hidden, 1]
|
|
129
|
+
self._init_parameters(size)
|
|
130
|
+
|
|
131
|
+
self._loss_history = []
|
|
132
|
+
for epoch in range(self._n_epochs):
|
|
133
|
+
activations, zs = self._forward(X)
|
|
134
|
+
y_pred = activations[-1]
|
|
135
|
+
|
|
136
|
+
loss = np.mean((y_pred - y)**2) / 2
|
|
137
|
+
|
|
138
|
+
if not np.isfinite(loss):
|
|
139
|
+
raise FloatingPointError("A perda divergiu; reduza learning_rate ou padronize X.")
|
|
140
|
+
self._loss_history.append(loss)
|
|
141
|
+
|
|
142
|
+
grads_W, grads_b = self._backward(y, activations, zs)
|
|
143
|
+
|
|
144
|
+
for l in range(len(self._weights)):
|
|
145
|
+
self._weights[l] -= self._lr * grads_W[l]
|
|
146
|
+
self._biases[l] -= self._lr * grads_b[l]
|
|
147
|
+
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
151
|
+
"""Prediz o valor contínuo de cada amostra.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
X (np.ndarray): Valores a prever, de tamanho (n_samples, n_features).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
np.ndarray: Valores preditos, de tamanho (n_samples,).
|
|
158
|
+
"""
|
|
159
|
+
if self._weights is None:
|
|
160
|
+
raise RuntimeError("Chame fit antes de predict.")
|
|
161
|
+
|
|
162
|
+
if not isinstance(X, np.ndarray):
|
|
163
|
+
raise TypeError(f"X deve ser np.ndarray, recebeu {type(X).__name__}.")
|
|
164
|
+
if X.ndim == 1:
|
|
165
|
+
X = X.reshape(1, -1)
|
|
166
|
+
if X.ndim != 2 or X.shape[1] != self._n_features:
|
|
167
|
+
raise ValueError(f"X deve ter shape (n_samples, {self._n_features}).")
|
|
168
|
+
if not np.isfinite(X).all():
|
|
169
|
+
raise ValueError("X contém NaN ou infinito.")
|
|
170
|
+
|
|
171
|
+
activations, _ = self._forward(X)
|
|
172
|
+
return activations[-1].ravel()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from saikit_learn.regression.minimos_quadrados import OLS
|
|
2
|
+
from saikit_learn.regression.ridge import Ridge
|
|
3
|
+
from saikit_learn.regression.lasso import Lasso
|
|
4
|
+
from saikit_learn.regression.regressao_polinomial import PolReg
|
|
5
|
+
|
|
6
|
+
__all__ = ['OLS', 'Ridge', 'Lasso', 'PolReg']
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from saikit_learn.regression.utils.validacao import verify_X_y, verify_array, NotFittedError
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class Lasso:
|
|
5
|
+
def __init__(self, lmbd : float = 1.0, intercepto : bool = True):
|
|
6
|
+
"""Regressão Lasso por descida por coordenadas.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
lmbd (float, opcional): Força da penalidade L1 (>= 0).. Padrão é 1.0.
|
|
10
|
+
intercepto (bool, opcional): Se True, ajusta um intercepto não penalizado. Padrão é True.
|
|
11
|
+
"""
|
|
12
|
+
if (isinstance(lmbd, bool)
|
|
13
|
+
or not isinstance(lmbd, (int, float, np.integer, np.floating))
|
|
14
|
+
or not np.isfinite(lmbd) or lmbd < 0):
|
|
15
|
+
raise ValueError("lmbd deve ser um número real finito e não negativo.")
|
|
16
|
+
|
|
17
|
+
self.lmbd = lmbd
|
|
18
|
+
self.intercepto = intercepto
|
|
19
|
+
self._fitted = False
|
|
20
|
+
|
|
21
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
22
|
+
"""Treina o modelo dos X de acordo com os valores y dados.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
X (np.ndarray): Atributos com tamanho (n_samples, n_features)
|
|
26
|
+
y (np.ndarray): Valores alvos de tamanho (n_sample)
|
|
27
|
+
"""
|
|
28
|
+
X, y = verify_X_y(X, y)
|
|
29
|
+
n, d = X.shape
|
|
30
|
+
self.n_features_in_ = d
|
|
31
|
+
|
|
32
|
+
if self.intercepto:
|
|
33
|
+
x_mean, y_mean = X.mean(axis=0), y.mean()
|
|
34
|
+
else:
|
|
35
|
+
x_mean, y_mean = np.zeros(d), 0.0
|
|
36
|
+
Xc, yc = X - x_mean, y - y_mean
|
|
37
|
+
|
|
38
|
+
col_sq = (Xc ** 2).sum(axis=0) / n
|
|
39
|
+
w = np.zeros(d)
|
|
40
|
+
residual = yc.copy()
|
|
41
|
+
|
|
42
|
+
for _ in range(1000):
|
|
43
|
+
w_original = w.copy()
|
|
44
|
+
for j in range(d):
|
|
45
|
+
if col_sq[j] == 0:
|
|
46
|
+
continue
|
|
47
|
+
w_old = w[j]
|
|
48
|
+
rho = Xc[:, j] @ residual / n + col_sq[j] * w_old
|
|
49
|
+
w_new = np.sign(rho) * max(abs(rho) - self.lmbd, 0.0) / col_sq[j]
|
|
50
|
+
if w_new != w_old:
|
|
51
|
+
residual -= Xc[:, j] * (w_new - w_old)
|
|
52
|
+
w[j] = w_new
|
|
53
|
+
if np.allclose(w, w_original, atol=1e-8, rtol=0):
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
self.coef_ = w
|
|
57
|
+
self.intercept_ = float(y_mean - x_mean @ w)
|
|
58
|
+
self._fitted = True
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def predict(self, X : np.ndarray):
|
|
63
|
+
"""Estima os alvos para X de tamanho (n_samples, n_features).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
X (np.ndarray): Atributos, shape (n_samples, n_features).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
np.ndarray: Valores estimados, shape (n_samples,).
|
|
70
|
+
"""
|
|
71
|
+
if not self._fitted:
|
|
72
|
+
raise NotFittedError(f"{self.__class__.__name__} não foi ajustado ainda.")
|
|
73
|
+
|
|
74
|
+
X = verify_array(X)
|
|
75
|
+
|
|
76
|
+
if X.shape[1] != self.n_features_in_:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Número de features difere: esperado {self.n_features_in_}, obtido {X.shape[1]}."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return X @ self.coef_ + self.intercept_
|
|
82
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from saikit_learn.regression.utils.validacao import verify_array, verify_X_y, NotFittedError
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class OLS:
|
|
5
|
+
def __init__(self, intercepto: bool = True):
|
|
6
|
+
self.intercepto = intercepto
|
|
7
|
+
self._fitted = False
|
|
8
|
+
|
|
9
|
+
def _design(self, x: np.ndarray) -> np.ndarray:
|
|
10
|
+
"""Monta a matriz de design usada no ajuste.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
x (np.ndarray): Matriz de atributos, de tamanho (n_samples, n_features).
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
np.ndarray: Matriz de design, de tamanho (n_samples, n_features + 1) com intercepto ou (n_samples, n_features) sem intercepto.
|
|
17
|
+
"""
|
|
18
|
+
if self.intercepto:
|
|
19
|
+
X = np.column_stack([np.ones(x.shape[0]), x])
|
|
20
|
+
return X
|
|
21
|
+
return x
|
|
22
|
+
|
|
23
|
+
def fit(self, X: np.ndarray, y:np.ndarray):
|
|
24
|
+
"""Treina o modelo dos X de acordo com os valores y dados.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
X (np.ndarray): Atributos de treino, de shape (n_samples, n_features).
|
|
28
|
+
y (np.ndarray): Valores-alvo, de shape (n_samples,).
|
|
29
|
+
"""
|
|
30
|
+
X, y = verify_X_y(X, y)
|
|
31
|
+
self.n_features_in_ = X.shape[1]
|
|
32
|
+
X = self._design(X)
|
|
33
|
+
try:
|
|
34
|
+
beta = np.linalg.solve(X.T @ X, X.T @ y)
|
|
35
|
+
except np.linalg.LinAlgError:
|
|
36
|
+
raise ValueError("X tem colunas linearmente dependentes; OLS não tem solução única.")
|
|
37
|
+
|
|
38
|
+
self.intercept_ = float(beta[0]) if self.intercepto else 0.0
|
|
39
|
+
self.coef_ = beta[1:] if self.intercepto else beta
|
|
40
|
+
self._fitted = True
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
44
|
+
"""Estima os valores de X
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
X (np.ndarray): Valores a serem estimados, de tamanho (n_samples, n_features).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
np.ndarray: Valores estimados pelo método, de tamanho (n_samples,).
|
|
51
|
+
"""
|
|
52
|
+
if not self._fitted:
|
|
53
|
+
raise NotFittedError(
|
|
54
|
+
f'{self.__class__.__name__} não foi treinado ainda, chame fit.'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
X = verify_array(X)
|
|
58
|
+
|
|
59
|
+
if self.n_features_in_ != X.shape[1]:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f'a dimensão das features difere; necessário: {self.n_features_in_}; obtido: {X.shape[1]}'
|
|
62
|
+
)
|
|
63
|
+
return X @ self.coef_ + self.intercept_
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from saikit_learn.regression.utils.validacao import verify_array, verify_X_y, NotFittedError
|
|
2
|
+
from saikit_learn.regression.minimos_quadrados import OLS
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
class PolReg:
|
|
6
|
+
def __init__(self, d : int, intercepto: bool = True):
|
|
7
|
+
"""Inicializa o modelo de regressão polinomial.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
d (int): Grau do polinômio ajustado.
|
|
11
|
+
intercepto (bool, opcional): Se True, inclui o termo constante (intercepto) no modelo. Padrão é True.
|
|
12
|
+
"""
|
|
13
|
+
self.d = d
|
|
14
|
+
self.intercepto = intercepto
|
|
15
|
+
self._fitted = False
|
|
16
|
+
|
|
17
|
+
def _design(self, X : np.ndarray) -> np.ndarray:
|
|
18
|
+
"""Monta a matriz de potências usada no ajuste.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
X (np.ndarray): Atributo de entrada, de tamanho (n_samples, 1).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
np.ndarray: Matriz de potências, de tamanho (n_samples, d).
|
|
25
|
+
"""
|
|
26
|
+
return np.hstack([X ** k for k in range(1, self.d + 1)])
|
|
27
|
+
|
|
28
|
+
def fit(self, X : np.ndarray, y : np.ndarray):
|
|
29
|
+
"""Treina o modelo polinomial de grau d com os dados fornecidos.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
X (np.ndarray): Atributo de treino, de tamanho (n_samples, 1).
|
|
33
|
+
y (np.ndarray): Valores-alvo, de tamanho (n_samples,).
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(self.d, (int, np.integer)) or isinstance(self.d, bool) or self.d < 1:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f'd deve ser um número inteiro positivo; obtido: {self.d}'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
X, y = verify_X_y(X, y)
|
|
41
|
+
self.n_features_in_ = X.shape[1]
|
|
42
|
+
|
|
43
|
+
if X.shape[1] != 1:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f'a dimensão das features está incorreta; necessário: 1; obtido: {self.n_features_in_}'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.model_ = OLS(intercepto=self.intercepto).fit(self._design(X), y)
|
|
49
|
+
self._fitted = True
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def predict(self, X : np.ndarray) -> np.ndarray:
|
|
53
|
+
"""Estima os valores de saída para os valores de X.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
X (np.ndarray): Valores a serem estimados, de shape (n_samples, 1).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
np.ndarray: Valores estimados pelo polinômio ajustado, de tamanho (n_samples,).
|
|
60
|
+
"""
|
|
61
|
+
if not self._fitted:
|
|
62
|
+
raise NotFittedError(
|
|
63
|
+
f'{self.__class__.__name__} não foi fittado ainda'
|
|
64
|
+
)
|
|
65
|
+
X = verify_array(X)
|
|
66
|
+
if self.n_features_in_ != X.shape[1]:
|
|
67
|
+
ValueError(
|
|
68
|
+
f'a dimensão das features distingue; necessário: {self.n_features_in_}; obtido: {X.shape[1]}'
|
|
69
|
+
)
|
|
70
|
+
return self.model_.predict(self._design(X))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from saikit_learn.regression.utils.validacao import verify_array, verify_X_y, NotFittedError
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class Ridge:
|
|
5
|
+
def __init__(self, lmbd: float = 1.0, intercepto : bool = True):
|
|
6
|
+
"""Inicializa o modelo de regressão Ridge.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
lmbd (float, opcional): Força da penalização L2 aplicada aos coeficientes. Valores maiores encolhem mais e 0 equivale ao OLS. Padrão é 1.0.
|
|
10
|
+
intercepto (bool, opcional): Se True, inclui o termo constante (intercepto) no modelo, sem penalizá-lo. Padrão é True.
|
|
11
|
+
"""
|
|
12
|
+
self.lmbd = lmbd
|
|
13
|
+
self.intercepto = intercepto
|
|
14
|
+
self._fitted = False
|
|
15
|
+
|
|
16
|
+
def fit(self, X: np.ndarray, y: np.ndarray):
|
|
17
|
+
"""Treina o modelo Ridge com os dados fornecidos.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
X (np.ndarray): Atributos de treino, de shape (n_samples, n_features).
|
|
21
|
+
y (np.ndarray): Valores-alvo, de shape (n_samples,).
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(self.lmbd, bool):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f'lambda deve ser um número; obtido: {type(self.lmbd)}'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
lmbd = float(self.lmbd)
|
|
30
|
+
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f'lambda deve ser um número; obtido: {type(self.lmbd)}'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not np.isfinite(lmbd) or isinstance(lmbd, bool) or lmbd < 0:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f'lambda deve ser um número real não negativo; obtido: {self.lmbd}'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
X, y = verify_X_y(X, y)
|
|
42
|
+
self.n_features_in_ = X.shape[1]
|
|
43
|
+
|
|
44
|
+
if self.intercepto:
|
|
45
|
+
# não podemos penalizar o intercepto, então devemos cetralizar,
|
|
46
|
+
# treinar, e depois encontrar o intercepto
|
|
47
|
+
X_mean = X.mean(axis=0)
|
|
48
|
+
y_mean = y.mean()
|
|
49
|
+
Xprime = X - X_mean
|
|
50
|
+
yprime = y - y_mean
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
Xprime, yprime = X, y
|
|
54
|
+
|
|
55
|
+
self.coef_ = np.linalg.solve(Xprime.T @ Xprime + self.lmbd * np.eye(self.n_features_in_), Xprime.T @ yprime)
|
|
56
|
+
self.intercept_ = float(y_mean - X_mean @ self.coef_) if self.intercepto else 0.0
|
|
57
|
+
self._fitted = True
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
61
|
+
"""Estima os valores de saída para os valores de X.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
X (np.ndarray): Valores a serem estimados, de tamanho n_samples, n_features).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
np.ndarray: Valores estimados pelo modelo, de tamanho (n_samples,).
|
|
68
|
+
"""
|
|
69
|
+
X = verify_array(X)
|
|
70
|
+
|
|
71
|
+
if not self._fitted:
|
|
72
|
+
raise NotFittedError(f'{self.__class__.__name__} não foi fittada ainda')
|
|
73
|
+
|
|
74
|
+
if self.n_features_in_ != X.shape[1]:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f'a dimensão das features está incorreta; necessário: {self.n_features_in_}; obtido: {X.shape[1]}'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return X @ self.coef_ + self.intercept_
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def verify_array(X, name='X'):
|
|
4
|
+
'''Converts the entry to a ndarray float 2D and verify its content
|
|
5
|
+
|
|
6
|
+
Raises:
|
|
7
|
+
|
|
8
|
+
ValueError: If X is not 2D, or is empty or contains NaN/inf.
|
|
9
|
+
'''
|
|
10
|
+
X = np.asarray(X, dtype=float)
|
|
11
|
+
if X.ndim != 2:
|
|
12
|
+
raise ValueError(
|
|
13
|
+
f'{name} must be 2D (n_sample, n_features); receive {X.ndim}D. '
|
|
14
|
+
'If there is only one feature, use X.reshape(-1, 1).'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if X.shape[0] == 0 or X.shape[1] == 0:
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f'{name} is empty; shape {X.shape}.'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if not np.all(np.isfinite(X)):
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f'{name} contains NaN or inf.'
|
|
25
|
+
)
|
|
26
|
+
return X
|
|
27
|
+
|
|
28
|
+
def verify_X_y(X, y):
|
|
29
|
+
'''Verify the content of the ndarrays and validate if they have the same sample size'''
|
|
30
|
+
X = verify_array(X, "X")
|
|
31
|
+
y = np.asarray(y, dtype=float)
|
|
32
|
+
# if y is a column matrix, converts to an column vector
|
|
33
|
+
if y.ndim == 2 and y.shape[1] == 1:
|
|
34
|
+
y = y.ravel()
|
|
35
|
+
|
|
36
|
+
if y.ndim != 1:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f'y must be 1D; receive {y.ndim}D.'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not np.all(np.isfinite(y)):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f'y contains NaN or inf'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if X.shape[0] != y.shape[0]:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f'X and y must have the same sample number; {X.shape[0]} != {y.shape[0]}'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return X, y
|
|
52
|
+
|
|
53
|
+
class NotFittedError(ValueError, AttributeError):
|
|
54
|
+
'''Raised when use a model (predict) before call fit'''
|