{ "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 06-30 21:58:31] 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, json_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": [ { "ename": "TypeError", "evalue": "'module' object is not callable", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m/tmp/ipykernel_3888/2679974815.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexperiment\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mjson_load\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'hitl_exp.json'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: 'module' object is not callable" ] } ], "source": [ "experiment = json_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": { "bento_obj_id": "140009627865944" }, "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": { "bento_obj_id": "140009822034240" }, "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": { "bento_obj_id": "140012816306816" }, "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": { "bento_obj_id": "140009627778744" }, "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.0, 'x_good': 0.0, 'x_moderate': 0.0, 'x_poor': 0.0, 'x_unknown': 0.0, 'y_excellent': 1.0, 'y_good': 1.0, 'y_moderate': 1.0, 'y_poor': 1.0, 'y_unknown': 1.0, 'z_excellent': 1000000.0, 'z_good': 1000000.0, 'z_moderate': 1000000.0, 'z_poor': 1000000.0, 'z_unknown': 1000000.0})" ] }, "execution_count": 7, "metadata": { "bento_obj_id": "140009821742024" }, "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": { "application/vnd.dataresource+json": { "data": [ { "arm_name": "0_1", "end_time": "2019-04-03T00:00:00.000Z", "index": 0, "mean": 495.7630483864, "metric_name": "metric_1", "n": 1599994, "sem": 2.6216409435, "start_time": "2019-03-30T00:00:00.000Z", "trial_index": 0 }, { "arm_name": "0_23", "end_time": "2019-04-03T00:00:00.000Z", "index": 1, "mean": 524.3677121973, "metric_name": "metric_1", "n": 1596356, "sem": 2.7316473644, "start_time": "2019-03-30T00:00:00.000Z", "trial_index": 0 }, { "arm_name": "0_56", "end_time": "2019-04-03T00:00:00.000Z", "index": 2, "mean": 21.8761495501, "metric_name": "metric_2", "n": 1600291, "sem": 0.0718543885, "start_time": "2019-03-30T00:00:00.000Z", "trial_index": 0 }, { "arm_name": "0_42", "end_time": "2019-04-03T00:00:00.000Z", "index": 3, "mean": 533.2995099946, "metric_name": "metric_1", "n": 1601500, "sem": 2.8198433102, "start_time": "2019-03-30T00:00:00.000Z", "trial_index": 0 }, { "arm_name": "0_43", "end_time": "2019-04-03T00:00:00.000Z", "index": 4, "mean": 21.338490998, "metric_name": "metric_2", "n": 1599307, "sem": 0.0694331648, "start_time": "2019-03-30T00:00:00.000Z", "trial_index": 0 } ], "schema": { "fields": [ { "name": "index", "type": "integer" }, { "name": "arm_name", "type": "string" }, { "name": "trial_index", "type": "integer" }, { "name": "end_time", "type": "datetime" }, { "name": "mean", "type": "number" }, { "name": "metric_name", "type": "string" }, { "name": "n", "type": "integer" }, { "name": "sem", "type": "number" }, { "name": "start_time", "type": "datetime" } ], "pandas_version": "0.20.0", "primaryKey": [ "index" ] } }, "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
arm_nametrial_indexend_timemeanmetric_namensemstart_time
00_102019-04-03495.763048metric_115999942.6216412019-03-30
10_2302019-04-03524.367712metric_115963562.7316472019-03-30
20_5602019-04-0321.876150metric_216002910.0718542019-03-30
30_4202019-04-03533.299510metric_116015002.8198432019-03-30
40_4302019-04-0321.338491metric_215993070.0694332019-03-30
\n", "
" ], "text/plain": [ " arm_name trial_index end_time mean metric_name n sem \\\n", "0 0_1 0 2019-04-03 495.763048 metric_1 1599994 2.621641 \n", "1 0_23 0 2019-04-03 524.367712 metric_1 1596356 2.731647 \n", "2 0_56 0 2019-04-03 21.876150 metric_2 1600291 0.071854 \n", "3 0_42 0 2019-04-03 533.299510 metric_1 1601500 2.819843 \n", "4 0_43 0 2019-04-03 21.338491 metric_2 1599307 0.069433 \n", "\n", " start_time \n", "0 2019-03-30 \n", "1 2019-03-30 \n", "2 2019-03-30 \n", "3 2019-03-30 \n", "4 2019-03-30 " ] }, "execution_count": 9, "metadata": { "bento_obj_id": "140009626802104" }, "output_type": "execute_result" } ], "source": [ "data = Data(pd.read_json('hitl_data.json'))\n", "data.df.head()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['0_1', '0_23', '0_56', '0_42', '0_43', '0_25', '0_44', '0_45',\n", " 'status_quo', '0_46', '0_27', '0_47', '0_48', '0_26', '0_49',\n", " '0_12', '0_5', '0_50', '0_51', '0_52', '0_0', '0_57', '0_58',\n", " '0_13', '0_59', '0_14', '0_6', '0_60', '0_61', '0_53', '0_62',\n", " '0_63', '0_7', '0_28', '0_15', '0_16', '0_17', '0_18', '0_19',\n", " '0_29', '0_2', '0_20', '0_21', '0_22', '0_54', '0_3', '0_30',\n", " '0_8', '0_10', '0_31', '0_24', '0_32', '0_33', '0_34', '0_35',\n", " '0_55', '0_36', '0_37', '0_38', '0_9', '0_39', '0_4', '0_11',\n", " '0_40', '0_41'], dtype=object)" ] }, "execution_count": 10, "metadata": { "bento_obj_id": "140009627159648" }, "output_type": "execute_result" } ], "source": [ "data.df['arm_name'].unique()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array(['metric_1', 'metric_2'], dtype=object)" ] }, "execution_count": 11, "metadata": { "bento_obj_id": "140009626807312" }, "output_type": "execute_result" } ], "source": [ "data.df['metric_name'].unique()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Search Space** \n", "\n", "The final component necessary for human-in-the-loop optimization is a SearchSpace. A SearchSpace defines the feasible region for our parameters, as well as their types.\n", "\n", "Here, we have both parameters and a set of constraints on those parameters. \n", "\n", "Without a SearchSpace, our models are unable to generate new candidates. By default, the models will read the search space off of the experiment, when they are told to generate candidates. SearchSpaces can also be specified by the user at this time. Sometimes, the first round of an experiment is too restrictive--perhaps the experimenter was too cautious when defining their initial ranges for exploration! In this case, it can be useful to generate candidates from new, expanded search spaces, beyond that specified in the experiment. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'x_excellent': RangeParameter(name='x_excellent', parameter_type=FLOAT, range=[0.0, 1.0]),\n", " 'x_good': RangeParameter(name='x_good', parameter_type=FLOAT, range=[0.0, 1.0]),\n", " 'x_moderate': RangeParameter(name='x_moderate', parameter_type=FLOAT, range=[0.0, 1.0]),\n", " 'x_poor': RangeParameter(name='x_poor', parameter_type=FLOAT, range=[0.0, 1.0]),\n", " 'x_unknown': RangeParameter(name='x_unknown', parameter_type=FLOAT, range=[0.0, 1.0]),\n", " 'y_excellent': RangeParameter(name='y_excellent', parameter_type=FLOAT, range=[0.1, 3.0]),\n", " 'y_good': RangeParameter(name='y_good', parameter_type=FLOAT, range=[0.1, 3.0]),\n", " 'y_moderate': RangeParameter(name='y_moderate', parameter_type=FLOAT, range=[0.1, 3.0]),\n", " 'y_poor': RangeParameter(name='y_poor', parameter_type=FLOAT, range=[0.1, 3.0]),\n", " 'y_unknown': RangeParameter(name='y_unknown', parameter_type=FLOAT, range=[0.1, 3.0]),\n", " 'z_excellent': RangeParameter(name='z_excellent', parameter_type=FLOAT, range=[50000.0, 5000000.0]),\n", " 'z_good': RangeParameter(name='z_good', parameter_type=FLOAT, range=[50000.0, 5000000.0]),\n", " 'z_moderate': RangeParameter(name='z_moderate', parameter_type=FLOAT, range=[50000.0, 5000000.0]),\n", " 'z_poor': RangeParameter(name='z_poor', parameter_type=FLOAT, range=[50000.0, 5000000.0]),\n", " 'z_unknown': RangeParameter(name='z_unknown', parameter_type=FLOAT, range=[50000.0, 5000000.0])}" ] }, "execution_count": 12, "metadata": { "bento_obj_id": "140009821640096" }, "output_type": "execute_result" } ], "source": [ "experiment.search_space.parameters" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[OrderConstraint(x_poor <= x_moderate),\n", " OrderConstraint(x_moderate <= x_good),\n", " OrderConstraint(x_good <= x_excellent),\n", " OrderConstraint(y_poor <= y_moderate),\n", " OrderConstraint(y_moderate <= y_good),\n", " OrderConstraint(y_good <= y_excellent)]" ] }, "execution_count": 13, "metadata": { "bento_obj_id": "140009797967816" }, "output_type": "execute_result" } ], "source": [ "experiment.search_space.parameter_constraints" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Model Fit\n", "\n", "Fitting BoTorch's GPEI will allow us to predict new candidates based on our first Sobol batch. \n", "Here, we make use of the default settings for GP-EI defined in the ModelBridge factory. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "gp = get_GPEI(\n", " experiment=experiment,\n", " data=data,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can validate the model fits using cross validation, shown below for each metric of interest. Here, our model fits leave something to be desired--the tail ends of each metric are hard to model. In this situation, there are three potential actions to take: \n", "\n", "1. Increase the amount of traffic in this experiment, to reduce the measurement noise.\n", "2. Increase the number of points run in the random batch, to assist the GP in covering the space.\n", "3. Reduce the number of parameters tuned at one time. \n", "\n", "However, away from the tail effects, the fits do show a strong correlations, so we will proceed with candidate generation. " ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "cv_result = cross_validate(gp)\n", "render(tile_cross_validation(cv_result))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The parameters from the initial batch have a wide range of effects on the metrics of interest, as shown from the outcomes from our fitted GP model. " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "render(tile_fitted(gp, rel=True))" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "METRIC_X_AXIS = 'metric_1'\n", "METRIC_Y_AXIS = 'metric_2'\n", "\n", "render(plot_multiple_metrics(\n", " gp,\n", " metric_x=METRIC_X_AXIS,\n", " metric_y=METRIC_Y_AXIS,\n", "))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Candidate Generation\n", "\n", "With our fitted GPEI model, we can optimize EI (Expected Improvement) based on any optimization config.\n", "We can start with our initial optimization config, and aim to simply maximize the playback smoothness, without worrying about the constraint on quality. " ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "unconstrained = gp.gen(\n", " n=3,\n", " optimization_config=OptimizationConfig(\n", " objective=Objective(objective_metric),\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's plot the tradeoffs again, but with our new arms. " ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "render(plot_multiple_metrics(\n", " gp,\n", " metric_x=METRIC_X_AXIS,\n", " metric_y=METRIC_Y_AXIS,\n", " generator_runs_dict={\n", " 'unconstrained': unconstrained,\n", " }\n", "))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Change Objectives" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With our unconstrained optimization, we generate some candidates which are pretty promising with respect to our objective! However, there is a clear regression in our constraint metric, above our initial 5% desired constraint. Let's add that constraint back in. " ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "constraint_5 = OutcomeConstraint(metric=constraint_metric, op=ComparisonOp.LEQ, bound=5)\n", "constraint_5_results = gp.gen(\n", " n=3, \n", " optimization_config=OptimizationConfig(\n", " objective=Objective(objective_metric),\n", " outcome_constraints=[constraint_5]\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This yields a *GeneratorRun*, which contains points according to our specified optimization config, along with metadata about how the points were generated. Let's plot the tradeoffs in these new points. " ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from ax.plot.scatter import plot_multiple_metrics\n", "render(plot_multiple_metrics(\n", " gp,\n", " metric_x=METRIC_X_AXIS,\n", " metric_y=METRIC_Y_AXIS,\n", " generator_runs_dict={\n", " 'constraint_5': constraint_5_results\n", " }\n", "))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is important to note that the treatment of constraints in GP EI is probabilistic. The acquisition function weights our objective by the probability that each constraint is feasible. Thus, we may allow points with a very small probability of violating the constraint to be generated, as long as the chance of the points increasing our objective is high enough. \n", "\n", "You can see above that the point estimate for each point is significantly below a 5% increase in the constraint metric, but that there is uncertainty in our prediction, and the tail probabilities do include probabilities of small regressions beyond 5%. " ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": true }, "outputs": [], "source": [ "constraint_1 = OutcomeConstraint(metric=constraint_metric, op=ComparisonOp.LEQ, bound=1)\n", "constraint_1_results = gp.gen(\n", " n=3, \n", " optimization_config=OptimizationConfig(\n", " objective=Objective(objective_metric),\n", " outcome_constraints=[constraint_1],\n", " )\n", ")" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "render(plot_multiple_metrics(\n", " gp,\n", " metric_x=METRIC_X_AXIS,\n", " metric_y=METRIC_Y_AXIS,\n", " generator_runs_dict={\n", " \"constraint_1\": constraint_1_results,\n", " }\n", "))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, let's view all three sets of candidates together. " ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "render(plot_multiple_metrics(\n", " gp,\n", " metric_x=METRIC_X_AXIS,\n", " metric_y=METRIC_Y_AXIS,\n", " generator_runs_dict={\n", " 'unconstrained': unconstrained,\n", " 'loose_constraint': constraint_5_results,\n", " 'tight_constraint': constraint_1_results,\n", " }\n", "))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating a New Trial\n", "\n", "Having done the analysis and candidate generation for three different optimization configs, we can easily create a new `BatchTrial` which combines the candidates from these three different optimizations. Each set of candidates looks promising -- the point estimates are higher along both metric values than in the previous batch. However, there is still a good bit of uncertainty in our predictions. It is hard to choose between the different constraint settings without reducing this noise, so we choose to run a new trial with all three constraint settings. However, we're generally convinced that the tight constraint is too conservative. We'd still like to reduce our uncertainty in that region, but we'll only take one arm from that set." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "BatchTrial(experiment_name='human_in_the_loop_tutorial', index=1, status=TrialStatus.CANDIDATE)" ] }, "execution_count": 25, "metadata": { "bento_obj_id": "140009539295832" }, "output_type": "execute_result" } ], "source": [ "# We can add entire generator runs, when constructing a new trial. \n", "trial = experiment.new_batch_trial().add_generator_run(unconstrained).add_generator_run(constraint_5_results)\n", "\n", "# Or, we can hand-pick arms. \n", "trial.add_arm(constraint_1_results.arms[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The arms are combined into a single trial, along with the `status_quo` arm. Their generator can be accessed from the trial as well. " ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Arm(name='1_0', parameters={'x_excellent': 0.7508829334076487, 'x_good': 0.40367960200772224, 'x_moderate': 0.3140989976643642, 'x_poor': 0.14559932559274122, 'x_unknown': 0.6670211538978944, 'y_excellent': 2.5425636846330546, 'y_good': 1.9418098243025033, 'y_moderate': 0.9858391295658283, 'y_poor': 0.38273584643959624, 'y_unknown': 1.5806965342880184, 'z_excellent': 4489287.686108519, 'z_good': 3540253.5809771204, 'z_moderate': 2964805.1608829396, 'z_poor': 2033780.6048510857, 'z_unknown': 2032062.1986594186}),\n", " Arm(name='1_1', parameters={'x_excellent': 0.6476003872239288, 'x_good': 0.31744410468794715, 'x_moderate': 0.17169895733661983, 'x_poor': 0.07453169788730113, 'x_unknown': 0.8642007362896725, 'y_excellent': 2.447230141007133, 'y_good': 1.5376602958384886, 'y_moderate': 0.6811637025094822, 'y_poor': 0.3318520722136259, 'y_unknown': 2.2510516551441038, 'z_excellent': 4072426.2914976524, 'z_good': 3806352.1749653243, 'z_moderate': 1645911.1218927982, 'z_poor': 988167.2494331661, 'z_unknown': 2661963.3926857742}),\n", " Arm(name='1_2', parameters={'x_excellent': 0.8054293536015693, 'x_good': 0.4404336669655842, 'x_moderate': 0.40141237536705926, 'x_poor': 0.22362006144561955, 'x_unknown': 0.5903430271180998, 'y_excellent': 2.617804090324439, 'y_good': 2.298442483961, 'y_moderate': 1.1690922032735336, 'y_poor': 0.5681654145954245, 'y_unknown': 1.3031360054446643, 'z_excellent': 4462167.1702239, 'z_good': 3731098.73420372, 'z_moderate': 3994655.203366427, 'z_poor': 2673298.8942999635, 'z_unknown': 1872273.8740227316}),\n", " Arm(name='1_3', parameters={'x_excellent': 0.7781327371696715, 'x_good': 0.57174929946374, 'x_moderate': 0.38386054557497773, 'x_poor': 0.1483239531374575, 'x_unknown': 0.6290782831583654, 'y_excellent': 2.5413971960197395, 'y_good': 1.8911813925901382, 'y_moderate': 1.0329065458855364, 'y_poor': 0.41007035875080056, 'y_unknown': 1.6406159955920543, 'z_excellent': 4255174.283604716, 'z_good': 3499788.950775458, 'z_moderate': 3071450.711177156, 'z_poor': 2269641.4509550007, 'z_unknown': 2090271.054327287}),\n", " Arm(name='1_4', parameters={'x_excellent': 0.6900739925384755, 'x_good': 0.5544791798816763, 'x_moderate': 0.22055916168207798, 'x_poor': 0.10245330233132562, 'x_unknown': 0.8355320141299903, 'y_excellent': 2.4681759096597897, 'y_good': 1.3517329904980873, 'y_moderate': 0.7109854013391809, 'y_poor': 0.2659656900117545, 'y_unknown': 2.069519817354787, 'z_excellent': 4019003.1305046123, 'z_good': 3708773.5492286514, 'z_moderate': 1891304.5997673508, 'z_poor': 1257805.979820268, 'z_unknown': 3209971.194920286}),\n", " Arm(name='1_5', parameters={'x_excellent': 0.84017169665951, 'x_good': 0.5080744603806646, 'x_moderate': 0.4093403112065996, 'x_poor': 0.26313460758317314, 'x_unknown': 0.5983032148893116, 'y_excellent': 2.589525158599443, 'y_good': 2.2354290056846433, 'y_moderate': 1.1617987885088201, 'y_poor': 0.7150067923774204, 'y_unknown': 1.5015776169699209, 'z_excellent': 3959983.5534502217, 'z_good': 3990619.622250669, 'z_moderate': 4302002.350836964, 'z_poor': 2736761.6846693275, 'z_unknown': 2962895.922472194}),\n", " Arm(name='1_6', parameters={'x_excellent': 0.7934346148309306, 'x_good': 0.7255504688128516, 'x_moderate': 0.46906013571592303, 'x_poor': 0.12673747942806995, 'x_unknown': 0.6730366227643254, 'y_excellent': 2.5406749421774055, 'y_good': 1.8477325872737815, 'y_moderate': 0.9485910267823123, 'y_poor': 0.2917996437995578, 'y_unknown': 1.4650474269621556, 'z_excellent': 3823503.8905472592, 'z_good': 3244042.3595880833, 'z_moderate': 2447219.757960169, 'z_poor': 2597221.69228601, 'z_unknown': 1804522.1057251126}),\n", " Arm(name='status_quo', parameters={'x_excellent': 0.0, 'x_good': 0.0, 'x_moderate': 0.0, 'x_poor': 0.0, 'x_unknown': 0.0, 'y_excellent': 1.0, 'y_good': 1.0, 'y_moderate': 1.0, 'y_poor': 1.0, 'y_unknown': 1.0, 'z_excellent': 1000000.0, 'z_good': 1000000.0, 'z_moderate': 1000000.0, 'z_poor': 1000000.0, 'z_unknown': 1000000.0})]" ] }, "execution_count": 26, "metadata": { "bento_obj_id": "140009573436168" }, "output_type": "execute_result" } ], "source": [ "experiment.trials[1].arms" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The original `GeneratorRuns` can be accessed from within the trial as well. This is useful for later analyses, allowing introspection of the `OptimizationConfig` used for generation (as well as other information, e.g. `SearchSpace` used for generation)." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[GeneratorRunStruct(generator_run=GeneratorRun(3 arms, total weight 3.0), weight=1.0),\n", " GeneratorRunStruct(generator_run=GeneratorRun(3 arms, total weight 3.0), weight=1.0),\n", " GeneratorRunStruct(generator_run=GeneratorRun(1 arms, total weight 1.0), weight=1.0)]" ] }, "execution_count": 27, "metadata": { "bento_obj_id": "140009539240520" }, "output_type": "execute_result" } ], "source": [ "experiment.trials[1]._generator_run_structs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we can see the unconstrained set-up used for our first set of candidates. " ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OptimizationConfig(objective=Objective(metric_name=\"metric_1\", minimize=False), outcome_constraints=[])" ] }, "execution_count": 28, "metadata": { "bento_obj_id": "140009539294936" }, "output_type": "execute_result" } ], "source": [ "experiment.trials[1]._generator_run_structs[0].generator_run.optimization_config" ] } ], "metadata": { "kernelspec": { "display_name": "python3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 2 }