Skip to content

Basic Usage

This page walks through the two workflows you will use most:

  • Hyperparameter tuning with GASearchCV
  • Feature selection with GAFeatureSelectionCV

Every code block below is executed to build this page, so what you copy is exactly what ran — including the outputs and figures.

Prerequisites

  • sklearn-genetic-opt installed (pip install sklearn-genetic-opt)
  • Optional plotting extra for the figures: pip install sklearn-genetic-opt[plot]
  • Basic familiarity with scikit-learn's fit / predict API

Hyperparameter Tuning

We tune an MLPClassifier on the handwritten digits dataset (10 classes, 64 pixel features). First, the imports and data:

python
import numpy as np
from sklearn.datasets import load_digits
from sklearn.metrics import accuracy_score
from sklearn.model_selection import StratifiedKFold, train_test_split
from sklearn.neural_network import MLPClassifier

from sklearn_genetic import EvolutionConfig, GASearchCV, PopulationConfig, RuntimeConfig
from sklearn_genetic.space import Categorical, Continuous, Integer

RANDOM_STATE = 42

X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, stratify=y, random_state=RANDOM_STATE
)
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=RANDOM_STATE)
print(f"train={X_train.shape}  test={X_test.shape}  classes={len(np.unique(y))}")
text
train=(1203, 64)  test=(594, 64)  classes=10

Define the search space

The keys in param_grid are estimator parameter names. Each value is a space the genetic algorithm samples from:

  • Integer — whole numbers in a range
  • Continuous — floats in a range (optionally distribution="log-uniform")
  • Categorical — a fixed list of choices
python
param_grid = {
    "hidden_layer_sizes": Categorical([(32,), (64,), (32, 16)]),
    "alpha": Continuous(1e-5, 1e-1, distribution="log-uniform"),
    "learning_rate_init": Continuous(1e-4, 1e-1, distribution="log-uniform"),
    "activation": Categorical(["relu", "tanh"]),
    "batch_size": Integer(32, 256),
}

GASearchCV is configured with small config objects. EvolutionConfig controls the population and generations; PopulationConfig(initializer="smart") builds a diverse, well-spread starting population; RuntimeConfig controls parallelism and logging.

python
clf = MLPClassifier(max_iter=150, early_stopping=True, random_state=RANDOM_STATE)

search = GASearchCV(
    estimator=clf,
    random_state=RANDOM_STATE,
    cv=cv,
    scoring="accuracy",
    param_grid=param_grid,
    evolution_config=EvolutionConfig(population_size=8, generations=6),
    population_config=PopulationConfig(initializer="smart"),
    runtime_config=RuntimeConfig(n_jobs=-1, verbose=False),
)

search.fit(X_train, y_train)

print(f"Best CV accuracy : {search.best_score_:.4f}")
print(f"Best parameters  : {search.best_params_}")
text
Best CV accuracy : 0.9584
Best parameters  : {'hidden_layer_sizes': (64,), 'alpha': 0.00027470715996250945, 'learning_rate_init': 0.003590489008293675, 'activation': 'relu', 'batch_size': 70}

After fitting, GASearchCV behaves like any fitted scikit-learn estimator — it has already refit the best configuration on all of X_train:

python
y_pred = search.predict(X_test)
print(f"Holdout accuracy : {accuracy_score(y_test, y_pred):.4f}")
text
Holdout accuracy : 0.9579

Reading the evolution

Each generation is logged. Inspect the full history as a DataFrame — the columns explain how the population improved and how diverse it stayed:

ColumnMeaning
gengeneration number
fitnessmean CV score in the generation
fitness_bestbest CV score found so far
genotype_diversity1.0 = diverse, 0.0 = converged
unique_individual_ratiofraction of distinct configurations
stagnation_generationsconsecutive generations without improvement
python
import pandas as pd

history = pd.DataFrame(search.history)
print(history[["gen", "fitness", "fitness_best", "genotype_diversity",
               "unique_individual_ratio", "stagnation_generations"]].to_string(index=False))
