# 02 - Reverse Time Migration

This notebook is the second in a series of tutorial highlighting various aspects of seismic inversion based on Devito operators. In this second example we aim to highlight the core ideas behind seismic inversion, where we create an image of the subsurface from field recorded data. This tutorial follows on the modelling tutorial and will reuse the modelling operator and velocity model.

## Imaging requirement

Seismic imaging relies on two known parameters:

- **Field data** - or also called **recorded data**. This is a shot record corresponding to the true velocity model. In practice this data is acquired as described in the first tutorial. In order to simplify this tutorial we will generate synthetic field data by modelling it with the **true velocity model**.

- **Background velocity model**. This is a velocity model that has been obtained by processing and inverting the field data. We will look at this methods in the following tutorial as it relies on the method we are describing here. This velocity model is usually a **smooth version** of the true velocity model.

## Imaging computational setup

In this tutorial, we will introduce the back-propagation operator. This operator simulates the adjoint wave-equation, that is a wave-equation solved in a reversed time order. This time reversal led to the naming of the method we present here, called Reverse Time Migration. The notion of adjoint in exploration geophysics is fundamental as most of the wave-equation based imaging and inversion methods rely on adjoint based optimization methods.

## Notes on the operators

As we have already described the creation of a forward modelling operator, we will use a thin wrapper function instead. This wrapper is provided by a utility class called `AcousticWaveSolver`, which provides all the necessary operators for seismic modeling, imaging and inversion. The `AcousticWaveSolver` provides a more concise API for common wave propagation operators and caches the Devito `Operator` objects to avoid unnecessary recompilation. Operators introduced for the first time in this tutorial will be properly described.

As before we initialize printing and import some utilities. We also raise the Devito log level to avoid excessive logging for repeated operator invocations.

```python
import numpy as np
%matplotlib inline

from devito import configuration
configuration['log-level'] = 'WARNING'
```

## Computational considerations

Seismic inversion algorithms are generally very computationally demanding and require a large amount of memory to store the forward wavefield. In order to keep this tutorial as lightweight as possible we are using a very simple
velocity model that requires low temporal and spatial resolution. For a more realistic model, a second set of preset parameters for a reduced version of the 2D Marmousi data set [1] is provided below in comments. This can be run to create some more realistic subsurface images. However, this second preset is more computationally demanding and requires a slightly more powerful workstation.

```python
# Configure model presets
from examples.seismic import demo_model

# Enable model presets here:
preset = 'layers-isotropic'  # A simple but cheap model (recommended)
# preset = 'marmousi2d-isotropic'  # A larger more realistic model

# Standard preset with a simple two-layer model
if preset == 'layers-isotropic':
    def create_model(grid=None):
        return demo_model('layers-isotropic', origin=(0., 0.), shape=(101, 101),
                          spacing=(10., 10.), nbl=20, grid=grid, nlayers=2)
    filter_sigma = (1, 1)
    nshots = 21
    nreceivers = 101
    t0 = 0.
    tn = 1000.  # Simulation last 1 second (1000 ms)
    f0 = 0.010  # Source peak frequency is 10Hz (0.010 kHz)

# A more computationally demanding preset based on the 2D Marmousi model
if preset == 'marmousi2d-isotropic':
    def create_model(grid=None):
        return demo_model('marmousi2d-isotropic', data_path='../../../../data/',
                          grid=grid, nbl=20)
    filter_sigma = (6, 6)
    nshots = 301  # Need good covergae in shots, one every two grid points
    nreceivers = 601  # One recevier every grid point
    t0 = 0.
    tn = 3500.  # Simulation last 3.5 second (3500 ms)
    f0 = 0.025  # Source peak frequency is 25Hz (0.025 kHz)
```

# True and smooth velocity models

First, we create the model data for the "true" model from a given demonstration preset. This model represents the subsurface topology for the purposes of this example and we will later use it to generate our synthetic data readings. We also generate a second model and apply a smoothing filter to it, which represents our initial model for the imaging algorithm. The perturbation between these two models can be thought of as the image we are trying to recover.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_velocity, plot_perturbation
from devito import gaussian_smooth

# Create true model from a preset
model = create_model()

# Create initial model and smooth the boundaries
model0 = create_model(grid=model.grid)
gaussian_smooth(model0.vp, sigma=filter_sigma)

# Plot the true and initial model and the perturbation between them
plot_velocity(model)
plot_velocity(model0)
plot_perturbation(model0, model)
```

## Acquisition geometry

Next we define the positioning and the wave signal of our source, as well as the location of our receivers. To generate the wavelet for our source we require the discretized values of time that we are going to use to model a single "shot",
which again depends on the grid spacing used in our model. For consistency this initial setup will look exactly as in the previous modelling tutorial, although we will vary the position of our source later on during the actual imaging algorithm.

```python
#NBVAL_IGNORE_OUTPUT
# Define acquisition geometry: source
from examples.seismic import AcquisitionGeometry

# First, position source centrally in all dimensions, then set depth
src_coordinates = np.empty((1, 2))
src_coordinates[0, :] = np.array(model.domain_size) * .5
src_coordinates[0, -1] = 20.  # Depth is 20m

# Define acquisition geometry: receivers

# Initialize receivers for synthetic and imaging data
rec_coordinates = np.empty((nreceivers, 2))
rec_coordinates[:, 0] = np.linspace(0, model.domain_size[0], num=nreceivers)
rec_coordinates[:, 1] = 30.

# Geometry

geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, t0, tn, f0=.010, src_type='Ricker')
# We can plot the time signature to see the wavelet
geometry.src.show()
```

# True and smooth data

We can now generate the shot record (receiver readings) corresponding to our true and initial models. The difference between these two records will be the basis of the imaging procedure.

For this purpose we will use the same forward modelling operator that was introduced in the previous tutorial, provided by the `AcousticWaveSolver` utility class. This object instantiates a set of pre-defined operators according to an initial definition of the acquisition geometry, consisting of source and receiver symbols. The solver objects caches the individual operators and provides a slightly more high-level API that allows us to invoke the modelling modelling operators from the initial tutorial in a single line. In the following cells we use this to generate shot data by only specifying the respective model symbol `m` to use, and the solver will create and return a new `Receiver` object the represents the readings at the previously defined receiver coordinates.

```python
# Compute synthetic data with forward operator
from examples.seismic.acoustic import AcousticWaveSolver

solver = AcousticWaveSolver(model, geometry, space_order=4)
true_d , _, _ = solver.forward(vp=model.vp)
```

```python
# Compute initial data with forward operator
smooth_d, _, _ = solver.forward(vp=model0.vp)
```

```python
#NBVAL_IGNORE_OUTPUT
# Plot shot record for true and smooth velocity model and the difference
from examples.seismic import plot_shotrecord

plot_shotrecord(true_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data - true_d.data, model, t0, tn)
```

# Imaging with back-propagation

As explained in the introduction of this tutorial, this method is based on back-propagation.

## Adjoint wave equation

If we go back to the modelling part, we can rewrite the simulation as a linear system solve:

\begin{equation}
\mathbf{A}(\mathbf{m}) \mathbf{u} = \mathbf{q}
\end{equation}

where $\mathbf{m}$ is the discretized square slowness, $\mathbf{q}$ is the discretized source and $\mathbf{A}(\mathbf{m})$ is the discretized wave-equation. The discretized wave-equation matricial representation is a lower triangular matrix that can be solve with forward substitution. The pointwise writing or the forward substitution leads to the time-stepping stencil.

On a small problem one could form the matrix explicitly and transpose it to obtain the adjoint discrete wave-equation:

\begin{equation}
\mathbf{A}(\mathbf{m})^T \mathbf{v} = \delta \mathbf{d}
\end{equation}

where $\mathbf{v}$ is the discrete **adjoint wavefield** and  $\delta \mathbf{d}$ is the data residual defined as the difference between the field/observed data and the synthetic data $\mathbf{d}_s = \mathbf{P}_r \mathbf{u}$. In our case we derive the discrete adjoint wave-equation from the discrete forward wave-equation to get its stencil.

## Imaging

Wave-equation based imaging relies on one simple concept:

- If the background velocity model is cinematically correct, the forward wavefield $\mathbf{u}$ and the adjoint wavefield $\mathbf{v}$ meet at the reflectors position at zero time offset.

The sum over time of the zero time-offset correlation of these two fields then creates an image of the subsurface. Mathematically this leads to the simple imaging condition:

\begin{equation}
  \text{Image} = \sum_{t=1}^{n_t} \mathbf{u}[t] \mathbf{v}[t]
\end{equation}

In the following tutorials we will describe a more advanced imaging condition that produces shaper and more accurate results.

## Operator

We will now define the imaging operator that computes the adjoint wavefield $\mathbf{v}$ and correlates it with the forward wavefield $\mathbf{u}$. This operator essentially consists of three components:
* Stencil update of the adjoint wavefield `v`
* Injection of the data residual at the adjoint source (forward receiver) location
* Correlation of `u` and `v` to compute the image contribution at each timestep

```python
# Define gradient operator for imaging
from devito import TimeFunction, Operator, Eq, solve
from examples.seismic import PointSource

def ImagingOperator(model, image):
    # Define the wavefield with the size of the model and the time dimension
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)

    u = TimeFunction(name='u', grid=model.grid, time_order=2, space_order=4,
                     save=geometry.nt)

    # Define the wave equation, but with a negated damping term
    eqn = model.m * v.dt2 - v.laplace + model.damp * v.dt.T

    # Use `solve` to rearrange the equation into a stencil expression
    stencil = Eq(v.backward, solve(eqn, v.backward))

    # Define residual injection at the location of the forward receivers
    dt = model.critical_dt
    residual = PointSource(name='residual', grid=model.grid,
                           time_range=geometry.time_axis,
                           coordinates=geometry.rec_positions)
    res_term = residual.inject(field=v.backward, expr=residual * dt**2 / model.m)

    # Correlate u and v for the current time step and add it to the image
    image_update = Eq(image, image - u * v)

    return Operator([stencil] + res_term + [image_update],
                    subs=model.spacing_map)
```

## Implementation of the imaging loop

As just explained, the forward wave-equation is solved forward in time while the adjoint wave-equation is solved in a reversed time order. Therefore, the correlation of these two fields over time requires to store one of the two fields. The computational procedure for imaging follows:

- Simulate the forward wave-equation with the background velocity model to get the synthetic data and save the full wavefield $\mathbf{u}$
- Compute the data residual
- Back-propagate the data residual and compute on the fly the image contribution at each time step.

This procedure is applied to multiple source positions (shots) and summed to obtain the full image of the subsurface. We can first visualize the varying locations of the sources that we will use.

```python
#NBVAL_IGNORE_OUTPUT

# Prepare the varying source locations
source_locations = np.empty((nshots, 2), dtype=np.float32)
source_locations[:, 0] = np.linspace(0., 1000, num=nshots)
source_locations[:, 1] = 30.

plot_velocity(model, source=source_locations)
```

```python
# Run imaging loop over shots
from devito import Function

# Create image symbol and instantiate the previously defined imaging operator
image = Function(name='image', grid=model.grid)
op_imaging = ImagingOperator(model, image)

for i in range(nshots):
    print('Imaging source %d out of %d' % (i+1, nshots))

    # Update source location
    geometry.src_positions[0, :] = source_locations[i, :]

    # Generate synthetic data from true model
    true_d, _, _ = solver.forward(vp=model.vp)

    # Compute smooth data and full forward wavefield u0
    smooth_d, u0, _ = solver.forward(vp=model0.vp, save=True)

    # Compute gradient from the data residual
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)
    residual = smooth_d.data - true_d.data
    op_imaging(u=u0, v=v, vp=model0.vp, dt=model0.critical_dt,
               residual=residual)

```

    Imaging source 1 out of 21
    Imaging source 2 out of 21
    Imaging source 3 out of 21
    Imaging source 4 out of 21
    Imaging source 5 out of 21
    Imaging source 6 out of 21
    Imaging source 7 out of 21
    Imaging source 8 out of 21
    Imaging source 9 out of 21
    Imaging source 10 out of 21
    Imaging source 11 out of 21
    Imaging source 12 out of 21
    Imaging source 13 out of 21
    Imaging source 14 out of 21
    Imaging source 15 out of 21
    Imaging source 16 out of 21
    Imaging source 17 out of 21
    Imaging source 18 out of 21
    Imaging source 19 out of 21
    Imaging source 20 out of 21
    Imaging source 21 out of 21

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_image

# Plot the inverted image
plot_image(np.diff(image.data, axis=1))
```

```python
from devito import norm
assert np.isclose(norm(image), 1e7, rtol=1e1)
```

And we have an image of the subsurface with a strong reflector at the original location.

## References

[1] _Versteeg, R.J. & Grau, G. (eds.) (1991): The Marmousi experience. Proc. EAGE workshop on Practical Aspects of Seismic Data Inversion (Copenhagen, 1990), Eur. Assoc. Explor. Geophysicists, Zeist._


# Lab 2 Exercises for COMP 432 Machine Learning

In this lab you'll translate mathematics from lecture into practical Numpy code. Specifically, you'll implement _linear least squares regression_ and _logistic regression_ "from scratch" and compare the results of your own implementations to those of *scikit-learn*, a popular machine learning package.

Warning: Many of the code cells in this notebook use the same simple variable names like `X` or `y` but assign them to be different data. If you run cells out of order, you may get unexpected results, so be careful when switching between exercises.

**Run the code cell below** to import the required packages.

```python
import numpy as np
import matplotlib.pyplot as plt
import sklearn
import sklearn.linear_model
```

Lab2 requires a good understanding of Numpy and Matplotlib. Please complete Lab1 before attempting Lab2.

## 1. Gradient-based optimization

Exercises 1.1–1.3 are to help people who are unfamiliar with mathematical optimization or with implementing optimization in Numpy. The goal is to translate math into code on the simplest example possible.

*Note.* We depict mathematical vectors as column-vectors but, in machine learning, it is often more convenient to stack $N$ vectors as if each vector were a row-vector, resulting in an $N \times D$ matrix rather than a $D \times N$ matrix. Similarly, the choice between whether to represent a Numpy vector as 1-dimensional ($\mathbb{R}^D$) or as an explicit row-vector ($\mathbb{R}^{1 \times D}$) or column-vector ($\mathbb{R}^{D \times 1}$) can be difficult.

### Exercise 1.1 — Compute a simple gradient with Numpy

