{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Using Ax for Human-in-the-loop Experimentation¶" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While Ax can be used in as a fully automated service, generating and deploying candidates Ax can be also used in a trial-by-trial fashion, allowing for human oversight. \n", "\n", "Typically, human intervention in Ax is necessary when there are clear tradeoffs between multiple metrics of interest. Condensing multiple outcomes of interest into a single scalar quantity can be really challenging. Instead, it can be useful to specify an objective and constraints, and tweak these based on the information from the experiment. \n", "\n", "To facilitate this, Ax provides the following key features:\n", "\n", "1. Constrained optimization\n", "2. Interfaces for easily modifying optimization goals\n", "3. Utilities for visualizing and deploying new trials composed of multiple optimizations. \n", "\n", "\n", "In this tutorial, we'll demonstrate how Ax enables users to explore these tradeoffs. With an understanding of the tradeoffs present in our data, we'll then make use of the constrained optimization utilities to generate candidates from multiple different optimization objectives, and create a conglomerate batch, with all of these candidates in together in one trial. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Experiment Setup\n", "\n", "For this tutorial, we will assume our experiment has already been created." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "[INFO 11-04 19:17:50] ax.utils.notebook.plotting: Injecting Plotly library into cell. Do not overwrite or delete cell.\n" ] }, { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from ax import Data, Metric, OptimizationConfig, Objective, OutcomeConstraint, ComparisonOp, load\n", "from ax.modelbridge.cross_validation import cross_validate\n", "from ax.modelbridge.factory import get_GPEI\n", "from ax.plot.diagnostic import tile_cross_validation\n", "from ax.plot.scatter import plot_multiple_metrics, tile_fitted\n", "from ax.utils.notebook.plotting import render, init_notebook_plotting\n", "\n", "import pandas as pd\n", "\n", "init_notebook_plotting()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "experiment = load('hitl_exp.json')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Initial Sobol Trial" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Bayesian Optimization experiments almost always begin with a set of random points. In this experiment, these points were chosen via a Sobol sequence, accessible via the `ModelBridge` factory.\n", "\n", "A collection of points run and analyzed together form a `BatchTrial`. A `Trial` object provides metadata pertaining to the deployment of these points, including details such as when they were deployed, and the current status of their experiment. \n", "\n", "Here, we see an initial experiment has finished running (COMPLETED status)." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "BatchTrial(experiment_name='human_in_the_loop_tutorial', index=0, status=TrialStatus.COMPLETED)" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "experiment.trials[0]" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "datetime.datetime(2019, 3, 29, 18, 10, 6)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "experiment.trials[0].time_created" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "65" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Number of arms in first experiment, including status_quo\n", "len(experiment.trials[0].arms)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "Arm(name='0_0', parameters={'x_excellent': 0.9715802669525146, 'x_good': 0.8615524768829346, 'x_moderate': 0.7668091654777527, 'x_poor': 0.34871453046798706, 'x_unknown': 0.7675797343254089, 'y_excellent': 2.900710028409958, 'y_good': 1.5137152910232545, 'y_moderate': 0.6775947093963622, 'y_poor': 0.4974367544054985, 'y_unknown': 1.0852564811706542, 'z_excellent': 517803.49761247635, 'z_good': 607874.5171427727, 'z_moderate': 1151881.2023103237, 'z_poor': 2927449.2621421814, 'z_unknown': 2068407.6935052872})" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Sample arm configuration\n", "experiment.trials[0].arms[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Experiment Analysis\n", "\n", "**Optimization Config**\n", "\n", "An important construct for analyzing an experiment is an OptimizationConfig. An OptimizationConfig contains an objective, and outcome constraints. Experiment's can have a default OptimizationConfig, but models can also take an OptimizationConfig as input independent of the default.\n", "\n", "**Objective:** A metric to optimize, along with a direction to optimize (default: maximize)\n", "\n", "**Outcome Constraint:** A metric to constrain, along with a constraint direction (<= or >=), as well as a bound. \n", "\n", "Let's start with a simple OptimizationConfig. By default, our objective metric will be maximized, but can be minimized by setting the `minimize` flag. Our outcome constraint will, by default, be evaluated as a relative percentage change. This percentage change is computed relative to the experiment's status quo arm. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Arm(name='status_quo', parameters={'x_excellent': 0, 'x_good': 0, 'x_moderate': 0, 'x_poor': 0, 'x_unknown': 0, 'y_excellent': 1, 'y_good': 1, 'y_moderate': 1, 'y_poor': 1, 'y_unknown': 1, 'z_excellent': 1000000, 'z_good': 1000000, 'z_moderate': 1000000, 'z_poor': 1000000, 'z_unknown': 1000000})" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "experiment.status_quo" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "objective_metric = Metric(name=\"metric_1\")\n", "constraint_metric = Metric(name=\"metric_2\")\n", "\n", "experiment.optimization_config = OptimizationConfig(\n", " objective=Objective(objective_metric),\n", " outcome_constraints=[\n", " OutcomeConstraint(metric=constraint_metric, op=ComparisonOp.LEQ, bound=5),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Data**\n", "\n", "Another critical piece of analysis is data itself! Ax data follows a standard format, shown below. This format is imposed upon the underlying data structure, which is a Pandas DataFrame. \n", "\n", "A key set of fields are required for all data, for use with Ax models. \n", "\n", "It's a good idea to double check our data before fitting models -- let's make sure all of our expected metrics and arms are present." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " | arm_name | \n", "metric_name | \n", "mean | \n", "sem | \n", "trial_index | \n", "start_time | \n", "end_time | \n", "n | \n", "
---|---|---|---|---|---|---|---|---|
0 | \n", "0_1 | \n", "metric_1 | \n", "495.763048 | \n", "2.621641 | \n", "0 | \n", "2019-03-30 | \n", "2019-04-03 | \n", "1599994 | \n", "
1 | \n", "0_23 | \n", "metric_1 | \n", "524.367712 | \n", "2.731647 | \n", "0 | \n", "2019-03-30 | \n", "2019-04-03 | \n", "1596356 | \n", "
2 | \n", "0_14 | \n", "metric_2 | \n", "21.460244 | \n", "0.069457 | \n", "0 | \n", "2019-03-30 | \n", "2019-04-03 | \n", "1600182 | \n", "
3 | \n", "0_53 | \n", "metric_2 | \n", "21.437433 | \n", "0.069941 | \n", "0 | \n", "2019-03-30 | \n", "2019-04-03 | \n", "1601081 | \n", "
4 | \n", "0_53 | \n", "metric_1 | \n", "548.387691 | \n", "2.893486 | \n", "0 | \n", "2019-03-30 | \n", "2019-04-03 | \n", "1601081 | \n", "