From 1d118f67452df459780d41453016314c784ec532 Mon Sep 17 00:00:00 2001 From: Miles Olson Date: Wed, 19 Feb 2025 08:21:26 -0800 Subject: [PATCH] Human in the Loop tutorial (#3388) Summary: Pull Request resolved: https://github.com/facebook/Ax/pull/3388 Tutorial demonstrating ask-tell usage for maximizing strength in a 3D printing project. Advanced features (MOO, storage, partial data, etc) not covered. Reviewed By: lena-kashtelyan Differential Revision: D69478989 --- .../human_in_the_loop/human_in_the_loop.ipynb | 569 ++++++++++++++++++ website/tutorials.json | 4 + 2 files changed, 573 insertions(+) create mode 100644 tutorials/human_in_the_loop/human_in_the_loop.ipynb diff --git a/tutorials/human_in_the_loop/human_in_the_loop.ipynb b/tutorials/human_in_the_loop/human_in_the_loop.ipynb new file mode 100644 index 00000000000..6aa8fd5768e --- /dev/null +++ b/tutorials/human_in_the_loop/human_in_the_loop.ipynb @@ -0,0 +1,569 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "30b21773-7e2c-41bf-990f-a6c2aa89be06", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "# Ask-tell Optimization in a Human-in-the-loop Setting\n", + "\n", + "Some optimization experiments, like the one described in LINK, can be conducted in a completely automated manner.\n", + "Other experiments may require a human in the loop, for instance a scientist manually conducting and evaluating each trial in a lab.\n", + "In this tutorial we demonstrate this ask-tell optimization in a human-in-the-loop setting by imagining the task of maximizing the strength of a 3D printed part using compression testing (i.e., crushing the part) where different print settings will have to be manually tried and evaluated.\n", + "\n", + "\n", + "### Background\n", + "\n", + "In 3D printing, several parameters can significantly affect the mechanical properties of the printed object:\n", + "\n", + "- **Infill Density**: The percentage of material used inside the object. Higher infill density generally increases strength but also weight and material usage.\n", + "- **Layer Height**: The thickness of each layer of material. Smaller layer heights can improve surface finish and detail but increase print time.\n", + "- **Infill Type**: The pattern used to fill the interior of the object. Different patterns (e.g., honeycomb, gyroid, lines, rectilinear) offer various balances of strength, speed, and material efficiency.\n", + "\n", + "- **Strength Measurement**: In this tutorial, we assume the strength of the 3D printed part is measured using compression testing, which evaluates how the object performs under compressive stress.\n", + "\n", + "### Learning Objectives\n", + "- Understand black box optimization concepts\n", + "- Define an optimization problem using Ax\n", + "- Configure and run an experiment using Ax's `Client`\n", + "- Analyze the results of the optimization\n", + "\n", + "### Prerequisites\n", + "- Familiarity with Python and basic programming concepts\n", + "- Understanding of adaptive experimentation and Bayesian optimization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "language": "markdown", + "originalKey": "8f38bebe-cf19-42e9-a73f-064df271299a", + "showInput": true + }, + "source": [ + "## Step 1: Import Necessary Modules" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739301999671, + "executionStopTime": 1739302002547, + "isAgentGenerated": false, + "language": "python", + "originalKey": "5e8f7986-eb55-4dae-b63e-329760017aab", + "outputsInitialized": true, + "requestMsgId": "5e8f7986-eb55-4dae-b63e-329760017aab", + "serverExecutionDuration": 2591.1049650749 + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "from ax.preview.api.client import Client\n", + "from ax.preview.api.configs import ExperimentConfig, RangeParameterConfig, ChoiceParameterConfig, ParameterType, GenerationStrategyConfig" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "a766f271-0d97-4c01-96dc-64e92b6cd713", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 2: Initialize Client\n", + "\n", + "Create an instance of the `Client` to manage the state of your experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739302212796, + "executionStopTime": 1739302213094, + "isAgentGenerated": false, + "language": "python", + "originalKey": "9a08d007-be86-42ec-8f32-f4102548c2e0", + "outputsInitialized": true, + "requestMsgId": "9a08d007-be86-42ec-8f32-f4102548c2e0", + "serverExecutionDuration": 1.4741730410606 + }, + "outputs": [], + "source": [ + "client = Client(random_seed=42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "ff991a0a-6522-4aa6-a7ef-d8a494a207bb", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 3: Configure Experiment\n", + "\n", + "Define the parameters for the 3D printing optimization problem.\n", + "The infill density and layer height can take on any value within their respective bounds so we will configure both using `RangeParameterConfig`s.\n", + "On the other hand, infill type be either have one of four distinct values: \"honeycomb\", \"gyroid\", \"lines\", or \"rectilinear\".\n", + "We will use a `ChoiceParameterConfig` to represent it in the optimization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739302215052, + "executionStopTime": 1739302216000, + "isAgentGenerated": false, + "language": "python", + "originalKey": "62ead57f-d835-4d67-a095-f64a818a97fe", + "outputsInitialized": true, + "requestMsgId": "62ead57f-d835-4d67-a095-f64a818a97fe", + "serverExecutionDuration": 2.0144580630586 + }, + "outputs": [], + "source": [ + "infill_density = RangeParameterConfig(name=\"infill_density\", parameter_type=ParameterType.FLOAT, bounds=(0, 100))\n", + "layer_height = RangeParameterConfig(name=\"layer_height\", parameter_type=ParameterType.FLOAT, bounds=(0.1, 0.4))\n", + "infill_type = ChoiceParameterConfig(name=\"infill_type\", parameter_type=ParameterType.STRING, values=[\"honeycomb\", \"gyroid\", \"lines\", \"rectilinear\"])\n", + "\n", + "experiment_config = ExperimentConfig(\n", + " name=\"3d_print_strength_experiment\",\n", + " parameters=[infill_density, layer_height, infill_type],\n", + " # The following arguments are optional\n", + " description=\"Maximize strength of 3D printed parts\",\n", + " owner=\"developer\",\n", + ")\n", + "\n", + "client.configure_experiment(experiment_config=experiment_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "d259ac8b-55c4-410f-bc8a-f690b877a7e2", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 4: Configure Optimization\n", + "We want to maximize the compressive strength of our part, so we will set the objective to `compressive_strength`.\n", + "However, we know that modifying the infill density, layer height, and infill type will affect the weight of the part as well.\n", + "We'll include a requirement that the part must not weigh more than 10 grams by setting an outcome constraint when we call `configure_experiment`.\n", + "\n", + "The following code will tell the `Client` that we intend to maximize compressive strength while keeping the weight less than 10 grams." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739302224108, + "executionStopTime": 1739302224462, + "isAgentGenerated": false, + "language": "python", + "originalKey": "76e7739a-148b-495f-a3a7-6e7fa9f5007d", + "outputsInitialized": true, + "requestMsgId": "76e7739a-148b-495f-a3a7-6e7fa9f5007d", + "serverExecutionDuration": 32.510571996681 + }, + "outputs": [], + "source": [ + "client.configure_optimization(objective=\"compressive_strength\", outcome_constraints=[\"weight <= 10\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "3882f4bd-c7c3-4fff-9754-a9c97b27c5a0", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 5: Run Trials\n", + "\n", + "Now the `Client` has been configured we can begin conducting the experiment.\n", + "Use `attach_trial` to attach any existing data, use `get_next_trials` to generate parameter suggestions, and use `complete_trial` to report manually observed results." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "ec740d5d-c42e-44d6-bc3b-1659df2aeb9c", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "### Attach Preexisting Trials\n", + "\n", + "Sometimes in our optimization experiments we may already have some previously collected data from manual \"trials\" conducted before the Ax experiment began.\n", + "This can be incredibly useful!\n", + "If we attach this data as custom trials, Ax will be able to use the data points in its optimization algorithm and improve performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739302378450, + "executionStopTime": 1739302378753, + "isAgentGenerated": false, + "language": "python", + "originalKey": "a46ec55d-4848-4ea2-8adf-5cc0eabee74f", + "outputsInitialized": true, + "requestMsgId": "a46ec55d-4848-4ea2-8adf-5cc0eabee74f", + "serverExecutionDuration": 78.725646948442 + }, + "outputs": [], + "source": [ + "# Pairs of previously evaluated parameterizations and associated metric readings\n", + "preexisting_trials = [\n", + " (\n", + " {\"infill_density\": 10.43, \"layer_height\": 0.3, \"infill_type\": \"gyroid\"},\n", + " {\"compressive_strength\": 1.74, \"weight\": 0.52},\n", + " ),\n", + " (\n", + " {\"infill_density\": 55.54, \"layer_height\": 0.12, \"infill_type\": \"lines\"},\n", + " {\"compressive_strength\": 4.63, \"weight\": 2.31},\n", + " ),\n", + " (\n", + " {\"infill_density\": 99.43, \"layer_height\": 0.35, \"infill_type\": \"rectilinear\"},\n", + " {\"compressive_strength\": 5.68, \"weight\": 2.84},\n", + " ),\n", + " (\n", + " {\"infill_density\": 41.44, \"layer_height\": 0.21, \"infill_type\": \"rectilinear\"},\n", + " {\"compressive_strength\": 3.95, \"weight\": 1.97},\n", + " ),\n", + " (\n", + " {\"infill_density\": 27.23, \"layer_height\": 0.37, \"infill_type\": \"honeycomb\"},\n", + " {\"compressive_strength\": 7.36, \"weight\": 3.31},\n", + " ),\n", + " (\n", + " {\"infill_density\": 33.57, \"layer_height\": 0.24, \"infill_type\": \"honeycomb\"},\n", + " {\"compressive_strength\": 13.99, \"weight\": 6.29},\n", + " ),\n", + "]\n", + "\n", + "for parameters, data in preexisting_trials:\n", + " # Attach the parameterization to the Client as a trial and immediately complete it with the preexisting data\n", + " trial_index = client.attach_trial(parameters=parameters)\n", + " client.complete_trial(trial_index=trial_index, raw_data=data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "b0d142bb-ec8f-40ea-91c7-4dcaff78e017", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "### Ask for trials\n", + "\n", + "Now, let's have Ax suggest which trials to evaluate so that we can find the optimal configuration more efficiently.\n", + "We'll do this by calling `get_next_trials`.\n", + "We'll make use of Ax's support for parallelism, i.e. suggesting more than one trial at a time -- this can allow us to conduct our experiment much faster!\n", + "If our lab had three identical 3D printers, we could ask Ax for a batch of three trials and evaluate three different infill density, layer height, and infill types at once.\n", + "\n", + "Note that there will always be a tradeoff between \"parallelism\" and optimization performance since the quality of a suggested trial is often proportional to the amount of data Ax has access to, see LINK for a more detailed explanation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739303219138, + "executionStopTime": 1739303228549, + "isAgentGenerated": false, + "language": "python", + "originalKey": "ca30046b-8991-48dd-9054-f77a69ca3818", + "outputsInitialized": true, + "requestMsgId": "ca30046b-8991-48dd-9054-f77a69ca3818", + "serverExecutionDuration": 9018.6875869986 + }, + "outputs": [], + "source": [ + "trials = client.get_next_trials(maximum_trials=3)\n", + "trials" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "befe8927-94f8-49ad-960c-bac135191cc7", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "### Tell Ax the results\n", + "\n", + "In a real-world scenerio we would print parts using the three suggested parameterizations and measure the compressive strength and weight manually, though in this tutorial we will simulate by calling a function.\n", + "Once the data is collected we will tell Ax the result by calling `complete_trial`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739303299312, + "executionStopTime": 1739303299558, + "isAgentGenerated": false, + "language": "python", + "originalKey": "c5608094-e4f6-444e-9feb-3f21b3ee0d5c", + "outputsInitialized": true, + "requestMsgId": "c5608094-e4f6-444e-9feb-3f21b3ee0d5c", + "serverExecutionDuration": 31.877135974355 + }, + "outputs": [], + "source": [ + "def evaluate(\n", + " infill_density: float, layer_height: float, infill_type: str\n", + ") -> dict[str, float]:\n", + " strength_map = {\"lines\": 1, \"rectilinear\": 2, \"gyroid\": 5, \"honeycomb\": 10}\n", + " weight_map = {\"lines\": 1, \"rectilinear\": 2, \"gyroid\": 3, \"honeycomb\": 9}\n", + "\n", + " return {\n", + " \"compressive_strength\": (\n", + " infill_density / layer_height * strength_map[infill_type]\n", + " )\n", + " / 100,\n", + " \"weight\": (infill_density / layer_height * weight_map[infill_type]) / 200,\n", + " }\n", + "\n", + "\n", + "for trial_index, parameters in trials.items():\n", + " client.complete_trial(trial_index=trial_index, raw_data=evaluate(**parameters))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "customInput": null, + "language": "markdown", + "originalKey": "4cdda4f1-a7aa-4ee9-af0a-c1b59d605d21", + "showInput": true + }, + "source": [ + "We'll repeat this process a number of times.\n", + "Typically experimentation will continue until a satisfactory combination has been found, experimentation resources (in this example our 3D printing filliment) have been exhausted, or we feel we have spent enough time on optimization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customInput": null, + "executionStartTime": 1739304312859, + "executionStopTime": 1739304423646, + "language": "python", + "originalKey": "326411ea-a32d-4adf-af57-90dcb2068210", + "outputsInitialized": true, + "requestMsgId": "326411ea-a32d-4adf-af57-90dcb2068210", + "serverExecutionDuration": 110571.35569898, + "showInput": true + }, + "outputs": [], + "source": [ + "# Ask Ax for the next trials\n", + "trials = client.get_next_trials(maximum_trials=3)\n", + "trials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "customInput": null, + "executionStartTime": 1739304518872, + "executionStopTime": 1739304519200, + "language": "python", + "originalKey": "350e994b-04af-40ce-a06c-49b0ac50cfeb", + "outputsInitialized": true, + "requestMsgId": "350e994b-04af-40ce-a06c-49b0ac50cfeb", + "serverExecutionDuration": 33.687502960674, + "showInput": true + }, + "outputs": [], + "source": [ + "# Tell Ax the result of those trials\n", + "for trial_index, parameters in trials.items():\n", + " client.complete_trial(trial_index=trial_index, raw_data=evaluate(**parameters))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "customInput": null, + "language": "python", + "originalKey": "7ec66975-ce39-4b23-b34b-c28433915204", + "showInput": true + }, + "outputs": [], + "source": [ + "# Ask Ax for the next trials\n", + "trials = client.get_next_trials(maximum_trials=3)\n", + "trials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "customInput": null, + "language": "python", + "originalKey": "18489525-1f14-4285-806e-f3218743975f", + "showInput": true + }, + "outputs": [], + "source": [ + "# Tell Ax the result of those trials\n", + "for trial_index, parameters in trials.items():\n", + " client.complete_trial(trial_index=trial_index, raw_data=evaluate(**parameters))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "b65f8045-23b4-49f1-ae7a-b7c3e936bfc9", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 6: Analyze Results\n", + "\n", + "At any time during the experiment you may analyze the results of the experiment.\n", + "Most commonly this means extracting the parameterization from the best performing trial you conducted.\n", + "The best trial will have the optimal objective value **without violating any outcome constraints**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "isAgentGenerated": false, + "language": "python", + "originalKey": "06b6978f-53f6-45fa-8383-b9c19ec33a82", + "outputsInitialized": false + }, + "outputs": [], + "source": [ + "best_parameters, prediction, index, name = client.get_best_parameterization()\n", + "print(\"Best Parameters:\", best_parameters)\n", + "print(\"Prediction (mean, variance):\", prediction)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "8c64d2d0-394c-49db-8594-0b50b45524ff", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Step 7: Compute Analyses\n", + "\n", + "Ax can also produce a number of analyses to help interpret the results of the experiment via `client.compute_analyses`.\n", + "Users can manually select which analyses to run, or can allow Ax to select which would be most relevant.\n", + "In this case Ax selects the following:\n", + "* **Parrellel Coordinates Plot** shows which parameterizations were evaluated and what metric values were observed -- this is useful for getting a high level overview of how thoroughly the search space was explored and which regions tend to produce which outcomes\n", + "* **Scatter Plot** shows both metric values for each trial -- this is useful for understanding the tradeoffs between competing metrics, like compressive strength and weight.\n", + "* **Interaction Analysis Plot** shows which parameters have the largest affect on the function and plots the most important parameters as 1 or 2 dimensional surfaces\n", + "* **Summary** lists all trials generated along with their parameterizations, observations, and miscellaneous metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "executionStartTime": 1739304525265, + "executionStopTime": 1739304526128, + "isAgentGenerated": false, + "language": "python", + "originalKey": "c5572f5c-7efe-4945-88b2-73229813ee7c", + "outputsInitialized": true, + "requestMsgId": "c5572f5c-7efe-4945-88b2-73229813ee7c", + "serverExecutionDuration": 554.08962001093 + }, + "outputs": [], + "source": [ + "client.compute_analyses()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "isAgentGenerated": false, + "language": "markdown", + "originalKey": "a255016e-650c-4e96-a931-136ac4f79fef", + "outputsInitialized": false, + "showInput": true + }, + "source": [ + "## Conclusion\n", + "\n", + "This tutorial demonstrates how to use Ax's `Client` for optimizing the strength of 3D printed parts in a human-in-the-loop setting. By iteratively collecting data and refining parameters, you can effectively apply black box optimization to real-world experiments." + ] + } + ], + "metadata": { + "fileHeader": "", + "fileUid": "2ee3b48f-213b-4325-a33c-488fe0f4f900", + "isAdHoc": false, + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + }, + "operator_data": [] + } +} diff --git a/website/tutorials.json b/website/tutorials.json index 343eff31a3d..cedd47c994c 100644 --- a/website/tutorials.json +++ b/website/tutorials.json @@ -3,6 +3,10 @@ { "id": "ask_tell", "title": "Ask-tell Optimization with Ax" + }, + { + "id": "human_in_the_loop", + "title": "Ask-tell Optimization in a Human-in-the-loop Setting" } ] }