Skip to content

SHADE: Success-History Based Adaptive Differential Evolution

SHADE es una variante auto-adaptativa del algoritmo de evolución diferencial (Differential Evolution, DE), diseñada para resolver problemas de optimización continua con alta eficiencia y robustez. En el contexto de clustering con restricciones, SHADE ha demostrado ser eficaz para encontrar particiones de calidad optimizando una función objetivo que combina compacidad y violación de restricciones.

¿Qué es la Evolución Diferencial (DE)?

DE es un algoritmo evolutivo basado en poblaciones que optimiza una función objetivo real mediante la combinación lineal de soluciones. Sus operadores clave son:

  • Mutación: combina soluciones para crear una nueva (vector mutante).
  • Cruce (crossover): mezcla el vector mutante con uno original (vector trial).
  • Selección: elige entre el vector original y el trial según el fitness.

Motivación de SHADE

Aunque DE es potente, su rendimiento depende fuertemente de los parámetros de control:

  • Factor de escala \( F \)
  • Tasa de cruce \( CR \)

SHADE resuelve este problema mediante una adaptación basada en la historia de éxito, es decir, aprende qué combinaciones de parámetros han funcionado bien en el pasado y los reutiliza con probabilidad creciente.

El algoritmo presenta los siguientes fases:

  1. Inicializar una población de soluciones con vectores reales.
  2. Para cada generación:
  3. Elegir un conjunto de padres.
  4. Generar un vector mutante usando una estrategia como current-to-pbest/1.
  5. Cruzar el mutante con el padre para obtener el trial.
  6. Evaluar la calidad del trial.
  7. Reemplazar si mejora la solución.
  8. Registrar los valores de \( F \) y \( CR \) que generaron soluciones exitosas.
  9. Actualizar una memoria histórica con los \( F \) y \( CR \) exitosos.
  10. Repetir hasta convergencia.

Success-History Adaptation

SHADE mantiene dos vectores de memoria:

  • \( M_F \): historial de factores de escala exitosos.
  • \( M_{CR} \): historial de tasas de cruce exitosas.

Para generar los parámetros de un nuevo individuo, SHADE:

  • Selecciona aleatoriamente un índice \( k \) del historial.
  • Usa una distribución Cauchy centrada en \( M_F[k] \) para \( F \).
  • Usa una distribución Normal centrada en \( M_{CR}[k] \) para \( CR \).
  • Los nuevos valores se ajustan si salen de sus rangos válidos.

Este mecanismo favorece gradualmente los valores que han tenido éxito, manteniendo diversidad.

Aplicación a Clustering con Restricciones

En clustering con restricciones, cada individuo representa una partición del conjunto de datos:

  • Puede codificarse como un vector real que luego se decodifica a una asignación discreta.
  • La función de fitness incluye:
  • Variación intra-cluster (e.g., suma de distancias).
  • Penalización por violación de must-link y cannot-link.
  • La estrategia current-to-pbest/1 favorece la convergencia sin perder diversidad.
  • SHADE puede integrar búsquedas locales para acelerar la explotación.

SHADE en Clustering con Restricciones

SHADE ha sido adaptado con éxito para abordar el problema de clustering con restricciones instancia-instancia, donde el objetivo es encontrar una partición de los datos que:

  1. Minimice la varianza intra-cluster (cohesión).
  2. Respete un conjunto de restricciones suaves tipo:
  3. Must-Link (ML): dos instancias deben ir al mismo cluster.
  4. Cannot-Link (CL): dos instancias no deben ir al mismo cluster.

Dado que estas restricciones pueden ser contradictorias o ruidosas, SHADE incorpora una función objetivo penalizada que permite tolerar violaciones a cambio de obtener una mejor calidad general de partición.

Representación de la Solución

En este contexto, cada individuo en la población de SHADE representa una partición de los datos. Hay varias formas de codificar esto:

  • Una codificación entera directa (no diferenciable) donde cada posición representa la asignación de un punto a un cluster.
  • Una codificación con claves aleatorias (random keys) que se traduce a clusters mediante un decodificador determinista.
  • Una codificación real continua donde se utiliza un proceso de agrupamiento posterior (como agrupamiento por proximidad en un espacio latente).

