dragon-ml-toolbox 1.1.2__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.
Potentially problematic release.
This version of dragon-ml-toolbox might be problematic. Click here for more details.
- dragon_ml_toolbox-1.1.2.dist-info/METADATA +114 -0
- dragon_ml_toolbox-1.1.2.dist-info/RECORD +16 -0
- dragon_ml_toolbox-1.1.2.dist-info/WHEEL +5 -0
- dragon_ml_toolbox-1.1.2.dist-info/top_level.txt +1 -0
- ml_tools/MICE_imputation.py +178 -0
- ml_tools/__init__.py +0 -0
- ml_tools/data_exploration.py +751 -0
- ml_tools/datasetmaster.py +595 -0
- ml_tools/ensemble_learning.py +701 -0
- ml_tools/handle_excel.py +310 -0
- ml_tools/logger.py +145 -0
- ml_tools/particle_swarm_optimization.py +467 -0
- ml_tools/pytorch_models.py +227 -0
- ml_tools/trainer.py +366 -0
- ml_tools/utilities.py +168 -0
- ml_tools/vision_helpers.py +218 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import joblib
|
|
4
|
+
import xgboost as xgb
|
|
5
|
+
import lightgbm as lgb
|
|
6
|
+
from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
|
|
7
|
+
from sklearn.base import ClassifierMixin
|
|
8
|
+
from sklearn.preprocessing import StandardScaler
|
|
9
|
+
from typing import Literal, Union, Tuple, Dict
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
import polars as pl
|
|
12
|
+
from functools import partial
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObjectiveFunction():
|
|
16
|
+
"""
|
|
17
|
+
Callable objective function designed for optimizing continuous outputs from regression models.
|
|
18
|
+
|
|
19
|
+
The trained model must include a 'model' and a 'scaler'. Additionally 'feature_names' and 'target_name' will be parsed if present.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
trained_model_path : str
|
|
24
|
+
Path to a serialized model (joblib) compatible with scikit-learn-like `.predict`.
|
|
25
|
+
add_noise : bool
|
|
26
|
+
Whether to apply multiplicative noise to the input features during evaluation.
|
|
27
|
+
binary_features : int, default=0
|
|
28
|
+
Number of binary features located at the END of the feature vector. Model should be trained with continuous features first, followed by binary.
|
|
29
|
+
task : Literal, default 'maximization'
|
|
30
|
+
Whether to maximize or minimize the target.
|
|
31
|
+
"""
|
|
32
|
+
def __init__(self, trained_model_path: str, add_noise: bool=True, task: Literal["maximization", "minimization"]="maximization", binary_features: int=0) -> None:
|
|
33
|
+
self.binary_features = binary_features
|
|
34
|
+
self.is_hybrid = False if binary_features <= 0 else True
|
|
35
|
+
self.use_noise = add_noise
|
|
36
|
+
self._artifact = joblib.load(trained_model_path)
|
|
37
|
+
self.model = self._get_from_artifact('model')
|
|
38
|
+
self.scaler = self._get_from_artifact('scaler')
|
|
39
|
+
self.feature_names: list[str] = self._get_from_artifact('feature_names') # type: ignore
|
|
40
|
+
self.target_name: str = self._get_from_artifact('target_name') # type: ignore
|
|
41
|
+
self.task = task
|
|
42
|
+
self.check_model() # check for classification models and None values
|
|
43
|
+
|
|
44
|
+
def __call__(self, features_array: np.ndarray) -> float:
|
|
45
|
+
if self.use_noise:
|
|
46
|
+
features_array = self.add_noise(features_array)
|
|
47
|
+
if self.is_hybrid:
|
|
48
|
+
features_array = self._handle_hybrid(features_array)
|
|
49
|
+
|
|
50
|
+
if features_array.ndim == 1:
|
|
51
|
+
features_array = features_array.reshape(1, -1)
|
|
52
|
+
|
|
53
|
+
# scale features as the model expects
|
|
54
|
+
features_array = self.scaler.transform(features_array) # type: ignore
|
|
55
|
+
|
|
56
|
+
result = self.model.predict(features_array) # type: ignore
|
|
57
|
+
scalar = result.item()
|
|
58
|
+
# pso minimizes by default, so we return the negative value to maximize
|
|
59
|
+
if self.task == "maximization":
|
|
60
|
+
return -scalar
|
|
61
|
+
else:
|
|
62
|
+
return scalar
|
|
63
|
+
|
|
64
|
+
def add_noise(self, features_array):
|
|
65
|
+
noise_range = np.random.uniform(0.95, 1.05, size=features_array.shape)
|
|
66
|
+
new_feature_values = features_array * noise_range
|
|
67
|
+
return new_feature_values
|
|
68
|
+
|
|
69
|
+
def _handle_hybrid(self, features_array):
|
|
70
|
+
feat_continuous = features_array[:self.binary_features]
|
|
71
|
+
feat_binary = (features_array[self.binary_features:] > 0.5).astype(int) #threshold binary values
|
|
72
|
+
new_feature_values = np.concatenate([feat_continuous, feat_binary])
|
|
73
|
+
return new_feature_values
|
|
74
|
+
|
|
75
|
+
def check_model(self):
|
|
76
|
+
if isinstance(self.model, ClassifierMixin) or isinstance(self.model, xgb.XGBClassifier) or isinstance(self.model, lgb.LGBMClassifier):
|
|
77
|
+
raise ValueError(f"[Model Check Failed] ❌\nThe loaded model ({type(self.model).__name__}) is a Classifier.\nOptimization is not suitable for standard classification tasks.")
|
|
78
|
+
if self.model is None:
|
|
79
|
+
raise ValueError("Loaded model is None")
|
|
80
|
+
if self.scaler is None:
|
|
81
|
+
raise ValueError("Loaded scaler is None")
|
|
82
|
+
|
|
83
|
+
def _get_from_artifact(self, key: str):
|
|
84
|
+
val = self._artifact.get(key)
|
|
85
|
+
if key == "feature_names":
|
|
86
|
+
result = val if isinstance(val, list) and val else None
|
|
87
|
+
else:
|
|
88
|
+
result = val if val else None
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def __repr__(self):
|
|
92
|
+
return (f"<ObjectiveFunction(model={type(self.model).__name__}, scaler={type(self.scaler).__name__}, use_noise={self.use_noise}, is_hybrid={self.is_hybrid}, task='{self.task}')>")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _set_boundaries(lower_boundaries: Sequence[float], upper_boundaries: Sequence[float]):
|
|
96
|
+
assert len(lower_boundaries) == len(upper_boundaries), "Lower and upper boundaries must have the same length."
|
|
97
|
+
assert len(lower_boundaries) >= 1, "At least one boundary pair is required."
|
|
98
|
+
lower = np.array(lower_boundaries)
|
|
99
|
+
upper = np.array(upper_boundaries)
|
|
100
|
+
return lower, upper
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _set_feature_names(size: int, names: Union[list[str], None]):
|
|
104
|
+
if names is None:
|
|
105
|
+
return [str(i) for i in range(1, size+1)]
|
|
106
|
+
else:
|
|
107
|
+
assert len(names) == size, "List with feature names do not match the number of features"
|
|
108
|
+
return names
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _save_results(*dicts, save_dir: str, target_name: str):
|
|
112
|
+
combined_dict = dict()
|
|
113
|
+
for single_dict in dicts:
|
|
114
|
+
combined_dict.update(single_dict)
|
|
115
|
+
|
|
116
|
+
full_path = os.path.join(save_dir, f"results_{target_name}.csv")
|
|
117
|
+
pl.DataFrame(combined_dict).write_csv(full_path)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_pso(lower_boundaries: Sequence[float], upper_boundaries: Sequence[float], objective_function: ObjectiveFunction,
|
|
121
|
+
save_results_dir: str,
|
|
122
|
+
target_name: Union[str, None]=None,
|
|
123
|
+
feature_names: Union[list[str], None]=None,
|
|
124
|
+
swarm_size: int=100, max_iterations: int=100,
|
|
125
|
+
inequality_constrain_function=None,
|
|
126
|
+
post_hoc_analysis: Union[int, None]=None) -> Tuple[Dict[str, float | list[float]], Dict[str, float | list[float]]]:
|
|
127
|
+
"""
|
|
128
|
+
Executes Particle Swarm Optimization (PSO) to optimize a given objective function and saves the results.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
lower_boundaries : Sequence[float]
|
|
133
|
+
Lower bounds for each feature in the search space.
|
|
134
|
+
upper_boundaries : Sequence[float]
|
|
135
|
+
Upper bounds for each feature in the search space.
|
|
136
|
+
objective_function : ObjectiveFunction
|
|
137
|
+
A callable object encapsulating a regression model and its scaler.
|
|
138
|
+
save_results_dir : str
|
|
139
|
+
Directory path to save the results CSV file.
|
|
140
|
+
target_name : str or None, optional
|
|
141
|
+
Name of the target variable. If None, attempts to retrieve from the ObjectiveFunction object.
|
|
142
|
+
feature_names : list[str] or None, optional
|
|
143
|
+
List of feature names. If None, attempts to retrieve from the ObjectiveFunction or generate generic names.
|
|
144
|
+
swarm_size : int, default=100
|
|
145
|
+
Number of particles in the swarm.
|
|
146
|
+
max_iterations : int, default=100
|
|
147
|
+
Maximum number of iterations for the optimization algorithm.
|
|
148
|
+
inequality_constrain_function : callable or None, optional
|
|
149
|
+
Optional function defining inequality constraints to be respected by the optimization.
|
|
150
|
+
post_hoc_analysis : int or None, optional
|
|
151
|
+
If specified, runs the optimization multiple times to perform post hoc analysis. The value indicates the number of repetitions.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
Tuple[Dict[str, float | list[float]], Dict[str, float | list[float]]]
|
|
156
|
+
If `post_hoc_analysis` is None, returns two dictionaries:
|
|
157
|
+
- best_features_named: Feature values (after inverse scaling) that yield the best result.
|
|
158
|
+
- best_target_named: Best result obtained for the target variable.
|
|
159
|
+
|
|
160
|
+
If `post_hoc_analysis` is an integer, returns two dictionaries:
|
|
161
|
+
- all_best_features_named: Lists of best feature values (after inverse scaling) for each repetition.
|
|
162
|
+
- all_best_targets_named: List of best target values across repetitions.
|
|
163
|
+
|
|
164
|
+
Notes
|
|
165
|
+
-----
|
|
166
|
+
- PSO minimizes the objective function by default; if maximization is desired, it should be handled inside the ObjectiveFunction.
|
|
167
|
+
- Feature values are scaled before being passed to the model and inverse-transformed before result saving.
|
|
168
|
+
"""
|
|
169
|
+
lower, upper = _set_boundaries(lower_boundaries, upper_boundaries)
|
|
170
|
+
|
|
171
|
+
# feature names
|
|
172
|
+
if feature_names is None and objective_function.feature_names is not None:
|
|
173
|
+
feature_names = objective_function.feature_names
|
|
174
|
+
names = _set_feature_names(size=len(lower_boundaries), names=feature_names)
|
|
175
|
+
|
|
176
|
+
# target name
|
|
177
|
+
if target_name is None and objective_function.target_name is not None:
|
|
178
|
+
target_name = objective_function.target_name
|
|
179
|
+
if target_name is None:
|
|
180
|
+
target_name = "Target"
|
|
181
|
+
|
|
182
|
+
arguments = {
|
|
183
|
+
"func":objective_function,
|
|
184
|
+
"lb": lower,
|
|
185
|
+
"ub": upper,
|
|
186
|
+
"f_ieqcons": inequality_constrain_function,
|
|
187
|
+
"swarmsize": swarm_size,
|
|
188
|
+
"maxiter": max_iterations,
|
|
189
|
+
"processes": 1,
|
|
190
|
+
"particle_output": True
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if post_hoc_analysis is None:
|
|
194
|
+
# best_features, best_target = pso(**arguments)
|
|
195
|
+
best_features, best_target, _particle_positions, _target_values_per_position = pso(**arguments)
|
|
196
|
+
|
|
197
|
+
# inverse transformation
|
|
198
|
+
best_features = np.array(best_features).reshape(1, -1)
|
|
199
|
+
best_features_real = objective_function.scaler.inverse_transform(best_features).flatten() # type: ignore
|
|
200
|
+
|
|
201
|
+
# name features
|
|
202
|
+
best_features_named = {name: value for name, value in zip(names, best_features_real)}
|
|
203
|
+
best_target_named = {target_name: best_target}
|
|
204
|
+
|
|
205
|
+
# save results
|
|
206
|
+
_save_results(best_features_named, best_target_named, save_dir=save_results_dir, target_name=target_name)
|
|
207
|
+
|
|
208
|
+
return best_features_named, best_target_named
|
|
209
|
+
else:
|
|
210
|
+
all_best_targets = list()
|
|
211
|
+
all_best_features = [[] for _ in range(len(lower_boundaries))]
|
|
212
|
+
for _ in range(post_hoc_analysis):
|
|
213
|
+
# best_features, best_target = pso(**arguments)
|
|
214
|
+
best_features, best_target, _particle_positions, _target_values_per_position = pso(**arguments)
|
|
215
|
+
|
|
216
|
+
# inverse transformation
|
|
217
|
+
best_features = np.array(best_features).reshape(1, -1)
|
|
218
|
+
best_features_real = objective_function.scaler.inverse_transform(best_features).flatten() # type: ignore
|
|
219
|
+
|
|
220
|
+
for i, best_feature in enumerate(best_features_real):
|
|
221
|
+
all_best_features[i].append(best_feature)
|
|
222
|
+
all_best_targets.append(best_target)
|
|
223
|
+
|
|
224
|
+
# name features
|
|
225
|
+
all_best_features_named = {name: list_values for name, list_values in zip(names, all_best_features)}
|
|
226
|
+
all_best_targets_named = {target_name: all_best_targets}
|
|
227
|
+
|
|
228
|
+
# save results
|
|
229
|
+
_save_results(all_best_features_named, all_best_targets_named, save_dir=save_results_dir, target_name=target_name)
|
|
230
|
+
|
|
231
|
+
return all_best_features_named, all_best_targets_named # type: ignore
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
### SOURCE CODE FOR PSO ###
|
|
237
|
+
def _obj_wrapper(func, args, kwargs, x):
|
|
238
|
+
return func(x, *args, **kwargs)
|
|
239
|
+
|
|
240
|
+
def _is_feasible_wrapper(func, x):
|
|
241
|
+
return np.all(func(x)>=0)
|
|
242
|
+
|
|
243
|
+
def _cons_none_wrapper(x):
|
|
244
|
+
return np.array([0])
|
|
245
|
+
|
|
246
|
+
def _cons_ieqcons_wrapper(ieqcons, args, kwargs, x):
|
|
247
|
+
return np.array([y(x, *args, **kwargs) for y in ieqcons])
|
|
248
|
+
|
|
249
|
+
def _cons_f_ieqcons_wrapper(f_ieqcons, args, kwargs, x):
|
|
250
|
+
return np.array(f_ieqcons(x, *args, **kwargs))
|
|
251
|
+
|
|
252
|
+
def pso(func, lb, ub, ieqcons=[], f_ieqcons=None, args=(), kwargs={},
|
|
253
|
+
swarmsize=100, omega=0.5, phip=0.5, phig=0.5, maxiter=100,
|
|
254
|
+
minstep=1e-8, minfunc=1e-8, debug=False, processes=1,
|
|
255
|
+
particle_output=False):
|
|
256
|
+
"""
|
|
257
|
+
Perform a particle swarm optimization (PSO)
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
==========
|
|
261
|
+
func : function
|
|
262
|
+
The function to be minimized
|
|
263
|
+
lb : array
|
|
264
|
+
The lower bounds of the design variable(s)
|
|
265
|
+
ub : array
|
|
266
|
+
The upper bounds of the design variable(s)
|
|
267
|
+
|
|
268
|
+
Optional
|
|
269
|
+
========
|
|
270
|
+
ieqcons : list
|
|
271
|
+
A list of functions of length n such that ieqcons[j](x,*args) >= 0.0 in
|
|
272
|
+
a successfully optimized problem (Default: [])
|
|
273
|
+
f_ieqcons : function
|
|
274
|
+
Returns a 1-D array in which each element must be greater or equal
|
|
275
|
+
to 0.0 in a successfully optimized problem. If f_ieqcons is specified,
|
|
276
|
+
ieqcons is ignored (Default: None)
|
|
277
|
+
args : tuple
|
|
278
|
+
Additional arguments passed to objective and constraint functions
|
|
279
|
+
(Default: empty tuple)
|
|
280
|
+
kwargs : dict
|
|
281
|
+
Additional keyword arguments passed to objective and constraint
|
|
282
|
+
functions (Default: empty dict)
|
|
283
|
+
swarmsize : int
|
|
284
|
+
The number of particles in the swarm (Default: 100)
|
|
285
|
+
omega : scalar
|
|
286
|
+
Particle velocity scaling factor (Default: 0.5)
|
|
287
|
+
phip : scalar
|
|
288
|
+
Scaling factor to search away from the particle's best known position
|
|
289
|
+
(Default: 0.5)
|
|
290
|
+
phig : scalar
|
|
291
|
+
Scaling factor to search away from the swarm's best known position
|
|
292
|
+
(Default: 0.5)
|
|
293
|
+
maxiter : int
|
|
294
|
+
The maximum number of iterations for the swarm to search (Default: 100)
|
|
295
|
+
minstep : scalar
|
|
296
|
+
The minimum stepsize of swarm's best position before the search
|
|
297
|
+
terminates (Default: 1e-8)
|
|
298
|
+
minfunc : scalar
|
|
299
|
+
The minimum change of swarm's best objective value before the search
|
|
300
|
+
terminates (Default: 1e-8)
|
|
301
|
+
debug : boolean
|
|
302
|
+
If True, progress statements will be displayed every iteration
|
|
303
|
+
(Default: False)
|
|
304
|
+
processes : int
|
|
305
|
+
The number of processes to use to evaluate objective function and
|
|
306
|
+
constraints (default: 1)
|
|
307
|
+
particle_output : boolean
|
|
308
|
+
Whether to include the best per-particle position and the objective
|
|
309
|
+
values at those.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
=======
|
|
313
|
+
g : array
|
|
314
|
+
The swarm's best known position (optimal design)
|
|
315
|
+
f : scalar
|
|
316
|
+
The objective value at ``g``
|
|
317
|
+
p : array
|
|
318
|
+
The best known position per particle
|
|
319
|
+
pf: arrray
|
|
320
|
+
The objective values at each position in p
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
assert len(lb)==len(ub), 'Lower- and upper-bounds must be the same length'
|
|
325
|
+
assert hasattr(func, '__call__'), 'Invalid function handle'
|
|
326
|
+
lb = np.array(lb)
|
|
327
|
+
ub = np.array(ub)
|
|
328
|
+
assert np.all(ub>lb), 'All upper-bound values must be greater than lower-bound values'
|
|
329
|
+
|
|
330
|
+
vhigh = np.abs(ub - lb)
|
|
331
|
+
vlow = -vhigh
|
|
332
|
+
|
|
333
|
+
# Initialize objective function
|
|
334
|
+
obj = partial(_obj_wrapper, func, args, kwargs)
|
|
335
|
+
|
|
336
|
+
# Check for constraint function(s) #########################################
|
|
337
|
+
if f_ieqcons is None:
|
|
338
|
+
if not len(ieqcons):
|
|
339
|
+
if debug:
|
|
340
|
+
print('No constraints given.')
|
|
341
|
+
cons = _cons_none_wrapper
|
|
342
|
+
else:
|
|
343
|
+
if debug:
|
|
344
|
+
print('Converting ieqcons to a single constraint function')
|
|
345
|
+
cons = partial(_cons_ieqcons_wrapper, ieqcons, args, kwargs)
|
|
346
|
+
else:
|
|
347
|
+
if debug:
|
|
348
|
+
print('Single constraint function given in f_ieqcons')
|
|
349
|
+
cons = partial(_cons_f_ieqcons_wrapper, f_ieqcons, args, kwargs)
|
|
350
|
+
is_feasible = partial(_is_feasible_wrapper, cons)
|
|
351
|
+
|
|
352
|
+
# Initialize the multiprocessing module if necessary
|
|
353
|
+
if processes > 1:
|
|
354
|
+
import multiprocessing
|
|
355
|
+
mp_pool = multiprocessing.Pool(processes)
|
|
356
|
+
|
|
357
|
+
# Initialize the particle swarm ############################################
|
|
358
|
+
S = swarmsize
|
|
359
|
+
D = len(lb) # the number of dimensions each particle has
|
|
360
|
+
x = np.random.rand(S, D) # particle positions
|
|
361
|
+
v = np.zeros_like(x) # particle velocities
|
|
362
|
+
p = np.zeros_like(x) # best particle positions
|
|
363
|
+
fx = np.zeros(S) # current particle function values
|
|
364
|
+
fs = np.zeros(S, dtype=bool) # feasibility of each particle
|
|
365
|
+
fp = np.ones(S)*np.inf # best particle function values
|
|
366
|
+
g = [] # best swarm position
|
|
367
|
+
fg = np.inf # best swarm position starting value
|
|
368
|
+
|
|
369
|
+
# Initialize the particle's position
|
|
370
|
+
x = lb + x*(ub - lb)
|
|
371
|
+
|
|
372
|
+
# Calculate objective and constraints for each particle
|
|
373
|
+
if processes > 1:
|
|
374
|
+
fx = np.array(mp_pool.map(obj, x))
|
|
375
|
+
fs = np.array(mp_pool.map(is_feasible, x))
|
|
376
|
+
else:
|
|
377
|
+
for i in range(S):
|
|
378
|
+
fx[i] = obj(x[i, :])
|
|
379
|
+
fs[i] = is_feasible(x[i, :])
|
|
380
|
+
|
|
381
|
+
# Store particle's best position (if constraints are satisfied)
|
|
382
|
+
i_update = np.logical_and((fx < fp), fs)
|
|
383
|
+
p[i_update, :] = x[i_update, :].copy()
|
|
384
|
+
fp[i_update] = fx[i_update]
|
|
385
|
+
|
|
386
|
+
# Update swarm's best position
|
|
387
|
+
i_min = np.argmin(fp)
|
|
388
|
+
if fp[i_min] < fg:
|
|
389
|
+
fg = fp[i_min]
|
|
390
|
+
g = p[i_min, :].copy()
|
|
391
|
+
else:
|
|
392
|
+
# At the start, there may not be any feasible starting point, so just
|
|
393
|
+
# give it a temporary "best" point since it's likely to change
|
|
394
|
+
g = x[0, :].copy()
|
|
395
|
+
|
|
396
|
+
# Initialize the particle's velocity
|
|
397
|
+
v = vlow + np.random.rand(S, D)*(vhigh - vlow)
|
|
398
|
+
|
|
399
|
+
# Iterate until termination criterion met ##################################
|
|
400
|
+
it = 1
|
|
401
|
+
while it <= maxiter:
|
|
402
|
+
rp = np.random.uniform(size=(S, D))
|
|
403
|
+
rg = np.random.uniform(size=(S, D))
|
|
404
|
+
|
|
405
|
+
# Update the particles velocities
|
|
406
|
+
v = omega*v + phip*rp*(p - x) + phig*rg*(g - x)
|
|
407
|
+
# Update the particles' positions
|
|
408
|
+
x = x + v
|
|
409
|
+
# Correct for bound violations
|
|
410
|
+
maskl = x < lb
|
|
411
|
+
masku = x > ub
|
|
412
|
+
x = x*(~np.logical_or(maskl, masku)) + lb*maskl + ub*masku
|
|
413
|
+
|
|
414
|
+
# Update objectives and constraints
|
|
415
|
+
if processes > 1:
|
|
416
|
+
fx = np.array(mp_pool.map(obj, x))
|
|
417
|
+
fs = np.array(mp_pool.map(is_feasible, x))
|
|
418
|
+
else:
|
|
419
|
+
for i in range(S):
|
|
420
|
+
fx[i] = obj(x[i, :])
|
|
421
|
+
fs[i] = is_feasible(x[i, :])
|
|
422
|
+
|
|
423
|
+
# Store particle's best position (if constraints are satisfied)
|
|
424
|
+
i_update = np.logical_and((fx < fp), fs)
|
|
425
|
+
p[i_update, :] = x[i_update, :].copy()
|
|
426
|
+
fp[i_update] = fx[i_update]
|
|
427
|
+
|
|
428
|
+
# Compare swarm's best position with global best position
|
|
429
|
+
i_min = np.argmin(fp)
|
|
430
|
+
if fp[i_min] < fg:
|
|
431
|
+
if debug:
|
|
432
|
+
print('New best for swarm at iteration {:}: {:} {:}'\
|
|
433
|
+
.format(it, p[i_min, :], fp[i_min]))
|
|
434
|
+
|
|
435
|
+
p_min = p[i_min, :].copy()
|
|
436
|
+
stepsize = np.sqrt(np.sum((g - p_min)**2))
|
|
437
|
+
|
|
438
|
+
if np.abs(fg - fp[i_min]) <= minfunc:
|
|
439
|
+
print('Stopping search: Swarm best objective change less than {:}'\
|
|
440
|
+
.format(minfunc))
|
|
441
|
+
if particle_output:
|
|
442
|
+
return p_min, fp[i_min], p, fp
|
|
443
|
+
else:
|
|
444
|
+
return p_min, fp[i_min]
|
|
445
|
+
elif stepsize <= minstep:
|
|
446
|
+
print('Stopping search: Swarm best position change less than {:}'\
|
|
447
|
+
.format(minstep))
|
|
448
|
+
if particle_output:
|
|
449
|
+
return p_min, fp[i_min], p, fp
|
|
450
|
+
else:
|
|
451
|
+
return p_min, fp[i_min]
|
|
452
|
+
else:
|
|
453
|
+
g = p_min.copy()
|
|
454
|
+
fg = fp[i_min]
|
|
455
|
+
|
|
456
|
+
if debug:
|
|
457
|
+
print('Best after iteration {:}: {:} {:}'.format(it, g, fg))
|
|
458
|
+
it += 1
|
|
459
|
+
|
|
460
|
+
print('Stopping search: maximum iterations reached --> {:}'.format(maxiter))
|
|
461
|
+
|
|
462
|
+
if not is_feasible(g):
|
|
463
|
+
print("However, the optimization couldn't find a feasible design. Sorry")
|
|
464
|
+
if particle_output:
|
|
465
|
+
return g, fg, p, fp
|
|
466
|
+
else:
|
|
467
|
+
return g, fg
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MyNeuralNetwork(nn.Module):
|
|
6
|
+
def __init__(self, in_features: int, out_targets: int, hidden_layers: list[int]=[40,80,40], drop_out: float=0.2) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Creates a basic Neural Network.
|
|
9
|
+
|
|
10
|
+
* For Regression the last layer is Linear.
|
|
11
|
+
* For Classification the last layer is Logarithmic Softmax.
|
|
12
|
+
|
|
13
|
+
`out_targets` Is the number of expected output classes for classification; or `1` for regression.
|
|
14
|
+
|
|
15
|
+
`hidden_layers` takes a list of integers. Each position represents a hidden layer and its number of neurons.
|
|
16
|
+
|
|
17
|
+
* One rule of thumb is to choose a number of hidden neurons between the size of the input layer and the size of the output layer.
|
|
18
|
+
* Another rule suggests that the number of hidden neurons should be 2/3 the size of the input layer, plus the size of the output layer.
|
|
19
|
+
* Another rule suggests that the number of hidden neurons should be less than twice the size of the input layer.
|
|
20
|
+
|
|
21
|
+
`drop_out` represents the probability of neurons to be set to '0' during the training process of each layer. Range [0.0, 1.0).
|
|
22
|
+
"""
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
# Validate inputs and outputs
|
|
26
|
+
if isinstance(in_features, int) and isinstance(out_targets, int):
|
|
27
|
+
if in_features < 1 or out_targets < 1:
|
|
28
|
+
raise ValueError("Inputs or Outputs must be an integer value.")
|
|
29
|
+
else:
|
|
30
|
+
raise TypeError("Inputs or Outputs must be an integer value.")
|
|
31
|
+
|
|
32
|
+
# Validate layers
|
|
33
|
+
if isinstance(hidden_layers, list):
|
|
34
|
+
for number in hidden_layers:
|
|
35
|
+
if not isinstance(number, int):
|
|
36
|
+
raise TypeError("Number of neurons per hidden layer must be an integer value.")
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError("hidden_layers must be a list of integer values.")
|
|
39
|
+
|
|
40
|
+
# Validate dropout
|
|
41
|
+
if isinstance(drop_out, float):
|
|
42
|
+
if 1.0 > drop_out >= 0.0:
|
|
43
|
+
pass
|
|
44
|
+
else:
|
|
45
|
+
raise TypeError("drop_out must be a float value greater than or equal to 0 and less than 1.")
|
|
46
|
+
elif drop_out == 0:
|
|
47
|
+
pass
|
|
48
|
+
else:
|
|
49
|
+
raise TypeError("drop_out must be a float value greater than or equal to 0 and less than 1.")
|
|
50
|
+
|
|
51
|
+
# Create layers
|
|
52
|
+
layers = list()
|
|
53
|
+
for neurons in hidden_layers:
|
|
54
|
+
layers.append(nn.Linear(in_features=in_features, out_features=neurons))
|
|
55
|
+
layers.append(nn.BatchNorm1d(num_features=neurons))
|
|
56
|
+
layers.append(nn.ReLU())
|
|
57
|
+
layers.append(nn.Dropout(p=drop_out))
|
|
58
|
+
in_features = neurons
|
|
59
|
+
# Append output layer
|
|
60
|
+
layers.append(nn.Linear(in_features=in_features, out_features=out_targets))
|
|
61
|
+
|
|
62
|
+
# Check for classification or regression output
|
|
63
|
+
if out_targets > 1:
|
|
64
|
+
# layers.append(nn.Sigmoid())
|
|
65
|
+
layers.append(nn.LogSoftmax(dim=1))
|
|
66
|
+
|
|
67
|
+
# Create a container for layers
|
|
68
|
+
self._layers = nn.Sequential(*layers)
|
|
69
|
+
|
|
70
|
+
# Override forward()
|
|
71
|
+
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
|
72
|
+
X = self._layers(X)
|
|
73
|
+
return X
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MyConvolutionalNetwork(nn.Module):
|
|
77
|
+
def __init__(self, outputs: int, color_channels: int=3, img_size: int=256, drop_out: float=0.2):
|
|
78
|
+
"""
|
|
79
|
+
Create a basic Convolutional Neural Network with two convolution layers with a pooling layer after each convolution.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
`outputs`: Number of output classes (1 for regression).
|
|
83
|
+
|
|
84
|
+
`color_channels`: Color channels. Default is 3 (RGB).
|
|
85
|
+
|
|
86
|
+
`img_size`: Width and Height of image samples, must be square images. Default is 200.
|
|
87
|
+
|
|
88
|
+
`drop_out`: Neuron drop out probability. Default is 20%.
|
|
89
|
+
"""
|
|
90
|
+
super().__init__()
|
|
91
|
+
|
|
92
|
+
# Validate outputs number
|
|
93
|
+
integer_error = " must be an integer greater than 0."
|
|
94
|
+
if isinstance(outputs, int):
|
|
95
|
+
if outputs < 1:
|
|
96
|
+
raise ValueError("Outputs" + integer_error)
|
|
97
|
+
else:
|
|
98
|
+
raise TypeError("Outputs" + integer_error)
|
|
99
|
+
# Validate color channels
|
|
100
|
+
if isinstance(color_channels, int):
|
|
101
|
+
if color_channels < 1:
|
|
102
|
+
raise ValueError("Color Channels" + integer_error)
|
|
103
|
+
else:
|
|
104
|
+
raise TypeError("Color Channels" + integer_error)
|
|
105
|
+
# Validate image size
|
|
106
|
+
if isinstance(img_size, int):
|
|
107
|
+
if img_size < 1:
|
|
108
|
+
raise ValueError("Image size" + integer_error)
|
|
109
|
+
else:
|
|
110
|
+
raise TypeError("Image size" + integer_error)
|
|
111
|
+
# Validate drop out
|
|
112
|
+
if isinstance(drop_out, float):
|
|
113
|
+
if 1.0 > drop_out >= 0.0:
|
|
114
|
+
pass
|
|
115
|
+
else:
|
|
116
|
+
raise TypeError("Drop out must be a float value greater than or equal to 0 and less than 1.")
|
|
117
|
+
elif drop_out == 0:
|
|
118
|
+
pass
|
|
119
|
+
else:
|
|
120
|
+
raise TypeError("Drop out must be a float value greater than or equal to 0 and less than 1.")
|
|
121
|
+
|
|
122
|
+
# 2 convolutions, 2 pooling layers
|
|
123
|
+
self._cnn_layers = nn.Sequential(
|
|
124
|
+
nn.Conv2d(in_channels=color_channels, out_channels=(color_channels * 2), kernel_size=5, stride=1, padding=1),
|
|
125
|
+
nn.MaxPool2d(kernel_size=4, stride=(4,4)),
|
|
126
|
+
nn.Conv2d(in_channels=(color_channels * 2), out_channels=(color_channels * 3), kernel_size=3, stride=1, padding=0),
|
|
127
|
+
nn.AvgPool2d(kernel_size=2, stride=(2,2))
|
|
128
|
+
)
|
|
129
|
+
# Calculate output features
|
|
130
|
+
flat_features = int(int((int((img_size + 2 - (5-1))//4) - (3-1))//2)**2) * (color_channels * 3)
|
|
131
|
+
|
|
132
|
+
# Make a standard ANN
|
|
133
|
+
ann = MyNeuralNetwork(in_features=flat_features, hidden_layers=[int(flat_features*0.5), int(flat_features*0.2), int(flat_features*0.005)],
|
|
134
|
+
out_targets=outputs, drop_out=drop_out)
|
|
135
|
+
self._ann_layers = ann._layers
|
|
136
|
+
|
|
137
|
+
# Join CNN and ANN
|
|
138
|
+
self._structure = nn.Sequential(self._cnn_layers, nn.Flatten(), self._ann_layers)
|
|
139
|
+
|
|
140
|
+
# Send to CUDA if available
|
|
141
|
+
# if torch.cuda.is_available():
|
|
142
|
+
# self.to('cuda')
|
|
143
|
+
|
|
144
|
+
# Override forward()
|
|
145
|
+
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
|
146
|
+
X = self._structure(X)
|
|
147
|
+
return X
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MyLSTMNetwork(nn.Module):
|
|
151
|
+
def __init__(self, features: int=1, hidden_size: int=100, recurrent_layers: int=1, dropout: float=0, reset_memory: bool=False, **kwargs):
|
|
152
|
+
"""
|
|
153
|
+
Create a simple Recurrent Neural Network to predict 1 time step into the future of sequential data.
|
|
154
|
+
|
|
155
|
+
The sequence should be a 2D tensor with shape (sequence_length, number_of_features).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
* `features`: Number of features representing the sequence. Defaults to 1.
|
|
159
|
+
* `hidden_size`: Hidden size of the LSTM model. Defaults to 100.
|
|
160
|
+
* `recurrent_layers`: Number of recurrent layers to use. Defaults to 1.
|
|
161
|
+
* `dropout`: Probability of dropping out neurons in each recurrent layer, except the last layer. Defaults to 0.
|
|
162
|
+
* `reset_memory`: Reset the initial hidden state and cell state for the recurrent layers at every epoch. Defaults to False.
|
|
163
|
+
* `kwargs`: Create custom attributes for the model.
|
|
164
|
+
|
|
165
|
+
Custom forward() parameters:
|
|
166
|
+
* `batch_size=1` (int): batch size for the LSTM net.
|
|
167
|
+
* `return_last_timestamp=False` (bool): Return only the value at `output[-1]`
|
|
168
|
+
"""
|
|
169
|
+
# validate input size
|
|
170
|
+
if not isinstance(features, int):
|
|
171
|
+
raise TypeError("Input size must be an integer value.")
|
|
172
|
+
# validate hidden size
|
|
173
|
+
if not isinstance(hidden_size, int):
|
|
174
|
+
raise TypeError("Hidden size must be an integer value.")
|
|
175
|
+
# validate layers
|
|
176
|
+
if not isinstance(recurrent_layers, int):
|
|
177
|
+
raise TypeError("Number of recurrent layers must be an integer value.")
|
|
178
|
+
# validate dropout
|
|
179
|
+
if isinstance(dropout, (float, int)):
|
|
180
|
+
if 0 <= dropout < 1:
|
|
181
|
+
pass
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError("Dropout must be a float in range [0.0, 1.0)")
|
|
184
|
+
else:
|
|
185
|
+
raise TypeError("Dropout must be a float in range [0.0, 1.0)")
|
|
186
|
+
|
|
187
|
+
super().__init__()
|
|
188
|
+
|
|
189
|
+
# Initialize memory
|
|
190
|
+
self._reset = reset_memory
|
|
191
|
+
self._memory = None
|
|
192
|
+
|
|
193
|
+
# hidden size and features shape
|
|
194
|
+
self._hidden = hidden_size
|
|
195
|
+
self._features = features
|
|
196
|
+
|
|
197
|
+
# RNN
|
|
198
|
+
self._lstm = nn.LSTM(input_size=features, hidden_size=self._hidden, num_layers=recurrent_layers, dropout=dropout)
|
|
199
|
+
|
|
200
|
+
# Fully connected layer
|
|
201
|
+
self._ann = nn.Linear(in_features=self._hidden, out_features=features)
|
|
202
|
+
|
|
203
|
+
# Parse extra parameters
|
|
204
|
+
for key, value in kwargs.items():
|
|
205
|
+
setattr(self, key, value)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def forward(self, seq: torch.Tensor, batch_size: int=1, return_last_timestamp: bool=False) -> torch.Tensor:
|
|
209
|
+
# reset memory
|
|
210
|
+
if self._reset:
|
|
211
|
+
self._memory = None
|
|
212
|
+
# reshape sequence to feed RNN
|
|
213
|
+
seq = seq.view(-1, batch_size, self._features)
|
|
214
|
+
# Pass sequence through RNN
|
|
215
|
+
seq, self._memory = self._lstm(seq, self._memory)
|
|
216
|
+
# Detach hidden state and cell state to prevent backpropagation error
|
|
217
|
+
self._memory = tuple(m.detach() for m in self._memory)
|
|
218
|
+
# Reshape outputs
|
|
219
|
+
seq = seq.view(-1, self._hidden)
|
|
220
|
+
# Pass sequence through fully connected layer
|
|
221
|
+
output = self._ann(seq)
|
|
222
|
+
# Return prediction of 1 time step in the future
|
|
223
|
+
if return_last_timestamp:
|
|
224
|
+
return output[-1].view(1,-1) #last item as a tensor.
|
|
225
|
+
else:
|
|
226
|
+
return output
|
|
227
|
+
|