at Center for Statistics and Machine Learning, Princeton on Dec 10, 2018 [jupyter][google colab][reveal]
Neil D. Lawrence, Amazon Cambridge and University of Sheffield

#### Abstract

Machine learning is a data driven endeavour, but real world systems are physical and mechanistic. In this talk we will review approaches to integrating machine learning with real world systems. Our focus will be on emulation (otherwise known as surrogate modeling).

## Boulton and Watt’s Steam Engine

James Watt’s steam engine contained an early machine learning device. In the same way that modern systems are component based, his engine was composed of components. One of which is a speed regulator sometimes known as Watt’s governor. The two balls in the center of the image, when spun fast, rise, and through a linkage mechanism.

The centrifugal governor was made famous by Boulton and Watt when it was deployed in the steam engine. Studying stability in the governor is the main subject of James Clerk Maxwell’s paper on the theoretical analysis of governors (Maxwell, 1867). This paper is a founding paper of control theory. In an acknowledgment of its influence, Wiener used the name cybernetics to describe the field of control and communication in animals and the machine (Wiener, 1948). Cybernetics is the Greek word for governor, which comes from the latin for helmsman.

A governor is one of the simplest artificial intelligence systems. It senses the speed of an engine, and acts to change the position of the valve on the engine to slow it down.

Although it’s a mechanical system a governor can be seen as automating a role that a human would have traditionally played. It is an early example of artificial intelligence.

The centrifugal governor has several parameters, the weight of the balls used, the length of the linkages and the limits on the balls movement.

Two principle differences exist between the centrifugal governor and artificial intelligence systems of today.

1. The centrifugal governor is a physical system and it is an integral part of a wider physical system that it regulates (the engine).
2. The parameters of the governor were set by hand, our modern artificial intelligence systems have their parameters set by data.

This has the basic components of sense and act that we expect in an intelligent system, and this system saved the need for a human operator to manually adjust the system in the case of overspeed. Overspeed has the potential to destroy an engine, so the governor operates as a safety device.

The first wave of automation did bring about sabotoage as a worker’s response. But if machinery was sabotaged, for example, if the linkage between sensor (the spinning balls) and action (the valve closure) was broken, this would be obvious to the engine operator at start up time. The machine could be repaired before operation.

## What is Machine Learning?

Machine learning allows us to extract knowledge from data to form a prediction.

$\text{data} + \text{model} \stackrel{\text{compute}}{\rightarrow} \text{prediction}$

A machine learning prediction is made by combining a model with data to form the prediction. The manner in which this is done gives us the machine learning algorithm.

Machine learning models are mathematical models which make weak assumptions about data, e.g. smoothness assumptions. By combining these assumptions with the data, we observe we can interpolate between data points or, occasionally, extrapolate into the future.

Machine learning is a technology which strongly overlaps with the methodology of statistics. From a historical/philosophical view point, machine learning differs from statistics in that the focus in the machine learning community has been primarily on accuracy of prediction, whereas the focus in statistics is typically on the interpretability of a model and/or validating a hypothesis through data collection.

The rapid increase in the availability of compute and data has led to the increased prominence of machine learning. This prominence is surfacing in two different but overlapping domains: data science and artificial intelligence.

## From Model to Decision

The real challenge, however, is end-to-end decision making. Taking information from the environment and using it to drive decision making to achieve goals.

## Artificial Intelligence and Data Science

Artificial intelligence has the objective of endowing computers with human-like intelligent capabilities. For example, understanding an image (computer vision) or the contents of some speech (speech recognition), the meaning of a sentence (natural language processing) or the translation of a sentence (machine translation).

### Supervised Learning for AI

The machine learning approach to artificial intelligence is to collect and annotate a large data set from humans. The problem is characterized by input data (e.g. a particular image) and a label (e.g. is there a car in the image yes/no). The machine learning algorithm fits a mathematical function (I call this the prediction function) to map from the input image to the label. The parameters of the prediction function are set by minimizing an error between the function’s predictions and the true data. This mathematical function that encapsulates this error is known as the objective function.

This approach to machine learning is known as supervised learning. Various approaches to supervised learning use different prediction functions, objective functions or different optimization algorithms to fit them.

For example, deep learning makes use of neural networks to form the predictions. A neural network is a particular type of mathematical function that allows the algorithm designer to introduce invariances into the function.

An invariance is an important way of including prior understanding in a machine learning model. For example, in an image, a car is still a car regardless of whether it’s in the upper left or lower right corner of the image. This is known as translation invariance. A neural network encodes translation invariance in convolutional layers. Convolutional neural networks are widely used in image recognition tasks.

An alternative structure is known as a recurrent neural network (RNN). RNNs neural networks encode temporal structure. They use auto regressive connections in their hidden layers, they can be seen as time series models which have non-linear auto-regressive basis functions. They are widely used in speech recognition and machine translation.

Machine learning has been deployed in Speech Recognition (e.g. Alexa, deep neural networks, convolutional neural networks for speech recognition), in computer vision (e.g. Amazon Go, convolutional neural networks for person recognition and pose detection).

The field of data science is related to AI, but philosophically different. It arises because we are increasingly creating large amounts of data through happenstance rather than active collection. In the modern era data is laid down by almost all our activities. The objective of data science is to extract insights from this data.

Classically, in the field of statistics, data analysis proceeds by assuming that the question (or scientific hypothesis) comes before the data is created. E.g., if I want to determine the effectiveness of a particular drug, I perform a design for my data collection. I use foundational approaches such as randomization to account for confounders. This made a lot of sense in an era where data had to be actively collected. The reduction in cost of data collection and storage now means that many data sets are available which weren’t collected with a particular question in mind. This is a challenge because bias in the way data was acquired can corrupt the insights we derive. We can perform randomized control trials (or A/B tests) to verify our conclusions, but the opportunity is to use data science techniques to better guide our question selection or even answer a question without the expense of a full randomized control trial (referred to as A/B testing in modern internet parlance).

