machinelearning-mnpkllps 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ import numpy as np
2
+
3
+
4
+ def validade_matrix(X): #função auxiliar para verificar se os dados da amostra é válido
5
+ X = np.asarray(X, dtype=float)
6
+ if X.ndim != 2 or X.shape[0] == 0 or X.shape[1] == 0: #número de linha e Núemrod de colunas, dimensão dois porque temos as observações e também temos os atributos de cada observação
7
+ raise TypeError("X deve ser 2D e não vazio: (n_amostras, n_atributos)")
8
+
9
+ if not np.all(np.isfinite(X)): #np.all verifica todos os valores são verdadeiros, np.infinite retorna veradeiro ou falso para cada valor, vendp se não é Nan ou infinito
10
+ raise ValueError("X contém NaN ou infinito")
11
+ return X
12
+
13
+
14
+ def validate_positive_integer(valor, nome): #função auxiliar para verificar se o valor é válido
15
+ if not isinstance(valor, (int, np.integer)) or isinstance(valor, bool): #verifica se o valor é um inteiro
16
+ raise ValueError(f"{nome} deve ser um inteiro positivo.")
17
+
18
+ if valor < 1:
19
+ raise ValueError(f"{nome} deve ser >= 1.") # verifica se o valor é maior ou igual 1
20
+
21
+ return int(valor)
22
+
23
+
24
+ class KMeans:
25
+ """
26
+ `Kmeans(k, max_iteracao=100)`
27
+
28
+ Agrupa as amostras em `k` clusters (algoritmo de Lloyd): sorteia `k` pontos distintos como centroides, atribui cada amostra ao centroide mais próximo e recalcula cada centroide como a média do seu cluster, até convergir ou atingir `max_iteracao`.
29
+
30
+ | Parâmetro | Descrição |
31
+ |---|---|
32
+ | `k` | Número de clusters (inteiro >= 1) |
33
+ | `max_iteracao` | Limite de iterações (inteiro >= 1) |
34
+
35
+ | Método | O que faz |
36
+ |---|---|
37
+ | `fit(X)` | Ajusta o modelo e guarda `centroides` `(k, n_atributos)`. Retorna `self`. Erro se `k` > nº de amostras ou nº de pontos distintos |
38
+ | `estimar(X)` | Retorna o índice do centroide mais próximo de cada amostra, formato `(n_amostras,)` |
39
+
40
+ Observação: a inicialização é aleatória (sem semente), então a numeração dos clusters pode mudar entre execuções.
41
+ """
42
+ def __init__(self, k, max_iterations=100):
43
+ self.k = validate_positive_integer(k, "k")
44
+ self.max_iterations = validate_positive_integer(max_iterations, "max_iterations") #verificando se o valor é válido
45
+ self.centroids = None # vetor que vai armazemar os centroids (vetor)
46
+
47
+ def predict(self, X):
48
+ if self.centroids is None:
49
+ raise RuntimeError("chame fit antes de estimar")
50
+
51
+ X = validate_matrix(X)
52
+
53
+ if X.shape[1] != self.centroids.shape[1]: # shape seria: X.shape = (qnt_amostra, qnt_atributos), shape[1] é o número de colunas, ou seja, o número de atributos
54
+ raise ValueError(f"X tem {X.shape[1]} atributos, mas o modelo foi treinado com {self.centroids.shape[1]} atributos")
55
+
56
+ distancias = np.sum((X[:, None, :] - self.centroids[None, :, :]) ** 2, axis=2) # (amostra, centroide, coordenada)
57
+ # Com sum(axis=2), somamos as coordenadas: (amostra, centroide), contendo as distâncias euclidianas ao quadrado
58
+
59
+ return np.argmin(distancias, axis=1) # Para cada amostra, retorna o índice do centroide mais próximo. Formato: (amostra) contendo o índice do centróide mais próximo.
60
+
61
+ def fit(self, X):
62
+ X = validate_matrix(X)
63
+
64
+ if self.k > len(X): #verifica se o número de clusters é maior que o número de amostras
65
+ raise ValueError(f"k={self.k} é maior que o número de amostras ({len(X)})")
66
+
67
+ distintos = np.unique(X, axis=0) #verifica se o número de clusters é maior que o núemor de amostras distintos
68
+ if self.k > len(distintos):
69
+ raise ValueError(f"há apenas {len(distintos)} pontos distintos em X, menos que k={self.k}")
70
+
71
+ rng = np.random.default_rng() # gerador de números aleatórios
72
+ indices = rng.choice(len(distintos), self.k, replace=False) # seleciona k índices aleatórios (índices distintos)
73
+ self.centroids = distintos[indices].copy() #copiando os centroids escolhidos
74
+
75
+ for _ in range(self.max_iterations):
76
+ conjunto = self.predict(X) # atribuindo cada amostra ao centroide mais próximo
77
+ novos = self.centroids.copy() # inicializando os valores dos novos centroids da iteração
78
+
79
+ for i in range(self.k): # para cada centroide calcular seu novo valor como a média dos pontos do seu conjunto
80
+ pontos = X[conjunto == i] # selecionar todos os X[j] cuja conjunto[j] = i
81
+ if len(pontos) > 0:
82
+ novos[i] = pontos.mean(axis=0)
83
+
84
+ if np.array_equal(self.centroids, novos): # se o valor dos novos centroids não alterou, então convergiu
85
+ break
86
+
87
+ self.centroids = novos #atualizando o valor dos centroids
88
+ return self
89
+
90
+
91
+ class PCA:
92
+ """
93
+ ## `Pca()`
94
+
95
+ Encontra as direções de maior variância: centraliza os dados, calcula a covariância amostral e usa `np.linalg.eigh` para obter autovalores e autovetores, ordenados do maior para o menor.
96
+
97
+ | Atributo | Descrição |
98
+ |---|---|
99
+ | `media` | Média de cada atributo `(n_atributos,)` |
100
+ | `componentes` | Autovetores nas colunas `(n_atributos, n_atributos)` |
101
+ | `autovalores` | Variância de cada componente, em ordem decrescente |
102
+
103
+ | Método | O que faz |
104
+ |---|---|
105
+ | `fit(X)` | Calcula `media`, `componentes` e `autovalores`. Exige >= 2 amostras. Retorna `self` |
106
+ | `transformar(X, n_componentes=None)` | Centraliza e projeta nos primeiros `n_componentes` (todos se `None`). Retorna `(n_amostras, n_componentes)` |
107
+ | `fit_transform(X, n_componentes=None)` | `fit` seguido de `transformar` |
108
+
109
+ Observações: o sinal de cada componente é arbitrário, e o PCA não padroniza os dados (use o pré-processamento se os atributos tiverem escalas diferentes).
110
+ """
111
+ def __init__(self):
112
+ self.mean = None # média de cada atributo original
113
+ self.components = None # autovetores ordenados
114
+ self.eigenvalues = None # variâncias associados aos autovetores (as direções)
115
+
116
+ def fit(self, X):
117
+ X = validate_matrix(X)
118
+ if len(X) < 2: # se a dimensão for <=1 não temos dimensão para reduzir, calcular covariância usa n - 1.
119
+ raise ValueError("PCA precisa de pelo menos 2 amostras")
120
+
121
+ self.mean = X.mean(axis=0) #média dos vetores colunas (amostra)
122
+ X_centralizado = X - self.mean #centralizando os dados
123
+
124
+ cov = X_centralizado.T @ X_centralizado / (len(X) - 1) #calculando a matriz de covariância amostral
125
+ valores, vetores = np.linalg.eigh(cov) #calculando os auto valores e os autovetores (padronizados)
126
+
127
+ indices = np.argsort(valores)[::-1] #pegando os índices dos eigenvalues em ordem decrescente
128
+ self.eigenvalues = np.maximum(valores[indices], 0.0) #substituindo os autovalroes negativos por 0, que podem surgir a instabilidade numérica do ponto flutuante (variância negativo não exite)
129
+ self.components = vetores[:, indices] #reordenando as colunas (veotres colunas)
130
+
131
+ return self
132
+
133
+ def transform(self, X, n_components=None):
134
+ if self.mean is None:
135
+ raise RuntimeError("chame fit antes de transformar")
136
+
137
+ X = validate_matrix(X)
138
+ if X.shape[1] != len(self.mean): # compatibilidade entre a dimensão dos dados e a média
139
+ raise ValueError(
140
+ f"X tem {X.shape[1]} atributos, esperado {len(self.mean)}"
141
+ )
142
+
143
+ n = self.components.shape[1] if n_components is None else validate_positive_integer(
144
+ n_components, "n_components" #usar todos os components se None, se não verificar se o valor dado é válido
145
+ )
146
+ if n > self.components.shape[1]: #se o número o n for maior que o número de atributos
147
+ raise ValueError(f"n_components={n} excede o número de atributos : {self.components.shape[1]}")
148
+
149
+ return (X - self.mean) @ self.components[:, :n]
150
+
151
+ def fit_transform(self, X, n_components=None):
152
+ return self.fit(X).transform(X, n_components)
153
+
154
+ # ====================================TESTES=======================================================================================================
155
+ if __name__ == "__main__":
156
+ rng = np.random.default_rng(0)
157
+
158
+ # KMeans
159
+ # dois grupos bem separados: um perto de (0,0) e outro perto de (10,10)
160
+ X = np.vstack([rng.normal(0, 0.5, (50, 2)), rng.normal(10, 0.5, (50, 2))])
161
+
162
+ modelo = KMeans(2).fit(X)
163
+ rotulos = modelo.predict(X)
164
+
165
+ assert len(set(rotulos[:50])) == 1 # primeiro grupo todo no mesmo cluster
166
+ assert len(set(rotulos[50:])) == 1 # segundo grupo todo no mesmo cluster
167
+ assert rotulos[0] != rotulos[50] # clusters diferentes
168
+ print("KMeans OK")
169
+
170
+ # PCA
171
+ # dados quase sobre a reta y = 2x
172
+ t = rng.normal(size=200)
173
+ X = np.column_stack([t, 2 * t]) + 0.01 * rng.normal(size=(200, 2))
174
+
175
+ PCA = PCA()
176
+ Z = PCA.fit_transform(X, n_components=1)
177
+
178
+ esperado = np.array([1, 2]) / np.sqrt(5) # direção da reta
179
+ assert np.isclose(abs(PCA.components[:, 0] @ esperado), 1, atol=1e-3) # abs: sinal é arbitrário
180
+ assert Z.shape == (200, 1)
181
+ print("PCA OK")
@@ -0,0 +1,292 @@
1
+ import numpy as np
2
+ from . import Functions
3
+ from . import metricas
4
+ from typing import Callable
5
+
6
+
7
+ class NeuralNetwork:
8
+ def __init__(self, layers: list[int], activate_function: Functions.functions_name | list[Functions.functions_name],
9
+ output_activation: Functions.functions_name = 'identity', function_dictionary: dict[str, tuple[Callable, Callable]] = Functions.functions,
10
+ bias_interval: tuple[float, float] = (0, 1), weight_interval: tuple[float, float] = (0, 1),
11
+ seed:int = 782) -> None:
12
+ """
13
+ Intancia um elemento a classe NeuralNetwork com pesos e vieses aleatórios dentro do intervalo estipulado.
14
+
15
+ Parâmetros
16
+ -----------
17
+ layers list[int] : Quantas layers tem e qual a largura de cada;
18
+ activate_function str | list[str] : Quais funções de ativação serão usadas a cada camada oculta;
19
+ output_activation str | list[str] : Qual a função ativação da camada de saida (identidade por padrão);
20
+ function_dictionary dict[str, tuple[Callable, Callable]] : Mapa de funções de ativação e suas derivadas;
21
+ baias_interval tuple[float, float] : Qual intervalo os vieses serão gerado ((0, 1) por padrão);
22
+ weight_interval tuple[float, float] : Qual intervalo os pesos serão gerado ((0, 1) por padrão);
23
+ seed int : Seed usada para gerar os vieses e pesos
24
+ """
25
+
26
+ self.original_seed = seed
27
+ self._rng = np.random.default_rng(seed)
28
+ self.layers = layers
29
+
30
+ self._state_linear: list[np.ndarray] = [np.array([]) for _ in range(1, len(layers))]
31
+ self._state_layers: list[np.ndarray] = [np.array([]) for _ in range(len(layers))]
32
+
33
+ self.bias: list[np.ndarray] = [self.random_bias(i, *bias_interval) for i in range(1, len(layers))]
34
+ self.weight: list[np.ndarray] = [self.random_weight(i, *weight_interval) for i in range(1, len(layers))]
35
+
36
+ if isinstance(activate_function, list):
37
+ if len(activate_function) != len(layers)-2:
38
+ raise ValueError("ERROR: Quantidade de funções diferente dos layers ocultos")
39
+ self.activate_functions: list[str] = activate_function.copy()
40
+ elif isinstance(activate_function, str):
41
+ self.activate_functions: list[str] = [activate_function] * (len(layers) - 2)
42
+ else :
43
+ raise TypeError("ERROR: Tipo da activete_function está incorreto")
44
+
45
+ self.activate_functions += [output_activation]
46
+ self._function_dictionary = function_dictionary.copy()
47
+
48
+ self._check_activate(self.activate_functions, self._function_dictionary)
49
+
50
+ @staticmethod
51
+ def _check_activate(activate_functions: list[str], function_dictionary: dict[str, tuple[Callable, Callable]]) -> None:
52
+ """
53
+ Verifica se todas as funções de ativação usadas estão no mapa
54
+
55
+ Parâmetros
56
+ -----------
57
+ activate_function list[str] : Quais funções de ativação serão usadas a cada camada oculta;
58
+ function_dictionary dict[str, tuple[Callable, Callable]] : Mapa de funções de ativação e suas derivadas.
59
+ """
60
+ for function_name in activate_functions:
61
+ if function_name not in function_dictionary:
62
+ raise KeyError(f"ERROR: Função de ativação inválida: {function_name}")
63
+
64
+ @staticmethod
65
+ def _check_input(x: np.ndarray) -> np.ndarray:
66
+ """
67
+ Verifica se x é valido e arruma suas dimenções
68
+
69
+ Parâmetros
70
+ -----------
71
+ x np.ndarray : Array a se verificar.
72
+
73
+ Retorna
74
+ -----------
75
+ x com as dimenções atualizadas
76
+ """
77
+
78
+ if isinstance(x, float) or isinstance(x, int):
79
+ x = np.array([x])
80
+ elif isinstance(x, list) or isinstance(x, tuple):
81
+ x = np.array(x)
82
+
83
+ if x.ndim > 2:
84
+ raise ValueError("Entrada invalida, x deve ser um vetor ou matriz")
85
+ elif x.ndim == 1:
86
+ if len(x) == 1:
87
+ x = x.reshape(-1, 1)
88
+ else:
89
+ x = x.reshape(1, -1)
90
+
91
+ return x
92
+
93
+ def random_bias(self, level: int, low: float = 0, high: float = 1) -> np.ndarray:
94
+ """
95
+ Gera um vetor viés uniformemente aleatório entre [low, high]
96
+
97
+ Parâmetros
98
+ -----------
99
+ level int : Para qual layer será gerado o viés;
100
+ low float : Valor mínimo dos vieses (0 por padrão);
101
+ high float : Valor maximo dos vieses (1 por padrão).
102
+
103
+ Retorna
104
+ -----------
105
+ Vetor dos vieses com o mesma largura do layer[nivel]
106
+ """
107
+
108
+ array = self._rng.uniform(low, high, (self.layers[level], 1))
109
+
110
+ return array
111
+
112
+ def random_weight(self, level: int, low: float = 0, high: float = 1) -> np.ndarray:
113
+ """
114
+ Gera uma matriz peso uniformemente aleatória entre [low, high]
115
+
116
+ Parâmetros
117
+ -----------
118
+ level int : Para qual layer será gerado o peso;
119
+ low float : Valor mínimo dos pesos (0 por padrão);
120
+ high float : Valor maximo dos pesos (1 por padrão).
121
+
122
+ Retorna
123
+ -----------
124
+ matriz dos pesos com dimenção da largura do layer[nivel] x layer[nivel-1]
125
+ """
126
+
127
+ array = self._rng.uniform(low, high, (self.layers[level], self.layers[level-1]))
128
+
129
+ return array
130
+
131
+ def train(self, X: np.ndarray, y: np.ndarray, loss: Callable = metricas.MSE, loss_diff: Callable = metricas.DMSE, learn_rate: float = 0.1,
132
+ batch: int = 10, epochs: int = 1000, tol = 1e-9) -> None:
133
+ """
134
+ Treina a rede neural a partir dos dados X, y
135
+
136
+ Parâmetros
137
+ -----------
138
+ X np.ndarray : Matriz P x N dos dados de treino;
139
+ y np.ndarray : Matriz S x N das saidas de treino;
140
+ loss Callable : Função perda para aprendizado;
141
+ loss_diff Callable : Derivada da função perda para backpropagation;
142
+ learn_rate float : Taxa de aprendizado (0.1 por padrão);
143
+ batch int : Quantidade esperada de elementos por balde (10 por padrão);
144
+ epochs int : Quantidade de interações nos dados de treino (1000 por padrão).
145
+ """
146
+
147
+ self.loss = loss
148
+ self.loss_diff = loss_diff
149
+
150
+ loss_function: str = 'MSE' #COLOCAR PARA PARAMETROS
151
+
152
+ X = self._check_input(X)
153
+ y = self._check_input(y)
154
+
155
+
156
+ index: np.ndarray = np.arange(X.shape[1])
157
+
158
+ loss = self.loss(y, self.prevision(X))
159
+ for epoch in range(epochs):
160
+ self._rng.shuffle(index)
161
+
162
+ for i in range(0, X.shape[1], batch):
163
+ if X.shape[1] - i < 2*batch:
164
+ B = index[i:]
165
+ else:
166
+ B = index[i: i+batch]
167
+
168
+ self._backpropagation(X[:, B], y[:, B], loss_function = loss_function, learn_rate = learn_rate)
169
+
170
+ loss_aux = self.loss(y, self.prevision(X))
171
+
172
+ if abs(loss - loss_aux) < tol:
173
+ break
174
+ else:
175
+ loss = loss_aux
176
+
177
+ def _backpropagation(self, x: np.ndarray, y: np.ndarray, loss_function: str = 'MSE', learn_rate: float = 0.1) -> None:
178
+ """
179
+ Calcula as decidas de gradiente para cada peso e viés.
180
+
181
+ Parâmetros
182
+ -----------
183
+ x np.ndarray : Matriz P x N dos dados de treino;
184
+ y np.ndarray : Matriz S x N das saidas de treino;
185
+ loss_function str : Função perda utilizada para calcular o gradiente;
186
+ learn_rate float : Taxa de aprendizado (0.1 por padrão);
187
+ """
188
+
189
+
190
+ self.prevision(x, save_states=True)
191
+ cost: list[np.ndarray] = [np.array([]) for _ in range(1, len(self.layers))]
192
+
193
+ activate_function = self.activate_functions[-1]
194
+ cost[-1] = self.loss_diff(y, self._state_layers[-1]) * self._function_dictionary[activate_function][1](self._state_linear[-1])
195
+
196
+ for level in range(len(self.layers)-3, -1, -1):
197
+ activate_function = self.activate_functions[level]
198
+ cost[level] = self.weight[level+1].T @ cost[level+1] * self._function_dictionary[activate_function][1](self._state_linear[level])
199
+
200
+ for level in range(len(self.layers)-1):
201
+ self.weight[level] -= learn_rate * (cost[level] @ self._state_layers[level].T)
202
+ self.bias[level] -= learn_rate * np.sum(cost[level], axis=1, keepdims=True)
203
+
204
+
205
+ def prevision(self, x: float | np.ndarray, save_states: bool = False, classify: bool = False) -> np.ndarray:
206
+ """
207
+ Calcula o valor de saida da rede neural com pesos e vieses atuais
208
+
209
+ Parâmetros
210
+ -----------
211
+ x float | np.ndarray : Vetor ou matriz que queremos prever o resultado;
212
+ save_states bool : Define se queremos salvar os valores de cada layer ou não;
213
+ classify bool : Define se queremos retornar o vetor de probabilidade ou atribuir o elemento a uma classe.
214
+
215
+ Retorna
216
+ -----------
217
+ Valor de saida da rede neural com entrada x
218
+ """
219
+
220
+ x = self._check_input(x)
221
+
222
+ y: np.ndarray = x.copy()
223
+
224
+ if save_states:
225
+ self._state_layers[0] = y.copy()
226
+
227
+ for level in range(len(self.layers)-1):
228
+ bayes: np.ndarray = self.bias[level]
229
+ weight: np.ndarray = self.weight[level]
230
+ activate_func = self._function_dictionary[self.activate_functions[level]][0]
231
+
232
+ linear = bayes + weight @ y
233
+ layer = activate_func(linear)
234
+ if save_states:
235
+ self._state_linear[level] = linear
236
+ self._state_layers[level + 1] = layer
237
+
238
+ y = layer
239
+
240
+ if classify:
241
+ return np.argmax(y)
242
+
243
+ return y
244
+
245
+
246
+ if __name__ == "__main__":
247
+
248
+ print('-'*100)
249
+ print('Teste 1')
250
+ print('-'*100)
251
+ rn1 = NeuralNetwork([2, 4, 5, 2], activate_function="sigmoid", output_activation="sigmoid", bias_interval = (-1, 1), weight_interval = (-1, 1))
252
+
253
+ rng = np.random.default_rng(1)
254
+ X = rng.uniform(-4, 4, (2, 1000))
255
+ y = np.zeros(X.shape)
256
+
257
+ for i, x in enumerate(X.T):
258
+ if x[0]**2 + x[1]**2 <= 9:
259
+ y[:, i] = [1, 0]
260
+ else:
261
+ y[:, i] = [0, 1]
262
+
263
+ rn1.train(X, y, batch=50)
264
+
265
+ x1 = np.array([[1], [0]])
266
+ x2 = np.array([[3], [3]])
267
+ x3 = np.array([[5], [5]])
268
+
269
+ print(f"\nx_1=({x1[0]}, {x1[1]}) y_1 = {rn1.prevision(x1)}")
270
+ print(f"\ny_2 = {rn1.prevision(x2)}")
271
+ print(f"\ny_3 = {rn1.prevision(x3)}")
272
+
273
+ print(f"\nx_1=({x1[0]}, {x1[1]}) y_1 = {rn1.prevision(x1, classify=True)}")
274
+ print(f"\ny_2 = {rn1.prevision(x2, classify=True)}")
275
+ print(f"\ny_3 = {rn1.prevision(x3, classify=True)}")
276
+
277
+ print('-'*100)
278
+ print('Teste 2')
279
+ print('-'*100)
280
+
281
+ rn2 = NeuralNetwork([1, 4, 5, 1], activate_function="ReLU", output_activation="identity", bias_interval = (-0.1, 0.1), weight_interval = (-0.1, 0.1))
282
+
283
+ f = lambda x: np.exp(x)
284
+
285
+ X = np.linspace(-2, 3, 10000).reshape(1, -1)
286
+ y = f(X)
287
+
288
+ rn2.train(X, y, batch=50, learn_rate=0.001, epochs=10000)
289
+
290
+ for x1 in range(20):
291
+ print(f"\nx_1={x1}, y_1={rn2.prevision(x1)}")
292
+