Artículos / Machine Learning
Mutación y cruzamiento adaptativos con sklearn-genetic-opt
Programa mutación y cruzamiento con los adaptadores de sklearn-genetic-opt, comprueba sus curvas y compara la búsqueda con una configuración fija.

Los algoritmos genéticos usan dos operadores principales para explorar el espacio de hiperparámetros: mutación (cambios aleatorios en los individuos) y cruzamiento (combinación de dos soluciones para generar descendientes). Ambos operadores tienen probabilidades asociadas que controlan con qué frecuencia se aplican durante la evolución.
Puedes mantener estas probabilidades fijas o programar cómo cambian. Este artículo explica los cuatro adaptadores, comprueba sus curvas en una ejecución finita y muestra cómo compararlas con una configuración fija.
Los ejemplos usan sklearn-genetic-opt 0.14.0, que requiere Python 3.12 o posterior. Instala el extra de gráficos para reproducir la figura:
python -m pip install "sklearn-genetic-opt[plot]==0.14.0"
Parámetros fijos vs. adaptativos
Con un adaptador fijo, la probabilidad de mutación se mantiene entre generaciones. Una programación permite distribuir la exploración de otra forma:
- Al inicio: una probabilidad de mutación mayor puede explorar más configuraciones nuevas.
- Al final: una probabilidad menor puede conservar más combinaciones producidas por selección y cruzamiento.
Son decisiones que hay que evaluar, sin garantía de convergencia. Una probabilidad fija puede ser una buena referencia; la mejor política depende del espacio de búsqueda, el ruido de la evaluación y el presupuesto disponible.
El dilema exploración-explotación
Este trade-off es fundamental en todos los métodos de búsqueda heurística. En el contexto de búsqueda de hiperparámetros con algoritmos genéticos:
- Exploración: probar configuraciones diversas, arriesgarse a individuos menos prometedores para descubrir regiones inesperadamente buenas.
- Explotación: concentrar recursos computacionales en refinar las mejores configuraciones encontradas hasta el momento.
Los adaptadores expresan una programación de probabilidades. La selección, las distribuciones del espacio de búsqueda y el algoritmo genético elegido siguen determinando qué configuraciones se evalúan. Inspecciona la curva antes de entrenar y mide si ayuda en tu problema.
Adaptadores disponibles en sklearn-genetic-opt
La librería sklearn-genetic-opt ofrece cuatro tipos de adaptadores para las probabilidades de mutación y cruzamiento:
ConstantAdapter
El valor permanece constante. Pasar una probabilidad como mutation_probability=0.2 también crea un adaptador constante internamente. Para construirlo explícitamente, proporciona sus tres argumentos requeridos:
from sklearn_genetic import ConstantAdapter
# siempre retorna 0.2, sin importar la generación
adapter = ConstantAdapter(initial_value=0.2, end_value=0.2, adaptive_rate=0)
ExponentialAdapter
Se aproxima exponencialmente a end_value. Este ejemplo reduce la mutación desde una probabilidad inicial mayor; invertir los extremos produce una curva creciente.
from sklearn_genetic import ExponentialAdapter
adapter = ExponentialAdapter(initial_value=0.4, end_value=0.05, adaptive_rate=0.1)
InverseAdapter
Se aproxima a end_value con una curva inversa: cambia más rápido al inicio y luego se ralentiza. Este ejemplo aumenta el cruzamiento; esa dirección es una política elegida, sin ventaja intrínseca del adaptador.
from sklearn_genetic import InverseAdapter
adapter = InverseAdapter(initial_value=0.6, end_value=0.9, adaptive_rate=0.1)
PotentialAdapter
Usa la expresión geométrica end_value + (initial_value - end_value) * (1 - adaptive_rate)**t. Para 0 < adaptive_rate < 1, los cambios se reducen con el tiempo. Con los extremos del ejemplo crece hacia el objetivo, con los cambios más grandes al inicio.
from sklearn_genetic import PotentialAdapter
adapter = PotentialAdapter(initial_value=0.6, end_value=0.9, adaptive_rate=0.1)
Matemática detrás de ExponentialAdapter
La fórmula de decaimiento exponencial es:
value(t) = end_value + (initial_value - end_value) * exp(-adaptive_rate * t)
Aquí t es el paso del adaptador contado desde cero: la primera llamada a .step() usa t=0. La implementación de 0.14.0 define estas curvas. Para entender el efecto de adaptive_rate:
- Con
adaptive_rate=0.05: el decaimiento es lento, la transición ocurre gradualmente durante muchas generaciones. - Con
adaptive_rate=0.2: el decaimiento es rápido, la mayor parte de la transición ocurre en las primeras generaciones.
import numpy as np
import matplotlib.pyplot as plt
def exponential_adapter(t, initial=0.4, end=0.05, rate=0.1):
return end + (initial - end) * np.exp(-rate * t)
def inverse_adapter(t, initial=0.6, end=0.9, rate=0.1):
return end - (end - initial) / (1 + rate * t)
generations = np.arange(0, 25)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(generations, [exponential_adapter(t) for t in generations])
plt.title("ExponentialAdapter (mutación)")
plt.xlabel("Paso del adaptador"); plt.ylabel("Probabilidad")
plt.subplot(1, 2, 2)
plt.plot(generations, [inverse_adapter(t) for t in generations])
plt.title("InverseAdapter (cruzamiento)")
plt.xlabel("Paso del adaptador"); plt.ylabel("Probabilidad")
plt.tight_layout()
plt.show()
En los 25 pasos graficados, de t=0 a t=24, la mutación decrece de 0.4 a aproximadamente 0.0818 y el cruzamiento aumenta de 0.6 a aproximadamente 0.8118. Son valores a un número finito de pasos; end_value es el objetivo al que la curva se aproxima con el tiempo.
Ejemplo completo: GASearchCV con adaptadores
Veamos cómo integrar los adaptadores en una búsqueda real de hiperparámetros con un clasificador de scikit-learn:
from sklearn_genetic import (
GASearchCV, ExponentialAdapter, InverseAdapter, EvolutionConfig, OptimizationConfig
)
from sklearn_genetic.space import Integer, Continuous, Categorical
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, StratifiedKFold
# Datos de ejemplo
X, y = make_classification(
n_samples=1000, n_features=20, n_informative=10, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Espacio de búsqueda de hiperparámetros
param_grid = {
"n_estimators": Integer(50, 300),
"max_depth": Integer(3, 15),
"min_samples_split": Integer(2, 20),
"max_features": Continuous(0.1, 1.0),
"criterion": Categorical(["gini", "entropy"])
}
clf = RandomForestClassifier(random_state=42)
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
# GASearchCV con parámetros adaptativos
search = GASearchCV(
estimator=clf,
param_grid=param_grid,
scoring="accuracy",
cv=cv,
evolution_config=EvolutionConfig(
population_size=30,
generations=25,
mutation_probability=ExponentialAdapter(
initial_value=0.4,
end_value=0.05,
adaptive_rate=0.1
),
crossover_probability=InverseAdapter(
initial_value=0.6,
end_value=0.9,
adaptive_rate=0.1
)
),
optimization_config=OptimizationConfig(diversity_control=False),
random_state=42,
verbose=True,
n_jobs=1
)
search.fit(X_train, y_train)
print(f"\nMejores hiperparámetros: {search.best_params_}")
print(f"Mejor score CV: {search.best_score_:.4f}")
print(f"Score en test: {search.score(X_test, y_test):.4f}")
Pasa las probabilidades dentro de EvolutionConfig: sus campos prevalecen sobre los argumentos externos. El ejemplo desactiva el controlador adicional de diversidad para que no aumente la mutación sobre la curva programada. Usa un solo job para limitar el trabajo paralelo. La población, las generaciones, los folds de validación y el costo del estimador afectan el tiempo; los scores impresos salen de tu ejecución, sin constituir un benchmark prometido.
Usando solo ExponentialAdapter para mutación
Si prefieres un enfoque más conservador y solo adaptar la mutación:
search_mutation_only = GASearchCV(
estimator=RandomForestClassifier(random_state=42),
param_grid=param_grid,
scoring="accuracy",
cv=cv,
evolution_config=EvolutionConfig(
population_size=30,
generations=25,
mutation_probability=ExponentialAdapter(
initial_value=0.4,
end_value=0.05,
adaptive_rate=0.1
),
crossover_probability=0.6, # fijo
),
optimization_config=OptimizationConfig(diversity_control=False),
random_state=42,
n_jobs=1
)
search_mutation_only.fit(X_train, y_train)
Esto aísla la programación de mutación y mantiene fijo el cruzamiento. El algoritmo predeterminado eaMuPlusLambda requiere que el presupuesto combinado de cruzamiento y mutación no supere uno: fijar el cruzamiento en 0.6 deja espacio para la mutación inicial de 0.4. Un cruzamiento mayor puede recortar la probabilidad efectiva de mutación. Compáralo con una referencia que use el mismo cruzamiento fijo antes de decidir si programas ambos operadores.
Comparando fijo vs. adaptativo
Para inspeccionar dos ejecuciones con el mismo espacio, folds, población y límite de generaciones, compara sus historias de fitness:
# Búsqueda con parámetros fijos
search_fixed = GASearchCV(
estimator=RandomForestClassifier(random_state=42),
param_grid=param_grid,
scoring="accuracy",
cv=cv,
evolution_config=EvolutionConfig(
population_size=30,
generations=25,
mutation_probability=0.1, # fijo
crossover_probability=0.8, # fijo
),
optimization_config=OptimizationConfig(diversity_control=False),
random_state=42,
n_jobs=1
)
search_fixed.fit(X_train, y_train)
# Comparar historia de fitness
import pandas as pd
history_adaptive = pd.DataFrame(search.history)
history_fixed = pd.DataFrame(search_fixed.history)
print("Convergencia: score máximo por generación:")
print(f"{'Gen':>4} | {'Adaptativo':>12} | {'Fijo':>12}")
print("-" * 35)
for gen in [0, 5, 10, 15, 20, 24]:
a = history_adaptive.loc[gen, "fitness_max"]
f = history_fixed.loc[gen, "fitness_max"]
print(f"{gen:>4} | {a:>12.4f} | {f:>12.4f}")
Esta tabla describe una ejecución por política. Repite con varias semillas de búsqueda y registra las evaluaciones reales de candidatos y el tiempo: el mismo límite de generaciones puede tener costos distintos cuando la caché reutiliza evaluaciones. Elige la programación con datos de entrenamiento/validación y reserva test para la evaluación final. Un máximo de CV mayor en una historia no demuestra más velocidad, robustez o mejor generalización.
Cuándo usar cada adaptador
| Adaptador | Cuándo usar | Ejemplo |
|---|---|---|
ExponentialAdapter | Aproximación exponencial al objetivo | Fijar un punto de media transición para mutación |
InverseAdapter | Cambio inicial más rápido, seguido de una cola larga | Aproximarse a una probabilidad de cruzamiento elegida |
PotentialAdapter | Cambio geométrico con incrementos decrecientes | Probar otra velocidad de transición |
ConstantAdapter | Probabilidad fija | Establecer una referencia para ambos operadores |
Elige la curva cuya transición encaje con la ejecución que quieres probar y compárala con probabilidades fijas bajo un presupuesto de evaluación documentado.
Cómo elegir adaptive_rate
Para ExponentialAdapter, la mitad de la transición entre initial_value y end_value ocurre en t = ln(2) / adaptive_rate. Con adaptive_rate=0.1, la probabilidad está a mitad de camino cerca del paso 7; esto describe la curva, sin medir cuánta exploración útil hizo el optimizador.
Si quieres la media transición en el paso h, usa adaptive_rate = ln(2) / h. Comprueba la curva en tu presupuesto real de pasos antes de entrenar.
Conclusión
Un adaptador hace explícita la política de probabilidades. Empieza con valores fijos, inspecciona la programación en los pasos disponibles y compara ejecuciones reproducibles antes de incorporarla al flujo de entrenamiento.
ExponentialAdapter, InverseAdapter y PotentialAdapter son parte de sklearn-genetic-opt. Si no has probado la búsqueda evolutiva de hiperparámetros, empieza con Ajusta Tu Modelo de Scikit-learn Usando Algoritmos Evolutivos.
¿Vas a construir una plataforma de IA o de datos?
Si necesitas adaptar este enfoque a tu sistema, cuéntame qué datos tienes y qué decisión técnica necesitas resolver.