topologicpy 0.7.18__py3-none-any.whl → 0.7.20__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.
topologicpy/ANN.py ADDED
@@ -0,0 +1,1133 @@
1
+ # Copyright (C) 2024
2
+ # Wassim Jabi <wassim.jabi@gmail.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU Affero General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12
+ # details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License along with
15
+ # this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import os
18
+ import random
19
+ import copy
20
+ import warnings
21
+
22
+ try:
23
+ import numpy as np
24
+ except:
25
+ print("ANN - Installing required numpy library.")
26
+ try:
27
+ os.system("pip install numpy")
28
+ except:
29
+ os.system("pip install numpy --user")
30
+ try:
31
+ import numpy as np
32
+ print("ANN - numpy library installed correctly.")
33
+ except:
34
+ warnings.warn("ANN - Error: Could not import numpy.")
35
+
36
+ try:
37
+ import pandas as pd
38
+ except:
39
+ print("DGL - Installing required pandas library.")
40
+ try:
41
+ os.system("pip install pandas")
42
+ except:
43
+ os.system("pip install pandas --user")
44
+ try:
45
+ import pandas as pd
46
+ print("ANN - pandas library installed correctly.")
47
+ except:
48
+ warnings.warn("ANN - Error: Could not import pandas.")
49
+
50
+ try:
51
+ import torch
52
+ import torch.optim as optim
53
+ import torch.nn as nn
54
+ import torch.nn.functional as F
55
+ from torch.utils.data import DataLoader, TensorDataset
56
+ except:
57
+ print("ANN - Installing required torch library.")
58
+ try:
59
+ os.system("pip install torch")
60
+ except:
61
+ os.system("pip install torch --user")
62
+ try:
63
+ import torch
64
+ import torch.optim as optim
65
+ import torch.nn as nn
66
+ import torch.nn.functional as F
67
+ from torch.utils.data import DataLoader, TensorDataset
68
+ print("ANN - torch library installed correctly.")
69
+ except:
70
+ warnings.warn("ANN - Error: Could not import torch.")
71
+
72
+ try:
73
+ from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, mean_squared_error, mean_absolute_error, r2_score
74
+ from sklearn.model_selection import KFold, train_test_split
75
+ from sklearn.preprocessing import StandardScaler
76
+ from sklearn.datasets import load_breast_cancer, load_iris, load_wine, load_digits, fetch_california_housing
77
+ except:
78
+ print("ANN - Installing required scikit-learn library.")
79
+ try:
80
+ os.system("pip install -U scikit-learn")
81
+ except:
82
+ os.system("pip install -U scikit-learn --user")
83
+ try:
84
+ from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, mean_squared_error, mean_absolute_error, r2_score
85
+ from sklearn.model_selection import KFold, train_test_split
86
+ from sklearn.preprocessing import StandardScaler
87
+ from sklearn.datasets import load_breast_cancer, load_iris, load_wine, load_digits, fetch_california_housing
88
+ print("ANN - scikit-learn library installed correctly.")
89
+ except:
90
+ warnings.warn("ANN - Error: Could not import scikit. Please install it manually.")
91
+
92
+ import torch
93
+ import torch.nn as nn
94
+ import torch.optim as optim
95
+ from sklearn.datasets import fetch_california_housing, load_breast_cancer, load_iris
96
+ from sklearn.model_selection import train_test_split, KFold
97
+ from sklearn.preprocessing import StandardScaler
98
+ from sklearn.metrics import accuracy_score, mean_squared_error, mean_absolute_error, r2_score, precision_score, recall_score, f1_score, confusion_matrix
99
+ import numpy as np
100
+
101
+ import torch
102
+ import torch.nn as nn
103
+ import torch.optim as optim
104
+ from sklearn.datasets import fetch_california_housing, load_breast_cancer, load_iris
105
+ from sklearn.model_selection import train_test_split, KFold
106
+ from sklearn.preprocessing import StandardScaler
107
+ from sklearn.metrics import accuracy_score, mean_squared_error, mean_absolute_error, r2_score, precision_score, recall_score, f1_score, confusion_matrix
108
+ import numpy as np
109
+
110
+ class _ANN(nn.Module):
111
+ def __init__(self, input_size, hyperparameters, dataset=None):
112
+ super(_ANN, self).__init__()
113
+ self.title = hyperparameters['title']
114
+ self.task_type = hyperparameters['task_type']
115
+ self.cross_val_type = hyperparameters['cross_val_type']
116
+ self.k_folds = hyperparameters.get('k_folds', 5)
117
+ self.test_size = hyperparameters.get('test_size', 0.3)
118
+ self.validation_ratio = hyperparameters.get('validation_ratio', 0.1)
119
+ self.random_state = hyperparameters.get('random_state', 42)
120
+ self.batch_size = hyperparameters.get('batch_size', 32)
121
+ self.learning_rate = hyperparameters.get('learning_rate', 0.001)
122
+ self.epochs = hyperparameters.get('epochs', 100)
123
+ self.early_stopping = hyperparameters.get('early_stopping', False)
124
+ self.patience = hyperparameters.get('patience', 10)
125
+ self.interval = hyperparameters.get('interval',1)
126
+ self.mantissa = hyperparameters.get('mantissa', 4)
127
+
128
+ self.train_loss_list = []
129
+ self.val_loss_list = []
130
+
131
+ self.train_accuracy_list = []
132
+ self.val_accuracy_list = []
133
+
134
+ self.train_mse_list = []
135
+ self.val_mse_list = []
136
+
137
+ self.train_mae_list = []
138
+ self.val_mae_list = []
139
+
140
+ self.train_r2_list = []
141
+ self.val_r2_list = []
142
+ self.epoch_list = []
143
+
144
+ self.metrics = {}
145
+
146
+ layers = []
147
+ hidden_layers = hyperparameters['hidden_layers']
148
+
149
+ # Compute output_size based on task type and dataset
150
+ if self.task_type == 'regression':
151
+ output_size = 1
152
+ elif self.task_type == 'binary_classification':
153
+ output_size = 1
154
+ elif self.task_type == 'classification' and dataset is not None:
155
+ output_size = len(np.unique(dataset.target))
156
+ else:
157
+ print("ANN - Error: Invalid task type or dataset not provided for classification. Returning None.")
158
+ return None
159
+
160
+ # Create hidden layers
161
+ in_features = input_size
162
+ for hidden_units in hidden_layers:
163
+ layers.append(nn.Linear(in_features, hidden_units))
164
+ layers.append(nn.ReLU())
165
+ in_features = hidden_units
166
+
167
+ # Output layer
168
+ layers.append(nn.Linear(in_features, output_size))
169
+ self.model = nn.Sequential(*layers)
170
+
171
+ # Loss function based on task type
172
+ if self.task_type == 'regression':
173
+ self.loss_fn = nn.MSELoss()
174
+ elif self.task_type == 'binary_classification':
175
+ self.loss_fn = nn.BCEWithLogitsLoss()
176
+ else: # multi-category classification
177
+ self.loss_fn = nn.CrossEntropyLoss()
178
+
179
+
180
+
181
+ # Initialize best model variables
182
+ self.best_model_state = None
183
+ self.best_val_loss = np.inf
184
+
185
+ def forward(self, x):
186
+ return self.model(x)
187
+
188
+ def train_model(self, X_train, y_train, X_val=None, y_val=None):
189
+ self.train_loss_list = []
190
+ self.val_loss_list = []
191
+
192
+ self.train_accuracy_list = []
193
+ self.val_accuracy_list = []
194
+
195
+ self.train_mse_list = []
196
+ self.val_mse_list = []
197
+
198
+ self.train_mae_list = []
199
+ self.val_mae_list = []
200
+
201
+ self.train_r2_list = []
202
+ self.val_r2_list = []
203
+ self.epoch_list = []
204
+ optimizer = optim.Adam(self.parameters(), lr=self.learning_rate)
205
+ # Reinitialize optimizer for each fold
206
+ optimizer = optim.Adam(self.parameters(), lr=self.learning_rate)
207
+ current_patience = self.patience if self.early_stopping else self.epochs
208
+
209
+ # Convert to DataLoader for batching
210
+ train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
211
+ train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=self.batch_size, shuffle=True)
212
+
213
+ for epoch in range(self.epochs):
214
+ self.train()
215
+ epoch_loss = 0.0
216
+ correct_train = 0
217
+ total_train = 0
218
+
219
+ for inputs, targets in train_loader:
220
+ optimizer.zero_grad()
221
+ outputs = self(inputs)
222
+
223
+ if self.task_type == 'binary_classification':
224
+ outputs = outputs.squeeze()
225
+ targets = targets.float()
226
+ elif self.task_type == 'regression':
227
+ outputs = outputs.squeeze()
228
+
229
+ loss = self.loss_fn(outputs, targets)
230
+ epoch_loss += loss.item()
231
+
232
+ loss.backward()
233
+ optimizer.step()
234
+
235
+ # Calculate metrics for training set
236
+ if self.task_type == 'classification':
237
+ _, predicted = torch.max(outputs, 1)
238
+ correct_train += (predicted == targets).sum().item()
239
+ total_train += targets.size(0)
240
+ elif self.task_type == 'binary_classification':
241
+ predicted = torch.round(torch.sigmoid(outputs))
242
+ correct_train += (predicted == targets).sum().item()
243
+ total_train += targets.size(0)
244
+
245
+ if X_val is not None and y_val is not None:
246
+ self.eval()
247
+ with torch.no_grad():
248
+ val_outputs = self(X_val)
249
+ if self.task_type == 'binary_classification':
250
+ val_outputs = val_outputs.squeeze()
251
+ y_val = y_val.float()
252
+ elif self.task_type == 'regression':
253
+ val_outputs = val_outputs.squeeze()
254
+
255
+ val_loss = self.loss_fn(val_outputs, y_val)
256
+ val_loss_item = val_loss.item()
257
+
258
+ # Track the best model state
259
+ if val_loss < self.best_val_loss:
260
+ self.best_val_loss = val_loss
261
+ self.best_model_state = self.state_dict()
262
+ current_patience = self.patience if self.early_stopping else self.epochs
263
+ else:
264
+ if self.early_stopping:
265
+ current_patience -= 1
266
+
267
+ if self.early_stopping and current_patience == 0:
268
+ print(f'ANN - Information: Early stopping after epoch {epoch + 1}')
269
+ break
270
+
271
+ if (epoch + 1) % self.interval == 0:
272
+ self.epoch_list.append(epoch + 1)
273
+ avg_epoch_loss = epoch_loss / len(train_loader)
274
+ self.train_loss_list.append(round(avg_epoch_loss, self.mantissa))
275
+
276
+ if self.task_type == 'classification' or self.task_type == 'binary_classification':
277
+ train_accuracy = round(correct_train / total_train, self.mantissa)
278
+ self.train_accuracy_list.append(train_accuracy)
279
+ if X_val is not None and y_val is not None:
280
+ val_accuracy = (torch.round(torch.sigmoid(val_outputs)) if self.task_type == 'binary_classification' else torch.max(val_outputs, 1)[1] == y_val).float().mean().item()
281
+ val_accuracy = round(val_accuracy, self.mantissa)
282
+ self.val_accuracy_list.append(val_accuracy)
283
+ elif self.task_type == 'regression':
284
+ train_preds = self(X_train).detach().numpy().squeeze()
285
+ train_mse = round(mean_squared_error(y_train.numpy(), train_preds), self.mantissa)
286
+ train_mae = round(mean_absolute_error(y_train.numpy(), train_preds), self.mantissa)
287
+ train_r2 = round(r2_score(y_train.numpy(), train_preds), self.mantissa)
288
+ self.train_mse_list.append(train_mse)
289
+ self.train_mae_list.append(train_mae)
290
+ self.train_r2_list.append(train_r2)
291
+ if X_val is not None and y_val is not None:
292
+ val_preds = val_outputs.numpy().squeeze()
293
+ val_mse = round(mean_squared_error(y_val.numpy(), val_preds), self.mantissa)
294
+ val_mae = round(mean_absolute_error(y_val.numpy(), val_preds), self.mantissa)
295
+ val_r2 = round(r2_score(y_val.numpy(), val_preds), self.mantissa)
296
+ self.val_mse_list.append(val_mse)
297
+ self.val_mae_list.append(val_mae)
298
+ self.val_r2_list.append(val_r2)
299
+
300
+ if X_val is not None and y_val is not None:
301
+ self.val_loss_list.append(round(val_loss_item, self.mantissa))
302
+ print(f'Epoch [{epoch + 1}/{self.epochs}], Loss: {avg_epoch_loss:.4f}, Val Loss: {val_loss_item:.4f}')
303
+ else:
304
+ print(f'Epoch [{epoch + 1}/{self.epochs}], Loss: {avg_epoch_loss:.4f}')
305
+
306
+ def evaluate_model(self, X_test, y_test):
307
+ self.eval()
308
+ with torch.no_grad():
309
+ outputs = self(X_test)
310
+
311
+ if self.task_type == 'regression':
312
+ outputs = outputs.squeeze()
313
+ predictions = outputs.numpy()
314
+ mse = mean_squared_error(y_test.numpy(), outputs.numpy())
315
+ mae = mean_absolute_error(y_test.numpy(), outputs.numpy())
316
+ r2 = r2_score(y_test.numpy(), outputs.numpy())
317
+ #print(f'MSE: {mse:.4f}, MAE: {mae:.4f}, R^2: {r2:.4f}')
318
+ metrics = {'mae': round(mae, self.mantissa), 'mse': round(mse, self.mantissa), 'r2': round(r2, self.mantissa)}
319
+ elif self.task_type == 'binary_classification':
320
+ outputs = torch.sigmoid(outputs).squeeze()
321
+ predicted = (outputs > 0.5).int()
322
+ predictions = predicted.numpy()
323
+ accuracy = accuracy_score(y_test.numpy(), predicted.numpy())
324
+ precision = precision_score(y_test.numpy(), predicted.numpy(), zero_division=0)
325
+ recall = recall_score(y_test.numpy(), predicted.numpy(), zero_division=0)
326
+ f1 = f1_score(y_test.numpy(), predicted.numpy(), zero_division=0)
327
+ #print(f'Accuracy: {accuracy:.4f}, Precision: {precision:.4f}, Recall: {recall:.4f}, F1 Score: {f1:.4f}')
328
+ cm = self.confusion_matrix(y_test, predictions)
329
+ metrics = {'accuracy': round(accuracy, self.mantissa), 'precision': round(precision, self.mantissa), 'recall': round(recall, self.mantissa), 'f1': round(f1, self.mantissa), 'confusion_matrix': cm}
330
+ else: # multi-category classification
331
+ _, predicted = torch.max(outputs, 1)
332
+ predictions = predicted.numpy()
333
+ accuracy = accuracy_score(y_test.numpy(), predictions)
334
+ precision = precision_score(y_test.numpy(), predictions, average='macro', zero_division=0)
335
+ recall = recall_score(y_test.numpy(), predictions, average='macro', zero_division=0)
336
+ f1 = f1_score(y_test.numpy(), predictions, average='macro', zero_division=0)
337
+ cm = self.confusion_matrix(y_test, predicted.numpy())
338
+ #print(f'Accuracy: {accuracy:.4f}, Precision: {precision:.4f}, Recall: {recall:.4f}, F1 Score: {f1:.4f}')
339
+ metrics = {'accuracy': round(accuracy, self.mantissa), 'precision': round(precision, self.mantissa), 'recall': round(recall, self.mantissa), 'f1': round(f1, self.mantissa), 'confusion_matrix': cm}
340
+ self.metrics = metrics
341
+
342
+ return metrics, predictions
343
+
344
+ def confusion_matrix(self, y_test, predictions):
345
+ if self.task_type != 'regression':
346
+ cm = confusion_matrix(y_test.numpy(), predictions)
347
+ return cm.tolist()
348
+ else:
349
+ print("ANN - Error: Confusion matrix is not applicable for regression tasks. Returning None")
350
+ return None
351
+
352
+ def reset_parameters(self):
353
+ for layer in self.model:
354
+ if hasattr(layer, 'reset_parameters'):
355
+ layer.reset_parameters()
356
+
357
+ def cross_validate(self, X, y):
358
+ if 'hold' in self.cross_val_type:
359
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=self.test_size, random_state=self.random_state)
360
+ X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=self.validation_ratio, random_state=self.random_state)
361
+ self.train_model(X_train, y_train, X_val=X_val, y_val=y_val)
362
+ metrics, predictions = self.evaluate_model(X_test, y_test)
363
+ if self.task_type != 'regression':
364
+ self.confusion_matrix(y_test, predictions)
365
+ return metrics
366
+
367
+ elif 'fold' in self.cross_val_type:
368
+ kf = KFold(n_splits=self.k_folds, shuffle=True, random_state=self.random_state)
369
+ best_fold_index = -1
370
+ best_val_loss = np.inf
371
+ best_model_state = None
372
+
373
+ for fold_idx, (train_index, test_index) in enumerate(kf.split(X)):
374
+ # Reinitialize model parameters
375
+ self.reset_parameters()
376
+ print("Fold:", fold_idx+1)
377
+ X_train, X_test = X[train_index], X[test_index]
378
+ y_train, y_test = y[train_index], y[test_index]
379
+
380
+ X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=self.validation_ratio, random_state=self.random_state)
381
+
382
+ self.train_model(X_train, y_train, X_val=X_val, y_val=y_val)
383
+
384
+ print(f'Self Best Val Loss: {self.best_val_loss.item():.4f}')
385
+ if self.best_val_loss < best_val_loss:
386
+ best_val_loss = self.best_val_loss
387
+ best_fold_index = fold_idx
388
+ best_model_state = self.best_model_state
389
+
390
+ if best_fold_index == -1:
391
+ raise ValueError("No best fold found. Check early stopping and validation handling.")
392
+
393
+ print(f'Selecting best fold: {best_fold_index + 1}')
394
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=self.test_size, random_state=self.random_state)
395
+ X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=self.validation_ratio, random_state=self.random_state)
396
+ self.load_state_dict(best_model_state)
397
+ #print("Training on Best fold.")
398
+ #self.train_model(X_train, y_train, X_val=X_val, y_val=y_val)
399
+
400
+ metrics, predictions = self.evaluate_model(X_val, y_val)
401
+ if self.task_type != 'regression':
402
+ self.confusion_matrix(y_val, predictions)
403
+
404
+ return metrics
405
+
406
+ def save(self, path):
407
+ if path:
408
+ ext = path[-3:]
409
+ if ext.lower() != ".pt":
410
+ path = path + ".pt"
411
+ torch.save(self.state_dict(), path)
412
+
413
+ def load(self, path):
414
+ if path:
415
+ self.load_state_dict(torch.load(path))
416
+
417
+ class ANN():
418
+ @staticmethod
419
+ def DatasetByCSVPath(path, taskType='classification', description=""):
420
+ """
421
+ Returns a dataset according to the input CSV file path.
422
+
423
+ Parameters
424
+ ----------
425
+ path : str
426
+ The path to the folder containing the necessary CSV and YML files.
427
+ taskType : str , optional
428
+ The type of evaluation task. This can be 'classification' or 'regression'. The default is 'classification'.
429
+ description : str , optional
430
+ The description of the dataset. In keeping with the scikit BUNCH class, this will be saved in the DESCR parameter.
431
+
432
+ Returns
433
+ -------
434
+ sklearn.utils._bunch.Bunch
435
+ The created dataset.
436
+
437
+ """
438
+ import pandas as pd
439
+ import numpy as np
440
+ from sklearn.preprocessing import StandardScaler
441
+ from sklearn.model_selection import train_test_split
442
+ from sklearn.utils import Bunch
443
+
444
+ # Load the CSV file into a pandas DataFrame
445
+ df = pd.read_csv(path)
446
+
447
+ # Assume the last column is the target
448
+ features = df.iloc[:, :-1].values
449
+ target = df.iloc[:, -1].values
450
+
451
+ # Set target_names based on the name of the target column
452
+ target_names = [df.columns[-1]]
453
+
454
+ # Create a Bunch object
455
+ dataset = Bunch(
456
+ data=features,
457
+ target=target,
458
+ feature_names=df.columns[:-1].tolist(),
459
+ target_names=target_names,
460
+ frame=df,
461
+ DESCR=description,
462
+ )
463
+ return dataset
464
+
465
+ @staticmethod
466
+ def DatasetBySampleName(name):
467
+ """
468
+ Returns a dataset from the scikit-learn dataset samples.
469
+
470
+ Parameters
471
+ ----------
472
+ name : str
473
+ The name of the dataset. This can be one of ['breast_cancer', 'california_housing', 'digits', 'iris', 'wine']
474
+
475
+ trainRatio : float , optional
476
+ The ratio of the data to use for training and validation vs. the ratio to use for testing. The default is 0.6
477
+ which means that 60% of the data will be used for training and validation while 40% of the data will be reserved for testing.
478
+ randomState : int , optional
479
+ The randomState parameter is used to ensure reproducibility of the results. When you set the randomState parameter to a specific integer value,
480
+ it controls the shuffling of the data before splitting it into training and testing sets.
481
+ This means that every time you run your code with the same randomState value and the same dataset, you will get the same split of the data.
482
+ The default is 42 which is just a randomly picked integer number. Specify None for random sampling.
483
+ Returns
484
+ -------
485
+ dict
486
+ Returns the following dictionary:
487
+ XTrain, XTest, yTrain, yTest, inputSize, outputSize
488
+ XTrain is the list of features used for training
489
+ XTest is the list of features used for testing
490
+ yTrain is the list of targets used for training
491
+ yTest is the list of targets used for testing
492
+ inputSize is the size (length) of the input
493
+ outputSize is the size (length) of the output
494
+ """
495
+ # Load dataset
496
+ if name == 'breast_cancer':
497
+ dataset = load_breast_cancer()
498
+ elif name == 'california_housing':
499
+ dataset = fetch_california_housing()
500
+ elif name == 'digits':
501
+ dataset = load_digits()
502
+ elif name == 'iris':
503
+ dataset = load_iris()
504
+ elif name == 'wine':
505
+ dataset = load_wine()
506
+ else:
507
+ print(f"ANN.DatasetBySampleName - Error: Unsupported dataset: {name}. Returning None.")
508
+ return None
509
+ return dataset
510
+
511
+ @staticmethod
512
+ def DatasetSampleNames():
513
+ """
514
+ Returns the names of the available sample datasets from sci-kit learn.
515
+
516
+ Parameters
517
+ ----------
518
+
519
+ Returns
520
+ ----------
521
+ list
522
+ The list of names of available sample datasets
523
+ """
524
+ return ['breast_cancer', 'california_housing', 'digits', 'iris', 'wine']
525
+
526
+ @staticmethod
527
+ def DatasetSplit(X, y, testRatio=0.3, randomState=42):
528
+ """
529
+ Splits the input dataset according to the input ratios.
530
+
531
+ Parameters
532
+ ----------
533
+ X : list
534
+ The list of features.
535
+ y : list
536
+ The list of targets.
537
+ testRatio : float , optional
538
+ The ratio of the dataset to reserve as unseen data for testing. The default is 0.3
539
+ randomState : int , optional
540
+ The randomState parameter is used to ensure reproducibility of the results. When you set the randomState parameter to a specific integer value,
541
+ it controls the shuffling of the data before splitting it into training and testing sets.
542
+ This means that every time you run your code with the same randomState value and the same dataset, you will get the same split of the data.
543
+ The default is 42 which is just a randomly picked integer number. Specify None for random sampling.
544
+
545
+ Returns
546
+ -------
547
+ list
548
+ Returns the following list : [X_train, X_test, y_train,y_test]
549
+ X_train is the list of features used for training
550
+ X_test is the list of features used for testing
551
+ y_train is the list of targets used for training
552
+ y_test is the list of targets used for testing
553
+
554
+ """
555
+ if testRatio < 0 or testRatio > 1:
556
+ print("ANN.DatasetSplit - Error: testRatio parameter cannot be outside the range [0,1]. Returning None.")
557
+ return None
558
+ # First split: train and temp (remaining)
559
+ return train_test_split(X, y, test_size=testRatio, random_state=randomState)
560
+
561
+ @staticmethod
562
+ def Hyperparameters(title='Untitled',
563
+ taskType='classification',
564
+ testRatio = 0.3,
565
+ validationRatio = 0.2,
566
+ hiddenLayers = [12,12,12],
567
+ learningRate = 0.001,
568
+ epochs = 10,
569
+ batchSize = 1,
570
+ patience = 5,
571
+ earlyStopping = True,
572
+ randomState = 42,
573
+ crossValidationType = "holdout",
574
+ kFolds = 3,
575
+ interval = 1,
576
+ mantissa = 6):
577
+ """
578
+ title : str , optional
579
+ The desired title for the dataset. The default is "Untitled".
580
+ taskType : str , optional
581
+ The desired task type. This can be either 'classification' or 'regression' (case insensitive).
582
+ Classification is a type of supervised learning where the model is trained to predict categorical labels (classes) from input data.
583
+ Regression is a type of supervised learning where the model is trained to predict continuous numerical values from input data.
584
+ testRatio : float , optional
585
+ The split ratio between training and testing. The default is 0.3. This means that
586
+ 70% of the data will be used for training/validation and 30% will be reserved for testing as unseen data.
587
+ validationRatio : float , optional
588
+ The split ratio between training and validation. The default is 0.2. This means that
589
+ 80% of the validation data (left over after reserving test data) will be used for training and 20% will be used for validation.
590
+ hiddenLayers : list , optional
591
+ The number of hidden layers and the number of nodes in each layer.
592
+ If you wish to have 3hidden layers with 8 nodes in the first
593
+ 16 nodes in the second, and 4 nodes in the last layer, you specify [8,16,4].
594
+ The default is [12,12,12]
595
+ learningRate : float, optional
596
+ The desired learning rate. The default is 0.001. See https://en.wikipedia.org/wiki/Learning_rate
597
+ epochs : int , optional
598
+ The desired number of epochs. The default is 10. See https://en.wikipedia.org/wiki/Neural_network_(machine_learning)
599
+ batchSize : int , optional
600
+ The desired number of samples that will be propagated through the network at one time before the model's internal parameters are updated. Instead of updating the model parameters after every single training sample
601
+ (stochastic gradient descent) or after the entire training dataset (batch gradient descent), mini-batch gradient descent updates the model parameters after
602
+ a specified number of samples, which is determined by batchSize. The default is 1.
603
+ patience : int , optional
604
+ The desired number of epochs with no improvement in the validation loss after which training will be stopped if early stopping is enabled.
605
+ earlyStopping : bool , optional
606
+ If set to True, the training will stop if the validation loss does not improve after a certain number of epochs defined by patience. The default is True.
607
+ randomState : int , optional
608
+ The randomState parameter is used to ensure reproducibility of the results. When you set the randomState parameter to a specific integer value,
609
+ it controls the shuffling of the data before splitting it into training and testing sets.
610
+ This means that every time you run your code with the same randomState value and the same dataset, you will get the same split of the data.
611
+ The default is 42 which is just a randomly picked integer number. Specify None for random sampling.
612
+ crossValidationType : str , optional
613
+ The desired type of cross-validation. This can be one of 'holdout' or 'k-fold'. The default is 'holdout'
614
+ kFolds : int , optional
615
+ The number of splits (folds) to use if K-Fold cross validation is selected. The default is 5.
616
+ interval : int , optional
617
+ The desired epoch interval at which to report and save metrics data. This must be less than the total number of epochs. The default is 1.
618
+ mantissa : int , optional
619
+ The desired length of the mantissa. The default is 6.
620
+
621
+ Returns
622
+ -------
623
+ dict
624
+ Returns a dictionary with the following keys:
625
+ 'task_type'
626
+ 'test_ratio'
627
+ 'validation_ratio'
628
+ 'hidden_layers'
629
+ 'learning_rate'
630
+ 'epochs'
631
+ 'batch_size'
632
+ 'early_stopping'
633
+ 'patience'
634
+ 'random_tate'
635
+ 'cross_val_type'
636
+ 'kFolds'
637
+ 'interval'
638
+ 'mantissa'
639
+ """
640
+ return {
641
+ 'task_type': taskType,
642
+ 'test_ratio': testRatio,
643
+ 'validation_ratio': validationRatio,
644
+ 'hidden_layers': hiddenLayers,
645
+ 'learning_rate': learningRate,
646
+ 'epochs': epochs,
647
+ 'batch_size': batchSize,
648
+ 'early_stopping': earlyStopping,
649
+ 'patience': patience,
650
+ 'random_state': randomState,
651
+ 'cross_val_type': crossValidationType,
652
+ 'k_folds': kFolds,
653
+ 'interval': interval,
654
+ 'mantissa': mantissa}
655
+
656
+ @staticmethod
657
+ def HyperparametersBySampleName(name):
658
+ """
659
+ Returns the suggested initial hyperparameters to use for the dataset named in the name input parameter.
660
+ You can get a list of available sample datasets using ANN.SampleDatasets().
661
+
662
+ Parameters
663
+ ----------
664
+ name : str
665
+ The input name of the sample dataset. This must be one of ['breast_cancer', 'california_housing', 'digits', 'iris', 'wine']
666
+
667
+ Returns
668
+ -------
669
+ dict
670
+ Returns a dictionary with the following keys:
671
+ 'title'
672
+ 'task_type'
673
+ 'test_ratio'
674
+ 'validation_ratio'
675
+ 'hidden_layers'
676
+ 'learning_rate'
677
+ 'epochs'
678
+ 'batch_size'
679
+ 'early_stopping'
680
+ 'patience'
681
+ 'random_state'
682
+ 'cross_val_type'
683
+ 'k_folds'
684
+ 'interval'
685
+ 'mantissa'
686
+
687
+ """
688
+ hyperparameters = {
689
+ 'breast_cancer': {
690
+ 'title': 'Breast Cancer',
691
+ 'task_type': 'classification',
692
+ 'test_ratio': 0.3,
693
+ 'validation_ratio': 0.2,
694
+ 'hidden_layers': [30, 15],
695
+ 'learning_rate': 0.001,
696
+ 'epochs': 100,
697
+ 'batch_size': 32,
698
+ 'early_stopping': True,
699
+ 'patience': 10,
700
+ 'random_state': 42,
701
+ 'cross_val_type': "holdout",
702
+ 'k_folds': 3,
703
+ 'interval': 10,
704
+ 'mantissa': 6
705
+ },
706
+ 'california_housing': {
707
+ 'title': 'California Housing',
708
+ 'task_type': 'regression',
709
+ 'test_ratio': 0.3,
710
+ 'validation_atio': 0.2,
711
+ 'hidden_layers': [50, 25],
712
+ 'learning_rate': 0.001,
713
+ 'epochs': 50,
714
+ 'batch_size': 16,
715
+ 'early_stopping': False,
716
+ 'patience': 10,
717
+ 'random_state': 42,
718
+ 'cross_val_type': "k-fold",
719
+ 'k_folds': 3,
720
+ 'interval': 5,
721
+ 'mantissa': 6
722
+ },
723
+ 'digits': {
724
+ 'title': 'Digits',
725
+ 'task_type': 'classification',
726
+ 'test_ratio': 0.3,
727
+ 'validation_ratio': 0.2,
728
+ 'hidden_layers': [64, 32],
729
+ 'learning_rate': 0.001,
730
+ 'epochs': 50,
731
+ 'batch_size': 32,
732
+ 'early_stopping': True,
733
+ 'patience': 10,
734
+ 'random_state': 42,
735
+ 'cross_val_type': "holdout",
736
+ 'kFolds': 3,
737
+ 'interval': 5,
738
+ 'mantissa': 6
739
+ },
740
+ 'iris': {
741
+ 'title': 'Iris',
742
+ 'task_type': 'classification',
743
+ 'test_ratio': 0.3,
744
+ 'validation_ratio': 0.2,
745
+ 'hidden_layers': [10, 5],
746
+ 'learning_rate': 0.001,
747
+ 'epochs': 100,
748
+ 'batch_size': 16,
749
+ 'early_stopping': False,
750
+ 'patience': 10,
751
+ 'random_state': 42,
752
+ 'cross_val_type': "holdout",
753
+ 'k_folds': 3,
754
+ 'interval': 2,
755
+ 'mantissa': 6
756
+ },
757
+ 'wine': {
758
+ 'title': 'Wine',
759
+ 'task_type': 'classification',
760
+ 'test_ratio': 0.3,
761
+ 'validation_ratio': 0.2,
762
+ 'hidden_layers': [50, 25],
763
+ 'learning_rate': 0.001,
764
+ 'epochs': 100,
765
+ 'batch_size': 16,
766
+ 'early_stopping': False,
767
+ 'patience': 10,
768
+ 'random_state': 42,
769
+ 'cross_val_type': "holdout",
770
+ 'k_folds': 3,
771
+ 'interval': 2,
772
+ 'mantissa': 6
773
+ }
774
+ }
775
+
776
+ if name in hyperparameters:
777
+ return hyperparameters[name]
778
+ else:
779
+ print(f"ANN-HyperparametersBySampleDatasetName - Error: Dataset name '{name}' not recognized. Available datasets: {list(hyperparameters.keys())}. Returning None.")
780
+ return None
781
+
782
+ @staticmethod
783
+ def ModelData(model):
784
+ """
785
+ Returns the data of the model
786
+
787
+ Parameters
788
+ ----------
789
+ model : Model
790
+ The input model.
791
+
792
+ Returns
793
+ -------
794
+ dict
795
+ A dictionary containing the model data. The keys in the dictionary are:
796
+ 'epochs' (list of epoch numbers at which metrics data was collected)
797
+ 'training_loss' (LOSS)
798
+ 'validation_loss' (VALIDATION LOSS)
799
+ 'training_accuracy' (ACCURACY for classification tasks only)
800
+ 'validation_accuracy' (ACCURACYfor classification tasks only)
801
+ 'training_mae' (MAE for regression tasks only)
802
+ 'validation_mae' (MAE for regression tasks only)
803
+ 'training_mse' (MSE for regression tasks only)
804
+ 'validation_mse' (MSE for regression tasks only)
805
+ 'training_r2' (R^2 for regression tasks only)
806
+ 'validation_r2' (R^2 for regression tasks only)
807
+
808
+
809
+ """
810
+
811
+ return {
812
+ 'epochs': model.epoch_list,
813
+ 'training_loss': model.training_loss_list,
814
+ 'validation_loss': model.validation_loss_list,
815
+ 'training_accuracy': model.training_accuracy_list,
816
+ 'validation_accuracy': model.validation_accuracy_list,
817
+ 'training_mae': model.training_mae_list,
818
+ 'validation_mae': model.validation_mae_list,
819
+ 'training_mse': model.training_mse_list,
820
+ 'validation_mse': model.validation_mse_list,
821
+ 'training_r2': model.training_r2_list,
822
+ 'validation_r2': model.validation_r2_list
823
+ }
824
+
825
+ @staticmethod
826
+ def Initialize(hyperparameters, dataset):
827
+ """
828
+ Initializes an ANN model with the input dataset and hyperparameters.
829
+
830
+ Parameters
831
+ ----------
832
+ hyperparameters : dict
833
+ The hyperparameters dictionary. You can create one using ANN.Hyperparameters() or, if you are using a sample Dataset, you can get it from ANN.HyperParametersBySampleName.
834
+ dataset : sklearn.utils._bunch.Bunch
835
+ The input dataset.
836
+
837
+ Returns
838
+ -------
839
+ _ANNModel
840
+ Returns the trained model.
841
+
842
+ """
843
+ def prepare_data(dataset, task_type='classification'):
844
+ X, y = dataset.data, dataset.target
845
+
846
+ # Standardize features
847
+ scaler = StandardScaler()
848
+ X = scaler.fit_transform(X)
849
+ X = torch.tensor(X, dtype=torch.float32)
850
+ y = torch.tensor(y, dtype=torch.long if task_type != 'regression' else torch.float32)
851
+ return X, y
852
+
853
+ task_type = hyperparameters['task_type']
854
+ if task_type not in ['classification', 'regression']:
855
+ print("ANN.ModelInitialize - Error: The task type in the input hyperparameters parameter is not recognized. It must be either 'classification' or 'regression'. Returning None.")
856
+ return None
857
+ X, y = prepare_data(dataset, task_type=task_type)
858
+ model = _ANN(input_size=X.shape[1], hyperparameters=hyperparameters, dataset=dataset)
859
+ return model
860
+
861
+ @staticmethod
862
+ def Train(hyperparameters, dataset):
863
+ """
864
+ Trains the input model given the input features (X), and target (y).
865
+
866
+ Parameters
867
+ ----------
868
+ hyperparameters : dict
869
+ The hyperparameters dictionary. You can create one using ANN.Hyperparameters() or, if you are using a sample Dataset, you can get it from ANN.HyperParametersBySampleName.
870
+ dataset : sklearn.utils._bunch.Bunch
871
+ The input dataset.
872
+
873
+ Returns
874
+ -------
875
+ _ANNModel
876
+ Returns the trained model.
877
+
878
+ """
879
+ def prepare_data(dataset, task_type='classification'):
880
+ X, y = dataset.data, dataset.target
881
+
882
+ # Standardize features
883
+ scaler = StandardScaler()
884
+ X = scaler.fit_transform(X)
885
+ X = torch.tensor(X, dtype=torch.float32)
886
+ y = torch.tensor(y, dtype=torch.long if task_type != 'regression' else torch.float32)
887
+ return X, y
888
+
889
+ X, y = prepare_data(dataset, task_type=hyperparameters['task_type'])
890
+ model = _ANN(input_size=X.shape[1], hyperparameters=hyperparameters, dataset=dataset)
891
+ model.cross_validate(X, y)
892
+ return model
893
+
894
+ @staticmethod
895
+ def Test(model, hyperparameters, dataset):
896
+ """
897
+ Returns the labels (actual values) and predictions (predicted values) given the input model, features (X), and target (y).
898
+
899
+ Parameters
900
+ ----------
901
+ model : ANN Model
902
+ The input model.
903
+ X : list
904
+ The input list of features.
905
+ y : list
906
+ The input list of targets
907
+
908
+ Returns
909
+ -------
910
+ list, list
911
+ Returns two lists: metrics, and predictions.
912
+
913
+ """
914
+ def prepare_data(dataset, task_type='classification'):
915
+ X, y = dataset.data, dataset.target
916
+
917
+ # Standardize features
918
+ scaler = StandardScaler()
919
+ X = scaler.fit_transform(X)
920
+ X = torch.tensor(X, dtype=torch.float32)
921
+ y = torch.tensor(y, dtype=torch.long if task_type != 'regression' else torch.float32)
922
+ return X, y
923
+
924
+ X, y = prepare_data(dataset, task_type=hyperparameters['task_type'])
925
+ X_train, X_test, y_train,y_test = ANN.DatasetSplit(X, y, testRatio=hyperparameters['test_ratio'], randomState=hyperparameters['random_state'])
926
+ metrics, predictions = model.evaluate_model(X_test, y_test)
927
+ confusion_matrix = None
928
+ if hyperparameters['task_type'] != 'regression':
929
+ confusion_matrix = model.confusion_matrix(y_test, predictions)
930
+ return y_test, predictions, metrics, confusion_matrix
931
+
932
+ @staticmethod
933
+ def Figures(model, width=900, height=600, template="plotly", colorScale='viridis', colorSamples=10):
934
+ """
935
+ Creates Plotly Figures from the model data. For classification tasks this includes
936
+ a confusion matrix, loss, and accuracy figures. For regression tasks this includes
937
+ loss and MAE figures.
938
+
939
+ Parameters
940
+ ----------
941
+ model : ANN Model
942
+ The input model.
943
+ width : int , optional
944
+ The desired figure width in pixels. The default is 900.
945
+ height : int , optional
946
+ The desired figure height in pixels. The default is 900.
947
+ template : str , optional
948
+ The desired Plotly template to use for the scatter plot.
949
+ This can be one of ['ggplot2', 'seaborn', 'simple_white', 'plotly',
950
+ 'plotly_white', 'plotly_dark', 'presentation', 'xgridoff',
951
+ 'ygridoff', 'gridon', 'none']. The default is "plotly".
952
+ colorScale : str , optional
953
+ The desired type of plotly color scales to use (e.g. "viridis", "plasma"). The default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
954
+ colorSamples : int , optional
955
+ The number of discrete color samples to use for displaying the data. The default is 10.
956
+
957
+ Returns
958
+ -------
959
+ list
960
+ Returns a list of Plotly figures and a corresponding list of file names.
961
+
962
+ """
963
+ import plotly.graph_objects as go
964
+ from topologicpy.Plotly import Plotly
965
+ import numpy as np
966
+ figures = []
967
+ filenames = []
968
+ if model.task_type == 'classification':
969
+ confusion_matrix = model.metrics['confusion_matrix']
970
+ confusion_matrix_figure = Plotly.FigureByConfusionMatrix(confusion_matrix, width=width, height=height, colorScale=colorScale, colorSamples=colorSamples)
971
+ confusion_matrix_figure.update_layout(title=model.title+"<BR>Confusion Matrix")
972
+ figures.append(confusion_matrix_figure)
973
+ filenames.append("ConfusionMatrix")
974
+ data_lists = [[model.train_loss_list, model.val_loss_list], [model.train_accuracy_list, model.val_accuracy_list]]
975
+ label_lists = [['Training Loss', 'Validation Loss'], ['Training Accuracy', 'Validation Accuracy']]
976
+ titles = ['Training and Validation Loss', 'Training and Validation Accuracy']
977
+ titles = [model.title+"<BR>"+t for t in titles]
978
+ legend_titles = ['Loss Type', 'Accuracy Type']
979
+ xaxis_titles = ['Epoch', 'Epoch']
980
+ yaxis_titles = ['Loss', 'Accuracy']
981
+ filenames = yaxis_titles
982
+
983
+ elif model.task_type.lower() == 'regression':
984
+ data_lists = [[model.train_loss_list, model.val_loss_list], [model.train_mae_list, model.val_mae_list], [model.train_mse_list, model.val_mse_list], [model.train_r2_list, model.val_r2_list]]
985
+ label_lists = [['Training Loss', 'Validation Loss'], ['Training MAE', 'Validation MAE'], ['Training MSE', 'Validation MSE'],['Training R^2', 'Validation R^2']]
986
+ titles = ['Training and Validation Loss', 'Training and Validation MAE', 'Training and Validation MSE', 'Training and Validation R^2']
987
+ titles = [model.title+"<BR>"+t for t in titles]
988
+ legend_titles = ['Loss Type', 'MAE Type', 'MSE Type', 'R^2 Type']
989
+ xaxis_titles = ['Epoch', 'Epoch', 'Epoch', 'Epoch']
990
+ yaxis_titles = ['Loss', 'MAE', 'MSE', 'R^2']
991
+ filenames = yaxis_titles
992
+ else:
993
+ print("ANN.ModelFigures - Error: Could not recognize model task type. Returning None.")
994
+ return None
995
+ for i in range(len(data_lists)):
996
+ data = data_lists[i]
997
+ labels = label_lists[i]
998
+ title = titles[i]
999
+ legend_title = legend_titles[i]
1000
+ xaxis_title = xaxis_titles[i]
1001
+ yaxis_title = yaxis_titles[i]
1002
+ x = model.epoch_list
1003
+
1004
+
1005
+ figure = go.Figure()
1006
+ min_x = np.inf
1007
+ max_x = -np.inf
1008
+ min_y = np.inf
1009
+ max_y = -np.inf
1010
+ for j in range(len(data)):
1011
+ y = data[j]
1012
+ figure.add_trace(go.Scatter(x=x, y=y, mode='lines+markers', name=labels[j]))
1013
+ min_x = min(min_x, min(x))
1014
+ max_x = max(max_x, max(x))
1015
+ min_y = min(min_y, min(y))
1016
+ max_y = max(max_y, max(y))
1017
+
1018
+ figure.update_layout(
1019
+ xaxis=dict(range=[0, max_x+max_x*0.01]),
1020
+ yaxis=dict(range=[min_y-min_y*0.01, max_y+max_y*0.01]),
1021
+ title=title,
1022
+ xaxis_title=xaxis_title,
1023
+ yaxis_title=yaxis_title,
1024
+ legend_title= legend_title,
1025
+ template=template,
1026
+ width=width,
1027
+ height=height
1028
+ )
1029
+ figures.append(figure)
1030
+ return figures, filenames
1031
+
1032
+ @staticmethod
1033
+ def Metrics(model):
1034
+ """
1035
+ Returns the model performance metrics given the input labels and predictions, and the model's task type.
1036
+
1037
+ Parameters
1038
+ ----------
1039
+ model : ANN Model
1040
+ The input model.
1041
+ labels : list
1042
+ The input list of labels (actual values).
1043
+ predictions : list
1044
+ The input list of predictions (predicted values).
1045
+
1046
+ Returns
1047
+ -------
1048
+ dict
1049
+ if the task type is 'classification', this methods return a dictionary with the following keys:
1050
+ "Accuracy"
1051
+ "Precision"
1052
+ "Recall"
1053
+ "F1 Score"
1054
+ "Confusion Matrix"
1055
+ else if the task type is 'regression', this method returns:
1056
+ "Mean Squared Error"
1057
+ "Mean Absolute Error"
1058
+ "R-squared"
1059
+
1060
+ """
1061
+ metrics = model.metrics
1062
+ return metrics
1063
+
1064
+ @staticmethod
1065
+ def Save(model, path, overwrite=False):
1066
+ """
1067
+ Saves the model.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ model : Model
1072
+ The input model.
1073
+ path : str
1074
+ The file path at which to save the model.
1075
+ overwrite : bool, optional
1076
+ If set to True, any existing file will be overwritten. Otherwise, it won't. The default is False.
1077
+
1078
+ Returns
1079
+ -------
1080
+ bool
1081
+ True if the model is saved correctly. False otherwise.
1082
+
1083
+ """
1084
+ import os
1085
+
1086
+ if model == None:
1087
+ print("ANN.Save - Error: The input model parameter is invalid. Returning None.")
1088
+ return None
1089
+ if path == None:
1090
+ print("ANN.Save - Error: The input path parameter is invalid. Returning None.")
1091
+ return None
1092
+ if not overwrite and os.path.exists(path):
1093
+ print("ANN.Save - Error: a file already exists at the specified path and overwrite is set to False. Returning None.")
1094
+ return None
1095
+ if overwrite and os.path.exists(path):
1096
+ os.remove(path)
1097
+ # Make sure the file extension is .pt
1098
+ ext = path[len(path)-3:len(path)]
1099
+ if ext.lower() != ".pt":
1100
+ path = path+".pt"
1101
+ # Save the trained model
1102
+ torch.save(model.state_dict(), path)
1103
+ return True
1104
+
1105
+ @staticmethod
1106
+ def Load(model, path):
1107
+ """
1108
+ Loads the model state dictionary found at the input file path. The model input parameter must be pre-initialized using the ANN.Initialize() method.
1109
+
1110
+ Parameters
1111
+ ----------
1112
+ model : ANN object
1113
+ The input ANN model. The model must be pre-initialized using the ModelInitialize method.
1114
+ path : str
1115
+ File path for the saved model state dictionary.
1116
+
1117
+ Returns
1118
+ -------
1119
+ ANN model
1120
+ The neural network class.
1121
+
1122
+ """
1123
+ from os.path import exists
1124
+
1125
+ if not exists(path):
1126
+ print("ANN.Load - Error: The specified path does not exist. Returning None.")
1127
+ return None
1128
+ model.load(path)
1129
+ return model
1130
+
1131
+
1132
+
1133
+