text
 gen  fitness  fitness_best  genotype_diversity  unique_individual_ratio  stagnation_generations
   0 0.753325      0.946800            0.685714                    1.000                       0
   1 0.932253      0.946800            0.485714                    0.875                       1
   2 0.943890      0.955112            0.314286                    0.750                       0
   3 0.945449      0.955112            0.371429                    0.750                       1
   4 0.948462      0.955112            0.314286                    0.750                       2
   5 0.951995      0.955112            0.314286                    0.750                       3
   6 0.953242      0.958437            0.228571                    0.625                       0

fit_stats_ summarizes what the search actually spent — useful for spotting wasted effort (e.g. many skipped_invalid_candidates):

python
for key, value in search.fit_stats_.items():
    print(f"{key}: {value}")
text
evaluated_candidates: 104
unique_candidates: 102
cross_validate_calls: 102
cache_hits: 2
duplicate_candidates: 0
skipped_invalid_candidates: 0
population_parallel_batches: 7
population_serial_batches: 0
random_immigrants: 0
local_refinement_candidates: 0

Visualize convergence

With the [plot] extra installed, plot_fitness_evolution shows the best score climbing over generations, and plot_search_overview gives a compact diagnostic dashboard.

python
import matplotlib.pyplot as plt
from sklearn_genetic.plots import plot_search_overview

plot_search_overview(search, top_k=6)
plt.tight_layout()

Search overview dashboard: convergence, diversity, optimizer events, and top candidates

python
from sklearn_genetic.plots import plot_parameter_evolution

plot_parameter_evolution(search, parameters=["alpha", "learning_rate_init", "batch_size"])
plt.tight_layout()

How sampled values of alpha, learning rate, and batch size evolved over evaluations

Feature Selection

GAFeatureSelectionCV searches for the most useful subset of columns. To make the task concrete we take the Iris dataset and bolt on 10 columns of pure noise; a good selector should keep the real measurements and discard the noise.

python
from sklearn.datasets import load_iris
from sklearn.svm import SVC

from sklearn_genetic import GAFeatureSelectionCV

iris = load_iris()
rng = np.random.default_rng(RANDOM_STATE)
noise = rng.uniform(0, 10, size=(iris.data.shape[0], 10))
X_fs = np.hstack([iris.data, noise])
feature_names = list(iris.feature_names) + [f"noise_{i}" for i in range(10)]
y_fs = iris.target

Xtr, Xte, ytr, yte = train_test_split(
    X_fs, y_fs, test_size=0.33, stratify=y_fs, random_state=RANDOM_STATE
)

selector = GAFeatureSelectionCV(
    estimator=SVC(gamma="auto"),
    random_state=RANDOM_STATE,
    cv=3,
    scoring="accuracy",
    evolution_config=EvolutionConfig(population_size=10, generations=8,
                                     elitism=True, keep_top_k=2),
    population_config=PopulationConfig(initializer="smart"),
    runtime_config=RuntimeConfig(n_jobs=-1, verbose=False),
)
selector.fit(Xtr, ytr)

selected = [name for name, keep in zip(feature_names, selector.support_) if keep]
print(f"Selected {len(selected)} of {len(feature_names)} features:")
for name in selected:
    print(f"  - {name}")
print(f"Holdout accuracy : {accuracy_score(yte, selector.predict(Xte)):.4f}")
text
Selected 3 of 14 features:
  - sepal length (cm)
  - sepal width (cm)
  - petal length (cm)
Holdout accuracy : 0.9800

support_ is a boolean mask (True = kept). The search keeps the informative Iris measurements and drops most of the noise columns. Visualize the mask:

python
from sklearn_genetic.plots import plot_feature_selection

plot_feature_selection(selector, feature_names=feature_names)
plt.tight_layout()

Selected-feature mask over the four Iris features plus ten noise columns

Tips & Gotchas

  • Set RuntimeConfig(verbose=True) to watch the per-generation log live.
  • PopulationConfig(initializer="smart") is strongly recommended — it usually finds better solutions faster than a purely random start.
  • If accuracy is already near 1.0 on your data, switch to a more discriminative metric (e.g. roc_auc, balanced_accuracy).
  • Check fit_stats_["skipped_invalid_candidates"] after fitting — a non-zero value means some sampled configurations raised errors during fit.

Next Steps

Released under the MIT License.