
Gaussian process (GP): a prior over functions. After the data, a mean μ(x) and an uncertainty σ(x) at every input.

Bayesian optimization (BO): maximize a black-box objective in few evaluations. Fit a surrogate to every evaluation so far, choose the next input by maximizing an acquisition function, evaluate it, and repeat.
1. Fit a GP to all data
2. Score with an acquisition function, cheap
3. Run the best candidate
4. Add it to the data

The goal: small regret, small budget.

Exploitation: run the next experiment where the surrogate predicts a good value (high μ).
Exploration: run it where the surrogate is unsure (high σ).
You know it is good. You will never find out if the new place is better
Exploitation
Could be great, could be awful. You learn something either way
Exploration
A good acquisition function does some of both.
Keeps sampling next to the best point so far
Can sit on a local optimum forever: it never looks elsewhere
Samples wherever the band is widest
Learns the whole curve, ignores the goal: close to a careful random search
Every acquisition function is a recipe for the balance, with a knob.

z-score: how many GP standard deviations the predicted yield μ(x) sits above the best so far, f*. z > 0: the GP expects to beat f*. z < 0: it expects to fall short.
Probability of improvement (PI): the probability that the yield at x beats the best so far, f*, by more than ξ.

In short: PI runs the experiment most likely to beat the best so far, however small the gain.
Expected improvement (EI): the average amount by which the yield at x beats the best so far, counting a miss as zero.
If the yield at x turns out to be f, the gain is f − f*. Below f*, the gain is 0
Average the gain over the GP's bell of possible yields
How far the mean is above f*, times Φ(z), the chance of beating f*. Large where μ is high
The bell's width σ times φ(z), the bell's height at z. Large where σ is large

LogEI: compute log EI directly, with formulas that stay accurate where EI is tiny. Same maximizer as EI: the log does not move the peak.

Upper confidence bound (UCB): an optimistic score, the GP mean plus κ standard deviations. LCB, μ − κσ, is the same idea for minimizing.


Same data, same GP: the acquisition function alone sets the next experiment.
Efficient global optimization (EGO): fit a kriging model (a GP), sample where expected improvement is largest, refit, repeat (Jones, Schonlau and Welch, 1998).

optimize_acqf maximizes the acquisition with L-BFGS-B, a gradient method. PyTorch's automatic differentiation supplies the gradientsR = 8.314 # J/(mol K)
def rate(T, k_ref, E):
# Arrhenius: rate constant (1/min) at T (degC), relative to 80 degC
T = T + 273.15
return k_ref * np.exp(-E / R * (1 / T - 1 / 353.15))
def yield_B(T, tau):
# steady-state yield of B in the tank, for A -> B -> C
k1 = rate(T, 0.15, 60e3) # A -> B
k2 = rate(T, 0.03, 110e3) # B -> C: needs more heat
return k1 * tau / ((1 + k1 * tau) * (1 + k2 * tau))
rng = np.random.default_rng(0)
def run_experiment(T):
# one experiment per row of T (degC): 10 min in the tank, noise 0.01
y = yield_B(T.numpy().ravel(), 10.0)
y = y + 0.01 * rng.standard_normal(T.shape[0])
return torch.tensor(y).unsqueeze(-1)
Arrhenius' law: heat speeds up each step
Yield of B at steady state. E₂ > E₁: the waste step needs more heat
The "lab": one noisy yield per temperature. The optimizer sees only this
bounds = torch.tensor([[20.0], [140.0]]) # temperature from 20 to 140 degC
torch.manual_seed(0)
# two old runs from the lab notebook
T = torch.tensor([[30.0], [50.0]])
y = run_experiment(T)
for i in range(10):
# 1. the surrogate: a GP fitted to every experiment so far
gp = SingleTaskGP(
T,
y,
input_transform=Normalize(d=1),
outcome_transform=Standardize(m=1),
)
mll = ExactMarginalLogLikelihood(gp.likelihood, gp)
fit_gpytorch_mll(mll)
Normalize: temperature to [0, 1]. Standardize: yields to mean 0, standard deviation 1 # 2. the acquisition: expected improvement over the best yield so far
acq = LogExpectedImprovement(gp, best_f=y.max())
# 3. the next experiment: where the acquisition is largest
T_next, _ = optimize_acqf(
acq,
bounds=bounds,
q=1,
num_restarts=10,
raw_samples=200,
)
# 4. run it, and add it to the data
y_next = run_experiment(T_next)
T = torch.cat([T, T_next])
y = torch.cat([y, y_next])
q=1: one experiment at a timeexperiment 1: T = 30.0 C, yield 0.050
experiment 2: T = 50.0 C, yield 0.181
experiment 3: T = 54.4 C, yield 0.236
experiment 4: T = 60.7 C, yield 0.306
experiment 5: T = 71.5 C, yield 0.419
experiment 6: T = 86.9 C, yield 0.430
experiment 7: T = 80.5 C, yield 0.474
experiment 8: T = 140.0 C, yield 0.023
experiment 9: T = 80.0 C, yield 0.454
experiment 10: T = 80.0 C, yield 0.449
experiment 11: T = 80.1 C, yield 0.455
experiment 12: T = 102.5 C, yield 0.219



