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.
@@ -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,3 @@
1
+ """Biblioteca de aprendizado de máquina (projeto final de Aprendizado de Máquina I, IMPA Tech)"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .lda import LDA
2
+ from .qda import QDA
3
+
4
+ __all__ = ['QDA','LDA']
@@ -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,3 @@
1
+ from .logistic_regression import LogisticRegression
2
+
3
+ __all__ = ['LogisticRegression']
@@ -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,3 @@
1
+ from .knn import KNN
2
+
3
+ __all__ = ['KNN']
@@ -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,3 @@
1
+ from .mlp_regressor import MLPRegressor
2
+
3
+ __all__ = ['MLPRegressor']
@@ -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_
@@ -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'''