Consider the function
$$
f(\mathbf{x}) = \frac{1}{2}x_1^2 + (x_2-1)^2
$$

The gradient of this function is
$$
\nabla f(\mathbf{x}) = \begin{bmatrix}
\frac{\partial f}{\partial x_1}(\mathbf{x}) \\
\frac{\partial f}{\partial x_2}(\mathbf{x}) \\
\end{bmatrix}
= \begin{bmatrix}
x_1 \\
2(x_2-1) \\
\end{bmatrix}
$$

**Write two functions**, one that implements $f: \mathbb{R}^2 \rightarrow \mathbb{R}$ and one that implements $\nabla f : \mathbb{R}^2 \rightarrow \mathbb{R}^2$. However, rather than assuming argument $\mathbf{x} \in \mathbb{R}^2$, you should assume the argument is actually an $N \times 2$ matrix where each row represents a distinct value of $\mathbf{x}$ to be evaluated. This is because the functions will be used by 'vectorized' code. Your code should likewise be vectorized, without using Python loops.

```python
def f(x):
    """
    Given an ndarray 'x' with shape (N,2), returns an ndarray with shape (N,)
    where the value at index i is function f(.) applied to row x[i].
    """
    x1, x2 = np.transpose(x)
    return ((1/2) * x1**2) + (x2 - 1)**2

def f_grad(x):
    """
    Given an ndarray 'x' with shape (N,2), returns an ndarray with shape (N,2)
    where the two values in row i are the gradient of f(.) applied to row x[i].
    """
    x1, x2 = np.transpose(x)
    result = np.zeros_like(x)
    result[:,0] = x1
    result[:,1] = 2*(x2 - 1)
    return result.squeeze();
```

**Check your answer** by running the code cell below

```python
x = np.array([[3.5, 2.5], [-2.0, 0.5], [5.0, -3.0]])
y = f(x)
assert isinstance(y, np.ndarray), "Expected f(x) to return an ndarray"
assert y.shape == (len(x),), "Expected f(x) to return an ndarray of shape (N,)"
assert np.array_equal(y, [8.375, 2.25, 28.5]), "Wrong values returned by f(x):\n%s" % y
x_copy = x.copy()
g = f_grad(x_copy)
assert np.array_equal(x, x_copy), "f_grad(x) should not modify its argument"
assert isinstance(g, np.ndarray), "Expected f_grad(x) to return an ndarray"
assert g.shape == x.shape, "Expected f_grad(x) to return an ndarray of shape (N,2) when given "
assert np.array_equal(g, [[3.5,  3], [-2, -1], [5, -8]]), "Wrong values returned by f_grad(x):\n%s" % g
print("Correct!")
```

    [[0. 0.]
     [0. 0.]
     [0. 0.]]
    [ 3.5 -2.   5. ]
    [ 2.5  0.5 -3. ]
    [[ 3.5  3. ]
     [-2.  -1. ]
     [ 5.  -8. ]]
    Correct!

### Exercise 1.2 — Plotting $f(\mathbf{x})$ and $\nabla f(\mathbf{x})$ with vectorized code

If you plot the values of $f(\mathbf{x})$ from Exercise 1.1 using $\mathbf{x}=(x_1, x_2)$ over the square interval $[-3, 3]$, you can see the "shape" of the function. For example, if we use a grid size to $10 \times 10$ we get a coarse version of the plot:

If we use a grid size of $100 \times 100$ we get a smooth version:

We can see that the function has a global minimum at $\mathbf{x}=(0, 1)$.

Generating this kind of plot requires evaluating $f(\mathbf{x})$ at every point in a 2D grid interval.
The above plot was generated by a single call to Matplotlib's **[imshow](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.imshow.html)** function. The argument to _imshow_ was a $100 \times 100$ matrix where entry $[i,j]$ was assigned value $f(\mathbf{x})$ for a specific $\mathbf{x}$ on the grid. The _imshow_ function automatically mapped different values of $f(\mathbf{x})$ to different colours on a scale.

In Python, efficiently evaluating $f(\mathbf{x})$ at 10000 distinct values of $\mathbf{x}$ requires vectorized code, so that $f$ can be called once on a big $1000 \times 2$ array rather than being called 10000 times on small arrays of size $2$. An initial plotting function is given in the code cell below. Run it to reproduce the above plot.

**Write additional lines of plotting code** to enhance the plot in two ways:
1. Overlay the contours of $f(\mathbf{x})$ using Matplotlib's **[contour](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.contour.html)** function.
2. Overlay the gradient field $\nabla f(\mathbf{x})$ using Matplotlib's **[quiver](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.quiver.html)** function.

Your final plot should look something like this:

To generate the gradient field in a vectorized manner, apply the *eval_func_on_grid* function directly to your *f_grad* implementation from Exercise 1.1. For a vector field it's best to use a smaller *gridsize*, like $10 \times 10$. If you do not understand how the vectorized code works, investigate by adding lines to print the shapes of arrays, like `print(x1.shape)`, and if that fails try asking a TA.

```python
def eval_func_on_grid(func, gridsize):                # (You should be able to understand the code in this function)
    steps = np.linspace(-3, 3, gridsize)              # Make list of distinct grid values ranging from -3 to 3
    x1, x2 = np.meshgrid(steps, steps)                # Build a grid (x1[i,j], x2[i,j]) of distinct x values for each (i, j)
    x = np.column_stack([x1.ravel(), x2.ravel()])     # Stack each pair of x values into a new (gridsize*gridsize, 2) array
    y = func(x)                                       # Apply 'func', expecting a (gridsize*gridsize, :) array of outputs
    y = y.reshape(gridsize, gridsize, -1).squeeze()   # Reshape y from (gridsize*gridsize, :) to (gridsize, gridsize, :)
    return x1, x2, y

def plot_exercise12():
    # Evaluate f(x) on a dense 100x100 grid and plot it
    x1, x2, y = eval_func_on_grid(f, 100)

    # Plot the (100,100)
    plt.imshow(y, origin='lower', extent=(x1.min(), x1.max(), x2.min(), x2.max()))
    plt.colorbar()

    # Configure the plot
    plt.xlabel('$x_1$')
    plt.ylabel('$x_2$')
    plt.title('Plot of $f(\\mathbf{x})$')
    plt.gca().set_aspect('equal')  # Force the x and y axes to scale

    plt.contour(x1, x2, y, colors='white', linestyles='dashed')
    x3, x4, y2 = eval_func_on_grid(f_grad, 10)
    plt.quiver(x3, x4, y2[:,:,0], y2[:,:,1], color='white')

plot_exercise12()
```

### Exercise 1.3 — Implement gradient descent on a simple function

Gradient descent is an iterative algorithm that repeatedly takes steps in the direction opposite the gradient:

$$
\mathbf{x}_\text{new} = \mathbf{x}_\text{old} - \eta \nabla f(\mathbf{x}_\text{old})
$$

The step size is scaled by the *learning rate*, which is chosen to be some constant $\eta \gt 0$.

**Write a function** that runs several steps of gradient descent on the function $f$ from Exercise 1.1. Use the *f_grad* function that you wrote for Exercise 1.1.

```python
def gradient_descent_on_f(x_init, learn_rate, num_steps):
    """
    Runs num_steps of gradient descent from point x_init using
    the given learning rate, and returns the new x coordinate.
    """
    for step in range(num_steps):
        x_init = x_init - learn_rate * f_grad(x_init)
    return x_init
```

**Check your answer** by running the code cell below

```python
x = gradient_descent_on_f(np.array([[0, 1]]), 100.0, 1)
assert isinstance(x, np.ndarray), "Expected ndarray"
assert np.array_equal(x, [[0, 1]]), "Gradient descent shouldn't move away from optimal value!"
x = gradient_descent_on_f(np.array([[1, -2]]), 0.25, 1)
assert np.array_equal(x, [[0.75, -0.5]]), "The first gradient step seems to be wrong!"
x = gradient_descent_on_f(x, 0.1, 3)
assert np.allclose(x, [[0.54675, 0.232]]), "The gradient seems to be wrong after a few steps!"
print("Correct!")
```

    Correct!

**Plot the path of gradient descent** by running the code cell below. You should see a path of little red 'x' marks that converge near $\mathbf{x}^*=(0,1)$.

Your final plot should look something like this:

```python
# Plot the loss surface and level contours first.
plot_exercise12()

# Run gradient descent in chunks of 5 steps at a time, plotting the resulting 'x' after each chunk
learn_rate = 0.05
x_init = np.array([[-2.0, -1.0]])                             # Start from initial point (-2, -1)
for num_steps in range(0, 100, 5):                            # Repeatedly run gradient descent from the initial point,
    x = gradient_descent_on_f(x_init, learn_rate, num_steps)  # eventually running it for 100 steps.
    plt.plot(*x[0], 'xr');                                    # Add a little 'x' to the plot to show how far it got.
```

## 2. Linear least squares regression

Exercises 2.1–2.5 ask you to implement linear least squares regression, and to compare your results to applying the scikit-learn **[LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)** model.

### Exercise 2.1 — Vectorized code for generating predictions from a basic linear model

Recall from Lecture 1 that a basic linear model has the form:
$$
\hat{y}(\mathbf{x}, \mathbf{w}) = \mathbf{x}^T \mathbf{w}
$$
where
$$
\begin{align}
\mathbf{x} &= \begin{bmatrix} 1 & x_1 & \ldots & x_D \end{bmatrix}^T\\
\mathbf{w} &= \begin{bmatrix} w_0 & w_1 & \ldots & w_D \end{bmatrix}^T
\end{align}
$$

If both $\mathbf{x}$ and $\mathbf{w}$ are column vectors, the following Python function would evaluate the linear model $\hat{y}(\mathbf{x}, \mathbf{w})$ correctly:
```python
def linear_model_predict(x, w):
    """Returns a prediction from linear model y(x, w) at point x using parameters w."""
    return x.T @ w   # Return the inner product (dot product) of vectors x and w
```
However, we want a version of *linear_model_predict* that vectorizes across many $\mathbf{x}$ simultaneously. Specifically, given a matrix of inputs:

$$
X = \begin{bmatrix}
\mathbf{x}_1^T\\
\vdots \\
\mathbf{x}_N^T
\end{bmatrix}
$$

we want *linear_model_predict* to compute a vector of outputs:

$$
\hat{\mathbf{y}} = \begin{bmatrix}
\mathbf{x}_1^T \mathbf{w}\\
\vdots\\
\mathbf{x}_N^T \mathbf{w}
\end{bmatrix}
$$

However, if we substitute $x$ with $X$ we can no longer use expression `X.T @ w`; the matrix $X^T \in \mathbb{R}^{(D+1) \times N}$ isn't even the right shape to be on the left-hand side of the product. Writing vectorized code is full of annoying little problems like this.

**Write a function** that evaluates the linear model in vectorized fashion. Specifically, when given a matrix $X \in \mathbb{R}^{N \times (D+1)}$ as an argument, you should figure out what mathematical expression would result in the $\hat{\mathbf{y}}\in\mathbb{R}^N$ vector shown above. Hint: the solution is only a small change from `X.T @ w`.

```python
def linear_model_predict(X, w):
    """
    Returns predictions from linear model y(X, w) at each point X[i,:] using parameters w.
    Given X with shape (N,D+1), w must have shape (D+1,) and the result will have shape (N,).
    """
    return X @ w
```

**Check your answer** by running the code cell below.

```python
w = np.array([2, 0.5])                            # Parameters corresponding to the 1D line y = 2 + 0.5*x1
X = np.array([[1., -3.], [1.,  3.], [1.,  5.]])   # Evaluate at x1 = -3, 2, 5
y = linear_model_predict(X, w)                    # Predict y for all X using w
assert isinstance(y, np.ndarray), "Expected an ndarray!"
assert np.array_equal(y, [0.5, 3.5, 4.5]), "Wrong predictions!\n%s" % y
try:
    y = linear_model_predict(X, w.reshape(-1, 1))
except ValueError:
    raise AssertionError("Your answer works when 'w' is 1-dimensional, but not when it is a column vector. Try again.")
w = np.array([1, 0.5, 0.25])                      # Parameters corresponding to the 2D plane y = 1 + 0.5*x1 + 0.25*x2
X = np.array([[1., -3., 1.], [1.,  3., 0.], [1.,  5., -2.]])   # Evaluate at different (x1, x2) points
y = linear_model_predict(X, w)                    # Predict y for all X using w
assert np.array_equal(y, [-0.25, 2.5, 3.0]), "Wrong predictions for 2-dimensional feature space!\n%s" % y
print("Correct!")
```

    Correct!

**Plot several predictions at once** by running the code cell below.

```python
w = np.array([2, 0.5])           # Parameters corresponding to the 1D line y = 2 + 0.5*x1
x0 = np.ones(20)                 # A column of 1s so that the bias term w[0] gets added
x1 = np.linspace(-5, 5, 20)      # A column of x values ranging from [-5, 5]
X = np.column_stack([x0, x1])    # A 20x2 matrix where X[i,:] is the ith x vector
y = linear_model_predict(X, w)   # Evaluate all x values
plt.scatter(x1, y, 10, 'r')
plt.xlabel("$x_1$")
plt.ylabel("$y$")
plt.title("Sample predictions for linear model $y=2 + \\frac{1}{2}x_1$");
```

### Exercise 2.2 — Linear least squares regression by gradient descent

Here you'll implement a 'learning' algorithm for linear least squares regression. Recall from Lecture 1 that the least squares training objective is:

$$
\ell(\mathbf{w}) = \frac{1}{2} \sum_{i=1}^N (y - \hat{y}(\mathbf{x}_i, \mathbf{w}))^2
$$

The gradient for the above training objective is on the slide titled "Linear least squares *learning*" from Lecture 1. You'll need it.

**Write a function** to implement linear least squares regression by gradient descent. Use the `@` operator (matrix multiplication) in your answer.

```python
def linear_regression_by_gradient_descent(X, y, w_init, learn_rate=0.05, num_steps=500):
    """
    Fits a linear model by gradient descent.

    If the feature matrix X has shape (N,D) the targets y should have shape (N,)
    and the initial parameters w_init should have shape (D,).

    Returns a new parameter vector w that minimizes the squared error to the targets.
    """
    w = w_init
    for step in range(num_steps):
        gradient = ((X.T @ X) @ w) - (X.T @ y)
        w = w - (gradient * learn_rate)
    return w
```

**Check your answer** by running the code cell below.

