Mixed Search Spaces

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

Dimension Types

A search space is a Python dictionary mapping parameter names to dimension definitions. A dimension can be a tuple for a continuous range, a NumPy array for a discrete grid, a Python list for categorical choices, or an optional SciPy distribution object.

Continuous – a tuple (lower, upper):

"learning_rate": (0.001, 1.0)

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, typically provided as a list:

# Categorical choices
"kernel": ["linear", "rbf", "poly"]

Boolean – a special case of categorical with two values:

"use_bias": [True, False]

Distribution – a SciPy stats continuous distribution. SciPy is optional, and this type is available when the user passes a SciPy distribution object:

from scipy import stats

"learning_rate": stats.loguniform(1e-5, 1e-1)

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 scipy import stats
from gradient_free_optimizers import BayesianOptimizer

# Mixed search space for SVM hyperparameter tuning
search_space = {
    "C": (0.01, 100.0),                         # continuous
    "gamma": stats.loguniform(1e-5, 1e-1),      # distribution
    "degree": np.arange(2, 6),                    # discrete
    "kernel": ["linear", "rbf", "poly"],          # categorical
    "shrinking": [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. Distribution dimensions are optimized in quantile space and converted to ppf values before objective evaluation. 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