aisp 0.3.2__py3-none-any.whl → 0.4.0__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.
aisp/csa/_clonalg.py ADDED
@@ -0,0 +1,369 @@
1
+ """Clonal Selection Algorithm (CLONALG)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from typing import Optional, Callable, Dict, Literal
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ from ..utils.display import ProgressTable
12
+ from ..base import BaseOptimizer, set_seed_numba
13
+ from ..base.mutation import clone_and_mutate_binary, clone_and_mutate_ranged, \
14
+ clone_and_mutate_continuous, clone_and_mutate_permutation
15
+ from ..base.populations import generate_random_antibodies
16
+ from ..utils.sanitizers import sanitize_seed, sanitize_param, sanitize_bounds
17
+ from ..utils.types import FeatureTypeAll
18
+
19
+
20
+ class Clonalg(BaseOptimizer):
21
+ """Clonal Selection Algorithm (CLONALG).
22
+
23
+ The Clonal Selection Algorithm (CSA) is an optimization algorithm inspired by the biological
24
+ process of clonal selection and expansion of antibodies in the immune system [1]_. This
25
+ implementation of CLONALG has been adapted for the minimization or maximization of cost
26
+ functions in binary, continuous, ranged-value, and permutation problems.
27
+
28
+
29
+ Parameters
30
+ ----------
31
+ problem_size : int
32
+ Dimension of the problem to be minimized.
33
+ N : int, default=50
34
+ Number of memory cells (antibodies) in the population.
35
+ rate_clonal : float, default=10
36
+ Maximum number of possible clones of a cell. This value is multiplied by
37
+ cell_affinity to determine the number of clones.
38
+ rate_hypermutation : float, default=0.75
39
+ Rate of mutated clones, used as a scalar factor.
40
+ n_diversity_injection : int, default=5
41
+ Number of new random memory cells injected to maintain diversity.
42
+ selection_size : int, default=5
43
+ Number of the best antibodies selected for cloning.
44
+ affinity_function : Optional[Callable[..., npt.NDArray]], default=None
45
+ Objective function to evaluate candidate solutions in minimizing the problem.
46
+ feature_type : FeatureTypeAll, default='ranged-features'
47
+ Type of problem samples: binary, continuous, or based on value ranges.
48
+ Specifies the type of features: "continuous-features", "binary-features",
49
+ "ranged-features", or "permutation-features".
50
+ bounds : Optional[Dict], default=None
51
+ Definition of search limits when ``feature_type='ranged-features'``.
52
+ Can be provided in two ways:
53
+
54
+ * Fixed values: ``{'low': float, 'high': float}``
55
+ Values are replicated across all dimensions, generating equal limits for each
56
+ dimension.
57
+ * Arrays: ``{'low': list, 'high': list}``
58
+ Each dimension has specific limits. Both arrays must be
59
+ ``problem_size``.
60
+
61
+ mode : Literal["min", "max"], default="min"
62
+ Defines whether the algorithm minimizes or maximizes the cost function.
63
+ seed : Optional[int], default=None
64
+ Seed for random generation of detector values. If None, the value is random.
65
+
66
+ Notes
67
+ -----
68
+ This CLONALG implementation contains some changes based on the AISP context, for general
69
+ application to various problems, which may produce results different from the standard or
70
+ specific implementation. This adaptation aims to generalize CLONALG to minimization and
71
+ maximization tasks, in addition to supporting continuous, discrete, and permutation problems.
72
+
73
+ References
74
+ ----------
75
+ .. [1] BROWNLEE, Jason. Clonal Selection Algorithm. Clever Algorithms: Nature-inspired
76
+ Programming Recipes., 2011. Available at:
77
+ https://cleveralgorithms.com/nature-inspired/immune/clonal_selection_algorithm.html
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ problem_size: int,
83
+ N: int = 50,
84
+ rate_clonal: int = 10,
85
+ rate_hypermutation: float = 0.75,
86
+ n_diversity_injection: int = 5,
87
+ selection_size: int = 5,
88
+ affinity_function: Optional[Callable[..., npt.NDArray]] = None,
89
+ feature_type: FeatureTypeAll = 'ranged-features',
90
+ bounds: Optional[Dict] = None,
91
+ mode: Literal["min", "max"] = "min",
92
+ seed: Optional[int] = None
93
+ ):
94
+ super().__init__()
95
+ self.problem_size = sanitize_param(problem_size, 1, lambda x: x > 0)
96
+ self.N: int = sanitize_param(N, 50, lambda x: x > 0)
97
+ self.rate_clonal: int = sanitize_param(rate_clonal, 10, lambda x: x > 0)
98
+ self.rate_hypermutation: np.float64 = np.float64(
99
+ sanitize_param(
100
+ rate_hypermutation, 0.75, lambda x: x > 0
101
+ )
102
+ )
103
+ self.n_diversity_injection: int = sanitize_param(
104
+ n_diversity_injection, 5, lambda x: x > 0
105
+ )
106
+ self.selection_size: int = sanitize_param(
107
+ selection_size, 5, lambda x: x > 0
108
+ )
109
+ self._affinity_function = affinity_function
110
+ self.feature_type: FeatureTypeAll = feature_type
111
+
112
+ self._bounds = None
113
+ self._bounds_extend_cache = None
114
+ self.bounds = bounds
115
+
116
+ self.mode: Literal["min", "max"] = sanitize_param(
117
+ mode,
118
+ "min",
119
+ lambda x: x == "max"
120
+ )
121
+
122
+ self.seed: Optional[int] = sanitize_seed(seed)
123
+ if self.seed is not None:
124
+ np.random.seed(self.seed)
125
+ set_seed_numba(self.seed)
126
+
127
+ self.population = None
128
+
129
+ @property
130
+ def bounds(self) -> Optional[Dict]:
131
+ """Getter for the bounds attribute."""
132
+ return self._bounds
133
+
134
+ @bounds.setter
135
+ def bounds(self, value: Optional[Dict]):
136
+ """Setter for the bounds attribute."""
137
+ if self.feature_type == 'ranged-features':
138
+ self._bounds = sanitize_bounds(value, self.problem_size)
139
+ low_bounds = np.array(self._bounds['low'])
140
+ high_bounds = np.array(self._bounds['high'])
141
+ self._bounds_extend_cache = np.array([low_bounds, high_bounds])
142
+ else:
143
+ self._bounds = None
144
+ self._bounds_extend_cache = None
145
+
146
+ def optimize(
147
+ self,
148
+ max_iters: int = 50,
149
+ n_iter_no_change=10,
150
+ verbose: bool = True
151
+ ) -> npt.NDArray:
152
+ """Execute the optimization process and return the population.
153
+
154
+ Parameters
155
+ ----------
156
+ max_iters : int, default=50
157
+ Maximum number of interactions when searching for the best solution using clonalg.
158
+ n_iter_no_change: int, default=10
159
+ the maximum number of iterations without updating the best cell
160
+ verbose : bool, default=True
161
+ Feedback on interactions, indicating the best antibody.
162
+
163
+ Returns
164
+ -------
165
+ population : npt.NDArray
166
+ Antibody population after clonal expansion.
167
+ """
168
+ self.reset()
169
+ self.population = self._init_population_antibodies()
170
+
171
+ t = 1
172
+ antibodies = [(antibody, self.affinity_function(antibody)) for antibody in self.population]
173
+ best_cost = None
174
+ stop = 0
175
+ progress = ProgressTable(
176
+ {
177
+ "Iteration": 11,
178
+ f"Best Affinity ({self.mode})": 25,
179
+ "Worse Affinity": 20,
180
+ "Stagnation": 17},
181
+ verbose
182
+ )
183
+
184
+ while t <= max_iters:
185
+ p_select = self._select_top_antibodies(self.selection_size, antibodies)
186
+ self._record_best(p_select[0][1], p_select[0][0])
187
+
188
+ clones = self._clone_and_hypermutation(p_select)
189
+
190
+ p_rand = [
191
+ (antibody, self.affinity_function(antibody))
192
+ for antibody in self._diversity_introduction()
193
+ ]
194
+ antibodies = p_select
195
+ antibodies.extend(clones)
196
+ antibodies = self._select_top_antibodies(
197
+ self.N - self.n_diversity_injection, antibodies
198
+ )
199
+ antibodies.extend(p_rand)
200
+ if len(antibodies) > self.N:
201
+ antibodies = self._select_top_antibodies(self.N, antibodies)
202
+ if best_cost == self.best_cost:
203
+ stop += 1
204
+ else:
205
+ stop = 0
206
+ best_cost = self.best_cost
207
+ progress.update(
208
+ {
209
+ "Iteration": t,
210
+ f"Best Affinity ({self.mode})": f"{self.best_cost:>25.6f}",
211
+ "Worse Affinity": f"{antibodies[-1][1]:>20.6f}",
212
+ "Stagnation": stop
213
+ }
214
+ )
215
+ if stop == n_iter_no_change:
216
+ break
217
+
218
+ t += 1
219
+ progress.finish()
220
+ self.population = np.array([antibody for antibody, _ in antibodies]).astype(dtype=float)
221
+ return self.population
222
+
223
+ def _select_top_antibodies(self, n: int, antibodies: list[tuple]) -> list[tuple]:
224
+ """Select the antibodies with the highest or lowest values, depending on the mode.
225
+
226
+ Parameters
227
+ ----------
228
+ n : int
229
+ Number of antibodies to select.
230
+ antibodies : list[tuple]
231
+ Representing the antibodies and their associated score.
232
+
233
+ Returns
234
+ -------
235
+ List containing the `n` antibodies selected according to the defined min or max
236
+ criterion.
237
+ """
238
+ if self.mode == "max":
239
+ return heapq.nlargest(n, antibodies, key=lambda x: x[1])
240
+
241
+ return heapq.nsmallest(n, antibodies, key=lambda x: x[1])
242
+
243
+ def affinity_function(self, solution: npt.NDArray) -> np.float64:
244
+ """
245
+ Evaluate the affinity of a candidate cell.
246
+
247
+ Parameters
248
+ ----------
249
+ solution : npt.NDArray
250
+ Candidate solution to evaluate.
251
+
252
+ Returns
253
+ -------
254
+ affinity : float
255
+ Affinity value associated with the given cell.
256
+
257
+ Raises
258
+ ------
259
+ NotImplementedError
260
+ If no affinity function has been provided.
261
+ """
262
+ if not callable(self._affinity_function):
263
+ raise NotImplementedError(
264
+ "No affinity function to evaluate the candidate cell was provided."
265
+ )
266
+ return np.float64(self._affinity_function(solution))
267
+
268
+ def _init_population_antibodies(self) -> npt.NDArray:
269
+ """Initialize the antibody set of the population randomly.
270
+
271
+ Returns
272
+ -------
273
+ npt.NDArray
274
+ List of initialized antibodies.
275
+ """
276
+ return generate_random_antibodies(
277
+ self.N,
278
+ self.problem_size,
279
+ self.feature_type,
280
+ self._bounds_extend_cache
281
+ )
282
+
283
+ def _diversity_introduction(self):
284
+ """Introduce diversity into the antibody population.
285
+
286
+ Returns
287
+ -------
288
+ npt.NDArray
289
+ Array of new random antibodies for diversity introduction.
290
+ """
291
+ return generate_random_antibodies(
292
+ self.n_diversity_injection,
293
+ self.problem_size,
294
+ self.feature_type,
295
+ self._bounds_extend_cache
296
+ )
297
+
298
+ def _clone_and_mutate(
299
+ self,
300
+ antibody: npt.NDArray,
301
+ n_clone: int,
302
+ rate_hypermutation: float
303
+ ) -> npt.NDArray:
304
+ """
305
+ Generate mutated clones from an antibody, based on the feature type.
306
+
307
+ Parameters
308
+ ----------
309
+ antibody : npt.NDArray
310
+ Original antibody vector to be cloned and mutated.
311
+ n_clone : int
312
+ Number of clones to generate.
313
+
314
+ Returns
315
+ -------
316
+ npt.NDArray
317
+ Array of shape (n_clone, len(antibody)) containing mutated clones
318
+ """
319
+ if self.feature_type == "binary-features":
320
+ return clone_and_mutate_binary(antibody, n_clone)
321
+ if self.feature_type == "ranged-features" and self._bounds_extend_cache is not None:
322
+ return clone_and_mutate_ranged(
323
+ antibody, n_clone, self._bounds_extend_cache, rate_hypermutation
324
+ )
325
+ if self.feature_type == "permutation-features":
326
+ return clone_and_mutate_permutation(antibody, n_clone, rate_hypermutation)
327
+ return clone_and_mutate_continuous(antibody, n_clone, rate_hypermutation)
328
+
329
+ def _clone_and_hypermutation(
330
+ self,
331
+ population: list[tuple]
332
+ ) -> list:
333
+ """Clone and hypermutate the population's antibodies.
334
+
335
+ The clone list is returned with the clones and their affinities with respect to the cost
336
+ function.
337
+
338
+ Parameters
339
+ ----------
340
+ population: list
341
+ The list of antibodies (solutions) to be evaluated and cloned.
342
+
343
+ Returns
344
+ -------
345
+ list[npt.NDArray]
346
+ List of mutated clones.
347
+ """
348
+ clonal_m = []
349
+ min_affinity = min(item[1] for item in population)
350
+ max_affinity = max(item[1] for item in population)
351
+ affinity_range = max_affinity - min_affinity
352
+
353
+ for antibody, affinity in population:
354
+ if affinity_range == 0:
355
+ normalized_affinity = 1
356
+ else:
357
+ normalized_affinity = (affinity - min_affinity) / affinity_range
358
+ if self.mode == "min":
359
+ normalized_affinity = max(0.0, 1.0 - normalized_affinity)
360
+
361
+ num_clones = max(0, int(self.rate_clonal * normalized_affinity))
362
+ clones = self._clone_and_mutate(
363
+ antibody,
364
+ num_clones,
365
+ 1 - np.exp(-self.rate_hypermutation * normalized_affinity)
366
+ )
367
+ clonal_m.extend(clones)
368
+
369
+ return [(clone, self.affinity_function(clone)) for clone in clonal_m]
aisp/ina/__init__.py CHANGED
@@ -4,8 +4,8 @@ This module implements algorithms based on Network Theory Algorithms proposed by
4
4
 