```python
X = np.array([[1, 0.0], [1, 1.0], [1, 2.0]])
y = np.array([4.0, 3.0, 2.0])
w = linear_regression_by_gradient_descent(X, y, np.array([0.0, 0.0]))
assert isinstance(w, np.ndarray), "Expected ndarray!"
assert w.shape == (2,), "Wrong shape for final parameters!\n%s" % w
assert np.allclose(w, [4, -1]), "Wrong values for final parameters!\n%s" % w
print("Correct!")
```

    Correct!

### Exercise 2.3 — Linear least squares regression by direct solution

As discussed in class, the optimal parameters $\mathbf{w}^*$ for linear least squares regression can be solved *directly*, rather than iteratively.

**Write a function** to solve linear least squares regression directly. Use Numpy's matrix inverse function **[np.linalg.inv](https://numpy.org/devdocs/reference/generated/numpy.linalg.inv.html)** in your answer.

```python
def linear_regression_by_direct_solve(X, y):
    """Fits a linear model by directly solving for the optimal parameter vector w."""
    return np.linalg.inv(X.T @ X) @ y
```

```python
w = linear_regression_by_direct_solve(X, y)
assert isinstance(w, np.ndarray), "Expected ndarray!"
assert w.shape == (2,), "Wrong shape for final parameters!\n%s" % w
assert np.allclose(w, [4, -1]), "Wrong values for final parameters!\n%s" % w
print("Correct!")
```

### Exercise 2.4 — Run linear least squares regression and plot the result

For this exercise you'll need to define Numpy arrays that correspond to the following training data:

$$
X = \begin{bmatrix}
1 & -2.2\\
1 & -0.3\\
1 &  1.5\\
1 &  4.8
\end{bmatrix}, \quad
\mathbf{y} = \begin{bmatrix}
-1.2 \\
1.5\\
4.2\\
5.3
\end{bmatrix}
$$

**Write code** to create the following plot:

Your code should follow this sequence of steps:
1. Make ndarrays $X$ and $\mathbf{y}$ that contain the above training set.
2. Plot the training set in blue. Use the $x$ coordinates from the second column of $X$, ignoring the first column.
3. Run linear least squares regression on $(X, \mathbf{y})$ to get fitted parameters $\mathbf{w}$; use your *linear_regression_by_direct_solve* function.
4. Define a "test set" of 20 equally-spaced values of $x$ in range $[-5, 5]$. You will need to build a new matrix $X_\text{test}$ with $1$ in the first column and the 20 distinct $x$ values in the second column. See how this is done in the last code cell of Exercise 2.1.
5. Predict 20 $y$ values corresponding to the 20 rows of $X_\text{test}$ by applying a linear model with your fitted parameters $\mathbf{w}$. Do this with single call to your *linear_model_predict* function.
6. Plot the predictions on the test set.

```python
# i couldnt figure out how to not use a list :(
X = np.array([[1, -2.2], [1, -0.3], [1, 1.5], [1, 4.8]])
y = np.array([-1.2, 1.5, 4.2, 5.3])

plt.scatter(X[:,1], y, label='Training Points')

# and my direct solve doesnt work right :(
w = linear_regression_by_direct_solve(X, y)

ones = np.ones(20)
fives = np.arange(-5, 5.52631579, 0.52631579)
testSetX = np.stack([ones, fives], axis=1)
testSetY = linear_model_predict(testSetX, w)

plt.scatter(x1, testSetY, label='Test Predictions')

```

### Exercise 2.5 — Run scikit-learn LinearRegression

The scikit-learn package provides a **[LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)** object to perform linear least squares regression (also known as "ordinary" least squares).

**Write code to fit a LinearRegression model** using the same training matrix $X$ that you defined as part of Exercise 2.4. There are only two steps:
1. Create the _LinearRegression_ object. Use the *fit_intercept=False* option when creating the *LinearRegression* object (see [documentation](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)), since the $X$ matrix already has a column of 1s corresponding to an intercept parameter (the 'bias' parameter).
2. Fit the _LinearRegression_ object to the training matrix $X$ and targets $\mathbf{y}$. Use the object's **[fit](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html#sklearn.linear_model.LinearRegression.fit)** method.

The variable holding a reference to your _LinearRegression_ object should be called `linear_model`, so that your answer can be checked.

```python
linear_model = sklearn.linear_model.LinearRegression(fit_intercept=False)
linear_model.fit(X, y)
```

    LinearRegression(fit_intercept=False)

**Check your answer** by running the code cell below.

```python
assert 'linear_model' in globals(), "You didn't create a variable named 'linear_model'!"
assert isinstance(linear_model, sklearn.linear_model.LinearRegression), "Expected a LinearRegression instance!"
assert hasattr(linear_model, 'coef_'), "No model coefficients yet! You didn't fit the model to any data!"
assert linear_model.intercept_ == 0.0, "You forgot to disable fitting of the intercept!"
assert np.allclose(linear_model.coef_, [[1.57104472, 0.92521608]]), "The model parameters you learned seem incorrect!"
print("Correct!")
```

    Correct!

**Plot several LinearRegression model predictions at once** by running the code cell below.

```python
x0 = np.ones(20)                        # A column of 1s so that the bias term w[0] gets added
x1 = np.linspace(-5, 5, 20)             # A column of x values ranging from [-5, 5]
X_test = np.column_stack([x0, x1])      # A 20x2 matrix where X[i,:] is the ith x vector
y_test = linear_model.predict(X_test)   # Evaluate all x values
plt.scatter(x1, y_test, 10, 'r')
plt.scatter(X[:,1], y, edgecolor='b', facecolor='none')
plt.xlabel("$x_1$")
plt.ylabel("$y$")
plt.title("Sample predictions for LinearRegression model");
```

You can also compare the model's `coef_` attribute (coefficients, i.e. model parameters) to the parameter vector $\mathbf{w}$ that your own implementation gave from Exercise 2.4 (just use `print(w)` in your previous answer to see those values).

## 3. Logistic regression

Exercises 3.1–3.4 ask you to implement logistic regression, and to compare your results to applying the scikit-learn **[LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)** model.

### Exercise 3.1 — Vectorized code for generating predictions from a logistic model

Recall from Lecture 1 that the logistic model has the form:
$$
\hat{y}(\mathbf{x}, \mathbf{w}) = \sigma(\mathbf{x}^T \mathbf{w})
$$

where $\mathbf{x}$ and $\mathbf{w}$ are the same as for Exercise 2.1 and $\sigma(\cdot)$ is the logistic sigmoid function described in Lecture 1.

**Write a function** that evaluates the logistic model in vectorized fashion, just like you did for Exercise 2.1.

```python
def sigmoid(z):
    """Returns the element-wise logistic sigmoid of z."""
    return 1 / (1 + np.exp(-z))

def logistic_model_predict(X, w):
    """
    Returns predictions from logistic model y(x, w) at each point X[i,:] using parameters w.
    Given X with shape (N,D+1), w must have shape (D+1,) and the result will have shape (N,).
    """
    squish = X @ w
    return sigmoid(squish)
```

**Check your answer** by running the code cell below.

```python
y = sigmoid(np.array([-1., 0., 1.5]))
assert isinstance(y, np.ndarray), "Expected an ndarray!"
assert np.allclose(y, [0.26894142, 0.5, 0.81757448]), "Values from sigmoid() appear to be wrong!"
w = np.array([2, 1.5])                           # Parameters corresponding to logistic model y = sigmoid(2 + 1.5*x1)
X = np.array([[1., -2.], [1.,  0.], [1.,  2.]])  # Evaluate at x1 = -2, 0, 2
y = logistic_model_predict(X, w)                 # Predict y for all X using w
assert isinstance(y, np.ndarray), "Expected an ndarray!"
assert np.allclose(y, [0.26894142, 0.88079708, 0.99330715]), "Wrong returned!\n%s" % y
try:
    y = logistic_model_predict(X, w.reshape(-1, 1))
except ValueError:
    raise AssertionError("Your answer works when 'w' is 1-dimensional, but not when it is a column vector. Try again.")
w = np.array([1, 0.5, 0.25])                      # Parameters corresponding to the 2D plane y = 1 + 0.5*x1 + 0.25*x2
X = np.array([[1., -3., 1.], [1.,  3., 0.], [1.,  5., -2.]])   # Evaluate at different (x1, x2) points
y = logistic_model_predict(X, w)                  # Predict y for all X using w
assert np.allclose(y, [0.4378235, 0.92414182, 0.95257413]), "Wrong predictions for 2-dimensional feature space!\n%s" % y
print("Correct!")
```

    Correct!

**Plot several predictions at once** by running the code cell below.

```python
w = np.array([2, 1.5])                       # Parameters corresponding to logistic model y = sigmoid(2 + 1.5*x1)
x0 = np.ones(20)                             # A column of 1s so that the bias term w[0] gets added
x1 = np.linspace(-5, 5, 20)                  # A column of x values ranging from [-5, 5]
X_test = np.column_stack([x0, x1])           # A 20x2 matrix where X[i,:] is the ith x vector
y_test = logistic_model_predict(X_test, w)   # Evaluate all x values
plt.scatter(x1, y_test, 10, 'r')
plt.xlabel("$x_1$")
plt.ylabel("$y$")
plt.title("Sample predictions for logistic model $y=\sigma(2 + \\frac{3}{2}x_1)$");
```

### Exercise 3.2 — Logistic regression by gradient descent

Recall from Lecture 1 that the basic logistic regression training objective (learning objective) is:

$$
\ell_\text{LR}(\mathbf{w}) = \sum_{i=1}^N y_i \ln \sigma(\mathbf{w}^T \mathbf{x}_i) + (1-y_i) \ln \left(1-\sigma(\mathbf{w}^T \mathbf{x}_i)\right)
$$

The "basic" gradient for the above training objective is on a slide titled "Maximum likelihood estimate for LR" from Lecture 1, and reproduced here:

$$
\nabla \ell_\text{LR}(\mathbf{w}) = \sum_{i=1}^N (\sigma(\mathbf{w}^T \mathbf{x}_i) - y_i)\mathbf{x}_i
$$

**Write a function** to implement logistic regression by gradient descent. Your answer to _logistic_regression_grad_ should ideally be fully vectorized (no for-loops), but this may take a while to figure out. If you can't figure out the vectorization, it's OK — just compute the gradient however you can. Your answer to _logistic_regression_ should use your _logistic_regression_grad_ function to compute the gradient at each step.

Implementing _logistic_regression_grad_ is the hardest exercise in this lab because a vectorized implementation requires using the `@` matrix multiply operator to compute all the $\mathbf{w}^T \mathbf{x}$ products, reshaping the vector of residuals into a column-vector to use Numpy's broadcasting feature, and then summing over a specific axis (over training cases $i=1,\ldots,N$).

```python
def logistic_regression_grad(X, y, w):
    """Returns the gradient for basic logistic regression."""
    return (sigmoid(X @ w) - y) @ X

def logistic_regression(X, y, w_init, learn_rate=0.05, num_steps=500):
    """
    Fits a logistic model by gradient descent.

    If the feature matrix X has shape (N,D) the targets y should have shape (N,)
    and the initial parameters w_init should have shape (D,).

    Returns a new parameter vector w that minimizes the negative log likelihood of the targets.
    """
    w = w_init
    for step in range(num_steps):
        gradient = logistic_regression_grad(X, y, w)
        w = w - (gradient * learn_rate)
    return w
```

**Check your answer** by running the code cell below.

```python
X = np.array([[1, -1.0], [1, 1.0], [1, 2.0]])
y = np.array([0.0, 0.0, 1.0])
grad = logistic_regression_grad(X, y, np.array([0.0, 1.0]))
assert isinstance(grad, np.ndarray), "Expected ndarray from logistic_regression_grad!"
assert grad.shape == (2,), "Expected gradient to have shape (2,) but was %s" % (grad.shape,)
assert np.allclose(grad, [0.88079708, 0.22371131]), "Wrong value for gradient!"
grad = logistic_regression_grad(X, y, np.array([-1.0, 1.5]))
assert np.allclose(grad, [0.57911459, 0.30819531]), "Wrong value for gradient!"
w = logistic_regression(X, y, np.array([1.0, 0.0]))
assert isinstance(w, np.ndarray), "Expected ndarray from logistic_regression!"
assert w.shape == (2,), "Expected parameter vector w to have shape (2,) but was %s" % (w.shape,)
assert np.allclose(w, [-4.14100532, 2.95489589]), "Parameters found by gradient descent seem wrong!"
print("Correct!")
```

    Correct!

### Exercise 3.3 — Run logistic regression on data and plot the result

For this exercise you'll need to define Numpy arrays that correspond to the following training data:

$$
X = \begin{bmatrix}
1 & -4.1\\
1 & -2.8\\
1 & -0.7\\
1 &  3.5
\end{bmatrix}, \quad
\mathbf{y} = \begin{bmatrix}
0 \\
0\\
1\\
1
\end{bmatrix}
$$

**Write code** to create the following plot:

Your code should follow this sequence of steps, which are the same as for Exercise 2.4:
1. Make ndarrays $X$ and $\mathbf{y}$ that contain the above training set.
2. Plot the training set in blue. Use the $x$ coordinates from the second column of $X$, ignoring the first column.
3. Run logistic regression on $(X, \mathbf{y})$ to get fitted parameters $\mathbf{w}$; use your *logistic_regression* function, starting from $\mathbf{w}_\text{init} = \begin{bmatrix} 0.0, 1.0 \end{bmatrix}^T$
4. Define a "test set" of 20 equally-spaced values of $x$ in range $[-5, 5]$. You will need to build a new matrix $X_\text{test}$ with $1$ in the first column and the 20 distinct $x$ values in the second column.
5. Predict 20 $y$ values corresponding to the 20 rows of $X_\text{test}$ by applying a logistic model with your fitted parameters $\mathbf{w}$. Do this with single call to your *logistic_model_predict* function.
6. Plot the predictions on the test set.

```python
X = np.array([[1, -4.1], [1, -2.8], [1, -0.7], [1, 3.5]])
y = np.array([0, 0, 1, 1])

plt.scatter(X[:,1], y, label='Training Points')

w = logistic_regression(X, y, np.arange(0, 2, dtype='float32'))

ones = np.ones(20)
fives = np.arange(-5, 5.52631579, 0.52631579)
testSetX = np.stack([ones, fives], axis=1)

yValues = logistic_model_predict(testSetX, w)
# 6. Plot the test predictions. Aim for 1 line, plus a few lines to configure the plot (axis labels etc).
plt.scatter(x1, yValues, 15, label='Test Predicitons')
plt.legend()
```

