Surrogates 101

Let's start with something easy to get our hands dirty. Let's say we want to build a surrogate for $f(x) = \log(x) \cdot x^2+x^3$.

RBF

We will first use the radial basis surrogate for demonstrations.

# Importing the package
using Surrogates

# Defining the function
f = x -> log(x) * x^2 + x^3

# Sampling points from the function
lb = 1.0
ub = 10.0
x = sample(50, lb, ub, SobolSample())
y = f.(x)

# Constructing the surrogate
my_radial_basis = RadialBasis(x, y, lb, ub)

# Predicting at x=5.4
approx = my_radial_basis(5.4)
206.83097750423224

We can plot to see how well the surrogate performs compared to the true function.

using Plots

plot(x, y, seriestype = :scatter, label = "Sampled points",
    xlims = (lb, ub), legend = :top)
xs = 1.0:0.001:10.0
plot!(xs, f.(xs), label = "True function", legend = :top)
plot!(xs, my_radial_basis.(xs); label = "RBF", legend = :top)
Example block output

It fits quite well! Now, let's now see an example in 2D.

using Surrogates
using LinearAlgebra

f = x -> x[1] * x[2]

lb = [1.0, 2.0]
ub = [10.0, 8.5]
x = sample(50, lb, ub, SobolSample())
y = f.(x)

my_radial_basis = RadialBasis(x, y, lb, ub)

# Predicting at x=(1.0,1.4)
approx = my_radial_basis((1.0, 1.4))
7.393838848775784

Kriging

Let's now use the Kriging surrogate, which is a single-output Gaussian process. This surrogate has a nice feature - not only does it approximate the solution at a point, it also calculates the standard error at such a point.

using Surrogates

f = x -> exp(-x) * x^2 + x^3

lb = 0.0
ub = 10.0
x = sample(50, lb, ub, RandomSample())
y = f.(x)

p = 1.9
my_krig = Kriging(x, y, lb, ub, p = p)

# Predicting at x=5.4
approx = my_krig(5.4)

# Predicting error at x=5.4
std_err = std_error_at_point(my_krig, 5.4)
0.5142877888279456

Let's now optimize the Kriging surrogate using the lower confidence bound method. This is just a one-liner:

surrogate_optimize!(
    f, LCBS(), lb, ub, my_krig, RandomSample(); maxiters = 10, num_new_samples = 10)

Surrogate optimization methods have two purposes: they both sample the space in unknown regions and look for the minima at the same time.

Lobachevsky integral

The Lobachevsky surrogate has the nice feature of having a closed formula for its integral, which is something that other surrogates are missing. Let's compare it with QuadGK:

using Surrogates
using QuadGK

obj = x -> 3 * x + log(x)
a = 1.0
b = 4.0
x = sample(2000, a, b, SobolSample())
y = obj.(x)
alpha = 2.0
n = 6
my_loba = LobachevskySurrogate(x, y, a, b, alpha = alpha, n = n)

#1D integral
int_1D = lobachevsky_integral(my_loba, a, b)
int = quadgk(obj, a, b)
int_val_true = int[1] - int[2]
println(int_1D)
println(int_val_true)
25.045176852909208
25.04517738347907

NeuralSurrogate

Basic example of fitting a neural network on a simple function of two variables.

using Surrogates
using Flux
using Statistics
using SurrogatesFlux

f = x -> x[1]^2 + x[2]^2
# Flux models are in single precision by default.
# Thus, single precision will also be used here for our training samples.
bounds = Float32[-1.0, -1.0], Float32[1.0, 1.0]

x_train = sample(500, bounds..., SobolSample())
y_train = f.(x_train)

# Perceptron with one hidden layer of 20 neurons.
model = Chain(Dense(2, 20, relu), Dense(20, 1))

# Training of the neural network
learning_rate = 0.1
optimizer = Descent(learning_rate)  # Simple gradient descent. See Flux documentation for other options.
n_epochs = 50
sgt = NeuralSurrogate(x_train, y_train, bounds..., model = model,
    opt = optimizer, n_epochs = n_epochs)

# Testing the new model
x_test = sample(30, bounds..., RandomSample())
test_error = mean(abs2, sgt(x)[1] - f(x) for x in x_test)
0.09733105f0