• There is a gap between the world of data science and AI.
• The mapping of the virtual onto the physical world.
• E.g. Causal understanding.

## Supply Chain

On Sunday mornings in Sheffield, I often used to run across Packhorse Bridge in Burbage valley. The bridge is part of an ancient network of trails crossing the Pennines that, before Turnpike roads arrived in the 18th century, was the main way in which goods were moved. Given that the moors around Sheffield were home to sand quarries, tin mines, lead mines and the villages in the Derwent valley were known for nail and pin manufacture, this wasn’t simply movement of agricultural goods, but it was the infrastructure for industrial transport.

The profession of leading the horses was known as a Jagger and leading out of the village of Hathersage is Jagger’s Lane, a trail that headed underneath Stanage Edge and into Sheffield.

The movement of goods from regions of supply to areas of demand is fundamental to our society. The physical infrastructure of supply chain has evolved a great deal over the last 300 years.

## Cromford

Richard Arkwright is known as the father of the modern factory system. In 1771 he set up a Mill for spinning cotton yarn in the village of Cromford, in the Derwent Valley. The Derwent valley is relatively inaccessible. Raw cotton arrived in Liverpool from the US and India. It needed to be transported on packhorse across the bridleways of the Pennines. But Cromford was a good location due to proximity to Nottingham, where weavers where consuming the finished thread, and the availability of water power from small tributaries of the Derwent river for Arkwright’s water frames which automated the production of yarn from raw cotton.

By 1794 the Cromford Canal was opened to bring coal in to Cromford and give better transport to Nottingham. The construction of the canals was driven by the need to improve the transport infrastructure, facilitating the movement of goods across the UK. Canals, roads and railways were initially constructed by the economic need for moving goods. To improve supply chain.

The A6 now does pass through Cromford, but at the time he moved there there was merely a track. The High Peak Railway was opened in 1832, it is now converted to the High Peak Trail, but it remains the highest railway built in Britain.

Cooper (1991)

## Containerization

Containerization has had a dramatic effect on global economics, placing many people in the developing world at the end of the supply chain.

For example, you can buy Wild Alaskan Cod fished from Alaska, processed in China, sold in North America. This is driven by the low cost of transport for frozen cod vs the higher relative cost of cod processing in the US versus China. Similarly, Scottish prawns are also processed in China for sale in the UK.

This effect on cost of transport vs cost of processing is the main driver of the topology of the modern supply chain and the associated effect of globalization. If transport is much cheaper than processing, then processing will tend to agglomerate in places where processing costs can be minimized.

Large scale global economic change has principally been driven by changes in the technology that drives supply chain.

Supply chain is a large-scale automated decision making network. Our aim is to make decisions not only based on our models of customer behavior (as observed through data), but also by accounting for the structure of our fulfilment center, and delivery network.

Many of the most important questions in supply chain take the form of counterfactuals. E.g. “What would happen if we opened a manufacturing facility in Cambridge?” A counter factual is a question that implies a mechanistic understanding of a system. It goes beyond simple smoothness assumptions or translation invariants. It requires a physical, or mechanistic understanding of the supply chain network. For this reason, the type of models we deploy in supply chain often involve simulations or more mechanistic understanding of the network.

In supply chain Machine Learning alone is not enough, we need to bridge between models that contain real mechanisms and models that are entirely data driven.

This is challenging, because as we introduce more mechanism to the models we use, it becomes harder to develop efficient algorithms to match those models to data.

# UNCERTAINTY QUANTIFICATION

Machine learning aims to replicate processes through the direct use of data. When deployed in the domain of ‘artificial intelligence,’ the processes that it is replicating, or emulating, are cognitive processes.

The first trick in machine learning is to convert the process itself into a mathematical function. That function has a set of parameters which control its behaviour. What we call learning is the adaption of these parameters to change the behavior of the function. The choice of mathematical function we use is a vital component of the model.

## Emukit Playground

Emukit playground is a software toolkit for exploring the use of statistical emulation as a tool. It was built by Adam Hirst, during his software engineering internship at Amazon and supervised by Cliff McCollum.

You can explore Bayesian optimization of a taxi simulation.

## Uncertainty Quantification

Uncertainty quantification (UQ) is the science of quantitative characterization and reduction of uncertainties in both computational and real world applications. It tries to determine how likely certain outcomes are if some aspects of the system are not exactly known.

We will to illustrate different concepts of Uncertainty Quantification (UQ) and the role that Gaussian processes play in this field. Based on a simple simulator of a car moving between a valley and a mountain, we are going to illustrate the following concepts:

• Systems emulation. Many real world decisions are based on simulations that can be computationally very demanding. We will show how simulators can be replaced by emulators: Gaussian process models fitted on a few simulations that can be used to replace the simulator. Emulators are cheap to compute, fast to run, and always provide ways to quantify the uncertainty of how precise they are compared the original simulator.

• Emulators in optimization problems. We will show how emulators can be used to optimize black-box functions that are expensive to evaluate. This field is also called Bayesian Optimization and has gained an increasing relevance in machine learning as emulators can be used to optimize computer simulations (and machine learning algorithms) quite efficiently.

• Multi-fidelity emulation methods. In many scenarios we have simulators of different quality about the same measure of interest. In these cases the goal is to merge all sources of information under the same model so the final emulator is cheaper and more accurate than an emulator fitted only using data from the most accurate and expensive simulator.

## Mountain Car Simulator

