liveBook · Manning

Simulated annealing

As explained in Machine Learning Algorithms in Depth

Simulated annealing is a heuristic optimization method that probabilistically accepts higher-energy states according to a temperature-controlled schedule to escape local optima.

Simulated annealing (SA) searches for a low-energy state when an energy landscape may contain local optima. Unlike a search that accepts only improvements, it sometimes moves to a higher-energy state. Those temporary setbacks can open a route to a lower minimum later. section 9.4

Let E(x) be the energy at state x and T the temperature. Propose a new state x′ and compare E(x′) with E(x). Accept a lower-energy proposal with probability 1; accept a higher-energy proposal with a probability determined by the energy difference and T. A high initial temperature permits more unfavorable moves. As a cooling schedule lowers T, such moves become less likely. section 9.4

A run follows this loop:

  • Initialize the state and temperature, then evaluate the initial energy. section 9.4
  • Sample a proposal, evaluate its energy, and check for convergence. section 9.4
  • Apply the temperature-dependent acceptance rule, update the temperature, and repeat. Return the coordinates with the lowest energy found. section 9.4

Two-dimensional energy search

A Python implementation uses a target function E(x, y) and proposes coordinates from a multivariate normal distribution centered on the current point, with covariance 1.1I. Its cooling schedule multiplies T by 0.9 each iteration. The example also sets a 1,000-iteration limit and convergence parameters, making the proposal, cooling, and stopping choices explicit. section 9.4

Listing 9.5 Simulated annealing
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

class simulated_annealing():
    def __init__(self):
        self.max_iter = 1000
        self.conv_thresh = 1e-4
        self.conv_window = 10

        self.samples = np.zeros((self.max_iter, 2))
        self.energies = np.zeros(self.max_iter)
        self.temperatures = np.zeros(self.max_iter)

    def target(self, x, y):     #1
        z = 3*(1-x)**2 * np.exp(-x**2 - (y+1)**2) \
            - 10*(x/5 -x**3 - y**5) * np.exp(-x**2 - y**2) \
            - (1/3)*np.exp(-(x+1)**2 - y**2)
        return z

    def proposal(self, x, y):
        mean = np.array([x, y])
        cov =  1.1 * np.eye(2)
        x_new, y_new = np.random.multivariate_normal(mean, cov)
        return x_new, y_new

    def temperature_schedule(self, T, iter):
        return 0.9 * T

    def run(self, x_init, y_init):

        converged = False
        T = 1 
        self.temperatures[0] = T
        num_accepted = 0
        x_old, y_old = x_init, y_init
        energy_old = self.target(x_init, y_init)

        iter = 1
        while not converged:
            print("iter: {:4d}, temp: {:.4f}, energy = {:.6f}".format(iter, 
             T, energy_old))
            x_new, y_new = self.proposal(x_old, y_old)
            energy_new = self.target(x_new, y_new)

            if iter > 2*self.conv_window:     #2
                vals = self.energies[iter-self.conv_window : iter-1]
                if (np.std(vals) < self.conv_thresh):
                    converged = True
                #end if
            #end if  

            alpha = np.exp((energy_old - energy_new)/T)
            r = np.minimum(1, alpha)
            u = np.random.uniform(0, 1)
            if u < r:
                x_old, y_old = x_new, y_new
                num_accepted += 1
                energy_old = energy_new
            #end if
            self.samples[iter, :] = np.array([x_old, y_old])
            self.energies[iter] = energy_old

            T = self.temperature_schedule(T, iter)
            self.temperatures[iter] = T

            iter = iter + 1

            if (iter > self.max_iter): converged = True
        #end while

        niter = iter - 1
        acceptance_rate = num_accepted / niter
        print("acceptance rate: ", acceptance_rate)

        x_opt, y_opt = x_old, y_old

        return x_opt, y_opt, self.samples[:niter,:], self.energies[:niter], 
         self.temperatures[:niter] 

if __name__ == "__main__":

    SA = simulated_annealing()

    nx, ny = (1000, 1000)
    x = np.linspace(-2, 2, nx)
    y = np.linspace(-2, 2, ny)
    xv, yv = np.meshgrid(x, y)

    z = SA.target(xv, yv)
    plt.figure()
    plt.contourf(x, y, z)
    plt.title("energy landscape")
    plt.show()

    #find global minimum by exhaustive search
    min_search = np.min(z)
    argmin_search = np.argwhere(z == min_search)
    xmin, ymin = argmin_search[0][0], argmin_search[0][1]
    print("global minimum (exhaustive search): ", min_search)
    print("located at (x, y): ", x[xmin], y[ymin])

    #find global minimum by simulated annealing
    x_init, y_init = 0, 0
    x_opt, y_opt, samples, energies, temperatures = SA.run(x_init, y_init) 
    print("global minimum (simulated annealing): ", energies[-1])
    print("located at (x, y): ", x_opt, y_opt)

    plt.figure()
    plt.plot(energies)
    plt.title("SA sampled energies")
    plt.show()

    plt.figure()
    plt.plot(temperatures)
    plt.title("Temperature Schedule")
    plt.show()

Inspect the target function, proposal distribution, cooling schedule, and convergence parameters in the implementation. section 9.4

For difficult searches, different starting points and several restarts can improve the optima found. The traveling salesman problem is one example of an NP-hard problem that can be approximated with SA. section 9.6

How Machine Learning Algorithms in Depth covers it

How deep it goes
Develops the acceptance and cooling loop into pseudocode and a from-scratch implementation; does not establish a global-optimum guarantee.
Examples
Uses a two-dimensional energy landscape, sampled energies, and a traveling salesman problem application.
What it assumes
Assumes familiarity with probability and function minimization while defining the temperature-based search procedure.
What you can do after
Build it from scratch to search a two-dimensional energy function, and consider restarts when seeking better optima.
How it connects
Connects energy minimization, local optima, proposal distributions, cooling schedules, and NP-hard approximation.

Also explained in

Compare how 3 books explain simulated annealing →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Machine Learning Algorithms in Depth. The section links point to the parts of the book it draws on.