5
5
  Classes
6
6
  -------
7
- AiNet
8
- Artificial Immune Network implementation for clustering.
7
+ AiNet : Artificial Immune Network.
8
+ An unsupervised learning algorithm for clustering, based on the theory of immune networks.
9
9
  """
10
10
 
11
11
  from ._ai_network import AiNet
aisp/ina/_ai_network.py CHANGED
@@ -16,6 +16,7 @@ from ._base import BaseAiNet
16
16
  from ..base import set_seed_numba
17
17
  from ..base.mutation import clone_and_mutate_binary, clone_and_mutate_continuous, \
18
18
  clone_and_mutate_ranged
19
+ from ..base.populations import generate_random_antibodies
19
20
  from ..utils.distance import hamming, compute_metric_distance, get_metric_code
20
21
  from ..utils.sanitizers import sanitize_choice, sanitize_param, sanitize_seed
21
22
  from ..utils.types import FeatureType, MetricType
@@ -29,7 +30,7 @@ class AiNet(BaseAiNet):
29
30
  clustering and data compression tasks. The aiNet algorithm uses principles from immune
30
31
  network theory, clonal selection, and affinity maturation to compress high-dimensional
31
32
  datasets. [1]_
32
- For clustering, the class uses SciPys implementation of the **Minimum Spanning Tree**
33
+ For clustering, the class uses SciPy's implementation of the **Minimum Spanning Tree**
33
34
  (MST) to remove the most distant nodes and separate the groups. [2]_
34
35
 
35
36
  Parameters
@@ -58,13 +59,13 @@ class AiNet(BaseAiNet):
58
59
  Way to calculate the distance between the detector and the sample:
59
60
 
60
61
  * ``'Euclidean'`` ➜ The calculation of the distance is given by the expression:
61
- √( (x₁ x₂)² + (y₁ y₂)² + ... + (yn yn)²).
62
+ √( (x₁ - x₂)² + (y₁ - y₂)² + ... + (yn - yn)²).
62
63
 
63
64
  * ``'minkowski'`` ➜ The calculation of the distance is given by the expression:
64
- ( |X₁ Y₁|p + |X₂ Y₂|p + ... + |Xn Yn|p) ¹/ₚ.
65
+ ( |X₁ - Y₁|p + |X₂ - Y₂|p + ... + |Xn - Yn|p) ¹/ₚ.
65
66
 
66
67
  * ``'manhattan'`` ➜ The calculation of the distance is given by the expression:
67
- ( |x₁ x₂| + |y₁ y₂| + ... + |yn yn|).
68
+ ( |x₁ - x₂| + |y₁ - y₂| + ... + |yn - yn|).
68
69
 
69
70
  seed : Optional[int]
70
71
  Seed for the random generation of detector values. Defaults to None.
@@ -293,7 +294,7 @@ class AiNet(BaseAiNet):
293
294
  npt.NDArray
294
295
  List of initialized memories.
295
296
  """