To illustrate the above mentioned concepts we we use the mountain car simulator. This simulator is widely used in machine learning to test reinforcement learning algorithms. The goal is to define a control policy on a car whose objective is to climb a mountain. Graphically, the problem looks as follows:

The goal is to define a sequence of actions (push the car right or left with certain intensity) to make the car reach the flag after a number $T$ of time steps.

At each time step $t$, the car is characterized by a vector $\mathbf{ x}_{t} = (p_t,v_t)$ of states which are respectively the the position and velocity of the car at time $t$. For a sequence of states (an episode), the dynamics of the car is given by

$\mathbf{ x}_{t+1} = f(\mathbf{ x}_{t},\textbf{u}_{t})$

where $\textbf{u}_{t}$ is the value of an action force, which in this example corresponds to push car to the left (negative value) or to the right (positive value). The actions across a full episode are represented in a policy $\textbf{u}_{t} = \pi(\mathbf{ x}_{t},\theta)$ that acts according to the current state of the car and some parameters $\theta$. In the following examples we will assume that the policy is linear which allows us to write $\pi(\mathbf{ x}_{t},\theta)$ as

$\pi(\mathbf{ x},\theta)= \theta_0 + \theta_p p + \theta_vv.$ For $t=1,\dots,T$ now given some initial state $\mathbf{ x}_{0}$ and some some values of each $\textbf{u}_{t}$, we can simulate the full dynamics of the car for a full episode using Gym. The values of $\textbf{u}_{t}$ are fully determined by the parameters of the linear controller.

After each episode of length $T$ is complete, a reward function $R_{T}(\theta)$ is computed. In the mountain car example the reward is computed as 100 for reaching the target of the hill on the right hand side, minus the squared sum of actions (a real negative to push to the left and a real positive to push to the right) from start to goal. Note that our reward depend on $\theta$ as we make it dependent on the parameters of the linear controller.

## Emulate the Mountain Car

%pip install gym
import gym
env = gym.make('MountainCarContinuous-v0')

Our goal in this section is to find the parameters $\theta$ of the linear controller such that

$\theta^* = arg \max_{\theta} R_T(\theta).$

In this section, we directly use Bayesian optimization to solve this problem. We will use EmuKit so we first define the objective function.

import urllib.request
urllib.request.urlretrieve('https://raw.githubusercontent.com/lawrennd/talks/gh-pages/mountain_car.py','mountain_car.py')
import mountain_car as mc
import numpy as np

For each set of parameter values of the linear controller we can run an episode of the simulator (that we fix to have a horizon of $T=500$) to generate the reward. Using as input the parameters of the controller and as outputs the rewards we can build a Gaussian process emulator of the reward.

We start defining the input space, which is three-dimensional:

from emukit.core import ContinuousParameter, ParameterSpace
position_domain = [-1.2, +1]
velocity_domain = [-1/0.07, +1/0.07]
constant_domain = [-1, +1]

space = ParameterSpace(
[ContinuousParameter('position_parameter', *position_domain),
ContinuousParameter('velocity_parameter', *velocity_domain),
ContinuousParameter('constant', *constant_domain)])

To initalize the model we start sampling some initial points for the linear controller randomly.

from emukit.core.initial_designs import RandomDesign
design = RandomDesign(space)
n_initial_points = 25
initial_design = design.get_samples(n_initial_points)

Now run the simulation 25 times across our initial design.

y = target_function(initial_design)

Before we start any optimization, lets have a look to the behaviour of the car with the first of these initial points that we have selected randomly.

import numpy as np
random_controller = initial_design[0,:]
_, _, _, frames = mc.run_simulation(env, np.atleast_2d(random_controller), render=True)
anim=mc.animate_frames(frames, 'Random linear controller')

As we can see the random linear controller does not manage to push the car to the top of the mountain. Now, let’s optimize the regret using Bayesian optimization and the emulator for the reward. We try 50 new parameters chosen by the expected improvement acquisition function.

First we initizialize a Gaussian process emulator.

import GPy
kern = GPy.kern.RBF(3)
model_gpy = GPy.models.GPRegression(initial_design, y, kern, noise_var=1e-10)
from emukit.model_wrappers.gpy_model_wrappers import GPyModelWrapper
model_emukit = GPyModelWrapper(model_gpy, n_restarts=5)
model_emukit.optimize()

In Bayesian optimization an acquisition function is used to balance exploration and exploitation to evaluate new locations close to the optimum of the objective. In this notebook we select the expected improvement (EI). For further details have a look at the review paper of Shahriari et al. (2016).

from emukit.bayesian_optimization.acquisitions import ExpectedImprovement
acquisition = ExpectedImprovement(model_emukit)
from emukit.bayesian_optimization.loops.bayesian_optimization_loop import BayesianOptimizationLoop
bo = BayesianOptimizationLoop(space, model_emukit, acquisition=acquisition)
bo.run_loop(target_function, 50)
results= bo.get_results()

Now we visualize the result for the best controller that we have found with Bayesian optimization.

_, _, _, frames = mc.run_simulation(env, np.atleast_2d(results.minimum_location), render=True)
anim=mc.animate_frames(frames, 'Best controller after 50 iterations of Bayesian optimization')

The car can now make it to the top of the mountain! Emulating the reward function and using expected improvement acquisition helped us to find a linear controller that solves the problem.

## Data Efficient Emulation

In the previous section we solved the mountain car problem by directly emulating the reward but no considerations about the dynamics $\mathbf{ x}_{t+1} =g(\mathbf{ x}_{t},\textbf{u}_{t})$ of the system were made.

