Trusted Notebook" width="500 px" align="left">

# Getting Started with Qiskit Terra

Here, we provide an overview of working with Qiskit Terra. Qiskit Terra provides the basic building blocks necessary to program quantum computers. The basic concept of Qiskit Terra is an array of quantum circuits. A workflow using Terra consists of two stages: Build and Execute. Build allows you to make different quantum circuits that represent the problem you are solving, and Execute allows you to run them on different backends.  After the jobs have been run, the data is collected. There are methods for putting this data together, depending on the program. This either gives you the answer you wanted, or allows you to make a better program for the next instance (e.g., when running the variational quantum eigensolver (VQE) algorithm).

**Contents**

[Circuit basics](#circuit_basics)

[Simulating circuits with Qiskit Aer](#aer_simulation)

[Running circuits using the IBMQ provider](#ibmq_provider)

**Code imports**

```python
import numpy as np
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import execute
```

Circuit Basics
================

## Building the circuit

The basic elements needed for your first program are the QuantumCircuit, and QuantumRegister.

```python
# Create a Quantum Register with 3 qubits.
q = QuantumRegister(3, 'q')

# Create a Quantum Circuit acting on the q register
circ = QuantumCircuit(q)
```

Note: Naming the QuantumRegister is optional and not required.

After you create the circuit with its registers, you can add gates ("operations") to manipulate the registers. (See [Summary of Quantum Operations](../terra/summary_of_quantum_operations.ipynb) for a thorough discussion of quantum gates.) As you proceed through the documentation you will find more gates and circuits but the below is an example of a quantum circuit that makes a three-qubit GHZ state

$$|\psi\rangle = \left(|000\rangle+|111\rangle\right)/\sqrt{2}.$$

To create such a state, we start with a 3-qubit quantum register. By default, each qubit in the register is initialized to $|0\rangle$. To make the GHZ state, we apply the following gates:
* A Hadamard gate $H$ on qubit 0, which puts it into a superposition state.
* A controlled-X operation ($C_{X}$) between qubit 0 and qubit 1.
* A controlled-X operation between qubit 0 and qubit 2.

On an ideal quantum computer, the state produced by running this circuit would be the GHZ state above.

In Qiskit Terra, operations can be added to the circuit one-by-one, as shown below.

```python
# Add a H gate on qubit 0, putting this qubit in superposition.
circ.h(q[0])
# Add a CX (CNOT) gate on control qubit 0 and target qubit 1, putting
# the qubits in a Bell state.
circ.cx(q[0], q[1])
# Add a CX (CNOT) gate on control qubit 0 and target qubit 2, putting
# the qubits in a GHZ state.
circ.cx(q[0], q[2])
```

    <qiskit.extensions.standard.cx.CnotGate at 0x1129a9828>

## Visualize Circuit

You can visualize your circuit using Qiskit Terra `circuit_drawer`, which plots circuit in the form found in many textbooks.

```python
from qiskit.tools.visualization import circuit_drawer

circuit_drawer(circ)
```

In this circuit, the qubits are put in order with qubit zero at the top and qubit three at the bottom. The circuit is read left-to-right (meaning that gates which are applied earlier in the circuit show up further to the left).

## Simulating circuits using Qiskit Aer

Qiskit Aer is our package for simulating quantum circuits. It provides many different backends for doing a simulation.

### Statevector backend

The most common backend in Qiskit Aer is the `statevector_simulator`. This simulator returns the quantum
state which is a complex vector of dimensions $2^n$ where $n$ is the number of qubits
(so be careful using this as it will quickly get too large to run on your machine).

When representing the state of a multi-qubit system, the tensor order used in qiskit is different than that use in most physics textbooks. Suppose there are $n$ qubits, and qubit $j$ is labeled as $Q_{j}$. In most textbooks (such as Nielsen and Chuang's "Quantum Computation and Information"), the basis vectors for the $n$-qubit state space would be labeled as $Q_{0}\otimes Q_{1} \otimes \cdots \otimes Q_{n}$. **This is not the ordering used by qiskit!** Instead, qiskit uses an ordering in which the $n^{\mathrm{th}}$ qubit is on the _left_ side of the tesnsor product, so that the basis vectors are labeled as  $Q_n\otimes \cdots  \otimes  Q_1\otimes Q_0$.

For example, if qubit zero is in state 0, qubit 1 is in state 0, and qubit 2 is in state 1, qiskit would represent this state as $|100\rangle$, whereas most physics textbooks would represent it as $|001\rangle$.

This difference in labeling affects the way multi-qubit operations are represented as matrices. For example, qiskit represents a controlled-X ($C_{X}$) operation with qubit 0 being the control and qubit 1 being the target as

$$C_X = \begin{pmatrix} 1 & 0 & 0 & 0 \\  0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\\end{pmatrix}.$$

To run the above circuit using the statevector simulator first you need to import Aer and then set the backend to `statevector_simulator`.

```python
# Import Aer
from qiskit import Aer

# Run the quantum circuit on a statevector simulator backend
backend = Aer.get_backend('statevector_simulator')
```

Now we have chosen the backend its time to compile and run the quantum circuit. In Qiskit Terra we provide the `execute` function for this. ``execute`` returns a ``job`` object that encapsulates information about the job submitted to the backend.

Tip: You can obtain the above parameters in Jupyter. Simply place the text cursor on a function and press Shift+Tab.

```python
# Create a Quantum Program for execution
job = execute(circ, backend)
```

When you run a program a job object is made that has the following two useful methods
`job.status()` and `job.result()` which returns the status of the job a result object respectiviely.

Note: Jobs run asynchronous but when the result method is called it switches to synchronous and waits for it to finish before moving on to another task.

```python
result = job.result()
```

The results object contains the data and Qiskit Terra provides the method
`result.get_statevector(circ)` to return the statevector for the quantum circuit.

```python
outputstate = result.get_statevector(circ)
print("simulation: ", result )
print(np.around(outputstate,3))
```

    simulation:  COMPLETED
    [0.707+0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j
     0.707+0.j]

Qiskit Terra also provides a visualization toolbox to allow you to view these results. See [Qiskit visualizations](qiskit_visualizations.ipynb) for more information.

Below, we use the visualization function to plot the real and imaginary components of the state vector.

```python
from qiskit.tools.visualization import plot_state
plot_state(outputstate)
```

### Unitary backend

Qiskit Aer also includes a `unitary_simulator` that works _provided all the elements in the circuit are unitary operations_. This backend calculates the $2^n \times 2^n$ matrix representing the gates in the quantum circuit.

```python
# Run the quantum circuit on a unitary simulator backend
backend = Aer.get_backend('unitary_simulator')
job = execute(circ, backend)
result = job.result()

# Show the results
print("simulation: ", result )
print(np.around(result.get_unitary(circ), 3))
```

    simulation:  COMPLETED
    [[ 0.707+0.j  0.707-0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j
       0.   +0.j  0.   +0.j]
     [ 0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j
       0.707+0.j -0.707+0.j]
     [ 0.   +0.j  0.   +0.j  0.707+0.j  0.707-0.j  0.   +0.j  0.   +0.j
       0.   +0.j  0.   +0.j]
     [ 0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.707+0.j -0.707+0.j
       0.   +0.j  0.   +0.j]
     [ 0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.707+0.j  0.707-0.j
       0.   +0.j  0.   +0.j]
     [ 0.   +0.j  0.   +0.j  0.707+0.j -0.707+0.j  0.   +0.j  0.   +0.j
       0.   +0.j  0.   +0.j]
     [ 0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j
       0.707+0.j  0.707-0.j]
     [ 0.707+0.j -0.707+0.j  0.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j
       0.   +0.j  0.   +0.j]]

### OpenQASM backend

The simulators above are useful because they provide information about the state output by the ideal circuit and the matrix representation of the circuit. However, a real experiment terminates by _measuring_ each qubit (usually in the computational $|0\rangle, |1\rangle$ basis). Without measurement, we cannot gain information about the state. Measurements cause the quantum system to collapse into classical bits.

For example, suppose we make independent measurements on each qubit of the three-qubit GHZ state
$$|\psi\rangle = |000\rangle +|111\rangle)/\sqrt{2},$$
and let $xyz$ denote the bitstring that results. Recall that, under the qubit labeling used by Qiskit, $x$ would correspond to the outcome on qubit 2, $y$ to the outcome on qubit 1, and $z$ to the outcome on qubit 0. This representation of the bitstring puts the most significant bit (MSB) on the left, and the least significant bit (LSB) on the right. This is the standard ordering of binary bitstrings. We order the qubits in the same way, which is why Qiskit uses a non-standard tensor product order.

The probability of obtaining outcome $xyz$ is given by
$$\mathrm{Pr}(xyz) = |\langle xyz | \psi \rangle |^{2}.$$
By explicit computation, we see there are only two bitstrings that will occur: $000$ and $111$. If the bitstring $000$ is obtained, the state of the qubits is $|000\rangle$, and if the bitstring is $111$, the qubits are left in the state $|111\rangle$. The probability of obtaining 000 or 111 is the same; namely, 1/2:
$$\begin{align}
\mathrm{Pr}(000) &= |\langle 000 | \psi \rangle |^{2} = \frac{1}{2}\\
\mathrm{Pr}(111) &= |\langle 111 | \psi \rangle |^{2} = \frac{1}{2}.
\end{align}$$

To simulate a circuit that includes measurement, we need to add measurements to the original circuit above, and use a different Aer backend.

```python
# Create a Classical Register with 3 bits.
c = ClassicalRegister(3, 'c')
# Create a Quantum Circuit
meas = QuantumCircuit(q, c)
meas.barrier(q)
# map the quantum measurement to the classical bits
meas.measure(q,c)

# The Qiskit circuit object supports composition using
# the addition operator.
qc = circ+meas

#drawing the circuit
circuit_drawer(qc)
```

This circuit adds a classical register, and three measurements that are used to map the outcome of qubits to the classical bits.

To simulate this circuit, we use the ``qasm_simulator`` in Qiskit Aer. Each run of this circuit will yield either the bitstring 000 or 111. To build up statistics about the distribution of the bitstrings (to, e.g., estimate $\mathrm{Pr}(000)$), we need to repeat the circuit many times. The number of times the circuit is repeated can be specified in the ``execute`` function, via the ``shots`` keyword.

```python
# Use Aer's qasm_simulator
backend_sim = Aer.get_backend('qasm_simulator')

# Execute the circuit on the qasm simulator.
# We've set the number of repeats of the circuit
# to be 1024, which is the default.
job_sim = execute(qc, backend_sim, shots=1024)

# Grab the results from the job.
result_sim = job_sim.result()
```

Once you have a result object, you can access the counts via the function `get_counts(circuit)`. This gives you the _aggregated_ binary outcomes of the circuit you submitted.

```python
counts = result_sim.get_counts(qc)
print(counts)
```

    {'111': 524, '000': 500}

Approximately 50 percent of the time the output bitstring is 000. Qiskit Terra also provides a function `plot_histogram` which allows you to view the outcomes.

```python
from qiskit.tools.visualization import plot_histogram
plot_histogram(counts)
```

The estimated outcome probabilities $\mathrm{Pr}(000)$ and  $\mathrm{Pr}(111)$ are computed by taking the aggregate counts and dividing by the number of shots (times the circuit was repeated). Try changing the ``shots`` keyword in the ``execute`` function and see how the estimated probabilities change.

Running circuits using the IBMQ provider
=======================

To faciliate access to real quantum computing hardware, we have provided a simple API interface.
To access IBMQ devices, you'll need an API token. For the public IBM Q devices, you can generate an API token [here](https://quantumexperience.ng.bluemix.net/qx/account/advanced) (create an account if you don't already have one). For Q Network devices, login to the q-console, click your hub, group, and project, and expand "Get Access" to generate your API token and access url.

Our IBMQ provider lets you run your circuit on real devices or on our HPC simulator. Currently, this provider exists within Qiskit, and can be imported as shown below. For details on the provider, see [The IBMQ Provider](the_ibmq_provider.ipynb).

```python
from qiskit import IBMQ
```

After generating your API token, call, `IBMQ.save_account('MY_TOKEN')`. For Q Network users, you'll also need to include your access url: `IBMQ.save_account('MY_TOKEN', 'URL')`

This will store your IBMQ credentials in a local file.  Unless your registration information has changed, you only need to do this once.  You may now load your accounts by calling,

```python
IBMQ.load_accounts()
```

Once your account has been loaded, you can view the list of backends available to you.

```python
print("Available backends:")
IBMQ.backends()
```

    Available backends:

    [<IBMQBackend('ibmqx4') from IBMQ()>,
     <IBMQBackend('ibmqx5') from IBMQ()>,
     <IBMQBackend('ibmqx2') from IBMQ()>,
     <IBMQBackend('ibmq_16_melbourne') from IBMQ()>,
     <IBMQBackend('ibmq_qasm_simulator') from IBMQ()>]

### Running circuits on real devices

Today's quantum information processors are small and noisy, but are advancing at a fast pace. They provide a great opportunity to explore what [noisy, intermediate-scale quantum (NISQ)](https://arxiv.org/abs/1801.00862) computers can do.

The IBMQ provider uses a queue to allocate the devices to users. We now choose a device with the least busy queue which can support our program (has at least 3 qubits).

```python
from qiskit.backends.ibmq import least_busy

large_enough_devices = IBMQ.backends(filters=lambda x: x.configuration()['n_qubits'] > 3 and
                                                       not x.configuration()['simulator'])
backend = least_busy(large_enough_devices)
print("The best backend is " + backend.name())
```

    The best backend is ibmqx4

To run the circuit on the backend, we need to specify the number of shots and the number of credits we are willing to spend to run the circuit. Then, we execute the circuit on the backend using the ``execute`` function.

```python
shots = 1024           # Number of shots to run the program (experiment); maximum is 8192 shots.
max_credits = 3        # Maximum number of credits to spend on executions.

job_exp = execute(qc, backend=backend, shots=shots, max_credits=max_credits)
```

``job_exp`` has a ``.result()`` method that lets us get the results from running our circuit.

Note: When the .result() method is called, the code block will wait until the job has finished before releasing the cell.

```python
result_exp = job_exp.result()
```

Like before, the counts from the execution can be obtained using ```get_counts(qc)```

```python
counts_exp = result_exp.get_counts(qc)
plot_histogram(counts_exp)
```

### Simulating circuits using a HPC simulator

The IBMQ provider also comes with a remote optimized simulator called ``ibmq_qasm_simulator``. This remote simulator is capable of simulating up to 32 qubits. It can be used the
same way as the remote real backends.

```python
backend = IBMQ.get_backend('ibmq_qasm_simulator')
```

```python
shots = 1024           # Number of shots to run the program (experiment); maximum is 8192 shots.
max_credits = 3        # Maximum number of credits to spend on executions.

job_hpc = execute(qc, backend=backend, shots=shots, max_credits=max_credits)
```

```python
result_hpc = job_hpc.result()
```

```python
counts_hpc = result_hpc.get_counts(qc)
plot_histogram(counts_hpc)
```

### Retrieving a previously ran job

If your experiment takes longer to run then you have time to wait around, or if you simply want to retrieve old jobs back, the IBMQ backends allow you to do that.
First you would need to note your job's ID:

```python
jobID = job_exp.job_id()

print('JOB ID: {}'.format(jobID))
```

    JOB ID: 5be8ae5e17436b0052751909

Given a job ID, that job object can be later reconstructed from the backend using retrieve_job:

```python
job_get=backend.retrieve_job(jobID)
```

and then the results can be obtained from the new job object.

```python
job_get.result().get_counts(qc)
```

    {'00000': 367,
     '00001': 10,
     '00010': 30,
     '00011': 27,
     '00100': 22,
     '00101': 83,
     '00110': 50,
     '00111': 435}

```python

```


#  CS109B Data Science 2: Advanced Topics in Data Science

## Lab 4 - Bayesian Analysis

**Harvard University**

**Spring 2020**

**Instructors:** Mark Glickman, Pavlos Protopapas, and Chris Tanner

**Lab Instructors:** Chris Tanner and Eleni Angelaki Kaxiras

**Content:** Eleni Angelaki Kaxiras

---

```python
## RUN THIS CELL TO PROPERLY HIGHLIGHT THE EXERCISES
import requests
from IPython.core.display import HTML
styles = requests.get("https://raw.githubusercontent.com/Harvard-IACS/2019-CS109B/master/content/styles/cs109.css").text
HTML(styles)
```

```python
import pymc3 as pm
from pymc3 import summary
```

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
import pandas as pd
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
```

```python
print('Running on PyMC3 v{}'.format(pm.__version__))
```

    Running on PyMC3 v3.8

```javascript
%%javascript
IPython.OutputArea.auto_scroll_threshold = 20000;
```

    <IPython.core.display.Javascript object>

## Learning Objectives

By the end of this lab, you should be able to:
* Understand how probability distributions work.
* Apply Bayes Rule in calculating probabilities.
* Understand how to apply Bayesian analysis using PyMC3
* Avoid getting fired when talking to your Bayesian employer.

**This lab corresponds to Lectures 6, 7, and 8, and maps to Homework 3.**

## Table of Contents

1. The Bayesian Way of Thinking or Is this a Fair Coin?
2. [Intro to `pyMC3`](#pymc3).
3. [Bayesian Linear Regression](#blr).
4. [Try this at Home: Example on Mining Disasters](#no4).

## 1. The Bayesian way of Thinking

```
Here is my state of knowledge about the situation. Here is some data, I am now going to revise my state of knowledge.
```

Table Exercise: Discuss the statement above with your table mates and make sure everyone understands what it means and what constitutes Bayesian way of thinking. Finally, count the Bayesians among you.

### A. Bayes Rule

\begin{equation}
\label{eq:bayes}
P(A|\textbf{B}) = \frac{P(\textbf{B} |A) P(A) }{P(\textbf{B})}
\end{equation}

$P(A|\textbf{B})$ is the **posterior** distribution, prob(hypothesis | data)

$P(\textbf{B} |A)$ is the **likelihood** function, how probable is my data **B** for different values of the parameters

$P(A)$ is the marginal probability to observe the data, called the **prior**, this captures our belief about the data before observing it.

$P(\textbf{B})$ is the marginal distribution (sometimes called marginal likelihood)

Table Exercise: Solve the Monty Hall Paradox using Bayes Rule.

You are invited to play a game. There are 3 doors behind **one** of which are the keys to a brand new red Tesla. There is a goat behind each of the other two.

You are asked to pick one door, and let's say you pick **Door1**. The host knows where the keys are. Of the two remaining closed doors, he will always open the door that has a goat behind it. He'll say "I will do you a favor and open **Door2**". So he opens Door2 inside which there is, of course, a goat. He now asks you, do you want to open the initial Door you chose or change to **Door3**? Generally, in this game, when you are presented with this choice should you swap the doors?

**Initial Steps:**
- Start by defining the `events` of this probabilities game. One definition is:

    - $A_i$: car is behind door $i$

    - $B_i$ host opens door $i$

$i\in[1,2,3]$

- In more math terms, the question is: is the probability that the price is behind **Door 1** higher than the probability that the price is behind **Door2**, given that an event **has occured**?

$P(A_1|B_2) = \frac{P(B_2|A_1)P(A_1)}{P(B_2)} = \frac{P(B_2|A_1)P(A_1)}{P(B_2|A_1)P(A_1) + P(B_2|A_2)P(A_2) + P(B_2|A_3)P(A_3)} = \frac{\frac{1}{2} \cdot \frac{1}{3}}{\frac{1}{2}} = \frac{1}{3}$

$P(B_2|A_2) = 0$ because the host won't open a door with a prize behind it.

$P(B_2|A_3) = 1$ because the host won't open door 2, which is the one you picked.

$P(B_2) = P(B_2|A_1)P(A_1) + P(B_2|A_2)P(A_2) + P(B_2|A_3)P(A_3) = \frac{1}{2} \cdot \frac{1}{3} + 0 + 1\cdot \frac{1}{3} = \frac{1}{2}$

### B. Bayes Rule written with Probability Distributions

We have data that we believe come from an underlying distribution of unknown parameters. If we find those parameters, we know everything about the process that generated this data and we can make inferences (create new data).

$\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}$

#### But what is $\theta \;$?

$\theta$ is an unknown yet fixed set of parameters. In Bayesian inference we express our belief about what $\theta$ might be and instead of trying to guess $\theta$ exactly, we look for its **probability distribution**. What that means is that we are looking for the **parameters** of that distribution. For example, for a Poisson distribution our $\theta$ is only $\lambda$. In a normal distribution, our $\theta$ is often just $\mu$ and $\sigma$.

### C. A review of Common Probability Distributions

#### Discrete Distributions

The random variable has a **probability mass function (pmf)** which measures the probability that our random variable will take a specific value $y$, denoted $P(Y=y)$.

- **Bernoulli** (binary outcome, success has probability $\theta$, $one$ trial):
$
P(Y=k) =  \theta^k(1-\theta)^{1-k}
$

- **Binomial** (binary outcome, success has probability $\theta$, $n$ trials):
\begin{equation}
P(Y=k) =  {{n}\choose{k}} \cdot \theta^k(1-\theta)^{n-k}
\end{equation}

*Note*: Binomial(1,$p$) = Bernouli($p$)

- **Negative Binomial**

- **Poisson** (counts independent events occurring at a rate)
\begin{equation}
P\left( Y=y|\lambda \right) = \frac{{e^{ - \lambda } \lambda ^y }}{{y!}}
\end{equation}
y = 0,1,2,...

- **Discrete Uniform**

- **Categorical, or Multinulli** (random variables can take any of K possible categories, each having its own probability; this is a generalization of the Bernoulli distribution for a discrete variable with more than two possible outcomes, such as the roll of a die)

- **Dirichlet-multinomial** (a generalization of the beta distribution for many variables)

#### Continuous Distributions

The random variable has a **probability density function (pdf)**.
- **Uniform** (variable equally likely to be near each value in interval $(a,b)$)
\begin{equation}
P(X = x) = \frac{1}{b - a}
\end{equation}
anywhere within the interval $(a, b)$, and zero elsewhere.

- **Normal** (a.k.a. Gaussian)
\begin{equation}
X \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

    A Normal distribution can be parameterized either in terms of precision $\tau$ or standard deviation ($\sigma^{2}$. The link between the two is given by
\begin{equation}
\tau = \frac{1}{\sigma^{2}}
\end{equation}
 - Mean $\mu$
 - Variance $\frac{1}{\tau}$ or $\sigma^{2}$
 - Parameters: `mu: float`, `sigma: float` or `tau: float`

- **Beta** (variable ($\theta$) taking on values in the interval $[0,1]$, and parametrized by two positive parameters, $\alpha$ and $\beta$ that control the shape of the distribution.

*Note:*Beta is a good distribution to use for priors (beliefs) because its range is $[0,1]$ which is the natural range for a probability and because we can model a wide range of functions by changing the $\alpha$ and $\beta$ parameters.

\begin{equation}
\label{eq:beta}
P(\theta) = \frac{1}{B(\alpha, \beta)} {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1} \propto {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1}
\end{equation}

where the normalisation constant, $B$, is a beta function of $\alpha$ and $\beta$,

\begin{equation}
B(\alpha, \beta) = \int_{t=0}^1 t^{\alpha - 1} (1 - t)^{\beta - 1} dt.
\end{equation}

- **Exponential**

- **Gamma**

 #### Code Resources:
 - Statistical Distributions in numpy/scipy: [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html)
 - Statistical Distributions in pyMC3: [distributions in PyMC3](https://docs.pymc.io/api/distributions.html) (we will see those below).

Exercise: Plot a Discrete variable

Change the value of $\mu$ in the Poisson PMF and see how the plot changes. Remember that the y-axis in a discrete probability distribution shows the probability of the random variable having a specific value in the x-axis.

\begin{equation}
P\left( X=k \right) = \frac{{e^{ - \mu } \mu ^k }}{{k!}}
\end{equation}

**stats.poisson.pmf(x, mu)** $\mu$(mu) is our $\theta$ in this case.

```python
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 30)
for m in [0.5, 3, 8]:
    pmf = stats.poisson.pmf(x, m)
    plt.plot(x, pmf, 'o', alpha=0.5, label='$\mu$ = {}'.format(m))
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability', fontsize=12)
plt.legend(loc=1)
plt.ylim=(-0.1)
plt.show()
```

```python
pmf
```

    array([3.35462628e-04, 2.68370102e-03, 1.07348041e-02, 2.86261442e-02,
           5.72522885e-02, 9.16036616e-02, 1.22138215e-01, 1.39586532e-01,
           1.39586532e-01, 1.24076917e-01, 9.92615338e-02, 7.21902064e-02,
           4.81268043e-02, 2.96164949e-02, 1.69237114e-02, 9.02597941e-03,
           4.51298971e-03, 2.12375986e-03, 9.43893272e-04, 3.97428746e-04,
           1.58971498e-04, 6.05605708e-05, 2.20220258e-05, 7.65983504e-06,
           2.55327835e-06, 8.17049071e-07, 2.51399714e-07, 7.44888042e-08,
           2.12825155e-08, 5.87103876e-09])

```python
# same for binomial
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 22)
ns = [10, 17]
ps = [0.5, 0.7]
for n, p in zip(ns, ps):
    pmf = stats.binom.pmf(x, n, p)
    plt.plot(x, pmf, 'o', alpha=0.5, label='n = {}, p = {}'.format(n, p))