### Exercise 3.4 — Run scikit-learn LogisticRegression

The scikit-learn package provides a **[LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)** object to perform logistic regression.

**Write code to fit a LogisticRegression model** using the same training matrix $X$ that you defined as part of Exercise 3.3. There are only two steps:
1. Create the _LogisticRegression_ object. Use the *fit_intercept=False*, *penalty='none'*, and *solver='lbfgs'* options when creating the *LogisticRegression* object (see [documentation](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)). (L-BFGS is a more powerful gradient-based solver than mere gradient descent.)
2. Fit the _LogisticRegression_ object to the training matrix $X$ and targets $\mathbf{y}$. Use the object's **[fit](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression.fit)** method.

The variable holding a reference to your _LogisticRegression_ object should be called `logistic_model`, so that your answer can be checked.

A tweet regarding the fact that scikit-learn's LogisticRegression object applies regularization (a weight penalty) "by default":

```python
logistic_model = sklearn.linear_model.LogisticRegression(fit_intercept=False, penalty='none', solver='lbfgs')
logistic_model.fit(X, y)
```

    LogisticRegression(fit_intercept=False, penalty='none')

**Check your answer** by running the code cell below.

```python
assert 'logistic_model' in globals(), "You didn't create a variable named 'logistic_model'!"
assert isinstance(logistic_model, sklearn.linear_model.LogisticRegression), "Expected a LogisticRegression instance!"
assert hasattr(logistic_model, 'coef_'), "No model coefficients yet! You didn't fit the model to any data!"
assert logistic_model.intercept_ == 0.0, "You forgot to disable fitting of the intercept!"
assert np.allclose(logistic_model.coef_, [[18.5251137, 10.49283446]]), "The parameters seem incorrect! Not L-BFGS?"
print("Correct!")
```

    Correct!

Notice that the model parameters (coefficients) found by the _LogisticRegression_ are much larger than those found by your gradient descent solver. That is only because scikit-learn uses a more powerful optimization algorithm and can learn very sharp decision boundaries in fewer steps than mere gradient descent can. If you increase your *num_steps* argument your solver will find similarly large coefficients.

**Plot several LogisticRegression predictions at once** by running the code cell below.

```python
x0 = np.ones(50)                               # A column of 1s so that the bias term w[0] gets added
x1 = np.linspace(-5, 5, 50)                    # A column of x values ranging from [-5, 5]
X_test = np.column_stack([x0, x1])             # A 20x2 matrix where X[i,:] is the ith x vector
y_test = logistic_model.predict_proba(X_test)  # Evaluate all x values and get two probabilities back (class 0, class 1)
plt.scatter(x1, y_test[:,1], 10, 'r')          # Plot probability of class 1 only
plt.scatter(X[:,1], y, edgecolor='b', facecolor='none')
plt.xlabel("$x_1$")
plt.ylabel("$y$")
plt.title("Sample predictions for LogisticRegression model");
```


# 第 5 章 模型比较

模型应该被设计成帮助我们理解特定问题或某类相关问题的近似值，而不是真实世界的翻版，从这个意义上讲所有模型都是错误的。即使在有先验的情况下，模型也都是错误的，但每个模型的错误可能有所不同，而其中一些模型比其他模型更好地描述了给定问题。此前的章节将注意力集中在推断问题上，即如何从数据中学习参数的分布。本章将重点讨论一个互补问题：如何比较用于解释相同数据的多个模型。这是数据分析需解决的关键问题之一。

本章将讨论以下内容：

- 后验预测检查
- 奥卡姆剃刀---简单性和准确性
- 过拟合和欠拟合
- 信息准则
- 贝叶斯因子
- 正则化先验

---

## 5.1 最直观的模型比较方法 -- 后验预测分布

`第一章 概率思维`介绍了后验预测检查的概念，本章将用它来评估拟合出的模型对相同数据的解释程度。如前所述，所有的模型都是错误的，因此后验预测检查的目的并非判定某个模型是否错误，而是希望通过后验预测检查更好地把握模型的局限性，以做出适当改进。模型不会再现所有问题，但这并不是问题，因为构建模型都有特定目的，后验预测检查则是在该目的背景下评估模型的一种方式；因此当考虑了多个模型时，可使用后验预测检查来对它们进行比较。

让我们读取并绘制一个简单的数据集：

```
import matplotlib.pyplot as plt
import scipy.stats as stats
import numpy as np
import pandas as pd
import seaborn as sns
import pymc3 as pm
import arviz as az

az.style.use('arviz-darkgrid')
```

```
dummy_data = np.loadtxt('../data/dummy.csv')
x_1 = dummy_data[:, 0]
y_1 = dummy_data[:, 1]
order = 2
x_1p = np.vstack([x_1**i for i in range(1, order+1)])
x_1s = (x_1p - x_1p.mean(axis=1, keepdims=True)) / x_1p.std(axis=1, keepdims=True)
y_1s = (y_1 - y_1.mean()) / y_1.std()
plt.scatter(x_1s[0], y_1s)
plt.xlabel('x')
plt.ylabel('y')
```

图 5.1

现在，用两个略有不同的模型来拟合数据，第一个是线性模型，第二个是二阶多项式模型：

```
with pm.Model() as model_l:
    α = pm.Normal('α', mu=0, sd=1)
    β = pm.Normal('β', mu=0, sd=10)
    ϵ = pm.HalfNormal('ϵ', 5)
    μ = α + β * x_1s[0]
    y_pred = pm.Normal('y_pred', mu=μ, sd=ϵ, observed=y_1s)

    trace_l = pm.sample(2000)

with pm.Model() as model_p:
    α = pm.Normal('α', mu=0, sd=1)
    β = pm.Normal('β', mu=0, sd=10, shape=order)
    ϵ = pm.HalfNormal('ϵ', 5)
    μ = α + pm.math.dot(β, x_1s)
    y_pred = pm.Normal('y_pred', mu=μ, sd=ϵ, observed=y_1s)

    trace_p = pm.sample(2000)
```

现在绘制这两个模型的平均拟合曲线：

```
x_new = np.linspace(x_1s[0].min(), x_1s[0].max(), 100)
α_l_post = trace_l['α'].mean()
β_l_post = trace_l['β'].mean(axis=0)
y_l_post = α_l_post + β_l_post *x_new
plt.plot(x_new, y_l_post, 'C1', label='linear model')

α_p_post = trace_p['α'].mean()
β_p_post = trace_p['β'].mean(axis=0)
idx = np.argsort(x_1s[0])
y_p_post = α_p_post + np.dot(β_p_post, x_1s)
plt.plot(x_1s[0][idx], y_p_post[idx], 'C2', label=f'model order {order}')

α_p_post = trace_p['α'].mean()
β_p_post = trace_p['β'].mean(axis=0)
x_new_p = np.vstack([x_new**i for i in range(1, order+1)])
y_p_post = α_p_post + np.dot(β_p_post, x_new_p)
plt.scatter(x_1s[0], y_1s, c='C0', marker='.')
plt.legend()
```

图 5.2

图中二阶模型似乎做得更好，但线性模型也并没有那么糟糕。此时可以使用 PyMC3 来获得两个模型的后验预测样本，并执行检查：

```
y_l = pm.sample_posterior_predictive(trace_l, 2000, model=model_l)['y_pred']
y_p = pm.sample_posterior_predictive(trace_p, 2000, model=model_p)['y_pred']
```

正如已经看到的，后验预测检查通常使用可视化方式来执行，如下例所示：

```
plt.figure(figsize=(8, 3))
data = [y_1s, y_l, y_p]
labels = ['data', 'linear model', 'order 2']
for i, d in enumerate(data):
    mean = d.mean()
    err = np.percentile(d, [25, 75])
    plt.errorbar(mean, -i, xerr=[[-err[0]], [err[1]]], fmt='o')
    plt.text(mean, -i+0.2, labels[i], ha='center', fontsize=14)
    plt.ylim([-i-0.5, 0.5])
    plt.yticks([])
```

图 5.3

图 5.3 显示了数据、线性模型和二次多项式模型的均值和四分位数范围。该图对各模型的后验预测样本做了平均，而且两个模型的均值都复现得很好，分位数范围也不是很差。不过在实际问题中，一些小差异可能是值得注意的。可以尝试做更多不同曲线图来探索后验预测分布。例如，绘制均值和四分位数间相对于数据真实值的离散度。下图就是一个例子：

```
fig, ax = plt.subplots(1, 2, figsize=(10, 3), constrained_layout=True)

def iqr(x, a=0):
    return np.subtract(*np.percentile(x, [75, 25], axis=a))

for idx, func in enumerate([np.mean, iqr]):
    T_obs = func(y_1s)
    ax[idx].axvline(T_obs, 0, 1, color='k', ls='--')
    for d_sim, c in zip([y_l, y_p], ['C1', 'C2']):
    T_sim = func(d_sim, 1)
    p_value = np.mean(T_sim >= T_obs)
    az.plot_kde(T_sim, plot_kwargs={'color': c},
    label=f'p-value {p_value:.2f}', ax=ax[idx])
    ax[idx].set_title(func.__name__)
    ax[idx].set_yticks([])
    ax[idx].legend()
```

图 5.4

图 5.4 中黑色虚线表示根据真实数据计算的平均值和四分位数（因来自真实数据，为确切值而非分布）。图中曲线（与图 5.3 相同颜色代码）表示根据后验预测样本计算得出的均值分布（左图）或四分位数范围分布（右图）。图 5.4 还包括 `p-value` 值，该值来自于预测数据与实际数据的比较和计算。对于两个预测数据集合，我们计算了其平均值和四分位数范围，然后计算了两个统计量等于或大于根据实际数据统计量的比例。**一般而言，如果真实数据和预测结果一致，预期 `p-value` 值在 0.5 左右，否则将处于有偏的后验预测分布**。

```{tip}
贝叶斯 p 值只是一种衡量后验预测检查拟合度的数字方法。
```

贝叶斯 `p-value` 与频率派的 `p-value` 名字相似，定义基本上也相同：

```{math}
\text{Bayesian p-value}\triangleq p\left(T_{s i m} \geq T_{o b s} \mid y \right) \tag{式 5.1}  \label{式 5.1}
```

可以解释为：从模拟数据中获得与观测数据相同或更高统计量值的概率。$T$ 几乎可以是数据的任意统计量。在图 5.4 中，统计量是左侧的平均值和右侧的四分位数范围。通常 $T$ 应该在最初定义推断任务时就选择好。

这些 `p-value` 是贝叶斯的，因为其采样自后验预测分布。需要注意的是：贝叶斯的 `p-value` 不需要频率主义的任何零假设作为条件；事实上，我们拥有基于观测数据的整个后验分布。此外，贝叶斯也没有使用类似置信度的任何预定义阈值来声明统计显著性，当然也没有执行假设检验。这里只是试图计算一个数字来评估后验预测分布与数据集的拟合度。

无论使用曲线图还是数据摘要（如贝叶斯 `p-value` ），或是两者组合，后验预测检查都是非常灵活的。该概念可让分析师思考不同方法来探索后验预测分布，并使用合适的方法来讲述一个数据驱动的故事，包括但不限于模型比较。

在接下来几节中，我们探索一些其他模型比较的方法。

## 5.2 好模型的基本特征 -- 准确性与简约性的结合

假如对同一个问题（或数据）有两个模型，二者对数据解释得同样好，应该选哪个模型呢？有一个基本准则叫做**奥卡姆剃刀**，如果对同一现象有两种不同假说，应选用比较简单的那一种。关于奥卡姆剃刀的论证很多，其中一种说法与波普尔的可证伪性有关，还有一种说法是从实用角度提出的，因为简单模型相比复杂模型更容易理解，此外还有一种论证是基于贝叶斯统计的。这里不深入讨论该准则的论证细节，只将该准则当做一个有用而合理的常识。

在比较模型时，既要考虑简约型，也需要同时考虑**模型准确性**，即模型对数据拟合得怎么样。之前章节已出现过一些度量准确性的指标，如： $R^2$ 系数可视为线性回归中可解释方差的比例。但如果有两个模型，其中一个对数据的解释比另一个更准确，是否应该选更准确率的模型呢？

直觉上，似乎最好选择准确度高且简单的模型。但如果简单模型准确度最差，该怎么办？如何才能平衡这两种要素呢？为简化问题，此处引入一个例子来帮助理解如何平衡准确性与简约性。为了更形象些，该例使用一系列逐渐复杂的多项式来拟合同一个简单数据集，并且未采用贝叶斯方法，而是采用最小二乘估计来建模。当然，最小二乘估计其实可转化成带均匀先验的贝叶斯模型，因此，将其理解成贝叶斯方法也没问题。

```
x = np.array([4., 5., 6., 9., 12, 14.])
y = np.array([4.2, 6., 6., 9., 10, 10.])
plt.figure(figsize=(10, 5))
order = [0, 1, 2, 5]
plt.plot(x, y, 'o')
for i in order:
    x_n = np.linspace(x.min(), x.max(), 100)
    coeffs = np.polyfit(x, y, deg=i)
    ffit = np.polyval(coeffs, x_n)
    p = np.poly1d(coeffs)
    yhat = p(x)
    ybar = np.mean(y)
    ssreg = np.sum((yhat-ybar)**2)
    sstot = np.sum((y - ybar)**2)
    r2 = ssreg / sstot
    plt.plot(x_n, ffit, label=f'order {i}, $R^2$= {r2:.2f}')
plt.legend(loc=2)
plt.xlabel('x')
plt.ylabel('y', rotation=0)
```

图 5.5

### 5.2.1 参数太多导致过拟合

从图 5.5 可看出，模型复杂度增加时，对应的 $R^2$ 系数在上升。当多项式为 5 阶时，模型完美拟合了数据（ $R$^2$ 趋近于 1 表示更好地拟合了数据）。前面章节中讨论过，用多项式去解决实际问题并非特别好的办法。为什么 5 阶多项式能完美拟合所有数据呢？原因是模型中参数数量与样本数量相同，都是 6。也就是说，模型只是用另一种方式对数据进行了编码，并没有从数据中学到任何内容，只是记住了全部数据而已。此外，如果使用这几种模型做预测，5 阶多项式模型对数据的预测看起来也会非常奇怪。