We ran the simulator 25 times in the initial design, and 50 times in our Bayesian optimization loop. That required us to call the dynamics simulation $500\times 75 =37,500$ times, because each simulation of the car used 500 steps. In this section we will show how it is possible to reduce this number by building an emulator for $g(\cdot)$ that can later be used to directly optimize the control.

The inputs of the model for the dynamics are the velocity, the position and the value of the control so create this space accordingly.

import gym
env = gym.make('MountainCarContinuous-v0')
from emukit.core import ContinuousParameter, ParameterSpace
position_dynamics_domain = [-1.2, +0.6]
velocity_dynamics_domain = [-0.07, +0.07]
action_dynamics_domain = [-1, +1]

space_dynamics = ParameterSpace(
[ContinuousParameter('position_dynamics_parameter', *position_dynamics_domain),
ContinuousParameter('velocity_dynamics_parameter', *velocity_dynamics_domain),
ContinuousParameter('action_dynamics_parameter', *action_dynamics_domain)])

Next, we sample some input parameters and use the simulator to compute the outputs. Note that in this case we are not running the full episodes, we are just using the simulator to compute $\mathbf{ x}_{t+1}$ given $\mathbf{ x}_{t}$ and $\textbf{u}_{t}$.

from emukit.core.initial_designs import RandomDesign
design_dynamics = RandomDesign(space_dynamics)
n_initial_points = 500
initial_design_dynamics = design_dynamics.get_samples(n_initial_points)
import numpy as np
import mountain_car as mc
### --- Simulation of the (normalized) outputs
y_dynamics = np.zeros((initial_design_dynamics.shape[0], 2))
for i in range(initial_design_dynamics.shape[0]):
y_dynamics[i, :] = mc.simulation(initial_design_dynamics[i, :])
# Normalize the data from the simulation
y_dynamics_normalisation = np.std(y_dynamics, axis=0)
y_dynamics_normalised = y_dynamics/y_dynamics_normalisation

The outputs are the velocity and the position. Our model will capture the change in position and velocity on time. That is, we will model

$\Delta v_{t+1} = v_{t+1} - v_{t}$

$\Delta x_{t+1} = p_{t+1} - p_{t}$

with Gaussian processes with prior mean $v_{t}$ and $p_{t}$ respectively. As a covariance function, we use Matern52. We need therefore two models to capture the full dynamics of the system.

import GPy
kern_position = GPy.kern.Matern52(3)
position_model_gpy = GPy.models.GPRegression(initial_design_dynamics, y_dynamics[:, 0:1], kern_position, noise_var=1e-10)
kern_velocity = GPy.kern.Matern52(3)
velocity_model_gpy = GPy.models.GPRegression(initial_design_dynamics, y_dynamics[:, 1:2], kern_velocity, noise_var=1e-10)
from emukit.model_wrappers.gpy_model_wrappers import GPyModelWrapper
position_model_emukit = GPyModelWrapper(position_model_gpy, n_restarts=5)
velocity_model_emukit = GPyModelWrapper(velocity_model_gpy, n_restarts=5)

In general we might use much smarter strategies to design our emulation of the simulator. For example, we could use the variance of the predictive distributions of the models to collect points using uncertainty sampling, which will give us a better coverage of the space. For simplicity, we move ahead with the 500 randomly selected points.

Now that we have a data set, we can update the emulators for the location and the velocity.

position_model_emukit.optimize()
velocity_model_emukit.optimize()

We can now have a look to how the emulator and the simulator match. First, we show a contour plot of the car aceleration for each pair of can position and velocity. You can use the bar bellow to play with the values of the controler to compare the emulator and the simulator.

We can see how the emulator is doing a fairly good job approximating the simulator. On the edges, however, it struggles to captures the dynamics of the simulator.

Given some input parameters of the linear controlling, how do the dynamics of the emulator and simulator match? In the following figure we show the position and velocity of the car for the 500 time steps of an episode in which the parameters of the linear controller have been fixed beforehand. The value of the input control is also shown.

# change the values of the linear controller to observe the trajectories.
controller_gains = np.atleast_2d([0, .6, 1])  

We now make explicit use of the emulator, using it to replace the simulator and optimize the linear controller. Note that in this optimization, we don’t need to query the simulator anymore as we can reproduce the full dynamics of an episode using the emulator. For illustrative purposes, in this example we fix the initial location of the car.

We define the objective reward function in terms of the simulator.

### --- Optimize control parameters with emulator
car_initial_location = np.asarray([-0.58912799, 0])

And as before, we use Bayesian optimization to find the best possible linear controller.

The design space is the three continuous variables that make up the linear controller.

position_domain = [-1.2, +1]
velocity_domain = [-1/0.07, +1/0.07]
constant_domain = [-1, +1]

space = ParameterSpace(
[ContinuousParameter('position_parameter', *position_domain),
ContinuousParameter('velocity_parameter', *velocity_domain),
ContinuousParameter('constant', *constant_domain)])
from emukit.core.initial_designs import RandomDesign
design = RandomDesign(space)
n_initial_points = 25
initial_design = design.get_samples(n_initial_points)

Now run the simulation 25 times across our initial design.

y = target_function_emulator(initial_design)

Now we set up the surrogate model for the Bayesian optimization loop.

import GPy
kern = GPy.kern.RBF(3)
model_dynamics_emulated_gpy = GPy.models.GPRegression(initial_design, y, kern, noise_var=1e-10)
from emukit.model_wrappers.gpy_model_wrappers import GPyModelWrapper
model_dynamics_emulated_emukit = GPyModelWrapper(model_dynamics_emulated_gpy, n_restarts=5)
model_dynamics_emulated_emukit.optimize()

We set the acquisition function to be expected improvement.

