The last of the four types of problems has a continuous set of solutions , such as an interval. Local search moves between neighbors, but in a continuous set there is no natural neighbor. Instead, we take continuous steps in the direction in which the expected performance increases fastest, estimated by simulation. This notebook applies such a gradient method to a newsvendor that sells milk by the liter.
Learning outcomes
On completion of this notebook, you will be able to:
describe the gradient ascent algorithm and the role of the step size
estimate a gradient by simulation with finite differences
explain why common random numbers are needed for a useful finite-difference estimate
explain the downside of common random numbers: less noise also means less exploration
Gradient Ascent¶
When is continuous, for example the number of seconds that a traffic light stays red, we use the gradient of the expected performance. For a single decision variable, the gradient is the derivative: the slope of at . For several decision variables, it is the vector of partial derivatives, and it points in the direction in which increases fastest. We maximize, so we step from the current solution in the direction of the gradient, uphill:
If the slope at is positive, is larger than ; if it is negative, smaller. The step size determines how far we move. Near a maximum the slope is close to 0, so the steps automatically get smaller. Theory recommends (or a multiple of it): the steps shrink, and under fairly general conditions the method converges. In practice, the step size is often tuned to the problem, because can become very small long before a good solution is reached. Because we maximize, this method is called gradient ascent; for minimization we step in the opposite direction, with a minus sign, and it is called gradient descent.
Estimating the Gradient¶
As everything else in this lecture, can only be simulated, so we cannot compute its gradient: we estimate it. The finite-difference method simulates runs at and runs at , for a small , and uses the slope between the two averages:
With more decision variables, we estimate each partial derivative in this way, by changing one variable at a time. A side effect is that every iteration also gives an estimate of the performance of the current solution.
Here and are two very similar scenarios, so common random numbers make the difference much less noisy, as in Comparing Scenarios. This matters even more than there, because the difference is divided by the small number , which magnifies its noise.
Example: A Newsvendor for Milk¶
A shop sells fresh milk, which it orders every morning: liters, anywhere in . Milk costs €0.60 per liter and sells for €1 per liter, and milk that is left at the end of the day is thrown away. The demand is continuous: Gamma distributed with shape 10 and scale 1.5, so the mean demand is 15 liters. The profit is , as for the newsvendor, but now is continuous.
Each iteration simulates days at and at with , starting at with step size . The function gradient_ascent runs the method, with or without common random numbers; with crn=True, both order sizes face the same 100 simulated demands. As for the algorithms of Lecture 11, you should be able to read and follow this code; you do not need to be able to write it from scratch.
import numpy as np
price, cost = 1, 0.6
shape, scale = 10, 1.5
lower, upper = 0, 30
def profit(order, demand):
return price * np.minimum(demand, order) - cost * order
def gradient_ascent(start, n_iter, n, delta, crn, rng):
order = start
path = [order]
estimates = [] # y(pi_k) of every iteration
for k in range(1, n_iter + 1):
demand = rng.gamma(shape, scale, n)
if crn:
demand_plus = demand
else:
demand_plus = rng.gamma(shape, scale, n)
y_order = profit(order, demand).mean()
y_plus = profit(order + delta, demand_plus).mean()
gradient = (y_plus - y_order) / delta
estimates.append(y_order)
step_size = 20 / k
# take the step, but stay inside S = [lower, upper]
order = min(max(order + step_size * gradient, lower), upper)
path.append(order)
return path, estimates
rng = np.random.default_rng(1)
path_crn, estimates_crn = gradient_ascent(2, 100, 100, 0.1, True, rng)
for k in range(5):
print(
f"pi_{k} = {path_crn[k]:5.2f}: y(pi_{k}) = {estimates_crn[k]:.2f}, "
f"pi_{k + 1} = {path_crn[k + 1]:.2f}"
)
print(f"pi_100 = {path_crn[-1]:.2f}")pi_0 = 2.00: y(pi_0) = 0.80, pi_1 = 10.00
pi_1 = 10.00: y(pi_1) = 3.86, pi_2 = 12.87
pi_2 = 12.87: y(pi_2) = 4.37, pi_3 = 13.38
pi_3 = 13.38: y(pi_3) = 3.94, pi_4 = 12.65
pi_4 = 12.65: y(pi_4) = 4.41, pi_5 = 13.05
pi_100 = 13.22
At the demand is almost always larger than the order, so one extra liter is sold for €1 and costs €0.60: the gradient estimate is 0.4, and the first step takes us from 2 to . Closer to the mean demand, an extra liter is sold less often, the gradient gets smaller, and so do the steps.
For this simple example, the optimal order size can be calculated exactly: liters (how is outside the scope of this course; the code is hidden below). For real-life problems this is in general not possible, and we only use it to check the method. We now run the method once more without common random numbers:
Source
# exact optimum, only to check the method (outside the course's scope)
from scipy import stats
optimum = stats.gamma.ppf((price - cost) / price, shape, scale=scale)rng = np.random.default_rng(1)
path_indep, estimates_indep = gradient_ascent(2, 100, 100, 0.1, False, rng)
print(f"with CRN, pi_100 = {path_crn[-1]:.2f}")
print(f"without CRN, pi_100 = {path_indep[-1]:.2f}")with CRN, pi_100 = 13.22
without CRN, pi_100 = 12.49
Figure 1:Order size during 100 iterations of gradient ascent, with and without common random numbers, and the optimum .
Figure 1 shows the difference. With CRN, the method approaches the optimum in a few iterations and then stays close to it. Without CRN, the gradient estimates are so noisy that the order size jumps around: the second step even takes it back to 0 liters, and after 100 iterations it is still almost a liter below the optimum.
Local Optima¶
Like local search, a gradient method only finds a local optimum, so the result depends on the starting point. With CRN, the method climbs precisely to the nearest local optimum and stays there. Without CRN, the noise sometimes causes a large step past a dip to a better optimum, as in the volume-discount version of the example (Exercise 2), but it also makes the method less precise (Figure 1). So CRN trades exploration for precision; with CRN, we add exploration on purpose, for example by using several starting points.
A gradient is a local slope, so it cannot see a jump such as the one at 15 liters caused by the volume discount. For such problems, local search or ranking and selection over candidate solutions fits better.
References¶
Koole, G. (2019). An Introduction to Business Analytics. Chapter 8, “Simulation Optimization.”
Fu, M.C. (2002). “Optimization for simulation: Theory vs. practice.” INFORMS Journal on Computing, 14:192–215.