Función Objetivo

La función de evaluación para cada individuo incluye:

\[ \text{Fitness}(\mathbf{l}) = \underbrace{\sum_{i=1}^K \sum_{x_j \in C_i} \|x_j - \mu_i\|^2}_{\text{Varianza intra-cluster}} + \lambda \cdot \text{Infeasibility}(\mathbf{l}) \]

Donde:

  • \( \mu_i \) es el centroide del cluster \( C_i \).
  • La función Infeasibility cuenta las restricciones ML y CL violadas.
  • \( \lambda \) es un parámetro de penalización ajustable.

Este enfoque permite tratar restricciones como soft-constraints y controlar el grado de compromiso entre calidad de agrupamiento y cumplimiento estructural.

Referencia

Tanabe, R., & Fukunaga, A. (2013). Success-History Based Parameter Adaptation for Differential Evolution. IEEE Congress on Evolutionary Computation (CEC), pp. 71–78.

González-Almagro et al. (2020): SHADE se utilizó como núcleo en un algoritmo de clustering con restricciones basado en DE.

API

Bases: GeneticClustering

SHADE Clustering with Constraints (ShadeCC).

Adaptive genetic algorithm based on SHADE to solve clustering problems with constraints. It uses success history to dynamically adjust the parameters of differential evolution.

Attributes:

Name Type Description
population_size int

Number of solutions in the genetic population.

n_clusters int

Target number of clusters.

init str

Method for initializing centroids.

max_iter int

Maximum number of generations.

tol float

Convergence tolerance.

constraints ConstraintMatrix

Must-link and cannot-link constraints.

solution_archive ndarray

External solution archive to maintain diversity.