from emukit.bayesian_optimization.acquisitions import ExpectedImprovement
acquisition = ExpectedImprovement(model_emukit)

And we set up the main loop for the Bayesian optimization.

from emukit.bayesian_optimization.loops.bayesian_optimization_loop import BayesianOptimizationLoop
bo = BayesianOptimizationLoop(space, model_dynamics_emulated_emukit, acquisition=acquisition)
bo.run_loop(target_function_emulator, 50)
results = bo.get_results()
_, _, _, frames = mc.run_simulation(env, np.atleast_2d(results.minimum_location), render=True)
anim=mc.animate_frames(frames, 'Best controller using the emulator of the dynamics')
from IPython.core.display import HTML

And the problem is again solved, but in this case we have replaced the simulator of the car dynamics by a Gaussian process emulator that we learned by calling the dynamics simulator only 500 times. Compared to the 37,500 calls that we needed when applying Bayesian optimization directly on the simulator this is a significant improvement. Of course, in practice the car dynamics are very simple for this example.

## Mountain Car: Multi-Fidelity Emulation

In some scenarios we have simulators of the same environment that have different fidelities, that is that reflect with different level of accuracy the dynamics of the real world. Running simulations of the different fidelities also have a different cost: hight fidelity simulations are more expensive the cheaper ones. If we have access to these simulators we can combine high and low fidelity simulations under the same model.

So let’s assume that we have two simulators of the mountain car dynamics, one of high fidelity (the one we have used) and another one of low fidelity. The traditional approach to this form of multi-fidelity emulation is to assume that $f_i\left(\mathbf{ x}\right) = \rho f_{i-1}\left(\mathbf{ x}\right) + \delta_i\left(\mathbf{ x}\right)$ where $f_{i-1}\left(\mathbf{ x}\right)$ is a low fidelity simulation of the problem of interest and $f_i\left(\mathbf{ x}\right)$ is a higher fidelity simulation. The function $\delta_i\left(\mathbf{ x}\right)$ represents the difference between the lower and higher fidelity simulation, which is considered additive. The additive form of this covariance means that if $f_{0}\left(\mathbf{ x}\right)$ and $\left\{\delta_i\left(\mathbf{ x}\right)\right\}_{i=1}^m$ are all Gaussian processes, then the process over all fidelities of simuation will be a joint Gaussian process.

But with deep Gaussian processes we can consider the form $f_i\left(\mathbf{ x}\right) = g_{i}\left(f_{i-1}\left(\mathbf{ x}\right)\right) + \delta_i\left(\mathbf{ x}\right),$ where the low fidelity representation is non linearly transformed by $g(\cdot)$ before use in the process. This is the approach taken in Perdikaris et al. (2017). But once we accept that these models can be composed, a highly flexible framework can emerge. A key point is that the data enters the model at different levels, and represents different aspects. For example these correspond to the two fidelities of the mountain car simulator.

We start by sampling both of them at 250 random input locations.

import gym
env = gym.make('MountainCarContinuous-v0')
from emukit.core import ContinuousParameter, ParameterSpace
position_dynamics_domain = [-1.2, +0.6]
velocity_dynamics_domain = [-0.07, +0.07]
action_dynamics_domain = [-1, +1]

space_dynamics = ParameterSpace(
[ContinuousParameter('position_dynamics_parameter', *position_dynamics_domain),
ContinuousParameter('velocity_dynamics_parameter', *velocity_dynamics_domain),
ContinuousParameter('action_dynamics_parameter', *action_dynamics_domain)])

Next, we evaluate the high and low fidelity simualtors at those locations.

import numpy as np
import mountain_car as mc
d_position_hf = np.zeros((n_points, 1))
d_velocity_hf = np.zeros((n_points, 1))
d_position_lf = np.zeros((n_points, 1))
d_velocity_lf = np.zeros((n_points, 1))

# --- Collect high fidelity points
for i in range(0, n_points):
d_position_hf[i], d_velocity_hf[i] = mc.simulation(x_random[i, :])

# --- Collect low fidelity points
for i in range(0, n_points):
d_position_lf[i], d_velocity_lf[i] = mc.low_cost_simulation(x_random[i, :])

## Building the Multifidelity Emulation

It is time to build the multi-fidelity model for both the position and the velocity.

As we did in the previous section we use the emulator to optimize the simulator. In this case we use the high fidelity output of the emulator.

First we optimize the controller parameters

And we optimize using Bayesian optimzation.

from emukit.core import ContinuousParameter, ParameterSpace
position_domain = [-1.2, +1]
velocity_domain = [-1/0.07, +1/0.07]
constant_domain = [-1, +1]

space = ParameterSpace(
[ContinuousParameter('position_parameter', *position_domain),
ContinuousParameter('velocity_parameter', *velocity_domain),
ContinuousParameter('constant', *constant_domain)])
from emukit.core.initial_designs import RandomDesign
design = RandomDesign(space)
n_initial_points = 25
initial_design = design.get_samples(n_initial_points)

n_initial_points = 25 random_design = RandomDesign(design_space) initial_design = random_design.get_samples(n_initial_points) acquisition = GPyOpt.acquisitions.AcquisitionEI(model, design_space, optimizer=aquisition_optimizer) evaluator = GPyOpt.core.evaluators.Sequential(acquisition)}

bo_multifidelity = GPyOpt.methods.ModularBayesianOptimization(model, design_space, objective_multifidelity, acquisition, evaluator, initial_design)
bo_multifidelity.run_optimization(max_iter=50)
_, _, _, frames = mc.run_simulation(env, np.atleast_2d(bo_multifidelity.x_opt), render=True)
anim=mc.animate_frames(frames, 'Best controller with multi-fidelity emulator')