296
- return self._generate_random_antibodies(
297
+ return generate_random_antibodies(
297
298
  self.N,
298
299
  self._n_features,
299
300
  self._feature_type,
@@ -402,7 +403,7 @@ class AiNet(BaseAiNet):
402
403
  npt.NDArray
403
404
  Array of new random antibodies for diversity introduction.
404
405
  """
405
- return self._generate_random_antibodies(
406
+ return generate_random_antibodies(
406
407
  self.n_diversity_injection,
407
408
  self._n_features,
408
409
  self._feature_type,
@@ -455,7 +456,7 @@ class AiNet(BaseAiNet):
455
456
  """
456
457
  u = np.reshape(u, (1, -1))
457
458
  v = np.atleast_2d(v)
458
- distances = cdist(u, v, metric=self.metric, **self._metric_params)[0]
459
+ distances = cdist(u, v, metric=self.metric, **self._metric_params)[0] # type: ignore
459
460
 
460
461
  return 1 - (distances / (1 + distances))
461
462
 
@@ -478,8 +479,8 @@ class AiNet(BaseAiNet):
478
479
  if self._feature_type == "binary-features":
479
480
  return clone_and_mutate_binary(antibody, n_clone)
480
481
  if self._feature_type == "ranged-features" and self._bounds is not None:
481
- return clone_and_mutate_ranged(antibody, n_clone, self._bounds)
482
- return clone_and_mutate_continuous(antibody, n_clone)
482
+ return clone_and_mutate_ranged(antibody, n_clone, self._bounds, np.float64(1.0))
483
+ return clone_and_mutate_continuous(antibody, n_clone, np.float64(1.0))
483
484
 
484
485
  def _build_mst(self):
485
486
  """Construct the Minimum Spanning Tree (MST) for the antibody population.
@@ -523,6 +524,8 @@ class AiNet(BaseAiNet):
523
524
  ------
524
525
  ValueError
525
526
  If the Minimum Spanning Tree (MST) has not yet been created
527
+ If Population of antibodies is empty
528
+ If MST statistics (mean or std) are not available.
526
529
 
527
530
  Updates
528
531
  -------
@@ -534,6 +537,12 @@ class AiNet(BaseAiNet):
534
537
  if self._mst_structure is None:
535
538
  raise ValueError("The Minimum Spanning Tree (MST) has not yet been created.")
536
539
 
540
+ if self._population_antibodies is None or len(self._population_antibodies) == 0:
541
+ raise ValueError("Population of antibodies is empty")
542
+
543
+ if self._mst_mean_distance is None or self._mst_std_distance is None:
544
+ raise ValueError("MST statistics (mean or std) are not available.")
545
+
537
546
  if mst_inconsistency_factor is not None:
538
547
  self.mst_inconsistency_factor = mst_inconsistency_factor
539
548
 
aisp/ina/_base.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """Base Class for Network Theory Algorithms."""
2
2
 
3
3
  from abc import ABC
4
- from typing import Optional
5
4
 
6
5
  import numpy as np
7
6
  from numpy import typing as npt
@@ -82,42 +81,3 @@ class BaseAiNet(BaseClusterer, ABC):
82
81
  raise ValueError(
83
82
  "The array X contains values that are not composed only of 0 and 1."
84
83
  )
85
-
86
- @staticmethod
87
- def _generate_random_antibodies(
88
- n_samples: int,
89
- n_features: int,
90
- feature_type: FeatureType = "continuous-features",
91
- bounds: Optional[npt.NDArray[np.float64]] = None
92
- ) -> npt.NDArray:
93
- """
94
- Generate a random antibody population.
95
-
96
- Parameters
97
- ----------
98
- n_samples : int
99
- Number of antibodies (samples) to generate.
100
- n_features : int
101
- Number of features (dimensions) for each antibody.
102
- feature_type : FeatureType, default="continuous-features"
103
- Specifies the type of features: "continuous-features", "binary-features",
104
- or "ranged-features".
105
- bounds : np.ndarray
106
- Array (n_features, 2) with min and max per dimension.
107
-
108
- Returns
109
- -------
110
- npt.NDArray
111
- Array of shape (n_samples, n_features) containing the generated antibodies.
112
- Data type depends on the feature_type type (float for continuous/ranged, bool for
113
- binary).
114
- """
115
- if n_features <= 0:
116
- raise ValueError("Number of features must be greater than zero.")
117
-
118
- if feature_type == "binary-features":
119
- return np.random.randint(0, 2, size=(n_samples, n_features)).astype(np.bool_)
120
- if feature_type == "ranged-features" and bounds is not None:
121
- return np.random.uniform(low=bounds[0], high=bounds[1], size=(n_samples, n_features))
122
-
123
- return np.random.random_sample(size=(n_samples, n_features))
aisp/nsa/__init__.py CHANGED
@@ -3,6 +3,13 @@
3
3
  NSAs simulate the maturation process of T-cells in the immune system, where these cells learn to
4
4
  distinguish between self and non-self. Only T-cells capable of recognizing non-self elements are
5
5
  preserved.
6
+
7
+ Classes
8
+ -------
9
+ RNSA : Real-valued Negative Selection Algorithm.
10
+ A supervised learning algorithm for classification that uses real-valued detectors.
11
+ BNSA : Binary Negative Selection Algorithm.
12
+ A supervised learning algorithm for classification that uses binary detectors.
6
13
  """
7
14
 
8
15
  from ._binary_negative_selection import BNSA
@@ -237,4 +237,4 @@ class BNSA(BaseNSA):
237
237
  else:
238
238
  class_differences[_class_] = distances.sum() / self.N
239
239
 
240
- c.append(max(class_differences, key=class_differences.get))
240
+ c.append(max(class_differences, key=class_differences.get)) # type: ignore
@@ -40,11 +40,11 @@ class RNSA(BaseNSA):
40
40
  Way to calculate the distance between the detector and the sample:
41
41
 
42
42
  + ``'Euclidean'`` ➜ The calculation of the distance is given by the expression:
43
- √( (x₁ x₂)² + (y₁ y₂)² + ... + (yn yn)²).
43
+ √( (x₁ - x₂)² + (y₁ - y₂)² + ... + (yn - yn)²).
44
44
  + ``'minkowski'`` ➜ The calculation of the distance is given by the expression:
45
- ( |X₁ Y₁|p + |X₂ Y₂|p + ... + |Xn Yn|p) ¹/ₚ.
45
+ ( |X₁ - Y₁|p + |X₂ - Y₂|p + ... + |Xn - Yn|p) ¹/ₚ.
46
46
  + ``'manhattan'`` ➜ The calculation of the distance is given by the expression:
47
- ( |x₁ x₂| + |y₁ y₂| + ... + |yn yn|) .
47
+ ( |x₁ - x₂| + |y₁ - y₂| + ... + |yn - yn|) .
48
48
  max_discards : int, default=1000
49
49
  This parameter indicates the maximum number of consecutive detector discards, aimed at
50
50
  preventing a possible infinite loop in case a radius is defined that cannot generate
@@ -260,7 +260,7 @@ class RNSA(BaseNSA):
260
260
  average_distance[_class_] = np.average(
261
261
  [self.__distance(detector, line) for detector in detectores]
262
262
  )
263
- c.append(max(average_distance, key=average_distance.get))
263
+ c.append(max(average_distance, key=average_distance.get)) # type: ignore
264
264
  return np.array(c)
265
265
 
266
266
  def __checks_valid_detector(