plt.xlabel('x', fontsize=14)
plt.ylabel('f(x)', fontsize=14)
plt.legend(loc=1)
plt.show()
```

```python
# discrete uniform
plt.style.use('seaborn-darkgrid')
ls = [0]
us = [3] # watch out, this number can only be integer!
for l, u in zip(ls, us):
    x = np.arange(l, u+1)
    pmf = [1.0 / (u - l + 1)] * len(x)
    plt.plot(x, pmf, '-o', label='lower = {}, upper = {}'.format(l, u))
plt.xlabel('x', fontsize=12)
plt.ylabel('probability P(x)', fontsize=12)
plt.legend(loc=1)
plt.show()
```

Exercise: Plot a continuous variable

Change the value of $\mu$ in the Uniform PDF and see how the plot changes.

Remember that the y-axis in a continuous probability distribution does not shows the actual probability of the random variable having a specific value in the x-axis because that probability is zero!. Instead, to see the probability that the variable is within a small margin we look at the integral below the curve of the PDF.

The uniform is often used as a noninformative prior.

```
Uniform - numpy.random.uniform(a=0.0, b=1.0, size)
```

$\alpha$ and $\beta$ are our parameters. `size` is how many tries to perform.
Our $\theta$ is basically the combination of the parameters a,b. We can also call it
\begin{equation}
\mu = (a+b)/2
\end{equation}

```python
from scipy.stats import uniform