### Best Controller with Multi-Fidelity Emulator

And problem solved! We see how the problem is also solved with 250 observations of the high fidelity simulator and 250 of the low fidelity simulator.

# Emukit

The Emukit software we will be using across the next part of this module is a python software library that facilitates emulation of systems. The software’s origins go back to work done by Javier Gonzalez as part of his post-doctoral project at the University of Sheffield. Javier led the design and build of a Bayesian optimization software. The package GPyOpt worked with the SheffieldML software GPy for performing Bayesian optimization.

GPyOpt has a modular design that allows the user to provide their own surrogate models, the package is build with GPy as a surrogate model in mind, but other surrogate models can also be wrapped and integrated.

However, GPyOpt doesn’t allow the full flexibility of surrogate modelling for domains like experimental design, sensitivity analysis etc.

Emukit was designed and built for a more general approach. The software is MIT licensed and its design and implementation was led by Javier Gonzalez and Andrei Paleyes at Amazon. Building on the experience of GPyOpt, the aim with Emukit was to use the modularisation ideas embedded in GPyOpt, but to extend them beyond the modularisation of the surrogate models to modularisation of the acquisition function.

%pip install gpy
%pip install pyDOE
%pip install emukit

The software was initially built by the team in Amazon. As well as Javier Gonzalez (ML side) and Andrei Paleyes (Software Engineering) included Mark Pullin, Maren Mahsereci, Alex Gessner, Aaron Klein, Henry Moss, David-Elias Künstle as well as management input from Cliff McCollum and myself.

## MXFusion: Modular Probabilistic Programming on MXNet

One challenge for practitioners in Gaussian processes, is flexible software that allows the construction of the relevant GP modle. With this in mind, the Amazon Cambridge team has developed MXFusion. It is a modular probabilistic programming language focussed on efficient implementation of hybrid GP-neural network models, but with additional probabilistic programming capabilities.

We developed the framework for greater ease of transitioning models from ‘science’ to ‘production,’ our aim was to have code that could be created by scientists, but deployed in our systems through solutions such as AWS SageMaker.

\ericMeissner{15%}\zhenwenDai{15%}

## Key Requirements

Specialized inference methods + models, without requiring users to reimplement nor understand them every time. Leverage expert knowledge. Efficient inference, flexible framework. Existing frameworks either did one or the other: flexible, or efficient.

## What does it look like?

Modelling

Inference

m = Model()
m.mu = Variable()
m.s = Variable(transformation=PositiveTransformation())
m.Y = Normal.define_variable(mean=m.mu, variance=m.s)
• Variable

• Distribution

• Function

• log_pdf

• draw_samples

• Variational Inference

• MCMC Sampling (soon) Built on MXNet Gluon (imperative code, not static graph)

infr = GradBasedInference(inference_algorithm=MAP(model=m, observed=[m.Y]))
infr.run(Y=data)
• Model + Inference together form building blocks.
• Just doing modular modeling with universal inference doesn’t really scale, need specialized inference methods for specialized modelling objects like non-parametrics.

Common reinforcement learning methods suffer from data inefficiency, which can be a issue in real world applications where gathering sufficiently large amounts of data pose economic issues and may be impossible. propose a model-based policy search method known as PILCO in part to address this issue. PILCO uses a Gaussian process (GP) for learning the dynamics of the environment and optimizes a parametric policy function using the learned dynamics model.

We construct an implementation of PILCO using MXFusion. This implementation follows the main idea of PILCO and has a few enhancements in addition to the published implementation. The enhancements are as follows: * Use Monte Carlo integration instead of moment estimation. We approximate the expected reward using Monte Carlo integration instead of the proposed moment estimation approach. This removes the bias in the expected reward computation and enables a wide range of choices of kernels and policy functions. In the original work, only RBF and linear kernel and only linear and RBF network policy can be used. * Use automatic differentiation. Thanks to automatic differentiation, no gradient derivation is needed. * A unified interface of Gaussian process. MXFusion provides an unified inferface of GP modules. We allows us to easily switch among plan GP, variational sparse GP and stocastic variational GP implementations.

This notebook depends on MXNet, MXFusion and Open AI Gym. These packages can be installed into your Python environment by running the following commands.

Set the global configuration.

import os
import gym
import mxnet
import mxfusion
os.environ['MXNET_ENGINE_TYPE'] = 'NaiveEngine'
from mxfusion.common import config
config.DEFAULT_DTYPE = 'float64'

We use the inverted pendulum swingup problem as an example. We use the Pendulum-v0 environment in Open AI Gym. The task is to swing the pendulum up and balance it at the inverted position. This is a classical control problem and is known to be unsolvable with a linear controller.

To solve this problem with PILCO, we need three components:

• Execute a policy in an real environment (an Open AI Gym simulator in this example) and collect data.
• Fit a GP model as the model for the dynamics of the environment.
• Optimize the policy given the dynamics model learned from all the data that have been collected so far.

The overall PILCO algorithm is to iterate the above three steps until a policy that can solve the problem is found.

## Execute the Environment

import gym
env = gym.make('Pendulum-v0')

The state of the pendulum environment is a 3D vector. The first two dimensions are the 2D location of the end point of the pendulum. The third dimension encodes the angular speed of the pendulum. The action space is a 1D vector in [-2, 2].

We write a helper function for executing the environment with a given policy.