Source code in clustlib/gac/shade.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
class ShadeCC(GeneticClustering):
    """SHADE Clustering with Constraints (ShadeCC).

    Adaptive genetic algorithm based on SHADE to solve clustering problems
    with constraints. It uses success history to dynamically adjust the
    parameters of differential evolution.

    Attributes:
        population_size (int): Number of solutions in the genetic population.
        n_clusters (int): Target number of clusters.
        init (str): Method for initializing centroids.
        max_iter (int): Maximum number of generations.
        tol (float): Convergence tolerance.
        constraints (ConstraintMatrix): Must-link and cannot-link constraints.
        solution_archive (np.ndarray): External solution archive to maintain
            diversity.

    """

    solution_archive: np.ndarray
    percentage_best: float = 0.2

    def __init__(
        self,
        n_clusters=8,
        init="random",
        max_iter=300,
        tol=1e-4,
        custom_initial_centroids=None,
        constraints: Sequence[Sequence] | None = None,
        population_size=20,
        memory_size=20,
    ):
        """Initialize the SHADE algorithm for clustering with constraints.

        Args:
            n_clusters (int): Number of clusters to generate.
            init (str): Method for initializing centroids.
            max_iter (int): Maximum number of iterations.
            tol (float): Tolerance for convergence.
            custom_initial_centroids (Optional[np.ndarray]): User-defined centroids.
            constraints (Sequence[Sequence]): List of ML and CL constraints.
            population_size (int): Size of the genetic population.
            memory_size (int): Size of the memory to store successful solutions.

        """
        self._delta_centroid = None
        self.n_clusters = n_clusters
        self._centroids_distance = np.zeros((self.n_clusters, self.n_clusters))
        self.init = init
        self.max_iter = max_iter
        self.tol = tol
        self.custom_initial_centroids = custom_initial_centroids
        self.centroids = None
        self.constraints = constraints
        self._population_size = population_size
        self.memory_size = memory_size
        self._num_elite = np.ceil(self._population_size * self.percentage_best).astype(
            int
        )
        self.solution_archive = None
        self._dim = self.constraints.shape[0]

    @staticmethod
    def randint(low, high=None, size=None, exclude=None):
        """Generate a random integer, excluding certain values.

        Args:
            low (int): Lower bound of the range.
            high (int, optional): Upper bound of the range.
            size (int, optional): Number of values to generate.
            exclude (Sequence[int], optional): Values to exclude from the range.

        Returns:
            np.ndarray: Array of random integers excluding the specified values.

        """
        if exclude is None:
            return np.random.randint(low, high, size)
        else:
            return np.random.choice(
                [i for i in range(low, high) if i not in exclude], size=size
            )

    def _convergence(self):
        if self._delta is None:
            logger.debug("Delta is None, convergence cannot be checked.")
            return False

        return np.abs(np.linalg.norm(self._delta)) < self.tol

    def _fit(self):
        self.create_population()
        self.create_archive()

        iteration = 0
        while not self.stop_criteria(iteration):
            self.update()
            iteration += 1

        return self

    def create_population(self):
        """Create the initial population of solutions."""
        super().create_population()

        self._next_population = np.zeros(self.population.shape)
        self._next_population_fitness = np.zeros(self._population_size)

    def create_archive(self):
        """Initialize the external solution archive."""
        if self.solution_archive is None:
            self.solution_archive = np.zeros((0, self._dim))
        else:
            self.solution_archive = np.vstack(
                (self.solution_archive, np.zeros((0, self._dim)))
            )

        self._memory_CR = np.full(self.memory_size, 0.5)
        self._memory_F = np.full(self.memory_size, 0.5)

    def get_instances(self, parents_idx):
        """Retrieve instances from the population and the external archive.

        Args:
            parents_idx (np.ndarray): Indices of the selected parents.

        Returns:
            Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: Instances from
                the population and the external archive.

        """
        (idx, best, r1, r2) = parents_idx

        return (
            self.population[idx],
            self.population[best],
            self.population[r1],
            (
                self.population[r2]
                if r2 < self._population_size
                else self.solution_archive[r2 - self._population_size]
            ),
        )

    def select_parents(self):
        """Select parents from the population and the external archive.

        Returns:
            np.ndarray: Indices of the selected parents.

        """
        idx = self.randint(0, self._population_size)
        idx_best = self.randint(0, self._num_elite, exclude=[idx])
        idx_r1 = self.randint(0, self._population_size, exclude=[idx_best, idx])

        archive_size = self.solution_archive.shape[0]
        idx_r2 = self.randint(
            0, self._population_size + archive_size, exclude=[idx_best, idx_r1, idx]
        )

        return np.array([idx, idx_best, idx_r1, idx_r2])

    def crossover(self, parents, f_i, cr_i):
        """Perform differential crossover among multiple parents.

        Args:
            parents (np.ndarray): Parent solutions [element, best, r1, r2].
            f_i (float): Scaling factor.
            cr_i (float): Crossover rate.

        Returns:
            np.ndarray: Generated mutant chromosome.

        """
        element, best, r1, r2 = self.get_instances(parents)
        mutant = element + f_i * (best - element) + f_i * (r1 - r2)
        mutant = np.clip(mutant, 0, 1)

        cross_points_1 = np.random.rand(self._dim) <= cr_i
        cross_points_2 = np.random.rand(self._dim) < (1 / self._dim)
        cross_points = np.logical_or(cross_points_1, cross_points_2)

        return np.where(cross_points, mutant, element)

    def save_adaptive(self, delta_fitness, cr_i, f_i):
        """Save successful parameters for future adaptation.

        Args:
            delta_fitness (float): Improvement obtained in fitness.
            cr_i (float): Crossover rate used.
            f_i (float): Scaling factor used.

        """
        self._sf = np.append(self._sf, f_i)
        self._s_cr = np.append(self._s_cr, cr_i)
        self.fitness_delta = np.append(self.fitness_delta, delta_fitness)

    def update_adaptive(self):
        """Update the H_CR and H_F histories with weighted averages."""
        k = np.random.randint(0, self.memory_size)
        mean_wa = np.average(self.fitness_delta, weights=self._s_cr)
        self._memory_CR[k] = np.clip(mean_wa, 0.0, 1.0)

        w_k = self.fitness_delta / np.sum(self.fitness_delta)
        mean_wl = (w_k * (self._sf**2)).sum() / (w_k * self._sf).sum()
        self._memory_F[k] = np.clip(mean_wl, a_min=0.0, a_max=1.0)

    def create_adaptive_parameter(self):
        """Generate new adaptive parameters CR and F.

        Returns:
            Tuple[float, float]: Pair of adapted (CR, F).

        """
        r_i = np.random.randint(0, self.memory_size)
        while (
            f_i := scipy.stats.cauchy.rvs(loc=self._memory_F[r_i], scale=0.1)
        ) <= 0 and f_i > 1.0:
            continue

        while (cr_i := np.random.normal(self._memory_CR[r_i], 0.1)) <= 0 and cr_i > 1.0:
            continue

        return cr_i, f_i

    def mutation(self):
        """Generate mutation on the current individual.

        Returns:
            Tuple[np.ndarray, float, float]: Generated mutant, cr_i, and f_i.

        """
        parents = self.select_parents()
        cr_i, f_i = self.create_adaptive_parameter()
        mutant = self.crossover(parents, f_i, cr_i)
        return mutant, cr_i, f_i

    def _update(self):
        """Trains the SHADE algorithm on the data.

        Args:
            X (np.ndarray): Input data matrix.
            y (np.ndarray, optional): True labels if available.
            logger (Any, optional): Object for logging the training process.

        Returns:
            ShadeCC: Trained instance of the model.

        """
        self._s_cr = np.zeros((0, 0))
        self._sf = np.zeros((0, 0))
        self.fitness_delta = np.zeros((0, 0))

        for current_element in range(self._population_size):
            mutant, cr_i, f_i = self.mutation()
            mutant_fitness = self.fitness(mutant)
            current_fitness = self._population_fitness[current_element]

            if mutant_fitness < current_fitness:
                self._next_population[current_element] = mutant
                self._next_population_fitness[current_element] = mutant_fitness
                self.save_adaptive(current_fitness - mutant_fitness, cr_i, f_i)
            else:
                self._next_population[current_element] = self.population[
                    current_element
                ]
                self._next_population_fitness[current_element] = current_fitness

        self.population = self._next_population
        order = np.argsort(self._next_population_fitness)

        self._population_fitness = self._next_population_fitness[order]
        self.population = self.population[order]

        if len(self._s_cr) > 0 and len(self._sf) > 0:
            self.update_adaptive()

        self._labels = self.get_labels()
        self.get_centroids(self._labels)