假设收集了更多数据点。例如，收集到点 [(10，9)，(7，7)] （参见图 5.5）。与 1 阶或 2 阶模型相比，5 阶模型对这些点的解释效果如何？不是很好，对吧？5 阶模型没有在数据中学习任何有趣的模式，反而只是记住了一些东西，因此它在泛化到未来数据方面做得非常糟糕：

图 5.5

当一个模型与最初用于学习其参数的数据集非常吻合，但在拟合其他数据集却非常差时，被称为 `过拟合` 。过拟合是统计学和机器学习中一个普遍问题。描述过拟合问题的一个有效方法是将数据集视为由 `信号` 和 `噪声` 两部分组成。信号是想要从数据中了解到的东西，如果使用某个数据集，那是必然是因为我们认为该数据集中有一个信号，否则训练毫无意义；而噪声是数据中无用的部分，往往是测量误差、数据生成方式、数据损坏等因素带来的产物。当某个模型过于灵活，甚至能够学到噪声而隐藏信号时，该模型就会变得过拟合。避免过拟合是奥卡姆剃刀的确切理由之一。上例表明，如果仅关注模型对数据的解释能力，很容易被过拟合误导，因为理论上通过增加模型参数数量总是能够提高数据拟合的准确率。

### 5.2.2 参数太少导致欠拟合

继续关注该例，不过重点放在 0 阶模型上。在 0 阶模型中，所有 $\beta$ 参数都为 0，因而变量 $x$ 和 $y$ 间的线性关系变成了只描述结果变量的一个高斯模型。对于 0 阶模型来说，预测变量对模型不再有任何影响，模型只捕捉到结果变量的均值。换句话说，模型认为数据能够通过结果变量的均值以及一些高斯噪声来解释。我们称这种模型是欠拟合的，因为它实在太简单了，以至于不能从数据中获取有意义的模式。通常，一个参数很少的模型容易出现欠拟合。

### 5.2.3 简约性与准确性之间的平衡

经常与奥卡姆剃刀准则一起提到的是爱因斯坦的一句名言“事情应该尽可能简单，但不必过于简单”。我们在建模时需要保持某种平衡。理想状态下，模型既不过拟合也不欠拟合，因此，通常需要优化或者调整模型来权衡二者。

机器学习领域中，通常从`方差（variance）`和 `偏差（bias）` 两个角度来讨论和权衡二者：

- 高偏差（ `bias` ）是模型适应数据的能力不足导致的。高偏差可能使模型无法捕捉数据中一些关键模式，导致欠拟合。
- 高方差（ `variance`）是模型对数据中细节过于敏感导致的。高方差会使模型捕捉到数据中的噪声，导致过拟合。

图 5.5 中，0 阶模型具有较高偏差（和较低的方差），因为它偏向于在变量 $y$ 的平均值处返回一条平坦直线，而与 $x$ 值无关。5 阶模型具有较高的方差（和较低的偏差），你可以采用差别很大的方式设置六个点，会发现曲线将完美拟合其中的大多数点。

具有高偏差的模型具有更多偏见或惯性，而具有高方差的模型是思想更开放的模型。太有偏见的问题是没有能力容纳新证据；太开放的问题是最终会相信荒唐的东西。总体来说，如果提升其中一个方面，就会导致另外一方面的下降，这也是为什么人们称之 `偏差-方差平衡`，而我们最希望得到二者平衡的模型。

如何做到呢？这里有一些经验方法：

处理 variance 较大的问题
- 减少特征数量
- 使用更简单的模型
- 增大你的训练数据集
- 使用正则化
- 加入随机因子，例如采用 bagging 和 boosting 方法

处理 bias 较大的问题
- 增加特征数量
- 使用更复杂的模型
- 去掉正则化

## 5.3 模型准确性的主要度量方法 -- 交叉验证与信息准则等

在上例中，很容易看出 0 阶模型非常简单，而 5 阶模型相对数据过于复杂，但其他两个模型呢？要回答该问题，需要一种原则性的方式，在考虑准确性同时，兼顾考虑简约性。要做到这一点，需要引入几个新概念：

- **样本内精度**：基于拟合模型的样本数据测量得到的模型精度。
- **样本外精度**：用拟合模型的样本数据以外的数据测量得到的模型精度（也称为 `预测精度`）。

对于数据和模型的任意组合，样本内精度平均将小于样本外精度。使用样本内精确度会使我们认为拥有了一个比实际更好的模型。样本外测量比样本内测量更可取，但也存在问题。因此，合理的做法是放弃一部分样本数据，仅仅将其用于对模型的测试。但对大多数分析师来说，仅将花大成本得到的数据用作测试，似乎过于奢侈。为避免该问题，人们花了很多精力用于获得使用样本内数据来估计样本外精度的方法。其中两种方法包括：

- 交叉验证：这是一种经验性策略，将数据分为多个子集，并轮流将其中一个子集作为测试集，剩余子集作为训练集进行评估。
- 信息准则：这是几个相对简单的表达式的总称，可认为这些表达式能够近似执行交叉验证后获得的结果。

### 5.3.1 交叉验证（Cross-validation）

交叉验证是一种简单有效的解决方案，可在不遗漏数据的情况下评估模型。此过程的示意见下图。通常把数据分成大致相等的 $K$ 份，使用其中 $K-1$ 份训练模型 $A_1$，剩下的 1 份用来测试模型；然后，从训练集中重新选择不同的 $K-1$ 份用于训练模型 $A_2$，并用剩余的 1 份测试模型；如此直到完成所有 $K$ 轮，得到模型 $A_K$；然后对结果 $A$ 求平均。

上述交叉验证过程被称为 `K-折交叉验证` 。当 $K$ 与样本数量相同时（即 $ K = N$ 时），就是常称的 `留一法交叉验证 （LOO-CV）`。在执行留一法交叉验证时，如果数据数量太多，有时会出现轮数少于数据总数的情况。

图 5.7

交叉验证是机器学习从业者的谋生之本，有关更多细节，可以阅读 `Sebastian Raschka` 的《`Python Machine Learning`》一书，或 `Jack Vanderplas` 的《`Python Data Science Handbook`》。

交叉验证简单而强大，不过对某些模型或者量很大的数据而言，交叉验证的计算量可能超出可接受范围。因此，许多人尝试提出了一些更容易计算的量，来得到近似交叉验证的效果，或者应用到不能直接使用交叉验证的情况，其中比较出名的是**信息准则**。

### 5.3.2 信息准则

信息准则是一系列用来比较模型对数据拟合程度的方法，这类方法引入了一个惩罚项来平衡模型的复杂度。换句话说，信息准则形式化地表达了在本章开始建立的直觉，用一种合适的方式平衡模型的准确性和简约性。这些衡量方式的推导过程与信息论相关，超出了本书范围，我们只从实用的角度去理解这些概念。

#### **（1）Log 似然与离差**

一种衡量模型对数据的拟合程度的方法是计算模型预测结果与真实数据之间的均方差：

```{math}
\frac{1}{N} \sum_{i=1}^{N}\left(y_{i}-\mathrm{E}\left(y_{i} \mid \theta\right)\right)^{2}  \tag{式 5.2}  \label{式 5.2}
```

其中，$E(y_i|\theta)$ 是根据估计的参数值计算得到的预测值。

可以看到基本上就是观察值和预测值之间平均差值，求平方是为保证误差为正，不会相互抵消。相比其他的度量指标（比如绝对值误差），平方度量更强调较大的误差。

一种更通用的方法是计算 log 似然：

```{math}
\sum_{i=1}^{N} \log p\left(y_{i} \mid \theta\right)  \tag{式 5.3}  \label{式 5.3}
```

当似然服从正态分布时，已经证明 log 似然与二次均方误差成正比。由于历史原因，实践中人们通常不直接使用 log 似然，而是使用一个称作 `离差（deviance）` 的量：

```{math}
-2 \sum_{i=1}^{N} \log p\left(y_{i} \mid \theta\right)  \tag{式 5.4}  \label{式 5.4}
```

离差在贝叶斯方法和非贝叶斯方法中类似，区别在于：贝叶斯框架中 $θ$ 来自后验的采样。而在非贝叶斯方法中，$θ$ 是一个点估计。在使用离差时，需注意以下两点：

- 离差越小，log 似然值越大，模型的预测结果与数据越吻合。因此我们**希望离差越小越好**。
- 离差衡量的是样本内的模型精度，因而复杂模型通常会比简单模型的离差小，此时**需要给复杂模型加入惩罚项**。

下面我们将学习几个不同的信息准则方法，**它们的共同点是都使用了离差和正则项，区别在于离差和惩罚项的计算方式不同**。

#### **（2）AIC 信息准则**

AIC 信息准则（Akaike Information Criterion）是一个广泛应用的信息准则，其定义如下：

```{math}
\text{AIC} = -2\sum_{i=1}^{n} \log p\left(y_{i} \mid \hat{\theta}_{m l e}\right)+2 \text{pAIC}  \tag{式 5.5}  \label{式 5.5}
```

其中，$pAIC$ 表示参数的个数， $\hat{\theta}_{m l e}$ 为 $\theta$ 的最大似然估计。最大似然估计在非贝叶斯方法中经常用到，等价于贝叶斯方法中基于均匀先验的最大后验估计。注意这里 $\hat{\theta}_{mle}$ 是点估计而不是分布。

同样，此处 −2 也是出于历史原因。从实用角度来看，上式中的第 1 项考虑的是模型对数据的拟合效果，第 2 项衡量的是模型复杂度。因此，如果两个模型对数据的解释能力相同，但是其中一个比另一个的参数更多的话，AIC 会告诉我们应该选择参数更少的那个。

AIC 对非贝叶斯方法来说很有用，但对贝叶斯方法可能会有些问题。原因是 AIC 没有使用后验，因而将估计中的不确定信息丢失了，此外将均匀分布作为先验，对使用非均匀先验的模型来说不太合适。因为在使用非均匀先验时，不能简单地计算模型中参数的个数，合理使用非均匀先验实际上相当于对模型已经使用了正则，并且会降低过拟合的可能，也就是说带正则模型的有效参数个数可能比真实参数个数要少。类似情况在多层模型中也会出现，毕竟多层模型可视为从数据中学习先验的有效方式。

#### **（3） WAIC 通用信息准则**

`通用信息准则（Widely Available Information Criterion， WAIC）` 是 `AIC` 的完全贝叶斯版本。与 `AIC` 一样， `WAIC` 有两个项：一项衡量模型对数据的拟合效果；另外一项衡量模型的复杂程度。

```{math}
\text{ `WAIC` }=-2 \times lppd + 2 \times p_{WAIC} \tag{5.5}
```

如果您想更好地理解这两个术语是什么，请阅读后面的 `深入 WAIC` 部分。从应用角度看，只需要知道我们更喜欢较低的值。

#### **（4）Pareto 平滑重要性采样留一交叉验证**

`Pareto 平滑重要性采样留一交叉验证` 是一种用于近似 ` LOO-CV`  结果但不实际执行 K 次迭代的方法。该方法不是一个信息准则，但提供的结果与 `WAIC` 非常相似，并且在某些条件下， `WAIC` 和 `LOO` 都是渐近收敛的。该方法主要思想是通过对似然适当重新加权来近似 `LOO-CV` ，在统计学中可以通过重要性采样来实现。但普通的重要性采样结果不稳定，为引入了称为 `Pareto 平滑重要性采样 (PSIS)` 的新方法，用来计算更可靠的 `LOO` 估计值。该方法结果与 `AIC` 和 `WAIC` 类似，数值越低，模型估计预测的精度就越高。因此，通常更倾向于选择数值较低的模型。

#### **（5）DIC 与 BIC 准则**

另一种常见的信息准则是 `差分信息准则（DIC）` 。但无论在理论上还是在实践上， `WAIC` 都被证明比 `DIC` 更有效，因此推荐使用 `WAIC` 而不是 `DIC`。

另一个信息准则是 `贝叶斯信息准则（BIC）`，它类似于 Logistic 回归。 `BIC` 的提出是为了纠正 `AIC` 的一些问题，作者建议采用贝叶斯纠正。但 `BIC` 并不是真正的贝叶斯，实际上它与 `AIC` 非常相似。它假设平坦的先验，并使用最大似然估计。更重要的是， `BIC` 不同于 `AIC` 和 `WAIC` ，而更多涉及 `贝叶斯因子（Bayesian Factor）` 的概念，这点将在本章后面讨论。

### 5.3.3 最小描述长度

### 5.3.4 VC维和结构风险最小化（SRM）

### 5.3.5 自助法（Bootstrap Methods）

## 5.4 使用 PyMC3 做模型比较

采用 `ArviZ` 进行模型比较想像起来容易得多！

```
waic_l = az.waic(trace_l)
waic_l
```

如果你想计算 `LOO` 而不是 `WAIC` ，需要使用 `az.loo` 。对于 `WAIC` 和 `LOO` ，`PyMC3` 报告了四个值（见上表）：

- 一个点估计值
- 点估计的标准差（通过假设正态分布计算的，因此在样本量较低时可能不太可靠）
- 有效参数的数量
- 警告数量

```{note}
在计算 `WAIC` 或 `LOO` 时，可能会收到一些警告消息，指出计算的结果可能不可靠。此警告是根据经验确定的阈值提出的（请参阅相关文献资料）。虽然这不一定是错误，但可能表明这些度量计算存在问题。 `WAIC` 和 `LOO` 相对较新，或许需要开发更好的方法来获得其可靠性。

无论如何，如果出现警告的情况，首先应当确保有足够样本，并且是一个混合良好、可靠的样本（参见第 8 章，推断引擎）。如果仍然接收到警告， `LOO` 方法的提出者建议使用更健壮的模型，如使用学生 t 分布而不是高斯分布。如果上述建议都不起作用，那么可能需要考虑使用另一种方法，例如直接执行 `K-折交叉验证`。 `WAIC` 和 `LOO` 只能帮助你在一组给定的模型中进行选择，但不能帮助你决定一个模型是否真的是解决特定问题的好方法。因此， `WAIC` 和 `LOO` 应该得到后验预测检查以及任何其他信息和测试的补充，这些信息和测试可以帮助我们根据待解决的特定问题和领域知识来设置模型和数据。
```

由于 `WAIC` 和 `LOO` 总是以相对的方式进行解释，`ArviZ` 提供了两个辅助函数来简化比较。第一个是 `az.compare` ：

```
cmp_df = az.compare({'model_l':trace_l, 'model_p':trace_p}, method='BB-pseudo-BMA')
cmp_df
```