BO is random: the noise, the random candidates. One run is one sample
Random search at least. Also try a classic optimizer, if possible
Against experiments, with its spread across seeds
Count every experiment, the old runs included






All four: the same reactor, noise, budget of 30 and cold start. Maps: seed 0; star: the best.

Finite differences need cheap, noise-free evaluations. Lab experiments are expensive and noisy.

Choose by the cost of one evaluation, the noise, and the number of inputs.
50 expert chemists and engineers against BO, choosing reaction conditions
"Bayesian optimization outperforms human decision making in both average optimization efficiency (number of experiments) and consistency"
A mobile robot searching for a better photocatalyst
"operated autonomously over eight days, performing 688 experiments within a ten-variable experimental space, driven by a batched Bayesian search algorithm"
BO with early predictions of battery life
"among 224 candidates in 16 days (compared with over 500 days using exhaustive search without early prediction)"
Flowsheet optimization when each unit is an expensive black box
"The black box modules are substituted by metamodels based on a kriging interpolation"
Wings and other shapes, each evaluated by an expensive simulation
"Surrogate models expedite the search for promising designs by standing in for expensive design evaluations or simulations"
An automotive throttle valve, tuned on the test bench
"BO achieves on par or better performance than hand tuning with only around ten experimental evaluations"
Choosing a model's settings, such as the learning rate, when each training run takes hours
Tuning the Go program's settings, where each test is many games. Win rate against its old self: 50% to 66.5%
Facebook tuned the settings of a ranking system and of its server software. Each try is a live test with noisy results
Google tuned a chocolate chip cookie recipe (sugar, salt, vanilla, chips) by taste tests, in 144 experiments
Anywhere one evaluation is expensive and there is no formula.

The best yield may need a temperature the reactor cannot take. A second GP learns that limit from the same experiments, and BO proposes only runs likely to respect it

Four reactors: four experiments at once (q=4, qLogEI)

Yield and rate: the Pareto front, the best trade-offs (qEHVI)

Many cheap, rough runs and a few exact ones: BO picks which, and where
All four are in BoTorch.
Active learning: choose each next experiment to make the model good everywhere, usually where it is most uncertain.


Expensive, noisy, no gradients, few inputs
A GP prior over the curve, updated by every experiment




Fit, score, run, add