__init__(n_clusters=8, init='random', max_iter=300, tol=0.0001, custom_initial_centroids=None, constraints=None, population_size=20, memory_size=20)

Initialize the SHADE algorithm for clustering with constraints.

Parameters:

Name Type Description Default
n_clusters int

Number of clusters to generate.

8
init str

Method for initializing centroids.

'random'
max_iter int

Maximum number of iterations.

300
tol float

Tolerance for convergence.

0.0001
custom_initial_centroids Optional[ndarray]

User-defined centroids.

None
constraints Sequence[Sequence]

List of ML and CL constraints.

None
population_size int

Size of the genetic population.

20
memory_size int

Size of the memory to store successful solutions.

20
Source code in clustlib/gac/shade.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    n_clusters=8,
    init="random",
    max_iter=300,
    tol=1e-4,
    custom_initial_centroids=None,
    constraints: Sequence[Sequence] | None = None,
    population_size=20,
    memory_size=20,
):
    """Initialize the SHADE algorithm for clustering with constraints.

    Args:
        n_clusters (int): Number of clusters to generate.
        init (str): Method for initializing centroids.
        max_iter (int): Maximum number of iterations.
        tol (float): Tolerance for convergence.
        custom_initial_centroids (Optional[np.ndarray]): User-defined centroids.
        constraints (Sequence[Sequence]): List of ML and CL constraints.
        population_size (int): Size of the genetic population.
        memory_size (int): Size of the memory to store successful solutions.

    """
    self._delta_centroid = None
    self.n_clusters = n_clusters
    self._centroids_distance = np.zeros((self.n_clusters, self.n_clusters))
    self.init = init
    self.max_iter = max_iter
    self.tol = tol
    self.custom_initial_centroids = custom_initial_centroids
    self.centroids = None
    self.constraints = constraints
    self._population_size = population_size
    self.memory_size = memory_size
    self._num_elite = np.ceil(self._population_size * self.percentage_best).astype(
        int
    )
    self.solution_archive = None
    self._dim = self.constraints.shape[0]