import numpy as np
def run_one_episode(env, policy, initial_state=None, max_steps=200, verbose=False, render=False):
"""
Drives an episode of the OpenAI gym environment using the policy to decide next actions.
"""
observation = env.reset()
if initial_state is not None:
env.env.state = initial_state
observation = env.env._get_obs()
env._max_episode_steps = max_steps
step_idx = 0
done = False
total_reward = 0
frames = []
all_actions = []
all_observations = [observation]
while not done:
if render:
frames.append(env.render(mode = 'rgb_array'))
if verbose:
print(observation)
action = policy(observation)
observation, reward, done, info = env.step(action)
all_observations.append(observation)
all_actions.append(action)
total_reward += reward
step_idx += 1
if done or step_idx>=max_steps-1:
print("Episode finished after {} timesteps because {}".format(step_idx+1, "'done' reached" if done else "Max timesteps reached"))
break
if render:
fig = plt.figure()
ax = fig.gca()
fig.tight_layout()
patch = ax.imshow(frames[0])
ax.axis('off')
def animate(i):
patch.set_data(frames[i])
anim = animation.FuncAnimation(plt.gcf(), animate, frames = len(frames), interval=20)
else:
return total_reward, np.array(all_observations, dtype=np.float64,), np.array(all_actions, dtype=np.float64)

We first apply a random policy and see how the environment reacts. The random policy uniformly samples in the space of action.

def random_policy(state):
return env.action_space.sample()

The animation is generated with the following commands:

anim = run_one_episode(env, random_policy, max_steps=500, render=True, verbose=False)[-1]

with open('animation_random_policy.html', 'w') as f:
f.write(anim.to_jshtml())

## Pendulum

The dynamics model of pendulum can be written as $p(y_{t+1}|y_t, a_t)$ where $y_t$ is the state vector at the time $t$ and $a_t$ is the action taken at the time $t$.

PILCO uses a Gaussian process to model the above dynamics.

def prepare_data(state_list, action_list, win_in):
"""
Prepares a list of states and a list of actions as inputs to the Gaussian Process for training.
"""

X_list = []
Y_list = []

for state_array, action_array in zip(state_list, action_list):
# the state and action array shape should be aligned.
assert state_array.shape[0]-1 == action_array.shape[0]

for i in range(state_array.shape[0]-win_in):
Y_list.append(state_array[i+win_in:i+win_in+1])
X_list.append(np.hstack([state_array[i:i+win_in].flatten(), action_array[i:i+win_in].flatten()]))
X = np.vstack(X_list)
Y = np.vstack(Y_list)
return X, Y

In this example, we do a maximum likelihood estimate for the model hyper- parameters. In MXFusion, Gaussian process regression model is available as a module, which includes a dediated inference algorithm.

import mxnet as mx
from mxfusion import Model, Variable
from mxfusion.components.variables import PositiveTransformation
from mxfusion.components.distributions.gp.kernels import RBF
from mxfusion.modules.gp_modules import GPRegression
from mxfusion.inference import GradBasedInference, MAP

## Define and fit the model

def fit_model(state_list, action_list, win_in, verbose=True):
"""
Fits a Gaussian Process model to the state / action pairs passed in.
This creates a model of the environment which is used during
policy optimization instead of querying the environment directly.

See mxfusion.gp_modules for additional types of GP models to fit,
including Sparse GP and Stochastic Varitional Inference Sparse GP.
"""
X, Y = prepare_data(state_list, action_list, win_in)

m = Model()
m.N = Variable()
m.X = Variable(shape=(m.N, X.shape[-1]))
m.noise_var = Variable(shape=(1,), transformation=PositiveTransformation(),
initial_value=0.01)
m.kernel = RBF(input_dim=X.shape[-1], variance=1, lengthscale=1, ARD=True)
m.Y = GPRegression.define_variable(
X=m.X, kernel=m.kernel, noise_var=m.noise_var,
shape=(m.N, Y.shape[-1]))
m.Y.factor.gp_log_pdf.jitter = 1e-6

inference_algorithm=MAP(model=m, observed=[m.X, m.Y]))
infr.run(X=mx.nd.array(X, dtype='float64'),
Y=mx.nd.array(Y, dtype='float64'),
max_iter=1000, learning_rate=0.1, verbose=verbose)
return m, infr, X, Y

## Policy

PILCO computes the expected reward of a policy given the dynamics model. First, we need to define the parametric form of the policy. In this example, we use a neural network with one hidden layer. As the action space is [-2, 2], we apply a tanh transformation and multiply the come with two. This enforces the returned actions stay within the range.

We define a neural network with one hidden layer and and output constrained between [-2,2] for the policy.

from mxnet.gluon import HybridBlock
from mxnet.gluon.nn import Dense

class NNController(HybridBlock):
"""Define a neural network policy network."""
def __init__(self, prefix=None, params=None):
super(NNController, self).__init__(prefix=prefix, params=params)
self.dense1 = Dense(100, in_units=len(env.observation_space.high), dtype='float64', activation='relu')
self.dense2 = Dense(1, in_units=100, dtype='float64', activation='tanh')

def hybrid_forward(self, F, x):
out = self.dense2(self.dense1(x))*2 # Scale up the output
return out

policy = NNController()
policy.collect_params().initialize(mx.initializer.Xavier(magnitude=1))

To compute the expected reward, we also need to define a reward function. This reward function is defined by us according to the task. The main component is the height of the pendulum. We also penalize the force and the angular momentum.

class CostFunction(mx.gluon.HybridBlock):
"""
The goal is to get the pendulum upright and stable as quickly as possible.

Taken from the code for Pendulum.
"""
def hybrid_forward(self, F, state, action):
"""
:param state: [np.cos(theta), np.sin(theta), ~ momentum(theta)]
a -> 0 when pendulum is upright, largest when pendulum is hanging down completely.
b -> penalty for taking action
c -> penalty for pendulum momentum
"""
a_scale = 2.
b_scale = .001
c_scale = .1
a = F.sum(a_scale * (state[:,:,0:1] -1) ** 2, axis=-1)
b = F.sum(b_scale * action ** 2, axis=-1)
c = F.sum(c_scale * state[:,:,2:3] ** 2, axis=-1)
return (a + c + b)