Exploit high μ, explore high σ
SingleTaskGP: the GP
LogExpectedImprovement: the score
optimize_acqf: the next experiment
l15-bayesian-optimization.ipynb: BoTorch on the reactor, one knob then two
90 minutes of deck, 20 for the notebook. One example all the way: the reactor.
Coffee roasting works too: green, roasted, burnt.
We built the curve from the kinetics only to play the lab and check the answer. The optimizer never sees it.
The curves are sketches of first-order kinetics, A -> B -> C, not data. Saeman's paper is behind the ACS paywall.
From my seminar, as in Lecture 13.
The green diamond: where the uncertainty is largest.
How do you draw one of these random curves from the GP, in practice? Evaluate the GP on a grid of temperatures (here 241 of them). That gives a mean vector mu and a covariance matrix K. Factor K = L L^T (Cholesky). Draw a vector z of standard normal random numbers. The curve is mu + L z. In BoTorch the same thing is gp.posterior(T_grid).rsample().
Play. Prior first: same belief everywhere. Then 30, 50, 110, 80, 65 °C. The sample curves agree where we measured.
Where is Mockus in Shahriari et al. (2016)? The review spells him Močkus, with a caron, and names him only in the reference list. Page 17 of the PDF: "The term Bayesian optimization was coined in the seventies [115]". Reference [115] is J. Močkus, V. Tiesis and A. Žilinskas, "The application of Bayesian methods for seeking the extremum", 1978. The link opens that page.
The four panels are the first pass of the worked example: two old runs, EI peaks at 54 °C, run it, refit.
Step through slowly. Step 6: the jump to 140 °C, where the GP is unsure. After that it never goes back there.
What is kappa, the number on the slider? It is the weight on uncertainty in the upper confidence bound, mu(x) + kappa sigma(x): how many standard deviations of optimism we add to the GP mean. Kappa = 0: the score is just the mean, so the next run goes right next to the best point so far (58 °C). As kappa grows, the uncertainty term takes over and the pick moves to where the band is widest.
Slide κ from 0 up. At 0 it sits next to 54 °C. Around 2 it moves out. At 5 it goes where the band is widest.
What are the Greek letters? Xi (ξ) is the margin. It is in z. In plain terms, the smallest gain that counts: with xi = 0.01, a temperature counts as an improvement only if it beats 0.236 by more than 0.01. A larger xi ignores tiny gains right next to f*, so the search moves away from it. We use xi = 0. Capital and lowercase phi (Φ, φ) are NOT in z. They take z as input in PI and EI: Φ is the standard normal cumulative distribution, φ its density. They are defined on the next two slides.
PI in plain words. At 60 °C the GP does not give one yield. It gives a bell of possible yields, centred at 0.242, spread 0.046. The best yield so far is 0.236. PI is the share of the bell above 0.236: 55%. A 55% chance that 60 °C beats the best so far. Phi(z) is the same number reached through z. Shifting and rescaling the bell to a standard one turns the share above 0.236 into the share left of z = 0.13, and Phi(0.13) = 0.55. The weakness: a point right next to f*, with a narrow bell, gets PI near 1 for a gain of almost nothing. That is why EI weighs each gain by its size.
What is the gain f minus f*? If we ran the experiment at 60 °C and the yield came out f, the gain over the best so far would be f minus f*. If f is below f*, we gain nothing: the gain is 0, not negative. Bottom panel: the gain for every possible yield. Top panel: how likely each yield is. EI averages the gain over the bell: 0.022 at 60 °C. Each card lights up its term in the equation as it appears.
Why LogEI was created. Far from the data, EI is so small that the computer rounds it to exactly 0 (underflow; below about 1e-324 in double precision). Here, after 12 experiments, that happens at 12% of the temperatures (red). Where EI is exactly 0, its slope is 0 too, so the optimizer that follows the slope has no direction. Log EI is about -1000 there instead: a normal number with a slope. The log does not move the peak, so the next experiment is the same. Ament et al. (2023): EI's values "vanish numerically in many regions".
What is kappa exactly, besides the knob? How many standard deviations of optimism. UCB scores each temperature by the top of its band, mu + kappa sigma. Kappa = 2 is the top of the 95% band. What is beta in BoTorch? UpperConfidenceBound(gp, beta) computes mu + sqrt(beta) sigma, so beta = kappa squared: beta = 4 means kappa = 2. Not on this slide on purpose: BoTorch comes later. It is in the notes' BoTorch section.
The figure redraws the idea of the paper's Figs. 8 to 11 on our own function: the prediction alone only refines the local peak near the cluster; EI is largest in the gap. Jones (2001) is the follow-up taxonomy.
Is "automatic differentiation" true for BoTorch? Yes. Checked in the BoTorch 0.18.1 source: optimize_acqf calls gen_candidates_scipy, which runs SciPy's L-BFGS-B, and the gradients come from torch.autograd.grad. The GP is a formula, so the acquisition has exact gradients. Only the experiment itself has none.
Play. Watch Nelder-Mead and L-BFGS-B sit in the cold corner. Random search sprays points everywhere.
BO comes with no guarantee of finding the global optimum. It explores the whole range, which helps it avoid getting stuck, but nothing promises the best point. We do not call it "global".
The Shields et al. quote: the paper spells it "decisionmaking", one word. The slide writes "decision making".
Online experiments, in plain words. Facebook tuned two systems. A ranking system: the order in which items are shown to users. And its server software: the compiler flags, settings of the compiler that change how fast the servers run. Each try is a live test on real traffic, so every result is noisy (Letham et al., 2019).
Constraints, in plain words. The best yield may need a temperature the reactor cannot take, for example a hot spot above its limit. We can only find out by running the experiment. So a second GP learns that limit from the same experiments, and the acquisition is multiplied by the probability of staying within it. BO then proposes only runs likely to be safe.
The 70 °C limit and the rough model are made-up examples on the reactor. The batch is qLogEI on the GP after three experiments. The Pareto front is computed from the kinetics.
What is x_{n+1} = argmax_x sigma(x) doing? It reads: the next experiment is the input where the GP's uncertainty sigma(x) is largest, the widest point of the band. No mean in it at all: active learning does not care where the yield is high, only where the model is unsure.