这里有很多列：

- 第 1 列为 `WAIC` 的值。表格默认以该列升序排列，索引列则反映了该排序。
- 第 2 列是估计的有效参数个数。一般来说，参数越多的模型数据拟合越灵活，但也更可能导致过拟合。因此，可以将 `pwaic` 解释为惩罚性术语，也可将其解释为度量每个模型在拟合数据方面的灵活性。
- 第 3 列是 `WAIC` 的相对值，以排名最高的模型 `WAIC` 值为基准，列出各模型 `WAIC` 值与基准值间的相对差，第一个模型的值始终为 0 。
- 第 4 列为权重。在比较模型时，有时并不想选择量值指示最好的模型，而是希望通过平均若干模型来进行预测，并且通过加权平均，赋予不同模型适当的权重（见 5.5 节）。比较常用的方法是基于 `WAIC` 值给每个模型赋予一个 `Akaike 权重` 。在给定数据时，这些权重解释为每个模型的概率。此方法存在的一个问题是：由于该权重的计算基于 `WAIC` 的点估计，因此不确定性被忽略了。
- 第 5 列记录了 `WAIC` 值的标准差。标准差可用于评估 `WAIC` 估计的不确定度。
- 第 6 列记录了第 2 列相对值的标准差。由于 `WAIC` 的不确定性在不同模型之间相关程度不同，不同模型应当拥有不同的值。
- 第 7 列名为 `WARNING` 。值 1 表示 `WAIC` 的计算可能不可靠。

我们还可以通过使用 `az.plot_compare` 函数可视化上述信息。该函数接受 `az.compare` 的输出，并以 `Richard McElreath` 的《统计反思》一书中使用的样式生成汇总图：

```
az.plot_compare(cmp_df)
```

图 5.8

让我详细描述一下图 5.8：

- 空圆圈代表 `WAIC` 的值，与之相关的黑色误差条是 `WAIC` 的标准差。
- 最低的 `WAIC` 值用一条垂直的灰色虚线表示，以便于与其他 `WAIC` 值进行比较。
- 实心黑圆圈是每个模型的`样本内离差`，对于 `WAIC` 来说，它与相应的 `WAIC` 值相差 $2 \times pWAIC$ 。
- 除最佳模型外，其他模型会有一个三角形，表示该模型和最佳模型之间的 `WAIC` 相对值，配套的有一个灰色误差条，表示 `WAIC` 相对值的标准差。

依据信息准则来选择模型，可能是一种最简单的决策方式。此时只需要选择信息准则值较低的模型，而忽略其他模型即可。如果遵循这种方式，前例中的二次多项式模型可能是最佳选择。请注意，标准差不重叠给做出此选择提供了信心。相反，如果标准差是重叠的，则应该提供一个更微妙的答案。

## 5.5 贝叶斯模型评估方法 -- 贝叶斯因子

```{note}

背景：零假设显著性检验（NHST）是频率主义进行数据分析的主要工具。但在统计学领域，NHST 受到了广泛批评。越来越多的统计学者提倡使用贝叶斯方法检验研究假设，在实证研究中也有越来越多的学者使用贝叶斯因子进行数据分析。
```

在贝叶斯世界中，评估和比较模型的一种常见方式是 `贝叶斯因子（Bayes factor, BF）` 。 为理解什么是贝叶斯因子，让我们重温一遍贝叶斯定理：

```{math}
p(\theta \mid \mathcal{D})=\frac{p(\mathcal{D} \mid \theta) p(\theta)}{p(\mathcal{D})} \tag{式 5.9}  \label{式 5.9}
```

这里，$\mathcal{D}$ 表示数据。我们可以显式地基于给定模型 $M$ 计算条件概率关系：

```{math}
p\left(\theta \mid \mathcal{D}, M_{k}\right)=\frac{p\left(\mathcal{D} \mid \theta, M_{k}\right) p\left(\theta \mid M_{k}\right)}{p\left(\mathcal{D} \mid M_{k}\right)}\tag{式 5.10}   \label{式 5.10}
```

第一章中曾经介绍过，分母中的术语为边缘似然（或证据），可视为一个归一化常数。在进行单模型推断时，通常不需要真实计算它，而是基于一个常数因子来计算后验（如： MCMC 和 VI 方法都巧妙地规避了边缘似然的计算）。但对于模型比较和模型平均来说，边缘似然却是不得不计算的重要量。

如果主要目标是从 $k$ 个模型中选择一个最好的模型，我们可以只选择边缘似然 $p(\mathcal{D}|M_k)$ 最大的那个（因此被成为证据，可以证明哪个模型更能解释数据集）。一般来说， $p(\mathcal{D}|M_k)$ 值的大小本身并不能告诉我们太多信息，重要的是不同模型之间的相对值。因此，实践中经常计算两个边缘似然的比率，这个比率被称为贝叶斯因子：

```{math}
B F=\frac{p\left(\mathcal{D} \mid M_{0}\right)}{p\left(\mathcal{D} \mid M_{1}\right)} \tag{式 5.11}  \label{式 5.11}
```

当 $BF（M_0,M_1） > 1$ 时，模型 0 比模型 1 更好地解释了数据。

一些作者提出了带有范围的表格，以便于简化对 $BF$ 的解释。例如，下列符号列表显示了 ”支持模式 0 而不支持模式 1“ 的证据强度：

|  BF 值   | 证据强度 |
| :-----: | :------: |
|   1-3   |   初级   |
|  3-10   |   中等   |
|  10-30  |    强    |
| 30-100  |  非常强  |
| 大于 100 |   极端   |

不过需要注意的是：这些规则只是一些约定，最终结果始终应放在上下文中，并伴随足够细节，以便其他人可以检查是否同意我们的结论。

如果假设所有模型都具有相同先验概率，则使用 $p(\mathcal{D}|M_k)$ 来比较模型完全没有问题。否则，必须计算后验赔率：

```{math}
\underbrace{\frac{p\left(M_{0} \mid \mathcal{D}\right)}{p\left(M_{1} \mid \mathcal{D}\right)}}_{\text {posterior odds }}=\underbrace{\frac{p\left(\mathcal{D}\mid M_{0}\right)}{p\left(\mathcal{D} \mid M_{1}\right)}}_{\text {Bayes factors}} \underbrace{\frac{p\left(M_{0}\right)}{p\left(M_{1}\right)}}_{\text{prior odds} } \tag{式 5.12}  \label{式 5.12}
```

### 5.5.1 一些讨论

现在简要讨论有关边缘似然的一些关键事实。通过仔细检查定义，可以理解边缘似然的性质和应用效果：

```{math}
p\left(\mathcal{D} \mid M_{k}\right)=\int_{\theta_{k}} p\left(\mathcal{D} \mid \theta_{k}, M_{k}\right) p\left(\theta_{k}, M_{k}\right) d \theta_{k} \tag{式 5.13}  \label{式 5.13}
```

- **好处**：参数多的模型比参数少的模型具有更大惩罚。贝叶斯因子内置奥卡姆剃刀，因为参数数量越多，先验分布相对于似然就越宽。结合贝叶斯因子公式，越宽的先验积分（质量）越大，而越聚集的先验积分（质量）越小，从而间接实现了对参数数量的惩罚。
- **缺点**：计算边缘似然是艰巨的任务，因为要计算高维参数空间上的多变量函数积分，需要使用复杂方法进行数值求解。
- **尴尬之处**：边缘似然对先验取值的依赖过于敏感。

使用边缘似然来比较模型是一个好主意，因为复杂模型的惩罚已经包括在内。但同时先验信息的变化会影响边缘似然的计算，其中关键词是 ”敏感“ 。也就是说先验的微小变化，可能会对边缘似然的值产生很大影响。在前例中，标准差为 100 的正态先验与标准差为 1000 的正态先验变化很小，但相应的贝叶斯因子受这些变化却产生了较大变化。

另一个相关的批评是，贝叶斯因子可以被用作进行假设检验的贝叶斯统计方法。这种批评本身没错，但许多文章指出，推断方法比假设检验方法（无论是否为贝叶斯方法）更适合于大多数问题。

### 5.5.2 贝叶斯因子的计算

贝叶斯因子的计算可以被视为一个分层模型，其中超参数是分配给每个模型并从`类别分布`中采样的 `index` 。换句话说，我们同时对多个相互竞争的模型进行推断，并使用在模型间跳跃的离散变量。我们花在每个模型上的采样时间与 $p(M_k|\mathcal{D})$ 成正比。然后，应用公式 5.10 求出贝叶斯因子。

为举例说明贝叶斯因子的计算，我们再来一次抛硬币：

图 5.10

让我们创建一些数据，以便在示例中使用：

```
coins = 30
heads = 9
y_d = np.repeat([0, 1], [coins-heads, heads])
```

现在，来看一下 `PyMC3` 模型。为在之前的代码之间切换，我们使用了 `pm.math.switch` 函数。如果此函数的第一个参数的计算结果为 `true`，则返回第二个参数，否则返回第三个参数。请注意，还使用 `pm.math.eq` 函数来检查 `model_index` 变量是否等于 0 ：

```
with pm.Model() as model_BF:
    p = np.array([0.5, 0.5])
    model_index = pm.Categorical('model_index', p=p)
    m_0 = (4, 8)
    m_1 = (8, 4)
    m = pm.math.switch(pm.math.eq(model_index, 0), m_0, m_1)
    # a priori
    θ = pm.Beta('θ', m[0], m[1])
    # likelihood
    y = pm.Bernoulli('y', θ, observed=y_d)

    trace_BF = pm.sample(5000)

az.plot_trace(trace_BF)
```

图 5.11

现在，需要通过计算 `model_index` 变量来计算贝叶斯因子。请注意，我们已经包括了每个模型的先验值：

```
pM1 = trace_BF['model_index'].mean()
pM0 = 1 - pM1
BF = (pM0 / pM1) * (p[1] / p[0])
```

结果，我们得到的值为≈11，这意味着模型 0 比模型 1 高出一个数量级。这非常有意义，因为数据的正面值比预期的 $\theta=0.5$ 要少，两个模型之间的唯一区别是模型 0 的先验更兼容 $\theta<0.5$ （背面比正面多），模型 1 更兼容 $\theta>0.5$ （正面比背面多）。

### 5.5.3 计算贝叶斯因子时的常见问题

用我们定义的方式计算贝叶斯因子会有一些问题，比如当其中一个模型比另一个模型更好时，根据定义，我们会对更好的这个模型采样次数更多，这可能会导致我们对另外一个模型欠采样。另外，第 1 个问题是：即使某些参数没有用于拟合数据，也会更新。也就是说，当模型 0 被选择时，模型 1 中的参数也会更新，不过由于这部分参数并没有用于解释数据，值受限于先验。如果先验太模糊，有可能当我们选到模型 1 时，参数值距离上一次被接受的值太远了，因而该步被拒绝，从而导致采样会出现问题。

如果遇到此类问题，可以对模型进行两个调整以改进采样：

- 理想情况下，如果两个模型都访问相同次数，我们会得到一个更好的采样，因此我们对模型的先验做出调整（前一个模型中的 p 值），从而向原来访问频次较低的模型倾斜。这个过程对贝叶斯因子的计算不会有多大影响，因为我们在计算过程中包含了先验。

- 根据 Kruschke 以及其他人的建议，可以使用伪先验，其思想很简单：当没被选择的模型的参数出现自由漂移时，可以尝试手动限制它们，不过是在这个模型没被使用的时候。您可以找到一个在 `Kruschke` 书中使用的模型示例，我将该模型移植到了 [`PyMC3`](https://github.com/aloctavodia/Doing_bayesian_data_analysis) 中。

### 5.5.4 用序贯蒙特卡罗方法计算贝叶斯因子

另一种计算贝叶斯因子的方法是使用 `序贯蒙特卡罗 (SMC) 采样方法`。我们将在 `第 8 章-推断引擎` 中学习此方法的详细信息。现在只需要知道这个采样器计算的边缘似然估计是一个副产品，可以直接使用它来计算贝叶斯因子。要在 `PyMC3` 中使用 `SMC`，需将 `pm.SMC()` 传递给 `sample` 的 `step` 参数：

```
with pm.Model() as model_BF_0:
    θ = pm.Beta('θ', 4, 8)
    y = pm.Bernoulli('y', θ, observed=y_d)
    trace_BF_0 = pm.sample(2500, step=pm.SMC())

with pm.Model() as model_BF_1:
    θ = pm.Beta('θ', 8, 4)
    y = pm.Bernoulli('y', θ, observed=y_d)
    trace_BF_1 = pm.sample(2500, step=pm.SMC())

model_BF_0.marginal_likelihood / model_BF_1.marginal_likelihood
```

根据 `SMC 方法`，贝叶斯因子也在 11 左右，如果你想用 `PyMC3` 计算贝叶斯因子，我强烈推荐使用 SMC 方法。本书中提出的另一种方法在计算上更加繁琐，需要更多手动调整，主要是因为模型间的跳跃需要用户通过反复试验进行更多调整。从这点上来说，SMC 是一种自动化程度更高的方法。

### 5.5.5 贝叶斯因子与信息准则

如果对贝叶斯因子求对数，可以将两个边缘似然的比值转换成求差，这样比较边缘似然就与比较信息准则类似了。但是，衡量模型的数据拟合程度项以及惩罚项去哪儿了呢？前者包含在了似然部分，而后者包含在对先验取平均的部分。参数越多，先验空间相比似然就越大，因而平均之后似然就会较低，而且参数越多，先验就会越分散，因而在计算证据的时候惩罚越大。这也是为什么人们说贝叶斯理论会很自然地惩罚更复杂的模型，或者称贝叶斯理论自带奥卡姆剃刀。

此前说过，贝叶斯因子对先验过于敏感。这在执行推断时会导致本来不相关的差异，在计算贝叶斯因子时被证明为非常重要。现在我们来看一个例子，它将有助于阐明贝叶斯因子在做什么，信息准则在做什么，以及它们如何在相似的情况下专注于两个不同的方面。回到抛硬币例子的数据定义，现在设置 300 枚硬币和 90 个正面；这与以前的比例相同，但数据多了 10 倍。然后，分别运行每个模型：

```
traces = []
waics = []
for coins, heads in [(30, 9), (300, 90)]:
    y_d = np.repeat([0, 1], [coins-heads, heads])
    for priors in [(4, 8), (8, 4)]:
        with pm.Model() as model:
            θ = pm.Beta('θ', *priors)
            y = pm.Bernoulli('y', θ, observed=y_d)
            trace = pm.sample(2000)
            traces.append(trace)
            waics.append(az.waic(trace))
```

图 5.12

通过增加数据，我们几乎完全克服了先验，现在两个模型都做出了类似的预测。用 30 枚硬币和 9 个正面作为数据，可以看到的 $BF \approx 11$ ，如果用 300 个硬币和 90 个正面的数据重复计算，我们会看到 $BF \approx 25$ 。贝叶斯因子表明模型 0 比模型 1 更受青睐。当增加数据时，模型之间的决定变得更加清晰。这完全有道理，因为现在我们更确定模型 1 有一个与数据不一致的先验。

还要注意，随着数据量增加，两个模型的 $\theta$ 值趋于一致；实际上，两个模型的值都大约是 0.3。因此，如果决定用 $\theta$ 来预测新的结果，将与计算 $\theta$ 的分布的模型几乎没有什么不同。

现在，比较一下 `WAIC` 告诉我们的内容（参见图 5.13）。模型 0 的 `WAIC` 是 368.4，模型 1 的是 368.6，直觉上差别不大。比实际差异更重要的是，如果重新计算数据的信息准则，也就是 30 枚硬币和 9 个正面，你会得到模型 0 的 38.1 和模型 1 的 39.4 。也就是说，在增加数据时，相对差异变得越小，$\theta$ 的估计值越相近，与信息准则估计出的预测准确度的值就越相似。如果你用 `LOO` 代替 `WAIC` ，会发现本质上是一样的：

```
fig, ax = plt.subplots(1, 2, sharey=True)
labels = model_names
indices = [0, 0, 1, 1]
for i, (ind, d) in enumerate(zip(indices, waics)):
    mean = d.waic
    ax[ind].errorbar(mean, -i, xerr=d.waic_se, fmt='o')
    ax[ind].text(mean, -i+0.2, labels[i], ha='center')
ax[0].set_xlim(30, 50)
ax[1].set_xlim(330, 400)
plt.ylim([-i-0.5, 0.5])
plt.yticks([])
plt.subplots_adjust(wspace=0.05)
fig.text(0.5, 0, 'Deviance', ha='center', fontsize=14)
```

图 5.13

**贝叶斯因子关注的是哪个模型更好，而 `WAIC` （和 `LOO` ) 关注的是哪个模型能给出更好的预测。** 如果检查公式 5.5 和 5.11，你就会看到这些不同。 `WAIC` 和其他信息准则一样，以这样或那样的方式使用对数似然，先验并不直接作为计算的一部分。先验只间接参与，辅助我们估计。取而代之的是，贝叶斯因子直接使用先验，因为我们需要对先验值的整个范围内的似然进行平均。