r = uniform.rvs(size=1000) # random numbers from a uniform distribution
plt.plot(r, uniform.pdf(r),'r-', lw=5, alpha=0.6, label='uniform pdf')
plt.hist(r, density=True, histtype='stepfilled', alpha=0.2)
plt.ylabel(r'probability density')
plt.xlabel(f'random variable')
plt.legend(loc='best', frameon=False)
plt.show()
```

```python
from scipy.stats import beta

alphas = [0.5, 1.5, 3.0]
betas = [0.5, 1.5, 3.0]
x = np.linspace(0, 1, 1000)
colors = ['red', 'green', 'blue']

fig, ax = plt.subplots(figsize=(8, 5))

for a, b, colors in zip(alphas, betas, colors):
    dist = beta(a, b)
    plt.plot(x, dist.pdf(x), c=colors,
             label=f'a={a}, b={b}')

ax.set_ylim(0, 3)

ax.set_xlabel(r'$\theta$')
ax.set_ylabel(r'$p(\theta|\alpha,\beta)$')
ax.set_title('Beta Distribution')

ax.legend(loc='best')
fig.show();
```

Normal distribution

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.]
sigmas = [0.4, 1., 2., 0.4]
for mu, sigma in zip(mus, sigmas):
    pdf = stats.norm.pdf(x, mu, sigma)
    plt.plot(x, pdf, label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

Uniform

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.] # mean
sigmas = [0.4, 1., 2., 0.4] # std
for mu, sigma in zip(mus, sigmas):
      plt.plot(x, uniform.pdf(x, mu, sigma), lw=5, alpha=0.4, \
               label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()
```

### D. Is this a Fair Coin?

We do not want to promote gambling but let's say you visit the casino in **Monte Carlo**. You want to test your theory that casinos are dubious places where coins have been manipulated to have a larger probability for tails. So you will try to estimate how fair a coin is based on 100 flips.

You begin by flipping the coin. You get either Heads ($H$) or Tails ($T$) as our observed data and want to see if your posterior probabilities change as you obtain more data, that is, more coin flips. A nice way to visualize this is to plot the posterior probabilities as we observe more flips (data).

We will be using Bayes rule. $\textbf{D}$ is our data.

$\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}$

beta-binomial conjugacy.

prior: $Beta(\alpha, \beta)$, $\alpha$ is number of successes, $\beta$ is the number of failures

Data: out of n tosses, we see k heads

posterior: $Beta(\alpha + k, \beta + n - k)$

In the case of a coin toss when we observe $k$ heads in $n$ tosses:
$\begin{equation}
\label{eq:bayes}
P(\theta|\textbf{k}) = Beta(\alpha + \textbf{k}, \beta + n - \textbf{k})
\end{equation}$

we can say that $\alpha$ and $\beta$ play the roles of a "prior number of heads" and "prior number of tails".

```python
# play with the priors - here we manually set them but we could be sampling from a separate Beta
trials = np.array([0, 1, 3, 5, 10, 15, 20, 100, 200, 300])
heads = np.array([0, 1, 2, 4, 8, 10, 10, 50, 180, 150])
x = np.linspace(0, 1, 100)

# for simplicity we set a,b=1

plt.figure(figsize=(10,8))
for k, N in enumerate(trials):
    sx = plt.subplot(len(trials)/2, 2, k+1)
    posterior = stats.beta.pdf(x, 1 + heads[k], 1 + trials[k] - heads[k])
    plt.plot(x, posterior, alpha = 0.5, label=f'{trials[k]} tosses\n {heads[k]} heads');
    plt.fill_between(x, 0, posterior, color="#348ABD", alpha=0.4)
    plt.legend(loc='upper left', fontsize=10)
    plt.legend()
    plt.autoscale(tight=True)

plt.suptitle("Posterior probabilities for coin flips", fontsize=15);
plt.tight_layout()
plt.subplots_adjust(top=0.88)
```

P(head) = 0.5 for 20 tosses and 10 heads (as the center of posterior distribution is centered at 0.5). MLE = 10/20 = 0.5