create_adaptive_parameter()

Generate new adaptive parameters CR and F.

Returns:

Type Description

Tuple[float, float]: Pair of adapted (CR, F).

Source code in clustlib/gac/shade.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def create_adaptive_parameter(self):
    """Generate new adaptive parameters CR and F.

    Returns:
        Tuple[float, float]: Pair of adapted (CR, F).

    """
    r_i = np.random.randint(0, self.memory_size)
    while (
        f_i := scipy.stats.cauchy.rvs(loc=self._memory_F[r_i], scale=0.1)
    ) <= 0 and f_i > 1.0:
        continue

    while (cr_i := np.random.normal(self._memory_CR[r_i], 0.1)) <= 0 and cr_i > 1.0:
        continue

    return cr_i, f_i

create_archive()

Initialize the external solution archive.

Source code in clustlib/gac/shade.py
121
122
123
124
125
126
127
128
129
130
131
def create_archive(self):
    """Initialize the external solution archive."""
    if self.solution_archive is None:
        self.solution_archive = np.zeros((0, self._dim))
    else:
        self.solution_archive = np.vstack(
            (self.solution_archive, np.zeros((0, self._dim)))
        )

    self._memory_CR = np.full(self.memory_size, 0.5)
    self._memory_F = np.full(self.memory_size, 0.5)

create_population()

Create the initial population of solutions.

Source code in clustlib/gac/shade.py
114
115
116
117
118
119
def create_population(self):
    """Create the initial population of solutions."""
    super().create_population()

    self._next_population = np.zeros(self.population.shape)
    self._next_population_fitness = np.zeros(self._population_size)

crossover(parents, f_i, cr_i)

Perform differential crossover among multiple parents.

Parameters:

Name Type Description Default
parents ndarray

Parent solutions [element, best, r1, r2].

required
f_i float

Scaling factor.

required
cr_i float

Crossover rate.

required

Returns:

Type Description

np.ndarray: Generated mutant chromosome.

Source code in clustlib/gac/shade.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def crossover(self, parents, f_i, cr_i):
    """Perform differential crossover among multiple parents.

    Args:
        parents (np.ndarray): Parent solutions [element, best, r1, r2].
        f_i (float): Scaling factor.
        cr_i (float): Crossover rate.

    Returns:
        np.ndarray: Generated mutant chromosome.

    """
    element, best, r1, r2 = self.get_instances(parents)
    mutant = element + f_i * (best - element) + f_i * (r1 - r2)
    mutant = np.clip(mutant, 0, 1)

    cross_points_1 = np.random.rand(self._dim) <= cr_i
    cross_points_2 = np.random.rand(self._dim) < (1 / self._dim)
    cross_points = np.logical_or(cross_points_1, cross_points_2)

    return np.where(cross_points, mutant, element)

get_instances(parents_idx)

Retrieve instances from the population and the external archive.

Parameters:

Name Type Description Default
parents_idx ndarray

Indices of the selected parents.

required

Returns:

Type Description

Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: Instances from the population and the external archive.

Source code in clustlib/gac/shade.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def get_instances(self, parents_idx):
    """Retrieve instances from the population and the external archive.

    Args:
        parents_idx (np.ndarray): Indices of the selected parents.

    Returns:
        Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: Instances from
            the population and the external archive.

    """
    (idx, best, r1, r2) = parents_idx

    return (
        self.population[idx],
        self.population[best],
        self.population[r1],
        (
            self.population[r2]
            if r2 < self._population_size
            else self.solution_archive[r2 - self._population_size]
        ),
    )