## 5.6 模型平均

模型选择是一个比较简单的决策，但我们在选择模型时其实也正在抛弃有关模型的不确定性信息。这类似于计算好完整的后验后，只保留了后验的平均值。这会造成造成我们对模型过于自信。

```{note}
注意：

- 之前讨论的重点是参数的不确定性问题，此处为模型自身的不确定性。
- 能够解释数据集的模型很多，模型比较方法试图从其中选择一个最优的，而模型平均方法则认为所有模型的加权平均可能是最好的估计。
```

一种变通的方案是在执行模型选择时，报告和讨论不同模型的信息准则值、标准差等统计量以及后验预测检查情况。将所有这些数字和检查放在问题上下文中很重要，只有这样相关人士才能更好地感受到模型可能存在的局限性和缺点。在学术界中，可以使用该方法在论文、演示文稿等的讨论部分添加相关要素。

除了上述对各模型均做出报告和讨论的方法外，还有一种做法是充分利用模型比较中的出现的不确定性，执行模型平均。

```{note}
 从广义上理解，模型平均是集成方法的一种。
```

下面介绍其中几种比较常用的模型平均方法：

 ### 5.6.1 基于信息准则值的加权平均

本方法使用每个模型的加权平均值来生成 `元模型（meta-model）` 和 `元预测（meta-predictions）` 。基于某些信息准则值（如 `WAIC`）计算不同模型的权重，公式如下：

```{math}
w_{i}=\frac{e^{\frac{1}{2} d E_{i}}}{\sum_{j=1}^{M} e^{-\frac{1}{2} d E_{j}}} \tag{式 5.7}  \label{式 5.7}
```

这里 $dE_i$ 是第 $i$ 个模型相对于最佳模型（`WAIC`值最小的模型）的 `WAIC` 相对差值。除 `WAIC` 外，此处也可以使用其他信息准则值，如 `AIC` 或 `LOO` 等。此公式是根据 `WAIC` 值计算各模型相对概率的启发式方法。分母为归一化因子，`第 4 章 广义线性模型`中有过类似的表达式。

采用式 5.7 的权重对模型进行加权平均称为 `伪贝叶斯模型平均` 。真正的贝叶斯模型平均应当使用边缘似然，而非 `WAIC` 或 `LOO` 。不过尽管边缘似然在理论上很有吸引力，但在模型比较和模型平均中使用很少。大多还是选择 `WAIC` 或 `LOO` 而非边缘似然。在后续 `贝叶斯因子` 一节中，会有更多此方面的讨论。

使用 `PyMC3` ，可以通过将 `method=‘pseudo-BMA’` （伪贝叶斯模型平均）参数传递给 `az.compare` 函数来计算式 5.7 的权重。其问题是未考虑计算 $E_i$ 时的不确定性。通过高斯近似可以计算每一个 $E_i$ 的标准差。这也是函数 `az.waic`、`az.loo` 和 `az.compare` 在传递 `method=‘pseudo-BMA’` 参数时返回的误差值。此外，还可以使用 `贝叶斯自举（Bayesian bootstrapping）法` 来估计不确定性。这是一种比高斯近似更可靠的方法。通过将 `method=‘BB-pseudo-BMA’` 传递给 `az.compare` 函数即可实现。

### 5.6.2 基于预测分布堆叠的加权平均

另一种计算平均模型权重的方法被称为 `预测性分布堆叠（stacking of predictive distributions）` 。这在 `PyMC3` 中通过将 `method=‘stacking’` 传递给 `az.compare` 实现。其基本思想是通过最小化元模型和真实生成模型之间的差异，将多个模型组合到一个元模型中。当使用对数打分规则时，这等价于：

```{math}
\max _{n} \frac{1}{n} \sum_{i=1}^{n} \log \sum_{k=1}^{K} w_{k} p\left(y_{i} \mid y_{-i}, M_{k}\right) \tag{式 5.8}  \label{式 5.8}
```

这里，$n$ 是数据点的数量，$k$ 是模型的数量。为了强制实施方案，我们将 $w$ 约束为 $w_k \geq 0$ 并且 $\sum w_k =1$。量 $p(y_i|y_{-i},M_k)$ 是模型 $M_k$ 的留一预测性分布。根据留一法，计算需要拟合每个模型 $n$ 次，每次遗留一个数据点。幸运的是，`PyMC3` 可以使用 `WAIC` 或 `LOO` 来近似留一预测性分布。

### 5.6.3 其他模型平均方法

还有其他方法来平均模型，例如，显式构建包括所有感兴趣模型作为子模型的元模型。可以构建这样一个模型：我们对每个子模型的参数进行推断，同时计算每个模型的相对概率（有关此方面的示例，请参阅 `贝叶斯因子` 一节）。

除了对离散的模型求平均之外，有时候还可以将其看作是连续的。一个简单的例子就是，假设我们有一个抛硬币问题以及两个不同的模型，其中之一的先验偏向正面朝上，另一个偏向于反面朝上。则我们可以按照离散的模型平均方式，用两个模型去拟合并用 dIC 权重求平均；但也可以构建一个分层模型估计先验分布，而此时构建的不再是两个离散的模型了，而是一个连续模型，其中包含两个离散的模型作为特例。

哪种方法更好呢？还是要具体问题具体分析，最终使用哪一个取决于实际问题是更适合用离散模型还是连续模型去描述。

- 我们是否真的有很好的理由考虑离散模型，或者我们的问题更好地表示为连续模型？
- 对于问题来说，挑出一个模型很重要，因为我们是从相互竞争的解释角度思考的，或者平均是更好的想法，因为我们对预测更感兴趣，或者真的可以将流程生成过程视为子流程的平均吗？

所有这些问题都不是由统计数据来回答的，而是由领域知识背景下的统计数据来提供信息的。

以下只是如何从 `PyMC3` 获得加权后验预测样本的一个模拟示例。在这里，我们使用的是 `pm.sample_posterior_predictive_w` 函数（注意函数名称末尾的 `w` ）。`pm.sample_posterior_predictive` 和`pm.sample_posterior_predictive_w` 之间的区别在于，后者接受多个迹和模型，以及权重列表（默认值为所有模型的权重相同）。您可以通过 `az.compare` 或其他来源获取这些权重：

```
w = 0.5
y_lp = pm.sample_posterior_predictive_w([trace_l, trace_p],
                                        samples=1000,
                                        models=[model_l, model_p],
                                        weights=[w, 1-w])
_, ax = plt.subplots(figsize=(10, 6))
az.plot_kde(y_l, plot_kwargs={'color': 'C1'},
            label='linear model', ax=ax)
az.plot_kde(y_p, plot_kwargs={'color': 'C2'},
            label='order 2 model', ax=ax)
az.plot_kde(y_lp['y_pred'], plot_kwargs={'color': 'C3'},
           label='weighted model', ax=ax)
plt.plot(y_1s, np.zeros_like(y_1s), '|', label='observed data')
plt.yticks([])
plt.legend()
```

图 5.9

前面提到这是一个虚拟示例，因为与线性模型相比，二次多项式模型的 `WAIC` 值非常低，第一个模型的权重基本上是 1 ，而后者权重基本上是 0 ，为生成图 5.9 ，我假设了这两个模型具有相同的权重。

## 5.7 其他

### 5.7.1 正则先验

使用强信息和弱信息先验是在模型中引入偏差的一种方式，如果操作得当，这可能是一个非常好的方法，因为偏差可以防止过拟合，从而有助于模型做出泛化性能更好的预测。在不影响模型建模能力的情况下，添加偏差以减少泛化误差的想法称为正则化。这种正则化通常采用对模型参数数量的较大值实施惩罚的形式。正则先验是一种减少模型所能表示信息的方法，从而降低了模型捕获噪声而不是信号的机会。

正则化思想如此强大和有用，以至于本书中已经出现了很多次。在非贝叶斯统计中，正则化思想表现为对最小二乘法的两种修正： `岭回归` 和 `套索回归` 。从贝叶斯观点来看，岭回归可解释为 `对线性模型的贝塔系数采用标准差趋近于 0 的正态分布，使该系数趋向于零`。从该意义上说，我们一直在为本书中的每一个线性模型做类似岭回归的事情。另一方面，套索回归可以从贝叶斯的观点解释为 `从贝塔系数具有 Laplace 先验的模型计算出的后验分布图`。拉普拉斯分布看起来类似于高斯分布，但它的一阶导数在零处没有定义，因为它在零处有一个非常尖锐的峰值（参见图 5.14）。与正态分布相比，拉普拉斯分布使其概率质量更接近于零。使用这种先验的出发点是提供 `正则化` 的同时实现 `变量选择`。其思路是，由于峰值为零，预计先验会导致稀疏性，也就是说，我们创建了一个具有许多参数的模型，先验将自动使大多数参数为零，只保留对模型输出有贡献的相关变量。不幸的是，贝叶斯套索不是这样工作的，基本上是为了有很多参数，拉普拉斯先验迫使非零参数变小。幸运的是，并不是所有东西都丢失了 -- 有一些贝叶斯模型可以用来诱导稀疏性和执行变量选择。

值得注意的是，经典版本的岭回归和套索回归对应于点估计，而贝叶斯版本则给出了完整的后验分布结果：

图 5.14

### 5.7.2 深入 `WAIC`

如果展开公式 5.5，会得到以下结果：

```{math}
\text{WAIC}=-2 \sum_{i}^{n} \log \left(\frac{1}{S} \sum_{s=1}^{S} p\left(y_{i} \mid \theta^{s}\right)\right)+2 \sum_{i}^{n}\left(\text{V}_{s=1}^{S}\left(\log p\left(y_{i} \mid \theta^{s}\right)\right)\right. \tag{式 5.14}  \label{式 5.14}
```

该表达式中的两项看起来非常相似。第一项是式 5.5 中的`对数点预测密度（lppd）`，计算的是后验样本集 $S$ 的平均似然。我们对每个数据点都先求平均似然，然后取对数，最后对所有数据点求和。请将这一项与公式 5.3 和 5.4 进行比较。其实该项就是考虑了后验的样本内离差（deviance）。因此，如果我们认为计算对数似然是衡量模型适合性的好方法，那么在贝叶斯方法中，从后验计算对数似然就顺理成章。观测数据的 lddp 是对未来数据 lppd 的高估（此处意指样本内离差通常小于样本外离差），因此引入第二项来修正这种过高的估计。第二项计算后验样本的对数似然方差，我们对每个数据点执行此方差计算，然后对所有数据点进行汇总。为什么方差会给出惩罚条件？这与贝叶斯因子内置奥卡姆剃须刀的原理相似。有效参数越多，后验分布越大。当向模型添加结构时（如具有信息性/正则化的先验或分层依赖），与非正则化的模型相比，我们约束了后验，进而减少了有效参数的数量。

### 5.7.3 熵

#### （1）熵的定义

从数学上讲，熵定义为：

```{math}
H(p)=-\sum_{i} p_i\text{log} (p_i) \tag{式 5.15}  \label{式 5.15}
```

直观地说，分布越分散，其熵越大。通过运行以下代码并查看图 5.15，可以看到这一点：

```
np.random.seed(912)
x = range(0, 10)
q = stats.binom(10, 0.75)
r = stats.randint(0, 10)
true_distribution = [list(q.rvs(200)).count(i) / 200 for i in x]
q_pmf = q.pmf(x)
r_pmf = r.pmf(x)
_, ax = plt.subplots(1, 3, figsize=(12, 4), sharey=True,
                     constrained_layout=True)
for idx, (dist, label) in enumerate(zip([true_distribution, q_pmf, r_pmf], ['true_distribution', 'q', 'r'])):
    ax[idx].vlines(x, 0, dist, label=f'entropy ={stats.entropy(dist):.2f}')
    ax[idx].set_title(label)
    ax[idx].set_xticks(x)
    ax[idx].legend(loc=2, handlelength=0)
```

