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 +1133 -0
- topologicpy/Cell.py +0 -1
- topologicpy/Face.py +21 -3
- topologicpy/Graph.py +5 -3
- topologicpy/Plotly.py +2 -2
- topologicpy/Shell.py +12 -1
- topologicpy/Topology.py +7 -9
- topologicpy/Wire.py +16 -7
- topologicpy/version.py +1 -1
- {topologicpy-0.7.18.dist-info → topologicpy-0.7.20.dist-info}/METADATA +1 -1
- {topologicpy-0.7.18.dist-info → topologicpy-0.7.20.dist-info}/RECORD +14 -13
- {topologicpy-0.7.18.dist-info → topologicpy-0.7.20.dist-info}/WHEEL +1 -1
- {topologicpy-0.7.18.dist-info → topologicpy-0.7.20.dist-info}/LICENSE +0 -0
- {topologicpy-0.7.18.dist-info → topologicpy-0.7.20.dist-info}/top_level.txt +0 -0
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
|
+
|