Mixed Search Spaces

Gradient-Free-Optimizers natively supports continuous, discrete, and categorical dimensions in a single search space. There is no need to encode or transform parameter types – the library detects the dimension type from the array you provide and applies appropriate optimization logic for each type internally.

Dimension Types

A search space is a Python dictionary mapping parameter names to NumPy arrays of candidate values. The type of each dimension is determined by the array contents:

Continuous – floating-point values, typically created with np.linspace:

import numpy as np

# 200 evenly spaced floats from 0.001 to 1.0
"learning_rate": np.linspace(0.001, 1.0, 200)

Discrete – integer or evenly spaced numerical values, typically created with np.arange:

# Integers: 10, 20, 30, ..., 200
"n_estimators": np.arange(10, 210, 10)

Categorical – strings or other non-numeric values:

# Categorical choices
"kernel": np.array(["linear", "rbf", "poly"])

Boolean – a special case of categorical with two values:

"use_bias": np.array([True, False])

Mixing Types Freely

All dimension types can coexist in a single search space dictionary. The optimizer handles each dimension according to its type, so you can define mixed-type problems naturally:

import numpy as np
from gradient_free_optimizers import BayesianOptimizer

# Mixed search space for SVM hyperparameter tuning
search_space = {
    "C": np.linspace(0.01, 100, 200),           # continuous
    "degree": np.arange(2, 6),                    # discrete
    "kernel": np.array(["linear", "rbf", "poly"]),# categorical
    "shrinking": np.array([True, False]),          # boolean
}

def objective(para):
    from sklearn.svm import SVC
    from sklearn.model_selection import cross_val_score
    from sklearn.datasets import load_iris

    X, y = load_iris(return_X_y=True)
    clf = SVC(
        C=para["C"],
        degree=para["degree"],
        kernel=para["kernel"],
        shrinking=para["shrinking"],
    )
    return cross_val_score(clf, X, y, cv=3).mean()

opt = BayesianOptimizer(search_space)
opt.search(objective, n_iter=50)
print(opt.best_para)

Granularity

The optimizer samples from the values you provide, so the array length controls the resolution of each dimension. More values mean finer granularity but a larger search space:

# Coarse: 10 values
"x": np.linspace(-10, 10, 10)    # step size = 2.22

# Fine: 1000 values
"x": np.linspace(-10, 10, 1000)  # step size = 0.02

For discrete parameters like n_estimators, the step size in np.arange directly controls the granularity.

Why Native Mixed-Type Support Matters

Many optimization libraries require all dimensions to be the same type, or force you to encode categoricals as integers. GFO uses dimension-type-aware routing internally: the optimization logic that generates new candidate positions adapts its strategy per dimension. Continuous dimensions use perturbation-based moves, while categorical dimensions use swap-based moves. This means:

  • No manual encoding or decoding of categorical parameters

  • The optimizer uses moves that make sense for each type

  • Results are returned using the original values you defined