图 5.15

如图所示，图中的分布 $r$ 是三种分布中较广的一个，也是熵最大的一个。建议使用代码并探索熵是如何变化的（参见练习 10）。

#### （2） 熵与方差

熵和方差概念上相关，以至于很多人将其声明为度量 ”数据分布的方差“ 的一种特殊形式。不过尽管两个概念相关，但本质上并不相同。在某些情况下，熵增加意味着方差增加，例如高斯分布。然而，也存在方差增加但熵不变的例子。例如混合了两个高斯分布的混合模型，当增加两个高斯分布的众数之间距离的时候，导致大部分点到平均值的距离增加了，即方差增大了。但此时，熵受到的影响极小，因为随着众数之间距离的增加，众数之间的点的概率越来越小，因此它们对总熵的贡献逐步可以忽略不计。从熵的角度来看，如果从两个重叠的高斯开始，将一个相对于另一个移动，则在某个点上，将有两个分离的高斯。

#### （3） 最大熵原理

熵与信息及其不确定性也有关。事实上，更分散或更平坦的先验是弱信息先验。这不仅直观上是正确的，而且有熵的理论支撑。事实上，在贝叶斯学派中有一个群体，在用熵来证明弱信息先验或正则化先验是合理的。这就是`最大熵原理`：**我们总是期望在问题定义的约束下找到具有最大可能熵的分布**。这是一个可用数学方法解决的优化问题，但本书不讲解细节。下面仅列出一些常见约束条件下的最大熵分布：

- 无约束：均匀分布（连续或离散，取决于变量类型）
- 正均值：指数分布
- 给定方差：正态分布
- 只有 2 个非定序输出和 1 个常数均值：二项分布，如果有罕见事件，则为泊松分布（泊松可视为 $p$ 很小，$n$ 很大时的二项分布）

有趣的是，许多传统的广义线性模型（如在 `第 4 章 广义线性模型`中看到的模型），都是在给定的模型约束下，使用最大熵分布来定义的。

### 5.7.4 KL 散度

现在简单谈谈 `Kullback-Leibler(KL) 散度`，或简称 `KL 散度`。这是在阅读统计学、机器学习、信息论或统计力学文献时经常遇到的概念。你或许会说，`KL 散度`、`熵`、`边缘似然`等概念反复出现的原因很简单，因为所有这些学科都在讨论同一组问题，只是观点略有不同。`KL 散度` 非常有用，因为**它是衡量两个分布接近程度的一种方法**，其定义如下：

```{math}
D_{K L}(p \| q)=\sum_{i} p_{i} \log \frac{p_{i}}{q_{i}} \tag{式 5.16}  \label{式 5.16}
```

上式可读为 $q$ 到 $p$ 的 `Kullback-Leibler 散度`（两者顺序不能相反，因为 `KL 散度` 不符合交换率），其中 $p$ 和 $q$ 是两个概率分布。对于连续变量应该计算积分而非求和，但主要思想相同。

可以将 $D_{KL}({p||q})$ 散度解释为 **”通过使用概率分布 $q$ 来近似真实分布 $p$ 而引入的额外熵或不确定性“**。事实上，`KL 散度` 是两个熵之间的差值：

```{math}
D_{K L}(p \| q)=\underbrace{\sum_{i} p_{i} \log p_{i}}_{\text {entropy of p }}-\underbrace{\sum_{i} p_{i} \log q_{i}}_{\text {crossentropy of p,q }}=\sum_{i} p_{i}\left(\log p_{i}-\log q_{i}\right) \tag{式 5.17}  \label{式 5.17}
```

利用对数性质，可以重新排列式 5.17 以恢复式 5.16。从式 5.17 的角度来看，也可以将 $D_{KL}({p||q})$ 理解为 $p$ 相对于 $q$ 的相对熵（这一次顺着念）。

作为一个简单例子，我们可以使用 KL 散度来评估哪个分布（ $q$ 或 $r$ ）更接近真实分布。使用 Scipy，可以计算 $D_{KL}({真实分布||q})$  和 $D_{KL}({真实分布||r})$ ：

```
stats.entropy(true_distribution, q_pmf), stats.entropy(true_distribution,r_pmf)
```

如果运行上段代码，您将获得 $\approx 0.0096,\approx 0.7394$ 。因此可以判定，$q$ 比 $r$ 更接近于真实分布，因为它引入的额外不确定性更小。我希望您同意我的观点，即这个数值结果与您检查图 5.15 所预期的一致。

您可能很想将 KL 散度描述为距离，但它是不对称的，因此不是真实距离。如果运行下面代码，将获得 $\approx 2.7,\approx 0.7$ 。由此可见，结果数字是不同的。在此例中，可以看到 $r$ 是 $q$ 的更好近似，但反之可能不成立：

```
stats.entropy(r_pmf, q_pmf), stats.entropy(q_pmf, r_pmf)
```

 $D_{KL}({p||q})$ 表示 $q$ 有多像 $p$ 。也可从惊喜的角度来考虑，如果在预期 $p$ 的时候突然看到了 $q$ ，我们会有多惊讶。对一个事件的惊讶程度取决于用于判断该事件的信息。我在一个非常干旱的城市长大，每年可能会有一两场真正的暴风雨。然后。我搬到另一省份去上大学，我真的很震惊，至少在雨季，平均每周有一场真正的暴风雨！我的一些同学来自布宜诺斯艾利斯，这是阿根廷最潮湿多雨的省份之一。但对他们来说，降雨频率或多或少是意料之中的。更重要的是，他们可能因为空气不够潮湿，而认为天气可能会下多一点雨。

我们也可以使用 KL 散度来比较模型，因为它将给出哪个模型更接近真实分布的后验。但问题是我们并不知道真实分布。因此，KL 散度不能直接适用，但可用它作为论据来修正离差（式 5.3）。如果假设真实分布存在（如下式所示），则其应当独立于任何模型和常数，并以同样方式影响 KL 散度，而与用于近似真实分布的后验分布无关。因此，可以使用离差（依赖于每个模型的部分）来估计我们离真实分布相对有多近，即使我们不知道它。对于公式 5.17 ，通过使用一些代数，可以得到：

```{math}
\begin{align}
D_{K L}(p \| q)-D_{K L}(p \| r) &=\left(\sum_{i} p_{i} \log p_{i}-\sum_{i} p_{i} \log q_{i}\right)-\left(\sum_{i} p_{i} \log p_{i}-\sum_{i} p_{i} \log r_{i}\right) \tag{式 5.18}  \label{式 5.18}\\
 &=\sum_{i} p_{i} \log q_{i}-\sum_{i} p_{i} \log r_{i} \notag
end{align}
```

即使不知道 $p$，我们也可以得出结论，具有更大对数似然（或离差）的分布就是在 KL 散度中更接近真实分布的分布。实践中对数似然（或离差）是从有限样本拟合的模型中获得的。因此，还必须增加一个惩罚项，以纠正对离差的高估，这就引出了 `WAIC` 等信息准则。

## 5.8 总结

后验预测检查是一个通用概念和实践，它可以帮助我们了解模型捕获数据的能力，以及模型捕获我们感兴趣问题的各个方面的能力。我们可以只用一个模型进行后验预测检查，也可以用多个模型进行后验预测检查，因此也可以用它作为模型比较的一种方法。后验预测检查大多是通过可视化完成的，但像 `贝叶斯 p-value` 类似的数字摘要也很有帮助。

好的模型在复杂性和预测准确性之间有很好的平衡。我们用多项式回归的经典例子来说明这一特征。我们讨论了两种在不留数据的情况下估计样本外准确度的方法：交叉验证法和信息准则法。我们集中讨论了后者。从实践角度来看，信息准则是一系列平衡两种贡献的方法：一种是衡量模型与数据的拟合程度，另一种是惩罚复杂的模型。在众多可用信息准则中， `WAIC` 是贝叶斯模型中最有用的。另一个有用的方法是 `PSIS-LOO-CV` （或 `LOO` ），它在实践中提供了与 `WAIC` 非常相似的结果。 `WAIC` 和 `LOO` 可用于模型选择，也可用于模型平均。模型平均不是选择单个最佳模型，而是通过对所有可用模型进行加权平均来组合所有可用模型。

模型选择、比较和模型平均的另一种方法是贝叶斯因子，它是两个模型的边缘似然之比。贝叶斯因子的计算很有挑战性。本章介绍了使用 `PyMC3` 计算它们的两种方法：一种是直接尝试使用离散 index 估计每个模型的相对概率的分层模型，另一种是称为 `序贯蒙特卡罗（Sequential Monte Carlo，SMC）采样方法` ，我们建议使用后者。

贝叶斯因子对先验非常敏感，除了在计算上具有挑战性之外，使用贝叶斯因子也是有问题的。我们比较了贝叶斯因子和信息准则，并通过一个示例介绍了它们解决两个相关但不同的问题：一个侧重于确定正确的模型，另一个侧重于实现最佳预测或更低的泛化损失。所有这些方法都或多或少存在问题，但 `WAIC` 和 `LOO` 在实践中相对要健壮得多。

## 5.9 习题


# Value Iteration Networks
## Francesco Saverio Zuppichini

*Authors: Aviv Tamar, Yi Wu, Garrett Thomas, Sergey Levine, and Pieter Abbeel*

# Value Iteration Networks

Problem: *Planning* not able to generalise unseen domains

Solution: A module able to **learn** to plan

# Recap

### Markov Decision Process

is a 5-tuple $(S, A, P, R, \gamma)$ where:

$$
\begin{equation}
        \begin{array}{l}
         \text{finite set of states:} \\
                s \in S \\
         \text{finite set of actions:} \\
        a \in A \\
                 \text{state transition probabilities:} \\
        p(s' | s, a) = Pr \{S_{t + 1} = s' | S_t = s, A_t = a \} \\
                \text{expected reward for state-action-nexstate:}\\
        r(s',s, a) = \mathbb{E}[ R_{t + 1} | S_{t + 1} = s',  S_t = s, A_t = a ]  \\
        \end{array}
\end{equation}
$$

### Reward

$$G_t = \sum_{k = 0}^H \gamma^kr_{t + k + 1}$$

### Value Function

*how good* is to be in a state

$$
V_{\pi}(s) = \mathbb{E}[G_t | S_t = s]
$$

#### Bellman Equation

**recursive** relation

$$\begin{equation}
\begin{array}{l l}
V_{\pi}(s) & = \mathbb{E}_{\pi}\begin{bmatrix}G_t | S_t = s\end{bmatrix} \\
\\
& =  \mathbb{E}_{\pi}\begin{bmatrix}\sum\limits_{k = 0}^{\infty} \gamma^kR_{t + k + 1} | S_t = s \end{bmatrix} \\
\\
...
\\
& = \sum\limits_{a} \pi(a | s) \sum\limits_{s'}\sum\limits_{r} p(s', r | s, a)\begin{bmatrix} r + \gamma V_{\pi}(s')
\end{bmatrix}
\end{array}
\label{eq: value_bellman}
\end{equation}$$

Thus we can **PLAN**

# Value Iteration

Does not *generalise*

If we think about

$$
V_{n + 1}(s) = max_aQ_n(s,a) \quad \forall s
$$

where

$$
Q_n(s,a) = R(s,a) + \gamma \sum_{s'}P(s'|s,a)V_n(s')
$$

It is very similar to a **convolutional** and **max pooling** operation

*paper notation*

If we think about

$$
V_{n + 1}(s) = \underbrace{max_a}_{\text Max Pooling}Q_n(s,a) \quad \forall s
$$

where

$$
Q_n(s,a) = R(s,a) + \underbrace{\gamma \sum_{s'}P(s'|s,a)}_{\text{Convolution}}V_n(s')
$$

Convolution

$$
h_{l', i', j'} = \sigma(\sum_{l,i,j} W^{l'}_{l,i,j}X_{l, i'-i, j' - j})
$$

Max Pooling

$$
h_{l, i, j} ^{\text{maxpool}} - max_{l', j' \in N(i,j)} h_{l,i',j'}
$$

Thus we can represent Value Iteration with a **CNN**

# Value Iteration Network

Assume there is a un uknowm $\bar M$, solve it can add info to $\pi$ policy of $M$

We want to learn $ \bar R = f_r(\phi(s)) $ and $ \bar P = f_P(\phi(s)) $

$\bar R$ maps an image of the domain to a high reward at the goal and negative near an obstacle

$\bar P$ encodes deterministic movement in the grid-world that do not depend on the observations

Notice that

$$ \bar \pi^* = argmax_{\bar a} \bar R (\bar s, \bar a) + \gamma  \sum_{\bar s'} \bar P (\bar s'| \bar s, \bar a) \bar V^*(\bar s') $$

**local connectivity** the states with $\bar P (\bar s'| \bar s, \bar a)$ small subset of $\bar S$. **Attention**

# Value Iteration Module

$$
Q_{\bar a, i', j'} = \sum_{l, i, j} W^{\bar a}_{l,i,j} \bar R_{l, i' - i, j' - j}
$$

each channel in $\bar Q$ correspond to $\bar Q(\bar s,\bar a)$ for a particular $\bar a$

new $\bar V$ stacked to $\bar R$ and feed again

# Experiments

## Grid World

## Architecture

# Mars Rover Navigation

VIN learn to plan *from natural image input*

elevation/obstacles data not a part of the input

$84.8%$ success rate

# Continuous Control

Continuous states and continuous actions

Grid size 28x28

VIN train on coarse grid representation

# Conclusion

scalable architecture

It is able to learn!

easy to implement

#### Does it work?

```python
import torch

import matplotlib.pyplot as plt

from torch.utils.data import DataLoader

from datasets import GridWorldDataset
from config import *
from utils import *

%matplotlib notebook
plt.rcParams['figure.figsize'] = (14,4)
```

```python
world = WORLD_28X28

test_ds = GridWorldDataset(*world, train=False)
```

```python
vin = torch.load(get_model_path(world), map_location='cpu')

labels, s1, s2, obs = get_random_data(test_ds, device)

_, v, r_img = vin((s1, s2, obs), k=5)

fig = make_world_r_v_image(obs, v)

```

    <IPython.core.display.Javascript object>

https://github.com/FrancescoSaverioZuppichini/Value-Iteration-Network

```python
print('Thank you')
```

    Thank you