As we increase number of trials, heads is around half, so this coin is fair.

 [Top](#top)

## 2. Introduction to `pyMC3`

PyMC3 is a Python library for programming Bayesian analysis, and more specifically, data creation, model definition, model fitting, and posterior analysis. It uses the concept of a `model` which contains assigned parametric statistical distributions to unknown quantities in the model. Within models we define random variables and their distributions. A distribution requires at least a `name` argument, and other `parameters` that define it. You may also use the `logp()` method in the model to build the model log-likelihood function. We define and fit the model.

PyMC3 includes a comprehensive set of pre-defined statistical distributions that can be used as model building blocks. Although they are not meant to be used outside of a `model`, you can invoke them by using the prefix `pm`, as in `pm.Normal`.

#### Markov Chain Monte Carlo (MCMC) Simulations

PyMC3 uses the **No-U-Turn Sampler (NUTS)** and the **Random Walk Metropolis**, two Markov chain Monte Carlo (MCMC) algorithms for sampling in posterior space. Monte Carlo gets into the name because when we sample in posterior space, we choose our next move via a pseudo-random process. NUTS is a sophisticated algorithm that can handle a large number of unknown (albeit continuous) variables.

`pm.distributions` will have two important methods: `random()` and `logp()` with the following signatures:

`logp()` method to return a log-probability evaluated at the passed value argument. This method is used internally by all of the inference methods to calculate the model log-probability that is used for fitting models. The `random()` method is used to simulate values from the variable, and is used internally for posterior predictive checks

```python
with pm.Model() as model:
    # a scalar variable follows normal distribution
    z = pm.Normal('z', mu=0., sigma=5.) # name, params
    x = pm.Normal('x', mu=z, sigma=1., observed=5.)
print(x.logp({'z': 2.5}))
print(z.random(10, 100)[:10])
```

    -4.043938533204672
    [ 7.17624957  1.88396497 -3.84377077 -4.51319065  2.15195803 -0.74173189
     -0.06213591 -1.40335086 -3.67668208  0.46645415]

```python
z.random(10, 100) # point, size
```

    array([  4.20299819,   6.67756286,   1.86397596,   4.69772266,
            -1.74075718,  -6.1587605 ,   4.01425601,   1.47212497,
            -0.95908539,  -0.75957457,  -0.9965861 ,   4.40711468,
             0.86476568,  -1.78865136,   2.43830109,   7.48101036,
            -3.83437749,   0.97882911,  -4.60062897,   3.02594797,
            -7.87301058,  -2.37757347,  -5.25710143,  -2.86329763,
             4.04072601, -10.79232415,  -0.17469766,   6.95349508,
             5.65213913,  -0.74215824,  -0.26543113,   4.99100215,
             3.86390824,  -2.460854  ,   7.62924736,   1.20906703,
             4.80720307,   0.91501121,  -4.11779673,  -6.50301219,
            -0.53290222,  -1.55428558,   4.79833462,   1.57705118,
            -1.6667158 ,   8.146217  ,   2.22696357,  -0.45245175,
            -1.71733788,  -6.82240357,   1.4551234 ,  -1.83267745,
            11.78864822,  -2.17962573,   4.63795644,  -0.63709271,
             1.15939193,   1.33588019,   3.33174281,  -2.39045536,
            -4.36408123,  -1.5982427 ,   7.23550659,  -0.48821265,
             0.73843087,   3.23945607,   1.63729074,  -1.45938923,
             4.91725175,  -2.90260851,  -3.29998453, -13.40968767,
            -3.12695605,   3.36211579,  -7.19234678,   3.37934005,
             2.07161948,   1.68049247,  -3.56791466,  -2.78515778,
            -0.77515644,   8.75054104,   5.93503912,   7.73719646,
            -3.03921893,   0.6343688 ,   6.26068286,  -5.8339571 ,
             2.25379384,  -5.4811024 ,   3.324151  ,  -0.62969373,
             8.16379873,   0.04000131,  -2.90106212,   5.08299257,
             2.87763086,   3.48026559,   1.31247562,   2.81561549])

```python
model
```

$$
            \begin{array}{rcl}
            \text{z} &\sim & \text{Normal}(\mathit{mu}=0.0,~\mathit{sigma}=5.0)\\\text{x} &\sim & \text{Normal}(\mathit{mu}=\text{z},~\mathit{sigma}=1.0)
            \end{array}
            $$

**References**:

- *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55* [(https://doi.org/10.7717/peerj-cs.55)](https://doi.org/10.7717/peerj-cs.55)
- [Distributions in PyMC3](https://docs.pymc.io/api/distributions.html)
- [More Details on Distributions](https://docs.pymc.io/developer_guide.html)

Information about PyMC3 functions including descriptions of distributions, sampling methods, and other functions, is available via the `help` command.

```python
#help(pm.Poisson)
```

 [Top](#top)

## 3. Bayesian Linear Regression

Let's say we want to predict outcomes Y as normally distributed observations with an expected value $mu$ that is a linear function of two predictor variables, $\bf{x}_1$ and $\bf{x}_2$.

\begin{equation}
\mu = \alpha + \beta_1 \bf{x}_1 + \beta_2 x_2
\end{equation}

\begin{equation}
Y \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

where $\sigma^2$ represents the measurement error.

In this example, we will use $\sigma^2 = 10$

We also choose the parameters as normal distributions:

\begin{eqnarray}
\alpha \sim  \mathcal{N}(0,\,10) \\
\beta_i \sim  \mathcal{N}(0,\,10) \\
\sigma^2 \sim  |\mathcal{N}(0,\,10)|
\end{eqnarray}

We will artificially create the data to predict on. We will then see if our model predicts them correctly.

```python
# Initialize random number generator
np.random.seed(123)

# True parameter values
alpha, sigma = 1, 1 # in bayesian world, the answer is the mean
beta = [1, 2.5]

# Size of dataset
size = 100

# Predictor variable
X1 = np.linspace(0, 1, size)
X2 = np.linspace(0,.2, size)

# Simulate outcome variable
Y = alpha + beta[0]*X1 + beta[1]*X2 + np.random.randn(size)*sigma

fig, ax = plt.subplots(1,2, figsize=(10,6), sharex=True)
ax[0].scatter(X1,Y)
ax[1].scatter(X2,Y)
ax[0].set_xlabel(r'$x_1$', fontsize=14)
ax[0].set_ylabel(r'$Y$', fontsize=14)
ax[1].set_xlabel(r'$x_2$', fontsize=14)
ax[1].set_ylabel(r'$Y$', fontsize=14)
```

`observed`: observed data to be loaded into the distribution

Given distributions of $\alpha$, $\beta_i$ and $\sigma$

prior: prior distribution of $\mu = \alpha + \beta_1 \bf{x}_1 + \beta_2 x_2$

likelihood: Ys follows a normal distribution with mean $\mu = \alpha + \beta_1 \bf{x}_1 + \beta_2 x_2$ and variance $\sigma^2$

posterior: posterior distribution of $\mu$ after update of observed data

```python
from pymc3 import Model, Normal, HalfNormal

basic_model = Model()

with basic_model:

    # Priors for unknown model parameters, specifically create stochastic random variables
    # with Normal prior distributions for the regression coefficients,
    # and a half-normal distribution for the standard deviation of the observations, σ.
    alpha = Normal('alpha', mu=0, sd=10)
    beta = Normal('beta', mu=0, sd=10, shape=2)
    sigma = HalfNormal('sigma', sd=1)

    # Expected value of outcome - posterior
    mu = alpha + beta[0]*X1 + beta[1]*X2

    # Likelihood (sampling distribution) of observations
    # observed=Y: takes into account my data (simulated response variable in the previous chunk)
    Y_obs = Normal('Y_obs', mu=mu, sd=sigma, observed=Y)
```

The maximum a posteriori (MAP) estimate for a model, is the mode of the posterior distribution and is generally found using numerical optimization methods. This is often fast and easy to do, but only gives a point estimate for the parameters and can be biased if the mode isn’t representative of the distribution. PyMC3 provides this functionality with the find_MAP function.

```python
# model fitting with sampling
from pymc3 import NUTS, sample, find_MAP
from scipy import optimize

with basic_model:

    # obtain starting values via MAP (maximum a posterior probability (MAP))
    start = find_MAP(fmin=optimize.fmin_powell)

    # instantiate sampler
    step = NUTS(scaling=start)

    # draw 2000 posterior samples
    trace = sample(2000, step, start=start)
```

    logp = -164.5:   5%|▌         | 271/5000 [00:00<00:02, 1579.74it/s]

    Optimization terminated successfully.
             Current function value: 164.496957
             Iterations: 6
             Function evaluations: 271

    Multiprocess sampling (2 chains in 2 jobs)
    NUTS: [sigma, beta, alpha]
    Sampling 2 chains, 0 divergences: 100%|██████████| 5000/5000 [00:14<00:00, 339.94draws/s]
    The number of effective samples is smaller than 10% for some parameters.

alpha centered around 1 (true parameter)

all pararams have centered around the true value

```python
from pymc3 import traceplot

traceplot(trace);
```

```python
results = pm.summary(trace,
                     var_names=['alpha', 'beta', 'sigma'])
results
```

      mean
      sd
      hpd_3%
      hpd_97%
      mcse_mean
      mcse_sd
      ess_mean
      ess_sd
      ess_bulk
      ess_tail
      r_hat

      alpha
      1.010
      0.225
      0.606
      1.452
      0.005
      0.003
      2248.0
      2248.0
      2246.0
      2149.0
      1.0

      beta[0]
      1.351
      1.983
      -2.329
      5.006
      0.104
      0.078
      362.0
      320.0
      361.0
      606.0
      1.0

      beta[1]
      0.898
      9.812
      -16.505
      19.738
      0.505
      0.358
      377.0
      377.0
      375.0
      583.0
      1.0

      sigma
      1.145
      0.082
      0.994
      1.298
      0.001
      0.001
      3573.0
      3542.0
      3586.0
      2889.0
      1.0

This linear regression example is from the original paper on PyMC3: *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55 https://doi.org/10.7717/peerj-cs.55*

 [Top](#top)

## 4. Try this at Home: Example on Mining Disasters
We will go over the classical `mining disasters from 1851 to 1962` dataset.

This example is from the [pyMC3 Docs](https://docs.pymc.io/notebooks/getting_started.html).

```python
import pandas as pd
disaster_data = pd.Series([4, 5, 4, 0, 1, 4, 3, 4, 0, 6, 3, 3, 4, 0, 2, 6,
                           3, 3, 5, 4, 5, 3, 1, 4, 4, 1, 5, 5, 3, 4, 2, 5,
                           2, 2, 3, 4, 2, 1, 3, np.nan, 2, 1, 1, 1, 1, 3, 0, 0,
                           1, 0, 1, 1, 0, 0, 3, 1, 0, 3, 2, 2, 0, 1, 1, 1,
                           0, 1, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 1, 1, 0, 2,
                           3, 3, 1, np.nan, 2, 1, 1, 1, 1, 2, 4, 2, 0, 0, 1, 4,
                           0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1])
fontsize = 12
years = np.arange(1851, 1962)
plt.figure(figsize=(10,5))
#plt.scatter(years, disaster_data);
plt.bar(years, disaster_data)
plt.ylabel('Disaster count', size=fontsize)
plt.xlabel('Year', size=fontsize);
plt.title('Was there a Turning Point in Mining disasters from 1851 to 1962?', size=15);
```

#### Building the model

**Step1:** We choose the probability model for our experiment. Occurrences of disasters in the time series is thought to follow a **Poisson** process with a large **rate** parameter in the early part of the time series, and from one with a smaller **rate** in the later part. We are interested in locating the change point in the series, which perhaps is related to changes in mining safety regulations.

```
disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

We have two rates, `early_rate` if $t<=s$, and `late_rate` if $t>s$, where $s$ is the year the switch was made (a.k.a. the `switchpoint`).

**Step2:** Choose a prior distributions of the two rates, what we believe the rates were before we observed the data, and the switchpoint. We choose Exponential.
```
early_rate = pm.Exponential('early_rate', 1)
```

The parameters of this model are:

**Note:** Watch for missing values. Missing values are handled transparently by passing a MaskedArray or a pandas.DataFrame. Behind the scenes, another random variable, disasters.missing_values is created to model the missing values. If you pass a np.array with missing values you will get an error.

```python
with pm.Model() as disaster_model:

    # discrete
    switchpoint = pm.DiscreteUniform('switchpoint', lower=years.min(), upper=years.max(), testval=1900)

    # Priors for pre- and post-switch rates number of disasters
    early_rate = pm.Exponential('early_rate', 1)
    late_rate = pm.Exponential('late_rate', 1)

    # our theta - allocate appropriate Poisson rates to years before and after current
    # switch is an `if` statement in puMC3
    rate = pm.math.switch(switchpoint >= years, early_rate, late_rate)

    # our observed data as a likelihood function of the `rate` parameters
    # shows how we think our data is distributed
    disasters = pm.Poisson('disasters', rate, observed=disaster_data)
```

#### Model Fitting

```python
# there are defaults but we can also more explicitly set the sampling algorithms
with disaster_model:

    # for continuous variables
    step1 = pm.NUTS([early_rate, late_rate])

    # for discrete variables
    step2 = pm.Metropolis([switchpoint, disasters.missing_values[0]] )

    trace = pm.sample(10000, step=[step1, step2])
    # try different number of samples
    #trace = pm.sample(5000, step=[step1, step2])
```

#### Posterior Analysis

On the left side plots we notice that our early rate is between 2.5 and 3.5 disasters a year. In the late period it seems to be between 0.6 and 1.2 so definitely lower.

The right side plots show the samples we drew to come to our conclusion.

```python
pm.traceplot(trace, ['early_rate', 'late_rate', 'switchpoint'], figsize=(20,10));
```

```python
results = pm.summary(trace,
                     var_names=['early_rate', 'late_rate', 'switchpoint'])
results
```


## Practical Data Science - Classroom to Careers

### Session-1

The main objective of this session is to explain the pre-requisites before we deal with any kind of Data in general.

#### What is data analysis?

Data Analysis is a subset of Data Science which helps us to understand the underlying process that generates the data

#### What are the different kinds of data?

Data can be mainly categorized into 2 main types -

 - Structured Data
 - Unstructured data

Examples of **Structured data** include - Tabular data, and mainly easily interpretable by a machine (numeric or can be converted to numeric)

Examples of **Unstructured data** include - Audio, Image or text data etc. which cannot be directly understood by a machine. The way we can transform this data to be understood by machines will be covered in the later part of the course

### Tools we will be using for the course

**Python version 3.6 or higher with Anaconda4** distribution (includes Jupyter/IPython Notebook)

To install Anaconda please visit the following link and choose the version compatible with your Windows OS. Right-click on My Computer/PC and select properties to get the version of your Windows Operating System  -

https://repo.anaconda.com/archive/Anaconda3-2020.02-Windows-x86_64.exe (64 bit version)

https://repo.anaconda.com/archive/Anaconda3-2020.02-Windows-x86.exe (32 bit version)

### Course Contents

**1. Basic Probability and Statistics**

 - 1.1 Understanding and Representing Data
 - 1.2 What are probability distributions?
 - 1.3 Bayes Theorem

**2. Probability and Data Distributions**

 - 2.1 What are Random Variables?
 - 2.2 What is a Probability Density Function?
 - 2.3 Normal Distribution
 - 2.4 Binomial Distribution
 - 2.5 Poisson Distribution
 - 2.6 Exponential Distribution
 - 2.7 Geometric Distribution

**3. Introduction to Linear Algebra and Calculus**

 - 3.1 Matrix Addition and Multiplication
 - 3.2 Finding the determinant of a matrix
 - 3.3 Eigenvalues and Eigenvectors
 - 3.4 Functions
 - 3.5 Differentiation of commonly used functions

**4. Statistical Estimation Methods**

 - 4.1 Maximum Likelihood Estimates
 - 4.2 Information Gain
 - 4.3 Parametric Estimation
 - 4.4 Non-parametric estimation
 - 4.5 Supervised vs. Unsupervised Learning

**5. Regression**

 - 5.1 Linear Regression
 - 5.2 Logistic regression
 - 5.3 Determining the goodness of fit
 - 5.4 Decision Trees
 - 5.5 Ridge Regression
 - 5.6 Lasso Regression
 - 5.7 Regularization

**6. Unsupervised Learning**

 - 6.1 Hierarchical Clustering
 - 6.2 K-Means Clustering
 - 6.3 Association Rule mining
 - 6.4 Principal Component Analysis (PCA)
 - 6.5 Singular Value Decomposition (SVD)
 - 6.6 One-class SVM (Anomaly detection)

**7. Supervised Learning in ML**

 - 7.1 Bagging/Random Forests
 - 7.2 Support Vector Machines (SVM)
 - 7.3 Boosting/ Gradient Boosting
 - 7.4 Multi-output classification

**8. Introduction to Neural Networks and Cost function**

 - 8.1 Artificial Neural Networks
 - 8.2 Activation Fucntions
 - 8.3 Backpropagation
 - 8.4 Gradient Descent
 - 8.5 Cost function vs. Loss function

**9. Text Classification**

 - 9.1 Bag-of-Words
 - 9.2 Named Entity Recognition (NER)
 - 9.3 Stemming and Lemmatization
 - 9.2 Feature Extraction from Text
 - 9.3 Pre-trained Word Embeddings
 - 9.4 Text Clustering
 - 9.5 Topic Modeling

**10. Sequence Models**

 - 10.1 Introduction to Recurrent Neural Networks (RNNs)
 - 10.2 Long Short Term Memory Networks (LSTMs)
 - 10.3 Gated Recurrent Units
 - 10.4 Attention Layers
 - 10.5 Multi-Output Classification

**11 and 12 - To be decided**

### Normal Distribution

Also known as Gaussian Distribution, is the most popular Distribution one would hear in Statistics and ML world.

This distribution has a Curve centered at mean and characterized byt the following -

1) Mean

2) Standard Deviation (sigma)

The equation of Normal Distribution is -

\begin{align}
P(X=x) = Y = \frac{1}{\sigma\sqrt(2\pi)} * e^{-(x-\mu)^2/2\sigma^2}\\
\end{align}

Where x is a Random Variable and **mu** is the Mean and **sigma** is the Standard deviation

Regression techniques have assumptions built around the data coming from a Normal Distribution

```python
import numpy as np
import scipy
import seaborn as sns

# generate random numbers from N(0,1)
data_normal = np.random.normal(size=1000,loc=0,scale=100)

# Plot the normal distribution
ax = sns.distplot(data_normal,
                  bins=100,
                  kde=False,
                  color='blue',
                  hist_kws={"linewidth": 15,'alpha':1})
ax.set(xlabel='Normal', ylabel='Frequency')
```

#### Properties of normal distribution

 - The mean, mode and median are all equal.
 - The curve is symmetric at the center (i.e. around the mean, μ).
 - Exactly half of the values are to the left of center and exactly half the values are to the right.
 - The total area under the curve is 1.

#### Applications of Normal Distribution

**Problem 1**

An average light bulb manufactured by the Acme Corporation lasts 300 days with a standard deviation of 50 days. Assuming that bulb life is normally distributed, what is the probability that an Acme light bulb will last at most 365 days?

**Problem 2**

Suppose scores on an IQ test are normally distributed. If the test has a mean of 100 and a standard deviation of 10, what is the probability that a person who takes the test will score between 90 and 110?

### Binomial Distribution

A binomial distribution is characterized by the following -

 - There are **n** repeated trials (where n is a positive integer)
 - The trial can result in only 2 outcomes (bi = 2)
 - The probability of success for each trial **p** is same and does not change (0<=p<=1)
 - The outcome of the trials is independent. i.e., the out come of one trial does not influence the outcome of any other trial

 **Example - Flipping a fair coin**

 - The probability of success of head/tail is always 0.5
 - The probability of success remains same
 - The flipping is independent
 - The outcome is either a head or a tail (only 2 outcomes)

**Binomial Distribution Equation**

\begin{equation*}
P(E)   = {n \choose k} p^k (1-p)^{ n-k}
\end{equation*}

**k**: The number of successes that result from the binomial experiment.

**n**: The number of trials in the binomial experiment.

**p**: The probability of success on an individual trial.

**q**: The probability of failure on an individual trial (1-p, since thre are only 2 outcomes)

```python
from scipy.stats import binom
import warnings

warnings.filterwarnings(action='ignore')

binom.rvs(size=5,n=10,p=0.5)

data_binom = binom.rvs(n=10,p=0.5,size=10000)

ax = sns.distplot(data_binom,
                  kde=False,
                  color='blue',
                  hist_kws={"linewidth": 15,'alpha':1})

ax.set(xlabel='Binomial', ylabel='Frequency')
```

```python

```


```python
%matplotlib inline
import pandas as pd

import numpy as np
from __future__ import division
import itertools

import matplotlib.pyplot as plt
import seaborn as sns

import logging
logger = logging.getLogger()
```

17 Amortized Analysis
============

In an __amortized analysis__, we averget the time required to perform a sequence of data-structure operations over all the operations performed.

Amortized analsis differs from average-case analysis in that probability is not involved; an amortized analysis guarantees the _average performance of each operation in the worst case__.

Bear in mind that the charges assigned during an amortized analysis are for analysis purposes only.

When we perform an amortized analysis, we often gain insight into a particular data structure, and this insight can help us optimize the design.

### 17.1 Aggregate analysis
We show that for all $n$, a sequence of $n$ operations takes worst-case time $T(n)$ in total.

##### Stack operations
PUSH $geq$ POP + MULTIPOP
$1 \times O(n) = O(n)$

##### Incrementing a binary counter

```python
plt.imshow(plt.imread('./res/fig17_2.png'))
```

In general, for $i = 0, 1, \dotsc, k-1$, bit $A[i]$ flips $\lfloor \frac{n}{2^i} \rfloor$ times in a sequence of $n$ INCREMENT operations on an initially zero counter.

\begin{align}
    \sum_{i=0}^{k-1} \lfloor \frac{n}{2^i} \rfloor &< n \sum_{i=0}^{\infty} \frac{1}{2^i} \\
                                                   &= 2n
\end{align}

### 17.2 The accounting method
**credit**: the cost that an operation's amortized cost $\hat{c_i}$ exceeds its actual cost $c_i$.

requriments: $$\sum_{i=1}^{n} \hat{c_i} \geq \sum_{i=1}^{n} c_i$$

##### Stack operations
|      | $c_i$ | $\hat{c_i}$ |
|------|-------|-------------|
| PUSH |   1   |     2       |
| POP  |   1   |     0       |
|MULTIPOP|  min(k,s)  |   0  |

$2 \times O(n) = O(n)$

##### Incrementing a binary counter
set a bit to 1:   2
set a bit to 0:   0

The INCREMENT procedure sets at most one bit, $2 \times O(n) = O(n)$

### 17.3 The potential method
Let $D_i$ be the data structure that results after applying the $i$th operation to data structure $D_{i-1}$.

**potential function $\phi$**: maps each data structure $D_i$ to a real number $\phi(D_i)$.

$\hat{c_i} = c_i + \phi(D_i) - \phi(D_{i-1})$
hence, the total amortized cost of the $n$ operations is:
$$\sum_{i=1}^n \hat{c_i} = \sum_{i=1}^n c_i  + \phi(D_n) - \phi(D_0)$$

Different potential functions may yield different amortized costs yet still be upper bounds on the actual costs. The best potential function to use depends on the disired time bounds.

##### Stack operations
define: $\phi$ to be the number of objects in the stack.

for PUSH:
\begin{align}
    \hat{c_i} &= c_i + \phi(D_i) - \phi(D_{i-1}) \\
              &= 1 + (s+1) - s \\
              &= 2
\end{align}

for POP:
\begin{align}
    \hat{c_i} &= c_i + \phi(D_i) - \phi(D_{i-1}) \\
              &= 1 + (s-1) - s \\
              &= 0
\end{align}

for MULTIPOP:
\begin{align}
    \hat{c_i} &= c_i + \phi(D_i) - \phi(D_{i-1}) \\
              &= k + (s-k) - s \\
              &= 0
\end{align}

##### Incrementing a binary counter
define: $\phi$ to be $b_i$, the number of 1s in the counter after the $i$th operation.

Suppose: the $i$th INCREMENT operation reset $t_i$ bits.

for INCREMENT:
\begin{align}
    \hat{c_i} &= c_i + \phi(D_i) - \phi(D_{i-1}) \\
              &= (t_i + 1) + (b_{i-1} - t_i + 1) - b_{i-1} \\
              &= 2
\end{align}

### 17.4 Dynamic tables
**load factor**: $$\alpha(T) = \frac{\|\text{items of T}\|}{\|T\|}$$

#### 17.4.1 Table expansion
insert an item into a full table, we expand the table with twice spaces.

The cost of the $i$th operation is:
\begin{equation}
    c_i = \begin{cases}
        i \quad & \text{expand: if i - 1 is an exact power of 2} \\
        1 \quad & \text{otherwise}
    \end{cases}
\end{equation}

The total cost of $n$ TABLE-INSERT operations is therefore:
\begin{align}
    \sum_{i=1}^{n} c_i &\leq n + \sum_{j=0}^{\lfloor \lg n \rfloor} 2^j \\
                       &< n + 2n \\
                       &= 3n
\end{align}

#### 17.4.2 Table expansion and contraction
Halve the table size when deleting an item causes the table to become less than 1/4 full, rather than 1/2 full as before(引起振荡).


```python
%matplotlib inline
```

DCGAN 教程
==============

**翻译者**: `Antares博士 <http://www.studyai.com/antares>`_

介绍
------------

本教程将通过一个示例介绍DCGANs。我们将训练一个生成对抗网络(generative adversarial network, GAN)，
在给它展示许多名流的照片之后，产生新的名人。这里的大部分代码都来自 `pytorch/examples <https://github.com/pytorch/examples>`__ 的实现，
本文档将详细解释实现，并阐明该模型是如何工作的和为什么工作的。但别担心，不需要事先知道GANs，
但它可能需要第一次花一些时间来推理在表象的下面真正发生了什么。此外，为了时间，有一个或两个GPU可能是个好事儿。
让我们从头开始。

生成对抗网络
-------------------------------

什么是 GAN?
~~~~~~~~~~~~~~

GANS是一个框架，它教授DL模型以捕获训练数据的分布，这样我们就可以从相同的分布生成新的数据。
GANs 是由伊恩·古德费罗于2014年发明的，并首次在论文
`Generative Adversarial Nets <https://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf>`__
中进行了描述。它们由两种不同的模型组成，一种是生成器(*generator*)，另一种是判别器(*discriminator*)。
生成器的工作是生成看起来像训练图像的“假”图像。判别器的工作是查看图像并输出它是真实的训练图像还是来自生成器的假图像。
在训练过程中，生成器不断地试图通过生成越来越好的伪图像来胜过判别器，而判别器正在努力成为一名更好的侦探，
并正确地对真假图像进行分类。这个游戏的均衡是当生成器生成看起来像是直接来自训练数据的完美假象时，
判别器总是以50%的信心猜测生成器输出是真是假的。

现在，让我们从判别器开始，在整个教程中定义一些要使用的符号。假设 $x$ 是表示图像的数据。
$D(x)$ 是判别器网络，它输出 $x$ 来自训练数据而不是生成器的(标量)概率。这里，
由于我们处理的是图像，$D(x)$ 的输入是HWC大小为3x64x64的图像。
直觉上，当 $x$ 来自训练数据时， $D(x)$ 应该是高的，
当 $x$ 来自生成器时，$D(x)$  应该是低的。
$D(x)$ 也可以看作是一种传统的二元分类器。

对于生成器的表示法，设 $z$ 是从标准正态分布中采样的潜在空间向量(latent space vector)。
$G(z)$ 表示生成函数，它将潜在向量 $z$ 映射到数据空间。
$G$ 的目标是估计训练数据的分布 ($p_{data}$) ，从而从估计出的分布($p_g$)中生成假样本。

因此, $D(G(z))$ 是生成器 $G$ 输出的图像为真实图像的概率(标量)。
正如 `古德费罗的论文 <https://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf>`__, 所描述的那样，
$D$ 和 $G$ 玩了一个极小极大的博弈(minimax game)，其中 $D$
试图最大化它正确地分类真图像和假图像的概率($logD(x)$)，$G$ 试图最小化 $D$
预测其输出是假的的概率 ($log(1-D(G(x)))$) 。文中给出了GAN损失函数:

\begin{align}\underset{G}{\text{min}} \underset{D}{\text{max}}V(D,G) = \mathbb{E}_{x\sim p_{data}(x)}\big[logD(x)\big] + \mathbb{E}_{z\sim p_{z}(z)}\big[log(1-D(G(x)))\big]\end{align}

理论上，这个极小极大博弈的解是 在 $p_g = p_{data}$ 时，判别器只能随机猜测输入是真还是假。
然而，GANS的收敛理论仍在积极研究之中，而在现实中，模型并不总是训练到这一点。

什么又是 DCGAN?
~~~~~~~~~~~~~~~~

DCGAN是上述GANs的直接扩展，只是它在鉴别器和生成器中分别显式地使用卷积和卷积转置层。
它首先由Radford在文章 `Unsupervised Representation Learning With
Deep Convolutional Generative Adversarial Networks <https://arxiv.org/pdf/1511.06434.pdf>`__
提出了一种基于深层卷积生成对抗网络的无监督表示学习方法。
判别器由跨步卷积层(`strided convolution layers <https://pytorch.org/docs/stable/nn.html#torch.nn.Conv2d>`__ )、
批归一化层(`batch norm layers <https://pytorch.org/docs/stable/nn.html#torch.nn.BatchNorm2d>`__)
和 `LeakyReLU <https://pytorch.org/docs/stable/nn.html#torch.nn.LeakyReLU>`__ 激活函数构成。
输入是3x64x64图像，输出是 输入来自真实数据分布的 标量概率。
生成器由卷积转置层(`convolutional-transpose <https://pytorch.org/docs/stable/nn.html#torch.nn.ConvTranspose2d>`__)、
批归一化层和 `ReLU <https://pytorch.org/docs/stable/nn.html#relu>`__ 激活层组成。
输入是从标准正态分布中提取的潜在矢量(latent vector) $z$ ，输出是 3x64x64 的RGB图像。
跨步卷积转置层(strided conv-transpose layers)允许将潜在矢量(latent vector)变换为具有与图像相同的shape。
作者还就如何设置优化器、如何计算损失函数以及如何初始化模型的权重等方面给出了一些提示，这些都将在后面的章节中加以说明。

```python
from __future__ import print_function
#%matplotlib inline
import argparse
import os
import random
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import torch.optim as optim
import torch.utils.data
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

# Set random seem for reproducibility
manualSeed = 999
#manualSeed = random.randint(1, 10000) # use if you want new results
print("Random Seed: ", manualSeed)
random.seed(manualSeed)
torch.manual_seed(manualSeed)
```

输入
------

我们先来定义一些输入:

-  **dataroot** - dataset 文件夹根目录的路径。我们将在下一节中更多地讨论数据集。
-  **workers** - 用于用 DataLoader 加载数据的工作线程数。
-  **batch_size** - 训练中使用的批次大小。DCGAN 使用的批次大小为128。
-  **image_size** - 用于训练的图像的空间大小。此实现默认为64x64。
   如果需要另一个尺寸，则必须改变D和G的结构。有关更多细节，请参阅 `这里 <https://github.com/pytorch/examples/issues/70>`__ 。
-  **nc** - 输入图像的颜色通道数. 彩色图像是3通道的。
-  **nz** - 潜在向量(latent vector)的长度
-  **ngf** - 与通过生成器进行的特征映射的深度有关。
-  **ndf** - 设置通过鉴别器传播的特征映射的深度。
-  **num_epochs** - 要运行的训练回合(epoch)数。长期的训练可能会带来更好的效果，但也需要更长的时间。
-  **lr** - 用于训练的学习率. 就像在 DCGAN 论文中建议的, 这个参数设为 0.0002 。
-  **beta1** - Adam 优化器的beta1超参数。 就像在 DCGAN 论文中建议的, 这个参数设为 0.5 。
-  **ngpu** - 可用的 GPUs 数量。 如果没有GPU, 代码将会在 CPU 模式下运行。 如果有多个GPU,那就可以加速计算了。

```python
# Root directory for dataset
dataroot = "./data/celeba"

# Number of workers for dataloader
workers = 2

# Batch size during training
batch_size = 128

# Spatial size of training images. All images will be resized to this
#   size using a transformer.
image_size = 64

# Number of channels in the training images. For color images this is 3
nc = 3

# Size of z latent vector (i.e. size of generator input)
nz = 100

# Size of feature maps in generator
ngf = 64

# Size of feature maps in discriminator
ndf = 64

# Number of training epochs
num_epochs = 5

# Learning rate for optimizers
lr = 0.0002

# Beta1 hyperparam for Adam optimizers
beta1 = 0.5

# Number of GPUs available. Use 0 for CPU mode.
ngpu = 1
```

数据
----

在本教程中，我们将使用 `Celeb-A Faces  <http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html>`__  数据集，
该数据集可以在链接的站点上下载，也可以在GoogleDrive中下载。dataset将作为一个名为 *img_align_celeba.zip* 的文件下载。
下载完后，创建一个名为 *celeba* 的目录，并将zip文件解压缩到该目录中。
然后，将此笔记本的 *dataroot* 输入设置为您刚刚创建的renarba目录。由此产生的目录结构应该是：

::

   /path/to/celeba
       -> img_align_celeba
           -> 188242.jpg
           -> 173822.jpg
           -> 284702.jpg
           -> 537394.jpg
              ...

这是一个重要的步骤，因为我们将使用 ImageFolder 类，它需要在dataset的根文件夹中有子目录。
现在，我们可以创建 dataset ，dataloader ，设置设备运行，并最终可视化一些训练数据。

```python
# We can use an image folder dataset the way we have it setup.
# Create the dataset
dataset = dset.ImageFolder(root=dataroot,
                           transform=transforms.Compose([
                               transforms.Resize(image_size),
                               transforms.CenterCrop(image_size),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                           ]))
# Create the dataloader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
                                         shuffle=True, num_workers=workers)

# Decide which device we want to run on
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")

# Plot some training images
real_batch = next(iter(dataloader))
plt.figure(figsize=(8,8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),(1,2,0)))
```

实现
--------------

在设置了输入参数并准备好数据集之后，我们现在可以进入实现了。我们将从wigthts初始化策略开始，
然后详细讨论生成器、判别器、损失函数和训练循环。

权重初始化
~~~~~~~~~~~~~~~~~~~~~

从DCGAN的文献中，作者指出所有模型的权重都应从均值=0，stdev=0.2的正态分布中随机初始化。
权值函数以初始化模型作为输入，并重新初始化所有卷积、卷积-转置和批处理归一化层，以满足这一标准。
该函数在初始化后立即应用于模型。

```python
# custom weights initialization called on netG and netD
def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)
```

生成器(Generator)
~~~~~~~~~~~~~~~~~~~~~~

生成器 $G$ 被设计用于将潜在空间矢量($z$)映射到数据空间。由于我们的数据是图像，
将 $z$ 转换为数据空间意味着最终创建一个与训练图像(即3x64x64)相同大小的RGB图像。
在实践中，这是通过一系列strided 2d convolutional transpose layers 来实现的，
每个层与一个2d batch norm layer和一个relu activation层配对。
生成器的输出送入到一个tanh函数，将其输出值压缩在 $[-1,1]$ 的范围。
值得注意的是batch norm functions是在conv-transpose layers之后的，
因为这是DCGAN论文的一个关键贡献。这些层有助于训练期间的梯度流。
DCGAN文章中给出的生成器的结构如下所示。

.. figure:: /_static/img/dcgan_generator.png
   :alt: dcgan_generator

注意，我们在输入部分(*nz*, *ngf*, 和 *nc*) 中设置的输入如何影响代码中的生成器体系结构。
*nz* 是 z 输入向量的长度， *ngf*  与通过生成器传播的特征图的大小有关，
*nc* 是输出图像中的通道数(对于RGB图像设置为3)。下面是生成器的代码。

```python
# Generator Code

class Generator(nn.Module):
    def __init__(self, ngpu):
        super(Generator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is Z, going into a convolution
            nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # state size. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # state size. (ngf*4) x 8 x 8
            nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # state size. (ngf*2) x 16 x 16
            nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # state size. (ngf) x 32 x 32
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # state size. (nc) x 64 x 64
        )

    def forward(self, input):
        return self.main(input)
```

现在，我们可以实例化生成器并应用 ``weights_init`` 函数。
查看打印的模型，看看生成器对象是如何构造的。

```python
# 创建生成器对象
netG = Generator(ngpu).to(device)

# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
    netG = nn.DataParallel(netG, list(range(ngpu)))

# 应用 weights_init 函数 来随机初始化 所有权重到 mean=0, stdev=0.2.
netG.apply(weights_init)

# 打印输出模型
print(netG)
```

判别器(Discriminator)
~~~~~~~~~~~~~~~~~~~~~~~~~~~

如上所述，判别器 $D$ 是一种两类分类网络，它以图像为输入，输出 输入图像为真(而不是假)的标量概率。
这里，$D$ 接受一个 3x64x64 输入图像，通过一系列Conv2d、BatchNorm2d和LeakyReLU层处理它，
并通过 sigmoid 激活函数输出最终的概率。如果有必要的话，可以用更多的层来扩展这个体系结构，
但是使用strided convolution、BatchNorm和LeakyReLU是很有意义的。DCGAN的论文提到，
使用strided convolution而不是pooling来降采样是一种很好的做法，
因为它让网络学习自己的池化函数。此外，batch norm 和leaky relu函数促进了健康的梯度流，
这对于 $G$ 和 $D$ 的学习过程都是至关重要的。

Discriminator Code

```python
class Discriminator(nn.Module):
    def __init__(self, ngpu):
        super(Discriminator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input)
```

现在，和生成器一样，我们可以创建判别器，应用 ``weights_init`` 函数，并打印模型的结构。

```python
# 创建 Discriminator
netD = Discriminator(ngpu).to(device)

# Handle multi-gpu if desired
if (device.type == 'cuda') and (ngpu > 1):
    netD = nn.DataParallel(netD, list(range(ngpu)))

# 应用 weights_init 函数，随机初始化所有权重到 mean=0, stdev=0.2.
netD.apply(weights_init)

# 打印输出模型
print(netD)
```

损失函数和优化器
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

当 $D$ 和 $G$ 设置好以后, 我们可以指定它们如何通过损失函数和优化器学习。
我们将使用二值交叉熵损失(Binary Cross Entropy loss (`BCELoss <https://pytorch.org/docs/stable/nn.html#torch.nn.BCELoss>`__))
函数，在 PyTorch 中是如下定义的:

\begin{align}\ell(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = - \left[ y_n \cdot \log x_n + (1 - y_n) \cdot \log (1 - x_n) \right]\end{align}

注意这个函数提供目标函数中的两个对数组件的计算 (i.e. $log(D(x))$ 和 $log(1-D(G(z)))$) 。
我们可以使用 $y$ 指定 BCE 等式的哪一部分将被计算。 这将在训练过程中完成，稍后会讲到。但是理解我们如何通过
改变 $y$ 的值(i.e. GT labels) 去选择我们想要计算的损失函数的一部分是非常重要的。

接下来，我们将真标签定义为1，假标签定义为0。这些标签将用于计算 $D$ 和 $G$ 的损失，
这也是在原始GAN文章中使用的约定。最后，我们建立了两个分开的优化器，一个用于 $D$ ，
一个用于 $G$ 。正如DCGAN论文所指出的，两者都是Adam优化器，其学习速率为0.0002，Beta1=0.5。
为了跟踪生成器的学习过程，我们将从高斯分布(即固定噪声)中生成固定批次的潜在向量(latent vectors)。
在训练循环中，我们将周期性地将这个固定的噪声输入到  $G$  中。在迭代过程中，我们将看到图像从噪声中形成。

```python
# 初始化 BCELoss 函数
criterion = nn.BCELoss()

# 创建一批 latent vectors 用于可视化生成器的进度过程
fixed_noise = torch.randn(64, nz, 1, 1, device=device)

# 为在训练过程中的真假标签建立约定
real_label = 1
fake_label = 0

# 为 G 和 D 设置 Adam optimizers
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))
```

训练
~~~~~~~~

最后，现在我们已经定义了GAN框架的所有部分，我们可以对其进行训练。请注意，
训练GANs是一种艺术，因为不正确的超参数设置会导致模式崩溃，
而对错误的原因几乎没有解释。在这里，我们将密切遵循古德费罗论文中的算法1，
同时遵循在 `ganhacks <https://github.com/soumith/ganhacks>`__ 中显示的一些最佳实践。
也就是说，我们将“为真假图像构造不同的小批量”图像，
并调整G的目标函数，使 $logD(G(z))$ 最大化。训练分为两个主要部分。
第1部分更新判别器，第2部分更新生成器。

**Part 1 - 训练判别器(Discriminator) **

回想一下，训练判别器的目标是最大化将给定的输入正确分类为真或假的概率。
我们希望“通过提升判别器的随机梯度来更新判别器”。
实际上，我们希望最大化 $log(D(x)) + log(1-D(G(z)))$ 。
由于来自于ganhacks 的separate mini-batch的建议，
我们将用两个步骤来实现上述最大化的计算过程。首先从训练集构造一批真实样本，前向通过 $D$ ，
计算损失($log(D(x))$) ，然后计算后传梯度。
其次，用当前生成器构造一批假样本，通过 $D$ 向前传递该批样本，
计算损失 ($log(1-D(G(z)))$) ，并用反向传递累积梯度。
现在，有了全真和全假批次样本中积累的梯度，我们再调用判别器的优化器进行一步优化。

**Part 2 - 训练生成器(Generator) **

正如在最初的论文中所述，我们希望通过最小化 $log(1-D(G(z)))$ 来训练生成器，以产生更好的假样本。
正如前面提到的，Goodfellow没有提供足够的梯度，特别是在学习过程的早期。作为修正，
我们希望最大化 $log(D(G(z)))$ 。在代码中，我们通过以下方法实现了这一点：
用第1部分的判别器对生成器的输出进行分类，使用真标签作为GroundTruth计算G的损失,
，随后在向后传递中计算G的梯度，最后用优化器的 ``step`` 方法更新G的参数。
使用真标签作为GT标签用于损失函数的计算似乎有违直觉，但这允许我们使用BCELoss的 $log(x)$ 部分
(而不是 $log(1-x)$ 部分)，这正是我们想要的。

最后，我们将做一些统计报告，并在每个epoch结束时，我们将把固定批次噪声推到生成器中
以可视化地跟踪G的训练进度。所报告的训练统计数字如下：

-  **Loss_D** - 判别器损失，是所有真批次和所有假批次样本上的损失之和 ($log(D(x)) + log(D(G(z)))$)。
-  **Loss_G** - 生成器损失，用 $log(D(G(z)))$ 计算。
-  **D(x)** - 所有批次的真样本上判别器的平均输出(跨batch)。这个值应该开始接近1，然后当G变得更好时，理论上收敛到0.5。想想这是为什么。
-  **D(G(z))** - 所有批次的假样本上判别器的平均输出。这个值应该开始接近0，后面随着生成器越来越好就收敛到0.5。想想这是为什么。

**Note:** 这一步可能会花点时间, 这取决于你要运行多少个epoch以及如果你从数据集移除一些数据。

```python
# Training Loop

# Lists to keep track of progress
img_list = []
G_losses = []
D_losses = []
iters = 0

print("Starting Training Loop...")
# For each epoch
for epoch in range(num_epochs):
    # For each batch in the dataloader
    for i, data in enumerate(dataloader, 0):

        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################
        ## Train with all-real batch
        netD.zero_grad()
        # Format batch
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, device=device)
        # Forward pass real batch through D
        output = netD(real_cpu).view(-1)
        # Calculate loss on all-real batch
        errD_real = criterion(output, label)
        # Calculate gradients for D in backward pass
        errD_real.backward()
        D_x = output.mean().item()

        ## Train with all-fake batch
        # Generate batch of latent vectors
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # Generate fake image batch with G
        fake = netG(noise)
        label.fill_(fake_label)
        # Classify all fake batch with D
        output = netD(fake.detach()).view(-1)
        # Calculate D's loss on the all-fake batch
        errD_fake = criterion(output, label)
        # Calculate the gradients for this batch
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        # Add the gradients from the all-real and all-fake batches
        errD = errD_real + errD_fake
        # Update D
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################
        netG.zero_grad()
        label.fill_(real_label)  # fake labels are real for generator cost
        # Since we just updated D, perform another forward pass of all-fake batch through D
        output = netD(fake).view(-1)
        # Calculate G's loss based on this output
        errG = criterion(output, label)
        # Calculate gradients for G
        errG.backward()
        D_G_z2 = output.mean().item()
        # Update G
        optimizerG.step()

        # Output training stats
        if i % 50 == 0:
            print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))

        # Save Losses for plotting later
        G_losses.append(errG.item())
        D_losses.append(errD.item())

        # Check how the generator is doing by saving G's output on fixed_noise
        if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
            with torch.no_grad():
                fake = netG(fixed_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))

        iters += 1
```

结果
-------

最后，让我们来看看我们是如何做到的。在这里，我们将看到三个不同的结果。
首先，我们将看到D和G在训练中的损失是如何变化的。第二，我们将在每个epoch的固定噪声批次上可视化G的输出。
第三，我们将看到一批真数据，旁边是一批来自G的假数据。

**Loss versus training iteration**

下面是迭代过程中 D 与 G 的损失对比图。

```python
plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()
```

**G的进度的可视化**

记住，在每个训练回合(epoch)之后，我们是如何将generator的输出保存在固定噪声批次上的。
现在，我们可以用动画来可视化G的训练进度。按“播放”按钮启动动画。

```python
#%%capture
fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)

HTML(ani.to_jshtml())
```

**真图像(Real Images) vs. 假图像(Fake Images)**

最后, 让我们看看真图像和假图像吧！

```python
# 从 dataloader 中抓取一个批次的真图像
real_batch = next(iter(dataloader))

# Plot the real images
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))

# 绘制最后一个epoch的假图像
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()
```

下一步去哪里
----------------

我们的旅程已经到了尽头，但是有几个地方你可以从这里去。你可以：

-  训练更长的时间看看得到的结果有多好
-  修改此模型让其接收不同的数据集 和 可能改变的图像大小与模型架构
-  检查其他一些很酷的 GAN 项目 `这里 <https://github.com/nashory/gans-awesome-applications>`__ 。
-  创建一个 GANs 让它产生 `音乐 <https://deepmind.com/blog/wavenet-generative-model-raw-audio/>`__


```python
try:
    import openmdao.api as om
    import dymos as dm
except ImportError:
    !python -m pip install openmdao[notebooks]
    !python -m pip install dymos[docs]
    import openmdao.api as om
    import dymos as dm
```

# Two-Burn Orbit Raise

This example demonstrates the use of a Trajectory to encapsulate a
three-phase orbit raising maneuver with a burn-coast-burn phase
sequence. This example is based on the problem provided in
Enright {cite}`enright1991optimal`.

The dynamics are given by

\begin{align}
  \frac{dr}{dt} &= v_r \\
  \frac{d\theta}{dt} &= \frac{v_\theta}{r} \\
  \frac{dv_r}{dt} &= \frac{v^2_\theta}{r} - \frac{1}{r^2} + a_{thrust} \sin u_1 \\
  \frac{dv_\theta}{dt} &= - \frac{v_r v_\theta}{r} + a_{thrust} \cos u_1 \\
  \frac{da_{thrust}}{dt} &= \frac{a^2_{thrust}}{c} \\
  \frac{d \Delta v}{dt} &= a_{thrust}
\end{align}

The initial conditions are

\begin{align}
  r &= 1 \rm{\,DU} \\
  \theta &= 0 \rm{\,rad} \\
  v_r &= 0 \rm{\,DU/TU}\\
  v_\theta &= 1 \rm{\,DU/TU}\\
  a_{thrust} &= 0.1 \rm{\,DU/TU^2}\\
  \Delta v &= 0 \rm{\,DU/TU}
\end{align}

and the final conditions are

\begin{align}
  r &= 3 \rm{\,DU} \\
  \theta &= \rm{free} \\
  v_r &= 0 \rm{\,DU/TU}\\
  v_\theta &= \sqrt{\frac{1}{3}} \rm{\,DU/TU}\\
  a_{thrust} &= \rm{free}\\
  \Delta v &= \rm{free}
\end{align}

## Building and running the problem

The following code instantiates our problem, our trajectory, three
phases, and links them accordingly. The spacecraft initial position,
velocity, and acceleration magnitude are fixed. The objective is to
minimize the delta-V needed to raise the spacecraft into a circular
orbit at 3 Earth radii.

Note the call to _link\_phases_ which provides time,
position, velocity, and delta-V continuity across all phases, and
acceleration continuity between the first and second burn phases.
Acceleration is 0 during the coast phase. Alternatively, we could have
specified a different ODE for the coast phase, as in the example.

This example runs inconsistently with SLSQP but is solved handily by
SNOPT.

```python
om.display_source("dymos.examples.finite_burn_orbit_raise.finite_burn_eom")
```

```python
import numpy as np
import matplotlib.pyplot as plt
import openmdao.api as om

import dymos as dm
from dymos.examples.finite_burn_orbit_raise.finite_burn_eom import FiniteBurnODE

p = om.Problem(model=om.Group())

p.driver = om.pyOptSparseDriver()
p.driver.options['optimizer'] = 'IPOPT'
p.driver.declare_coloring()

traj = dm.Trajectory()

traj.add_parameter('c', opt=False, val=1.5, units='DU/TU',
                   targets={'burn1': ['c'], 'coast': ['c'], 'burn2': ['c']})

# First Phase (burn)

burn1 = dm.Phase(ode_class=FiniteBurnODE,
                 transcription=dm.GaussLobatto(num_segments=5, order=3, compressed=False))

burn1 = traj.add_phase('burn1', burn1)

burn1.set_time_options(fix_initial=True, duration_bounds=(.5, 10), units='TU')
burn1.add_state('r', fix_initial=True, fix_final=False, defect_scaler=100.0,
                rate_source='r_dot', units='DU')
burn1.add_state('theta', fix_initial=True, fix_final=False, defect_scaler=100.0,
                rate_source='theta_dot', units='rad')
burn1.add_state('vr', fix_initial=True, fix_final=False, defect_scaler=100.0,
                rate_source='vr_dot', units='DU/TU')
burn1.add_state('vt', fix_initial=True, fix_final=False, defect_scaler=100.0,
                rate_source='vt_dot', units='DU/TU')
burn1.add_state('accel', fix_initial=True, fix_final=False,
                rate_source='at_dot', units='DU/TU**2')
burn1.add_state('deltav', fix_initial=True, fix_final=False,
                rate_source='deltav_dot', units='DU/TU')
burn1.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg',
                  scaler=0.01, rate_continuity_scaler=0.001, rate2_continuity_scaler=0.001,
                  lower=-30, upper=30)
# Second Phase (Coast)
coast = dm.Phase(ode_class=FiniteBurnODE,
                 transcription=dm.GaussLobatto(num_segments=5, order=3, compressed=False))

coast.set_time_options(initial_bounds=(0.5, 20), duration_bounds=(.5, 50), duration_ref=50,
                       units='TU')
coast.add_state('r', fix_initial=False, fix_final=False, defect_scaler=100.0,
                rate_source='r_dot', targets=['r'], units='DU')
coast.add_state('theta', fix_initial=False, fix_final=False, defect_scaler=100.0,
                rate_source='theta_dot', targets=['theta'], units='rad')
coast.add_state('vr', fix_initial=False, fix_final=False, defect_scaler=100.0,
                rate_source='vr_dot', targets=['vr'], units='DU/TU')
coast.add_state('vt', fix_initial=False, fix_final=False, defect_scaler=100.0,
                rate_source='vt_dot', targets=['vt'], units='DU/TU')
coast.add_state('accel', fix_initial=True, fix_final=True,
                rate_source='at_dot', targets=['accel'], units='DU/TU**2')
coast.add_state('deltav', fix_initial=False, fix_final=False,
                rate_source='deltav_dot', units='DU/TU')

coast.add_parameter('u1', opt=False, val=0.0, units='deg', targets=['u1'])

# Third Phase (burn)
burn2 = dm.Phase(ode_class=FiniteBurnODE,
                 transcription=dm.GaussLobatto(num_segments=5, order=3, compressed=False))

traj.add_phase('coast', coast)
traj.add_phase('burn2', burn2)

burn2.set_time_options(initial_bounds=(0.5, 50), duration_bounds=(.5, 10), initial_ref=10,
                       units='TU')
burn2.add_state('r', fix_initial=False, fix_final=True, defect_scaler=100.0,
                rate_source='r_dot', units='DU')
burn2.add_state('theta', fix_initial=False, fix_final=False, defect_scaler=100.0,
                rate_source='theta_dot', units='rad')
burn2.add_state('vr', fix_initial=False, fix_final=True, defect_scaler=1000.0,
                rate_source='vr_dot', units='DU/TU')
burn2.add_state('vt', fix_initial=False, fix_final=True, defect_scaler=1000.0,
                rate_source='vt_dot', units='DU/TU')
burn2.add_state('accel', fix_initial=False, fix_final=False, defect_scaler=1.0,
                rate_source='at_dot', units='DU/TU**2')
burn2.add_state('deltav', fix_initial=False, fix_final=False, defect_scaler=1.0,
                rate_source='deltav_dot', units='DU/TU')

burn2.add_objective('deltav', loc='final', scaler=100.0)

burn2.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg',
                  scaler=0.01, lower=-90, upper=90)

burn1.add_timeseries_output('pos_x')
coast.add_timeseries_output('pos_x')
burn2.add_timeseries_output('pos_x')

burn1.add_timeseries_output('pos_y')
coast.add_timeseries_output('pos_y')
burn2.add_timeseries_output('pos_y')

# Link Phases
traj.link_phases(phases=['burn1', 'coast', 'burn2'],
                 vars=['time', 'r', 'theta', 'vr', 'vt', 'deltav'])

traj.link_phases(phases=['burn1', 'burn2'], vars=['accel'])

p.model.add_subsystem('traj', subsys=traj)

# Finish Problem Setup

# Needed to move the direct solver down into the phases for use with MPI.
#  - After moving down, used fewer iterations (about 30 less)

p.driver.add_recorder(om.SqliteRecorder('two_burn_orbit_raise_example.db'))

p.setup(check=True, mode='fwd')

# Set Initial Guesses
p.set_val('traj.parameters:c', value=1.5, units='DU/TU')

burn1 = p.model.traj.phases.burn1
burn2 = p.model.traj.phases.burn2
coast = p.model.traj.phases.coast

p.set_val('traj.burn1.t_initial', value=0.0)
p.set_val('traj.burn1.t_duration', value=2.25)
p.set_val('traj.burn1.states:r', value=burn1.interp('r', [1, 1.5]))
p.set_val('traj.burn1.states:theta', value=burn1.interp('theta', [0, 1.7]))
p.set_val('traj.burn1.states:vr', value=burn1.interp('vr', [0, 0]))
p.set_val('traj.burn1.states:vt', value=burn1.interp('vt', [1, 1]))
p.set_val('traj.burn1.states:accel', value=burn1.interp('accel', [0.1, 0]))
p.set_val('traj.burn1.states:deltav', value=burn1.interp('deltav', [0, 0.1]))
p.set_val('traj.burn1.controls:u1', value=burn1.interp('u1', [-3.5, 13.0]))

p.set_val('traj.coast.t_initial', value=2.25)
p.set_val('traj.coast.t_duration', value=3.0)

p.set_val('traj.coast.states:r', value=coast.interp('r', [1.3, 1.5]))
p.set_val('traj.coast.states:theta', value=coast.interp('theta', [2.1767, 1.7]))
p.set_val('traj.coast.states:vr', value=coast.interp('vr', [0.3285, 0]))
p.set_val('traj.coast.states:vt', value=coast.interp('vt', [0.97, 1]))
p.set_val('traj.coast.states:accel', value=coast.interp('accel', [0, 0]))

p.set_val('traj.burn2.t_initial', value=5.25)
p.set_val('traj.burn2.t_duration', value=1.75)

p.set_val('traj.burn2.states:r', value=burn2.interp('r', [1, 3.]))
p.set_val('traj.burn2.states:theta', value=burn2.interp('theta', [0, 4.0]))
p.set_val('traj.burn2.states:vr', value=burn2.interp('vr', [0, 0]))
p.set_val('traj.burn2.states:vt', value=burn2.interp('vt', [1, np.sqrt(1 / 3.)]))
p.set_val('traj.burn2.states:deltav', value=burn2.interp('deltav', [0.1, 0.2]))
p.set_val('traj.burn2.states:accel', value=burn2.interp('accel', [0.1, 0]))

p.set_val('traj.burn2.controls:u1', value=burn2.interp('u1', [0, 0]))

dm.run_problem(p)

#
# Plot results
#
traj = p.model.traj
exp_out = traj.simulate()

fig = plt.figure(figsize=(8, 4))
fig.suptitle('Two Burn Orbit Raise Solution')
ax_u1 = plt.subplot2grid((2, 2), (0, 0))
ax_deltav = plt.subplot2grid((2, 2), (1, 0))
ax_xy = plt.subplot2grid((2, 2), (0, 1), rowspan=2)

span = np.linspace(0, 2 * np.pi, 100)
ax_xy.plot(np.cos(span), np.sin(span), 'k--', lw=1)
ax_xy.plot(3 * np.cos(span), 3 * np.sin(span), 'k--', lw=1)
ax_xy.set_xlim(-4.5, 4.5)
ax_xy.set_ylim(-4.5, 4.5)

ax_xy.set_xlabel('x ($R_e$)')
ax_xy.set_ylabel('y ($R_e$)')

ax_u1.set_xlabel('time ($TU$)')
ax_u1.set_ylabel('$u_1$ ($deg$)')
ax_u1.grid(True)

ax_deltav.set_xlabel('time ($TU$)')
ax_deltav.set_ylabel('${\Delta}v$ ($DU/TU$)')
ax_deltav.grid(True)

t_sol = dict((phs, p.get_val('traj.{0}.timeseries.time'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
x_sol = dict((phs, p.get_val('traj.{0}.timeseries.pos_x'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
y_sol = dict((phs, p.get_val('traj.{0}.timeseries.pos_y'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
dv_sol = dict((phs, p.get_val('traj.{0}.timeseries.states:deltav'.format(phs)))
              for phs in ['burn1', 'coast', 'burn2'])
u1_sol = dict((phs, p.get_val('traj.{0}.timeseries.controls:u1'.format(phs), units='deg'))
              for phs in ['burn1', 'burn2'])

t_exp = dict((phs, exp_out.get_val('traj.{0}.timeseries.time'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
x_exp = dict((phs, exp_out.get_val('traj.{0}.timeseries.pos_x'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
y_exp = dict((phs, exp_out.get_val('traj.{0}.timeseries.pos_y'.format(phs)))
             for phs in ['burn1', 'coast', 'burn2'])
dv_exp = dict((phs, exp_out.get_val('traj.{0}.timeseries.states:deltav'.format(phs)))
              for phs in ['burn1', 'coast', 'burn2'])
u1_exp = dict((phs, exp_out.get_val('traj.{0}.timeseries.controls:u1'.format(phs),
                                    units='deg'))
              for phs in ['burn1', 'burn2'])

for phs in ['burn1', 'coast', 'burn2']:
    try:
        ax_u1.plot(t_exp[phs], u1_exp[phs], '-', marker=None, color='C0')
        ax_u1.plot(t_sol[phs], u1_sol[phs], 'o', mfc='C1', mec='C1', ms=3)
    except KeyError:
        pass

    ax_deltav.plot(t_exp[phs], dv_exp[phs], '-', marker=None, color='C0')
    ax_deltav.plot(t_sol[phs], dv_sol[phs], 'o', mfc='C1', mec='C1', ms=3)

    ax_xy.plot(x_exp[phs], y_exp[phs], '-', marker=None, color='C0', label='explicit')
    ax_xy.plot(x_sol[phs], y_sol[phs], 'o', mfc='C1', mec='C1', ms=3, label='implicit')

plt.show()
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('traj.burn2.states:deltav')[-1], 0.3995,
                  tolerance=2.0E-3)
```

## References

```{bibliography}
:filter: docname in docnames
```