cost = CostFunction()

The expected reward function can be written as $R = \mathbb{E}_{p(y_T, \ldots, y_0)}\left(\sum_{t=0}^\top r(y_t)\right)$ where $r(\cdot)$ is the reward function, $p(y_T, \ldots, y_0)$ is the joint distribution when applying the policy to the dynamics model: $p(y_T, \ldots, y_0) = p(y_0) \prod_{t=1}^\top p(y_t|y_{t-1}, a_{t-1}),$ where $a_{t-1} = \pi(y_{t-1})$ is the action taken at the time $t-1$, which is the outcome of the policy $\pi(\cdot)$.

The expected reward function is implemented as follows.

from mxfusion.inference.inference_alg import SamplingAlgorithm
class PolicyUpdateGPParametricApprox(SamplingAlgorithm):
"""Class for the policy update for PILCO."""
def compute(self, F, variables):

s_0 = self.initial_state_generator(self.num_samples)
a_0 = self.policy(s_0)
a_t_plus_1 = a_0
x_t = F.expand_dims(F.concat(s_0, a_0, dim=1), axis=1)

gp = self.model.Y.factor
sample_func = gp.draw_parametric_samples(F, variables, self.num_samples, self.approx_samples)
cost = 0
for t in range(self.n_time_steps):
s_t_plus_1 = sample_func(F, x_t)
cost = cost + self.cost_function(s_t_plus_1, a_t_plus_1)
a_t_plus_1 = mx.nd.expand_dims(self.policy(s_t_plus_1), axis=2)
x_t = mx.nd.concat(s_t_plus_1, a_t_plus_1, dim=2)
total_cost = F.mean(cost)
return total_cost, total_cost

We optimize the policy with respect to the expected reward by using a gradient optimizer.

from mxfusion.inference import GradTransferInference
def optimize_policy(policy, cost_func, model, infr, model_data_X, model_data_Y,
learning_rate=1e-2, num_time_steps=100,
num_samples=10, verbose=True):
"""
Takes as primary inputs a policy, cost function, and trained model.
Optimizes the policy for num_grad_steps number of iterations.
"""
mb_alg = PolicyUpdateGPParametricApprox(
model=model, observed=[model.X, model.Y], cost_function=cost_func,
policy=policy, n_time_steps=num_time_steps,
initial_state_generator=initial_state_generator,
num_samples=num_samples)

mb_alg, infr_params=infr.params, train_params=policy.collect_params())
infr_pred.run(
X=mx.nd.array(model_data_X, dtype='float64'),
Y=mx.nd.array(model_data_Y, dtype='float64'),
verbose=verbose, learning_rate=learning_rate)
return policy

## The Loop

We need to define a function that provides random initial states.

def initial_state_generator(num_initial_states):
"""
Starts from valid states by drawing theta and momentum
then computing np.cos(theta) and np.sin(theta) for state[0:2].s
"""
return mx.nd.array(
[env.observation_space.sample() for i in range(num_initial_states)],
dtype='float64')
num_episode = 20 # how many model fit + policy optimization episodes to run
num_samples = 100 # how many sample trajectories the policy optimization loop uses
num_grad_steps = 1000 # how many gradient steps the optimizer takes per episode
num_time_steps = 100 # how far to roll out each sample trajectory
learning_rate = 1e-3 # learning rate for the policy optimization

all_states = []
all_actions = []
for i_ep in range(num_episode):
# Run an episode and collect data.
if i_ep == 0:
policy_func = lambda x: env.action_space.sample()
else:
policy_func = lambda x: policy(mx.nd.expand_dims(mx.nd.array(x, dtype='float64'), axis=0)).asnumpy()[0]
total_reward, states, actions = run_one_episode(
env, policy_func, max_steps=num_time_steps)
all_states.append(states)
all_actions.append(actions)

# Fit a model.
model, infr, model_data_X, model_data_Y = fit_model(
all_states, all_actions, win_in=1, verbose=True)

# Optimize the policy.
policy = optimize_policy(
policy, cost, model, infr, model_data_X, model_data_Y,
num_samples=num_samples, learning_rate=learning_rate,
num_time_steps=num_time_steps)

Policy after the first episode (random exploration):

Policy after the 5th episode:

https://github.com/amzn/mxfusion

## Long term Aim

• Simulate/Emulate the components of the system.
• Validate with real world using multifidelity.
• Interpret system using e.g. sensitivity analysis.
• Perform end to end learning to optimize.
• Maintain interpretability.

## Thanks!

For more information on these subjects and more you might want to check the following resources.

# References

Cooper, B., 1991. Transformation of a valley: Derbyshire derwent. Scarthin Books.
Maxwell, J.C., 1867. On governors. Proceedings of the Royal Society of London 16, 270–283.
Perdikaris, P., Raissi, M., Damianou, A., Lawrence, N.D., Karniadakis, G.E., 2017. Nonlinear information fusion algorithms for data-efficient multi-fidelity modelling. Proceedings of the Royal Society of London A: Mathematical, Physical and Engineering Sciences 473. https://doi.org/10.1098/rspa.2016.0751
Shahriari, B., Swersky, K., Wang, Z., Adams, R.P., de Freitas, N., 2016. Taking the human out of the loop: A review of Bayesian optimization. Proceedings of the IEEE 104, 148–175. https://doi.org/10.1109/JPROC.2015.2494218
Wiener, N., 1948. Cybernetics: Control and communication in the animal and the machine. MIT Press, Cambridge, MA.