{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# An Introduction to Optimisation via Simulation. \n", "# How to choose the best setup for a system.\n", "## A tutorial for the Simulation Workshop 2021\n", "\n", "**Dr Christine S.M Currie and Tom Monks**\n", "\n", "This tutorial introduces Ranking and Selection procedures for stochastic computer simulation. We focus on in difference zone and optimal computer budget allocation procedures. The procedures are:\n", "\n", "* KN and KN++\n", "* OCBA and OCBA-m\n", "\n", "> The code implementing the Ranking and Selection procedures explored in this tutorial is available online with an MIT license https://github.com/TomMonks/sim-tools" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# run this first to install the OvS procedures so we can use them in the tutorial.\n", "# !pip install sim-tools" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from sim_tools.ovs.toy_models import (custom_gaussian_model,\n", " gaussian_sequence_model,\n", " random_gaussian_model,\n", " ManualOptimiser)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Simulation Models\n", "\n", "To keep the focus of this tutorial on Ranking and Selection procedures and what they do, and how they perform in practice, the models we introduce are deliberately simple. The following sections describe how to create a a set of simulation models where the outputs are independent guassian distributions. There are three options:\n", "\n", "* the means of these outputs distributions follow a sequence (e.g. 1, 2, 3, 4, 5) variance is 1.0. \n", "* the means and variances are user specified.\n", "* the means and variances are randomly generated." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a simple sequence of normal distributions\n", "\n", "A simple model can be created using the `guassian_sequance_model()` function. This creates a simulation model where the output of each design follows a $N - (\\mu_{i}, 1.0)$ :\n", "\n", "The function accepts three keyword arguments:\n", "\n", "* **start** - int, the first mean in the sequence (inclusive)\n", "* **end** - int, the last mean int the sequence (inclusive)\n", "* **step** - int, the difference between mean i and mean i + 1.\n", "\n", "For example, the following code creates a simulation model with 10 designs with means 1 to 10 and unit variance.\n", "\n", "```python\n", "model = guassian_sequence_model(1, 10)\n", "```\n", "\n", "To create a simulation model with 5 designs where $\\mu_{i+1} - \\mu_i = 2 $ :\n", "\n", "```python\n", "model = guassian_sequence_model(1, 10, step=2)\n", "```\n", "\n", "### Creating a custom model with known designs\n", "Instead of a sequence of normal distributions with unit variance, it is possible to create a custom set of designs with varying variances. Use the `custom_guassian_model` function for this task. For example to create a custom set of designs: \n", "\n", "```python\n", "means = [5, 8, 1, 2, 1, 7]\n", "variances = [0.1, 1.2, 1.4, 0.3, 0.8]\n", "\n", "custom_model = custom_guassian_model(means, variances)\n", "```\n", "\n", "The following code demonstrates how to create a sequence of 100 designs with variances that are 10% of the mean. \n", "\n", "```python\n", "n_designs = 100\n", "means = [i for i in range(n_designs+1)]\n", "variances = [j*0.1 for j in range(n_designs+1)]\n", "\n", "custom_model = custom_guassian_model(means, variances)\n", "```\n", "\n", "\n", "### Creating a model with randomly sampled designs\n", "\n", "To create a model with a set of unknown designs (within a specified mean and variance tolerance) use \n", "\n", "```python\n", "mean_low = 1.0\n", "mean_high = 15.0\n", "var_low = 0.1\n", "var_high = 2.0\n", "n_designs = 15\n", "\n", "model = random_guassian_model(mean_low, mean_high, var_low, var_high, n_designs)\n", "```\n", "\n", "Where:\n", "* **mean_low** - float, a lower bound on the means of the output distributions\n", "* **mean_high** - float, an upper bound on the means\n", "* **var_low** - float, a lower bound on the variance of the output distributions\n", "* **var_high** - float, an upper bound on the variances.\n", "* **n_designs** - int, the number of designs to create.\n", "\n", "---\n", "\n", "## A manual optimisation framework\n", "\n", "Before using the **Optimisation via Simulation** (OvS) procedures, it is recommended that you get a feel for the framework in which the OvS procedures operate. To do this we will create some models and explore them using a `ManualOptimiser`. This allows the user to run independent and multiple replications of the model yourself independent of any algorithm. The `ManualOptimiser` keeps track of the means, variances and number of replications run for each design.\n", "\n", "A `ManualOptimiser` object requires two parameters when it is created. \n", "\n", "* model - object, e.g. a model that is a sequence of normal distributions\n", "* n_designs - the number of designs to be considered." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "manual_opt = ManualOptimiser(model=gaussian_sequence_model(1, 10),\n", " n_designs=10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* We can print the optimiser object to help us remember what parameters we set." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ManualOptimiser(model=BanditCasino(), n_designs=10, verbose=False)\n" ] } ], "source": [ "print(manual_opt)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Follow Law and Kelton's advice and run 3 initial replications of each design." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "manual_opt.simulate_designs(replications=3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The optimiser keeps track of the allocation of replications across each design. For efficiency it doesn't store each individual observation, but it does compute a running mean and variance.\n", "\n", "* Let's have a look at the replication allocation between and results of each design." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], dtype=int32)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "manual_opt.allocations" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.7, 2.4, 4. , 3.7, 5. , 6. , 6.9, 8.8, 8.4, 9.8])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.set_printoptions(precision=1) # this is a hack to view at 1 decimal place.\n", "manual_opt.means" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.5, 1.9, 0.6, 0.8, 0.2, 0.9, 3.1, 0.5, 1. , 3.3])" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "manual_opt.vars" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's run 1 additional replication of the top 3 designs. \n", "Note - in python arrays are **zero indexed**. This means that design 1 has index 0." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "manual_opt.simulate_designs(design_indexes=[7, 8, 9], replications=1)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([3, 3, 3, 3, 3, 3, 3, 4, 4, 4], dtype=int32)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "manual_opt.allocations" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.7, 2.4, 4. , 3.7, 5. , 6. , 6.9, 8.7, 8.7, 9.5])" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "manual_opt.means" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.5, 1.9, 0.6, 0.8, 0.2, 0.9, 3.1, 0.4, 1. , 2.5])" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "manual_opt.vars" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Manual Optimisation of a model with unknown means." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Now have a go yourself**. This time create a model with random designs. Run as many replications of each design as you think is neccessary to make a decision about which design is best. Here we define best as the design with the largest mean value." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "#create random problem\n", "rnd_model = random_gaussian_model(mean_low=5, mean_high=25, \n", " var_low=0.5, var_high=2.5,\n", " n_designs=10)\n", "\n", "#create manual optimiser for you to use.\n", "manual_opt = ManualOptimiser(model=rnd_model, n_designs=10)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "#insert your code here...\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Indifference Zone Ranking and Selection" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Procedure **KN**\n", "The first Ranking and Selection (R&S) algorithm we will explore is **Procedure KN** developed by Kim and Nelson. This is an *Indifference Zone* (IZ) approach to R&S. IZ procedures exploit the the idea that the performance of one or more of the sub-optimal designs are in fact so close to the best performing system that a user does not care which one is chosen (the decision maker is said to be *indifferent* to this choice). To do this a user must specify a quantity $\\delta$. Procedure KN provides a theorectical guarantee to select the best system or a system within $\\delta$ of the best with probability of $1 - \\alpha$.\n", "\n", "A key feature of KN is that it only estimates the variance of each design once. This happens after an initial number of replications (specified by the user) $n_0$. \n", "\n", "To run Kim and Nelson's R&S procedure KN, create an instance of `ovs.indifference_zone.KN`\n", "\n", "An object of type KN takes the following parameters:\n", "\n", "* **model** - a simulation model\n", "* **n_designs** - int, the number of competing designs to compare\n", "* **delta** - float, the indifference zone\n", "* **alpha** - float, $PCS = 1-\\alpha$ (default=0.05)\n", "* **n_0** - int, $n_0$ the number of initial replications (default=2)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "from sim_tools.ovs.indifference_zone import KN, KNPlusPlus" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 1. A simple sequence of normal distributions.\n", "\n", "The first problem we will test KN against is selecting the largest mean from a sequence of 10 normal distributions (means raning from 1 to 10) with unit variance. We want a $PCS = 0.95$ and are indifferent to designs that are within an average of 1.0 of the best. \n", "\n", "For this simple problem, we will follow Law's longstanding advice of setting $n_0 = 5$ i.e 5 initial replications of each design.\n", "\n", "#### Setting up KN" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "N_DESIGNS = 10\n", "DELTA = 1.0\n", "ALPHA = 0.05\n", "N_0 = 5\n", "\n", "#first we create the simulation model. \n", "model = gaussian_sequence_model(1, N_DESIGNS)\n", "\n", "#then we create the KN R&S object and pass in our parameters.\n", "#note that this doesn't run the optimisation yet.\n", "kn = KN(model=model, \n", " n_designs=N_DESIGNS, \n", " delta=DELTA, \n", " alpha=ALPHA, \n", " n_0=N_0)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "KN(n_designs=10, delta=1.0, alpha=0.05, n_0=5, obj=max)\n" ] } ], "source": [ "#quickly check if KN is parameterised as expected\n", "print(kn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Running KN\n", "Now that we have created the KN object we can run the solver at any time we choose. To do this call the `KN.solve()` method. This method returns a design within the indifference zone $(1 - alpha$) of the time." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "best design\t[9]\n", "allocations\t[ 5 5 5 5 7 5 5 5 13 14]\n", "total reps\t69\n" ] } ], "source": [ "#this runs KN (you can run this cell multiple times if you wish)\n", "best_design = kn.solve()\n", "\n", "#print out the results\n", "print(f'best design\\t{best_design}')\n", "print(f'allocations\\t{kn._allocations}')\n", "print(f'total reps\\t{kn._allocations.sum()}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In general you should see that KN simulates the top 2-3 designs the most with the lower performing designs eliminated early on (possibly after the initial stage of replication)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Procedure **KN++**\n", "\n", "Kim and Nelson's KN++ procedure is an enhancement on KN. KN++ introduces an **update** step that recalculates the variance of the differences between designs. \n", "\n", "To run procedure KN++, create an instance of `ovs.indifference_zone.KNPlusPlus`\n", "\n", "### Example 2. Solving the simple problem using KN++\n", "\n", "We will run KN++ on the same simulation model as KN. In general you should see that KN++ uses less replication to find a design within the indifference zone." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "best design\t[9]\n", "allocations\t[5 5 5 6 5 6 6 7 6 8]\n", "total reps\t59\n" ] } ], "source": [ "knpp = KNPlusPlus(model=model, \n", " n_designs=N_DESIGNS, \n", " delta=DELTA, \n", " alpha=ALPHA, \n", " n_0=N_0)\n", "\n", "best_design = knpp.solve()\n", "print(f'best design\\t{best_design}')\n", "print(f'allocations\\t{knpp._allocations}')\n", "print(f'total reps\\t{knpp._allocations.sum()}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 1. Comparing KN and KN++ on a larger design.\n", "\n", "Your task is to compare KN and KN++ optimising a simulation model with 100 competing designs. These designs are a sequence of 100 normal distributions with unit variance. The objective is to find the design with the maximum mean.\n", "\n", "What do you notice about the replications required from each method?" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "#the code to set up the simulation model has been provided below.\n", "N_DESIGNS = 100\n", "DELTA = 1.0\n", "ALPHA = 0.05\n", "N_0 = 5\n", "\n", "#first we create the simulation model. \n", "model = gaussian_sequence_model(1, N_DESIGNS)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "#your code goes here...\n", "# hint 1. you need to create KN and KNPlusPlus objects, pass in the simulation model and run them. \n", "# hint 2. try running the models a few times. \n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 2: A more difficult optimisation problem\n", "\n", "We will now explore how the IZ procedures perform on a slightly harder optimisation problem where the mean of the designs are closer together and have differing variances.\n", "\n", "The problem is: \n", "\n", "```python\n", "means = [4, 4.1, 4.2, 4, 4.1, 4.3, 4, 4.1, 4.2, 4.2]\n", "variances = [1, 1, 1, 0.1, 0.1, 10, 10, 10, 10, 0.1]\n", "```\n", "\n", "The 'optimal' mean design is at index 5 (4.3). It is a very noisy design. We will set a $\\delta$ so that there are multiple designs within the IZ.\n", "\n", "Your task is to compare the performance of KN and KN++ given the following parameters:\n", "\n", "```python\n", "N_DESIGNS = 10\n", "N_0 = 10\n", "DELTA = 0.15\n", "ALPHA = 0.05\n", "```\n", "\n", "We are also setting a random seed before running each procedure. This ensures that the first stage of replications returns the same samples.\n", "\n", "Questions: \n", "* Do the methods return the 'optimal' design or another design within the indifference zone?\n", "* Which procedure require the most replication effort? Does it vary?\n", "* Are there any differences in designs selected and the number of replications needed? Try different values of `SEED`.\n", "* What happens if you try difference values of $n_0$ e.g. 20 and 50?\n", "* What happens if you change $\\alpha$ to 0.1? \n", "\n", "\n", "