mutation()

Generate mutation on the current individual.

Returns:

Type Description

Tuple[np.ndarray, float, float]: Generated mutant, cr_i, and f_i.

Source code in clustlib/gac/shade.py
238
239
240
241
242
243
244
245
246
247
248
def mutation(self):
    """Generate mutation on the current individual.

    Returns:
        Tuple[np.ndarray, float, float]: Generated mutant, cr_i, and f_i.

    """
    parents = self.select_parents()
    cr_i, f_i = self.create_adaptive_parameter()
    mutant = self.crossover(parents, f_i, cr_i)
    return mutant, cr_i, f_i

randint(low, high=None, size=None, exclude=None) staticmethod

Generate a random integer, excluding certain values.

Parameters:

Name Type Description Default
low int

Lower bound of the range.

required
high int

Upper bound of the range.

None
size int

Number of values to generate.

None
exclude Sequence[int]

Values to exclude from the range.

None

Returns:

Type Description

np.ndarray: Array of random integers excluding the specified values.

Source code in clustlib/gac/shade.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@staticmethod
def randint(low, high=None, size=None, exclude=None):
    """Generate a random integer, excluding certain values.

    Args:
        low (int): Lower bound of the range.
        high (int, optional): Upper bound of the range.
        size (int, optional): Number of values to generate.
        exclude (Sequence[int], optional): Values to exclude from the range.

    Returns:
        np.ndarray: Array of random integers excluding the specified values.

    """
    if exclude is None:
        return np.random.randint(low, high, size)
    else:
        return np.random.choice(
            [i for i in range(low, high) if i not in exclude], size=size
        )

save_adaptive(delta_fitness, cr_i, f_i)

Save successful parameters for future adaptation.

Parameters:

Name Type Description Default
delta_fitness float

Improvement obtained in fitness.

required
cr_i float

Crossover rate used.

required
f_i float

Scaling factor used.

required
Source code in clustlib/gac/shade.py
197
198
199
200
201
202
203
204
205
206
207
208
def save_adaptive(self, delta_fitness, cr_i, f_i):
    """Save successful parameters for future adaptation.

    Args:
        delta_fitness (float): Improvement obtained in fitness.
        cr_i (float): Crossover rate used.
        f_i (float): Scaling factor used.

    """
    self._sf = np.append(self._sf, f_i)
    self._s_cr = np.append(self._s_cr, cr_i)
    self.fitness_delta = np.append(self.fitness_delta, delta_fitness)

select_parents()

Select parents from the population and the external archive.

Returns:

Type Description

np.ndarray: Indices of the selected parents.

Source code in clustlib/gac/shade.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def select_parents(self):
    """Select parents from the population and the external archive.

    Returns:
        np.ndarray: Indices of the selected parents.

    """
    idx = self.randint(0, self._population_size)
    idx_best = self.randint(0, self._num_elite, exclude=[idx])
    idx_r1 = self.randint(0, self._population_size, exclude=[idx_best, idx])

    archive_size = self.solution_archive.shape[0]
    idx_r2 = self.randint(
        0, self._population_size + archive_size, exclude=[idx_best, idx_r1, idx]
    )

    return np.array([idx, idx_best, idx_r1, idx_r2])

update_adaptive()

Update the H_CR and H_F histories with weighted averages.

Source code in clustlib/gac/shade.py
210
211
212
213
214
215
216
217
218
def update_adaptive(self):
    """Update the H_CR and H_F histories with weighted averages."""
    k = np.random.randint(0, self.memory_size)
    mean_wa = np.average(self.fitness_delta, weights=self._s_cr)
    self._memory_CR[k] = np.clip(mean_wa, 0.0, 1.0)

    w_k = self.fitness_delta / np.sum(self.fitness_delta)
    mean_wl = (w_k * (self._sf**2)).sum() / (w_k * self._sf).sum()
    self._memory_F[k] = np.clip(mean_wl, a_min=0.0, a_max=1.0)