[The 1st IAA-CSIC Severo Ochoa School on Statistics, Data Mining and Machine Learning](https://www.granadacongresos.com/sostat)

[Zeljko Ivezic, University of Washington](http://faculty.washington.edu/ivezic/)

[This notebook](https://github.com/carmensg/IAA_School2019/tree/master/lectures/Day3-ZeljkoIvezic/notebooks/classification.ipynb)
### November 6, 2019

# Lecture 3: Classification

### o Introduction to classification
### o Classification loss (risk)
### o Receiver Operating Characteristic curves
### o Generative Classification and examples
### o Discriminative Classification and examples
### o Decision Trees

```python
# first things first...
import numpy as np
from matplotlib import pyplot as plt
from scipy import stats
from scipy import integrate
from scipy.stats import norm
from scipy.stats import cauchy
# astroML tools
from astroML.plotting import hist
from astroML.plotting.mcmc import convert_to_stdev
from astroML.plotting import setup_text_plots
from astroML.plotting.mcmc import convert_to_stdev
setup_text_plots(fontsize=8, usetex=True)
import warnings; warnings.simplefilter('ignore')
```

## Classification and density estimation

In density estimation we estimate joint probability distributions from multivariate data sets to identify the inherent clustering. This is essentially  unsupervised classification

If we have labels for some of these data points (e.g., an object is tall, short, red, or blue) we can develop a relationship between the label and the properties of a source. This is  supervised classification

Here “supervised” means that there is prior information about the number and properties of clusters: for a training sample, we know the so-called “class labels” (for each data point in the training sample, we know to which cluster it belongs; characterizing these known clusters is typically easier than finding and characterizing unknown clusters)

Classification, regression, and density estimation are all related. For example, the regression function
$ŷ =f(y|x⃗ )$ is the best estimated value of $y$ given a value of $x⃗$.

In classification $y$ is categorical and $f(y|x⃗)$ is the called the discriminant function

Using density estimation for classification is referred to as **generative classification** (we have a full model of the density for each class or we have a model which describes how data could be generated from each class).

Classification that finds the decision boundary that separates classes is called **discriminative classification**

Both have their place and role in astrophysical classification.

 ## Classification loss: how well are we doing

The first question we need to address is how we score (defined the success of our classification)

We can define a _loss function_. A zero-one loss function assigns a value of one for a misclassification and zero for a correct classification (i.e. we will want to minimize the loss).

If $\hat{y}$ is the best guess value of $y$, the classification loss, $L(y,\widehat{y})$, is

> $L(y,\widehat{y}) = \delta(y \neq \widehat{y})$

which means

>$\begin{eqnarray} L(y,\hat{y}) & = & \left\{ \begin{array}{cl} 1 & \mbox{if $y\neq\hat{y}$}, \\  0 & \mbox{otherwise.}       	           \end{array} \right. \end{eqnarray}$

The expectation (mean) value of the loss $\mathbb{E} \left[ L(y,\hat{y}) \right] = p(y\neq \hat{y})$  is called the classification risk

This is related to regression loss functions: $L(y, \hat{y}) = (y - \hat{y})^2$ and risk $\mathbb{E}[(y - \hat{y})^2]$.

We can then define:

> $ {\rm completeness} = \frac{\rm true\ positives}
  {\rm true\ positives + false\ negatives}
$

> $  {\rm contamination} = \frac{\rm false\ positives}
  {\rm true\ positives + false\ positives}
$

or

> $ {\rm true\ positive\ rate} = \frac{\rm true\ positives}
  {\rm true\ positives + false\ negatives}
$

> $  {\rm false\ positive\ rate} = \frac{\rm false\ positives}
  {\rm true\ negatives + false\ positives}
$

## Comparing the performance of classifiers

Best performance is a bit of a subjective topic (e.g. star-galaxy separation for correlation function studies or Galactic streams studies). We trade contamination as a function of completeness and this is science dependent.

**ROC curves: Receiver Operating Characteristic curves**

- Plot the true-positive vs the false-positive rate

- Initially used to analyze radar results in WWII (a very productive era for statistics...).

- One concern about ROC curves is that they are sensitive to the relative sample sizes (if there are many more background events than source events small false positive results can dominate a signal). For these cases we we can plot efficiency (1 - contamination) vs completeness

```python
### Modeled after astroML book figure 9.17:
### https://www.astroml.org/book_figures/chapter9/fig_ROC_curve.html
from __future__ import print_function
from sklearn.naive_bayes import GaussianNB
from sklearn.discriminant_analysis import (LinearDiscriminantAnalysis,
                                           QuadraticDiscriminantAnalysis)
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_curve
from astroML.classification import GMMBayes
from astroML.utils import split_samples, completeness_contamination
from astroML.datasets import fetch_rrlyrae_combined
setup_text_plots(fontsize=16, usetex=True)

#----------------------------------------------------------------------
# get data and split into training & testing sets
X, y = fetch_rrlyrae_combined()
y = y.astype(int)
(X_train, X_test), (y_train, y_test) = split_samples(X, y, [0.75, 0.25],
                                                     random_state=0)
#------------------------------------------------------------
# Fit all the models to the training data
def compute_models(*args):
    names = []
    probs = []
    for classifier, kwargs in args:
        print(classifier.__name__)
        clf = classifier(**kwargs)
        clf.fit(X_train, y_train)
        y_probs = clf.predict_proba(X_test)[:, 1]

        names.append(classifier.__name__)
        probs.append(y_probs)

    return names, probs

names, probs = compute_models((GaussianNB, {}),
                              (LinearDiscriminantAnalysis, {}),
                              (QuadraticDiscriminantAnalysis, {}),
                              (LogisticRegression,
                               dict(class_weight='balanced')),
                              (KNeighborsClassifier,
                               dict(n_neighbors=10)),
                              (DecisionTreeClassifier,
                               dict(random_state=0, max_depth=12,
                                    criterion='entropy')),
                              (GMMBayes, dict(n_components=3, tol=1E-5,
                                              covariance_type='full')))

#------------------------------------------------------------
# Plot ROC curves and completeness/efficiency
fig = plt.figure(figsize=(14, 7))
fig.subplots_adjust(left=0.1, right=0.95, bottom=0.15, top=0.9, wspace=0.25)

# ax2 will show roc curves
ax1 = plt.subplot(121)

# ax1 will show completeness/efficiency
ax2 = plt.subplot(122)

labels = dict(GaussianNB='GNB',
              LinearDiscriminantAnalysis='LDA',
              QuadraticDiscriminantAnalysis='QDA',
              KNeighborsClassifier='KNN',
              DecisionTreeClassifier='DT',
              GMMBayes='GMMB',
              LogisticRegression='LR')

thresholds = np.linspace(0, 1, 1001)[:-1]

# iterate through and show results
for name, y_prob in zip(names, probs):
    fpr, tpr, thresh = roc_curve(y_test, y_prob)

    # add (0, 0) as first point
    fpr = np.concatenate([[0], fpr])
    tpr = np.concatenate([[0], tpr])

    ax1.plot(fpr, tpr, label=labels[name])

    comp = np.zeros_like(thresholds)
    cont = np.zeros_like(thresholds)
    for i, t in enumerate(thresholds):
        y_pred = (y_prob >= t)
        comp[i], cont[i] = completeness_contamination(y_pred, y_test)
    ax2.plot(1 - cont, comp, label=labels[name])

ax1.set_xlim(0, 0.04)
ax1.set_ylim(0, 1.02)
ax1.xaxis.set_major_locator(plt.MaxNLocator(5))
ax1.set_xlabel('false positive rate')
ax1.set_ylabel('true positive rate')
ax1.legend(loc=4)

ax2.set_xlabel('efficiency')
ax2.set_ylabel('completeness')
ax2.set_xlim(0, 1.0)
ax2.set_ylim(0.2, 1.02)

plt.show()
```

## Generative Classification

In generative classifiers we model class-conditional densities $p_k(\vec{x})$ given $p(\vec{x}|y=y_k)$

$p(y=y_k)$, or $\pi_k$ for short, is the probability of any point having class $k$ (equivalent to the prior probability of the class $k$).

Our goal is to estimate the $p_k$'s

 The discriminative function

$\hat{y} = f(y|\vec{x})$ represents the best guess of $y$ given a value of $\vec{x}$.

$f(y|\vec{x})$ is the _discriminant function_

For a simple 2-class example

> $\begin{eqnarray}
g(\vec{x}) & = &  \int y \, p(y|\vec{x}) \, dy \\
%    & = & \int y p(y|\vec{x}) \, dy \\
       & = & 1 \cdot p(y=1 | \vec{x}) + 0 \cdot p(y=0 | \vec{x}) = p(y=1 | \vec{x}).
%     & = & p(y=1 | \vec{x})
\end{eqnarray}
$

From Bayes rule

> $\begin{eqnarray} g(\vec{x})     & = & \frac{p(\vec{x}|y=1) \, p(y=1)}{p(\vec{x}|y=1) \, p(y=1)  + p(\vec{x}|y=0) \, p(y=0)} \\         & = & \frac{\pi_1 p_1(\vec{x})}{\pi_1 p_1(\vec{x}) + \pi_0 p_0(\vec{x})} \end{eqnarray}$

 Bayes Classifier

The discriminant function gives a binary predictor called a Bayes classifier

>$\begin{eqnarray} \widehat{y} & = & \left\{ \begin{array}{cl}       	           1 & \mbox{if $g(\vec{x}) > 1/2$}, \\       	           0 & \mbox{otherwise,}       	           \end{array}       	   \right. \\     & = & \left\{ \begin{array}{cl}               1 & \mbox{if $p(y=1|\vec{x}) > p(y=0|\vec{x})$}, \\               0 & \mbox{otherwise,}               \end{array}       \right. \\     & = & \left\{ \begin{array}{cl}               1 & \mbox{if $\pi_1 p_1(\vec{x}) > \pi_0 p_0(\vec{x})$}, \\               0 & \mbox{otherwise.}               \end{array}       \right.\end{eqnarray}$

 Decision Boundary

A set of $x$ values at which each class is equally likely;

>$
\pi_1 p_1(\vec{x}) = \pi_2 p_2(\vec{x});
$

> $g_1(\vec{x}) = g_2(\vec{x})$; $g_1(\vec{x}) - g_2(\vec{x}) = 0$;  $g(\vec{x}) = 1/2$; in a two-class problem

## Simplest classifier: Naive Bayes

We want $p(x_1,x_2,x_3...x_n|y)$ but if we assume that all attributes are conditionally independent this simplifies to

> $ p(x^i,x^j|y_k) = p(x^i|y)p(x^j|y_k)$

which can be written as

> $ p({x^0,x^1,x^2,\ldots,x^N}|y_k) = \prod_i p(x^i|y_k)$

From Bayes' rule and conditional independence we get

> $
  p(y_k | {x^0,x^1,\ldots,x^N}) =
  \frac{\prod_i p(x^i|y_k) p(y_k)}
       {\sum_j \prod_i p(x^i|y_j) p(y_j)}.
$

We calculate the most likely value of $y$ by maximizing over $y_k$,

## $ \widehat{y} = \arg \max_{y_k} \frac{\prod_i p(x^i|y_k) p(y_k)}
        {\sum_j \prod_i p(x^i|y_j) p(y_j)},
$

or

## $\widehat{y} = \arg \max_{y_k} \frac{\prod_i p_k(x^i) \pi_k}
        {\sum_j \prod_i p_j(x^i) \pi_j}.
$

## The rest of the class is now just estimating densities

>  $p(\vec{x}|y=y_k)$ and $\pi_k$ are learned from a set of training data.
- $\pi_k$ is just the frequency of the class $k$ in the training set
- $p(\vec{x}|y=y_k)$ is just the density (probability) of a object with class $k$ having the attributes $x$

If the training set does not cover the full parameter space $p_k(x^i)$ can be $0$ for some value of $y_k$ and $x^i$.  The posterior probability is then $p(y_k|\{x^i\}) = 0/0$ which is unfortunate! The trick is _Laplace smoothing_ : an offset $\alpha$ is added to the probability of each bin $p(\vec{x}|y=y_k)$ for all $i, k$ (equivalent to the addition of a Bayesian prior to the naive Bayes classifier).

## Gaussian Naive Bayes

In Gaussian naive Bayes $p_k(x^i)$ are modeled as one-dimensional normal distributions, with means $\mu^i_k$ and widths $\sigma^i_k$. The naive Bayes estimator is then

# $\hat{y} = \arg\max_{y_k}\left[\ln \pi_k - \frac{1}{2}\sum_{i=1}^N\left(2\pi(\sigma^i_k)^2 + \frac{(x^i - \mu^i_k)^2}{(\sigma^i_k)^2} \right) \right]$

 Note: this is the log of the Bayes criterion with no normalization constant

```python
from matplotlib import colors
from astroML.datasets import fetch_imaging_sample
from astroML.plotting.tools import draw_ellipse
from sklearn.naive_bayes import GaussianNB

def get_stars_and_galaxies(Nstars=10000, Ngals=10000):
    """Get the subset of star/galaxy data to plot"""
    data = fetch_imaging_sample()

    objtype = data['type']

    stars = data[objtype == 6][:Nstars]

    galaxies = data[objtype == 3][:Ngals]

    return np.concatenate([stars,galaxies]), np.concatenate([np.zeros(len(stars)),np.ones(len(galaxies))])

data, y = get_stars_and_galaxies(Nstars=10000, Ngals=10000)

# select r model mag and psf - model mag as columns
X = np.column_stack((data['rRaw'], data['rRawPSF'] - data['rRaw']))

#------------------------------------------------------------
# Fit the Naive Bayes classifier
clf = GaussianNB()
clf.fit(X, y)

# predict the classification probabilities on a grid
xlim = (14, 23)
ylim = (-1, 5)

xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 71),
                     np.linspace(ylim[0], ylim[1], 81))
Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])
Z = Z[:, 1].reshape(xx.shape)

#------------------------------------------------------------
# Plot the results
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
ax.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Oranges, zorder=2)

ax.contour(xx, yy, Z, [0.5], linewidths=2., colors='blue')

ax.set_xlim(xlim)
ax.set_ylim(ylim)

ax.set_xlabel('$x$')
ax.set_ylabel('$y$')

plt.show()
```

## GMM and Bayes classification

The natural extension to the Gaussian assumptions is to use GMM's to learn the density distribution.

The number of Gaussian components $K$ must be chosen for each class independently

```python
### Modeled after astroML book figure 4.2:
### https://www.astroml.org/book_figures/chapter4/fig_GMM_1D.html

from sklearn.mixture import GaussianMixture
setup_text_plots(fontsize=16, usetex=True)
#------------------------------------------------------------
# Set up the dataset.
#  We'll create our dataset by drawing samples from Gaussians.
random_state = np.random.RandomState(seed=1)
X = np.concatenate([random_state.normal(-1, 1.5, 350),
                    random_state.normal(0, 1, 500),
                    random_state.normal(3, 0.5, 150)]).reshape(-1, 1)

#------------------------------------------------------------
# Learn the best-fit GaussianMixture models
#  Here we'll use scikit-learn's GaussianMixture model. The fit() method
#  uses an Expectation-Maximization approach to find the best
#  mixture of Gaussians for the data

# fit models with 1-10 components
N = np.arange(1, 11)
models = [None for i in range(len(N))]

for i in range(len(N)):
    models[i] = GaussianMixture(N[i]).fit(X)

# compute the AIC and the BIC
AIC = [m.aic(X) for m in models]
BIC = [m.bic(X) for m in models]

#------------------------------------------------------------
# Plot the results
#  We'll use three panels:
#   1) data + best-fit mixture
#   2) AIC and BIC vs number of components
#   3) probability that a point came from each component

fig = plt.figure(figsize=(15, 5.2))
fig.subplots_adjust(left=0.12, right=0.97,
                    bottom=0.21, top=0.9, wspace=0.5)

# plot 1: data + best-fit mixture
ax = fig.add_subplot(131)
M_best = models[np.argmin(AIC)]

x = np.linspace(-6, 6, 1000)
logprob = M_best.score_samples(x.reshape(-1, 1))
responsibilities = M_best.predict_proba(x.reshape(-1, 1))
pdf = np.exp(logprob)
pdf_individual = responsibilities * pdf[:, np.newaxis]

ax.hist(X, 30, histtype='stepfilled', alpha=0.4, normed=True)
ax.plot(x, pdf, '-k')
ax.plot(x, pdf_individual, '--k')
ax.text(0.04, 0.96, "Best-fit Mixture",
        ha='left', va='top', transform=ax.transAxes)
ax.set_xlabel('$x$')
ax.set_ylabel('$p(x)$')

# plot 2: AIC and BIC
ax = fig.add_subplot(132)
ax.plot(N, AIC, '-k', label='AIC')
ax.plot(N, BIC, '--k', label='BIC')
ax.set_xlabel('n. components')
ax.set_ylabel('information criterion')
ax.legend(loc=2)

# plot 3: posterior probabilities for each component
ax = fig.add_subplot(133)

p = responsibilities
p = p[:, (1, 0, 2)]  # rearrange order so the plot looks better
p = p.cumsum(1).T

ax.fill_between(x, 0, p[0], color='gray', alpha=0.3)
ax.fill_between(x, p[0], p[1], color='gray', alpha=0.5)
ax.fill_between(x, p[1], 1, color='gray', alpha=0.7)
ax.set_xlim(-6, 6)
ax.set_ylim(0, 1)
ax.set_xlabel('$x$')
ax.set_ylabel(r'$p({\rm class}|x)$')

ax.text(-5, 0.3, 'class 1', rotation='vertical')
ax.text(0, 0.5, 'class 2', rotation='vertical')
ax.text(3, 0.3, 'class 3', rotation='vertical')

plt.show()
```

```python
### Modeled after astroML book figure 9.3:
### https://www.astroml.org/book_figures/chapter9/fig_rrlyrae_naivebayes.html
from __future__ import print_function

import numpy as np
from matplotlib import pyplot as plt

from sklearn.naive_bayes import GaussianNB
from astroML.datasets import fetch_rrlyrae_combined
from astroML.utils import split_samples
from astroML.utils import completeness_contamination
setup_text_plots(fontsize=16, usetex=True)

#----------------------------------------------------------------------
# get data and split into training & testing sets
X, y = fetch_rrlyrae_combined()
X = X[:, [1, 0, 2, 3]]  # rearrange columns for better 1-color results
(X_train, X_test), (y_train, y_test) = split_samples(X, y, [0.75, 0.25],
                                                     random_state=0)

N_tot = len(y)
N_st = np.sum(y == 0)
N_rr = N_tot - N_st
N_train = len(y_train)
N_test = len(y_test)
N_plot = 5000 + N_rr

#----------------------------------------------------------------------
# perform Naive Bayes
classifiers = []
predictions = []
Ncolors = np.arange(1, X.shape[1] + 1)

order = np.array([1, 0, 2, 3])

for nc in Ncolors:
    clf = GaussianNB()
    clf.fit(X_train[:, :nc], y_train)
    y_pred = clf.predict(X_test[:, :nc])

    classifiers.append(clf)
    predictions.append(y_pred)

completeness, contamination = completeness_contamination(predictions, y_test)

print("completeness", completeness)
print("contamination", contamination)

#------------------------------------------------------------
# Compute the decision boundary
clf = classifiers[1]
xlim = (0.7, 1.35)
ylim = (-0.15, 0.4)

xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 81),
                     np.linspace(ylim[0], ylim[1], 71))

Z = clf.predict_proba(np.c_[yy.ravel(), xx.ravel()])
Z = Z[:, 1].reshape(xx.shape)

#----------------------------------------------------------------------
# plot the results
fig = plt.figure(figsize=(15, 7.5))
fig.subplots_adjust(bottom=0.15, top=0.95, hspace=0.0,
                    left=0.1, right=0.95, wspace=0.2)

# left plot: data and decision boundary
ax = fig.add_subplot(121)
im = ax.scatter(X[-N_plot:, 1], X[-N_plot:, 0], c=y[-N_plot:],
                s=4, lw=0, cmap=plt.cm.binary, zorder=2)
im.set_clim(-0.5, 1)

im = ax.imshow(Z, origin='lower', aspect='auto',
               cmap=plt.cm.binary, zorder=1,
               extent=xlim + ylim)
im.set_clim(0, 1.5)
ax.contour(xx, yy, Z, [0.5], colors='k')

ax.set_xlim(xlim)
ax.set_ylim(ylim)

ax.set_xlabel('$u-g$')
ax.set_ylabel('$g-r$')

# Plot completeness vs Ncolors
ax = plt.subplot(222)
ax.plot(Ncolors, completeness, 'o-k', ms=6)

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.NullFormatter())

ax.set_ylabel('completeness')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

# Plot contamination vs Ncolors
ax = plt.subplot(224)
ax.plot(Ncolors, contamination, 'o-k', ms=6)

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%i'))

ax.set_xlabel('N colors')
ax.set_ylabel('contamination')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

plt.show()

```

## K-nearest neighbours

As with density estimation (and kernel density estimation)  the intuitive justification is that $p(y|x) \approx p(y|x')$ if $x'$ is very close to $x$.

The number of neighbors, $K$, regulates the complexity of the classification. In simplest form, a majority rule classification is adopted, where each of the $K$ points votes on the classification. Increasing $K$ decreases the variance in the classification but at the expense of an increase in the bias.

Weights can be assigned to  individual votes by weighting the vote by the distance to the nearest point.

```python
### Modeled after astroML book figure 9.7:
### https://www.astroml.org/book_figures/chapter9/fig_rrlyrae_knn.html
from __future__ import print_function
from sklearn.neighbors import KNeighborsClassifier
from astroML.datasets import fetch_rrlyrae_combined
from astroML.utils import split_samples
from astroML.utils import completeness_contamination
setup_text_plots(fontsize=16, usetex=True)

#----------------------------------------------------------------------
# get data and split into training & testing sets
X, y = fetch_rrlyrae_combined()
X = X[:, [1, 0, 2, 3]]  # rearrange columns for better 1-color results
(X_train, X_test), (y_train, y_test) = split_samples(X, y, [0.75, 0.25],
                                                     random_state=0)

N_tot = len(y)
N_st = np.sum(y == 0)
N_rr = N_tot - N_st
N_train = len(y_train)
N_test = len(y_test)
N_plot = 5000 + N_rr

#----------------------------------------------------------------------
# perform Classification

classifiers = []
predictions = []
Ncolors = np.arange(1, X.shape[1] + 1)
kvals = [1, 10]

for k in kvals:
    classifiers.append([])
    predictions.append([])
    for nc in Ncolors:
        clf = KNeighborsClassifier(n_neighbors=k)
        clf.fit(X_train[:, :nc], y_train)
        y_pred = clf.predict(X_test[:, :nc])

        classifiers[-1].append(clf)
        predictions[-1].append(y_pred)

completeness, contamination = completeness_contamination(predictions, y_test)

print("completeness", completeness)
print("contamination", contamination)

#------------------------------------------------------------
# Compute the decision boundary
clf = classifiers[1][1]
xlim = (0.7, 1.35)
ylim = (-0.15, 0.4)

xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 71),
                     np.linspace(ylim[0], ylim[1], 81))

Z = clf.predict(np.c_[yy.ravel(), xx.ravel()])
Z = Z.reshape(xx.shape)

#----------------------------------------------------------------------
# plot the results
fig = plt.figure(figsize=(15, 7.5))
fig.subplots_adjust(bottom=0.15, top=0.95, hspace=0.0,
                    left=0.1, right=0.95, wspace=0.2)

# left plot: data and decision boundary
ax = fig.add_subplot(121)
im = ax.scatter(X[-N_plot:, 1], X[-N_plot:, 0], c=y[-N_plot:],
                s=4, lw=0, cmap=plt.cm.binary, zorder=2)
im.set_clim(-0.5, 1)

im = ax.imshow(Z, origin='lower', aspect='auto',
               cmap=plt.cm.binary, zorder=1,
               extent=xlim + ylim)
im.set_clim(0, 2)

ax.contour(xx, yy, Z, [0.5], colors='k')

ax.set_xlim(xlim)
ax.set_ylim(ylim)

ax.set_xlabel('$u-g$')
ax.set_ylabel('$g-r$')

ax.text(0.02, 0.02, "k = %i" % kvals[1],
        transform=ax.transAxes)

# plot completeness vs Ncolors
ax = fig.add_subplot(222)

ax.plot(Ncolors, completeness[0], 'o-k', ms=6, label='k=%i' % kvals[0])
ax.plot(Ncolors, completeness[1], '^--k', ms=6, label='k=%i' % kvals[1])

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.NullFormatter())

ax.set_ylabel('completeness')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

# plot contamination vs Ncolors
ax = fig.add_subplot(224)
ax.plot(Ncolors, contamination[0], 'o-k', ms=6, label='k=%i' % kvals[0])
ax.plot(Ncolors, contamination[1], '^--k', ms=6, label='k=%i' % kvals[1])
ax.legend(loc='lower right',
          bbox_to_anchor=(1.0, 0.79))

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%i'))
ax.set_xlabel('N colors')
ax.set_ylabel('contamination')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

plt.show()
```

# Discriminative classification

Rather than a probabilistc description we directly model the decision boundary between two or more classes

For a two class example $\{0,1\}$, the discriminant function is given by $g(x) =  p(y=1 | x)$. Once we have it we can use the rule

## $ \widehat{y} = \left\{ \begin{array}{cl}       	           1 & \mbox{if $g(x) > 1/2$}, \\       	           0 & \mbox{otherwise,}       	           \end{array}       	   \right.$

to perform classification.

# Logistic regression

Assuming a binomial logistic regression and consider the linear model

## $
\begin{eqnarray}
  p(y=1|x) &=& \frac{\exp\left[\sum_j \theta_j x^j\right]}
  {1 + \exp\left[\sum_j \theta_j x^j\right]}\nonumber\\
  &=& p({\theta}),
\end{eqnarray}
$

where we define

## $
\mbox{logit}(p_i) = \log\left(\frac{p_i}{1-p_i}\right)
= \sum_j \theta_j x_i^j.
$

The name logistic regression comes from the fact that the function $e^x/(1+e^x)$ is called the logistic function.
- useful for categorical regression as values cannot go above or below 1 and 0.

Because $y$ is binary, it can be modeled as a Bernoulli distribution with (conditional) likelihood function

## $
L(\beta) = \prod_{i=1}^N p_i(\beta)^{y_i} (1-p_i(\beta))^{1-y_i}.
$

logistic regression is related to linear discriminant analysis (LDA). In LDA

## \begin{eqnarray}
\log\left( \frac{p(y=1|x)}{p(y=0|x)} \right) & = &
  - \frac{1}{2}(\mu_0+\mu_1)^T \Sigma^{-1} (\mu_1-\mu_0) \\ \nonumber
  & + & \log\left( \frac{\pi_0}{\pi_1} \right) + x^T \Sigma^{-1} (\mu_1-\mu_0) \\ \nonumber
  & = & \alpha_0 + \alpha^T x.
\end{eqnarray}

In logistic regression the model is by assumption

## $
\log\left( \frac{p(y=1|x)}{p(y=0|x)} \right) = \beta_0 + \beta^T x.
$

Logistic regression minimizes classification error rather than density estimation error.

## Support Vector Machines

Find the hyperplane that maximizes the distance of the closest point from either class. This distance is the margin (width of the line before it hits a point). We want the line that maximizes the margin (m).

The points on the margin are called _support vectors_

If we assume $y \in \{-1,1\}$, (+1 is maximum margin, -1 is minimum, 0 is the decision boundary)

The maximum is then just when $\beta_0 + \beta^T x_i = 1$ etc

The hyperplane which maximizes the margin is given by finding

> \begin{equation}
\max_{\beta_0,\beta}(m) \;\;\;
  \mbox{subject to} \;\;\; \frac{1}{||\beta||} y_i ( \beta_0 + \beta^T x_i )
  \geq m \,\,\, \forall \, i.
\end{equation}

The constraints can be written as $y_i ( \beta_0 + \beta^T x_i ) \geq m ||\beta|| $.

Thus the optimization problem is equivalent to minimizing
> \begin{equation}
\frac{1}{2} ||\beta|| \;\;\; \mbox{subject to} \;\;\; y_i
  ( \beta_0 + \beta^T x_i ) \geq 1 \,\,\, \forall \, i.
\end{equation}

This optimization  is a _quadratic programming_ problem (quadratic objective function with linear constraints)

Note that because SVM uses a metric which maximizes the margin rather than a measure over all points in the data sets, it is similar in spirit to the rank-based estimators

- The median of a distribution is unaffected by even large perturbations of outlying points, as long as those perturbations do not cross the boundary.
- In the same way, once the support vectors are determined, changes to the positions or numbers of points beyond the margin will not change the decision boundary.  For this reason, SVM can be a very powerful tool for discriminative classification.

- This is why there is a high completeness compared to the other methods: it does not matter that the background sources outnumber the RR Lyrae stars by a factor of $\sim$200 to 1. It simply determines the best boundary between the small RR Lyrae clump and the large background clump.
- This completeness, however, comes at the cost of a relatively large contamination level.

- SVM is not scale invariant so it often worth rescaling the data to [0,1] or to whiten it to have a mean of 0 and variance 1 (remember to do this to the test data as well!)
- The data dont need to be separable (we can put a constraint in minimizing the number of "failures")

```python
### Modeled after astroML book figure 9.10:
### https://www.astroml.org/book_figures/chapter9/fig_rrlyrae_svm.html
from __future__ import print_function
from sklearn.svm import SVC
from astroML.utils.decorators import pickle_results
from astroML.datasets import fetch_rrlyrae_combined
from astroML.utils import split_samples
from astroML.utils import completeness_contamination
setup_text_plots(fontsize=16, usetex=True)

#----------------------------------------------------------------------
# get data and split into training & testing sets
X, y = fetch_rrlyrae_combined()
X = X[:, [1, 0, 2, 3]]  # rearrange columns for better 1-color results

# SVM takes several minutes to run, and is order[N^2]
#  truncating the dataset can be useful for experimentation.
#X = X[::5]
#y = y[::5]

(X_train, X_test), (y_train, y_test) = split_samples(X, y, [0.75, 0.25],
                                                     random_state=0)

N_tot = len(y)
N_st = np.sum(y == 0)
N_rr = N_tot - N_st
N_train = len(y_train)
N_test = len(y_test)
N_plot = 5000 + N_rr

#----------------------------------------------------------------------
# Fit SVM
Ncolors = np.arange(1, X.shape[1] + 1)

@pickle_results('SVM_rrlyrae.pkl')
def compute_SVM(Ncolors):
    classifiers = []
    predictions = []

    for nc in Ncolors:
        # perform support vector classification
        clf = SVC(kernel='linear', class_weight='balanced')
        clf.fit(X_train[:, :nc], y_train)
        y_pred = clf.predict(X_test[:, :nc])

        classifiers.append(clf)
        predictions.append(y_pred)

    return classifiers, predictions

classifiers, predictions = compute_SVM(Ncolors)

completeness, contamination = completeness_contamination(predictions, y_test)

print("completeness", completeness)
print("contamination", contamination)

#------------------------------------------------------------
# compute the decision boundary
clf = classifiers[1]
w = clf.coef_[0]
a = -w[0] / w[1]
yy = np.linspace(-0.1, 0.4)
xx = a * yy - clf.intercept_[0] / w[1]

#----------------------------------------------------------------------
# plot the results
fig = plt.figure(figsize=(15, 7.5))
fig.subplots_adjust(bottom=0.15, top=0.95, hspace=0.0,
                    left=0.1, right=0.95, wspace=0.2)

# left plot: data and decision boundary
ax = fig.add_subplot(121)
ax.plot(xx, yy, '-k')
im = ax.scatter(X[-N_plot:, 1], X[-N_plot:, 0], c=y[-N_plot:],
                s=4, lw=0, cmap=plt.cm.binary, zorder=2)
im.set_clim(-0.5, 1)

ax.set_xlim(0.7, 1.35)
ax.set_ylim(-0.15, 0.4)

ax.set_xlabel('$u-g$')
ax.set_ylabel('$g-r$')

# plot completeness vs Ncolors
ax = fig.add_subplot(222)
ax.plot(Ncolors, completeness, 'o-k', ms=6)

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.NullFormatter())

ax.set_ylabel('completeness')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

# plot contamination vs Ncolors
ax = fig.add_subplot(224)
ax.plot(Ncolors, contamination, 'o-k', ms=6)

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%i'))

ax.set_xlabel('N colors')
ax.set_ylabel('contamination')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

plt.show()
```

```python
### Modeled after astroML book figure 10.23:
### https://www.astroml.org/book_figures/chapter10/fig_LINEAR_SVM.html
from __future__ import print_function

import numpy as np
from matplotlib import pyplot as plt
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from astroML.datasets import fetch_LINEAR_geneva

#----------------------------------------------------------------------
# This function adjusts matplotlib settings for a uniform feel in the textbook.
# Note that with usetex=True, fonts are rendered with LaTeX.  This may
# result in an error if LaTeX is not installed on your system.  In that case,
# you can set usetex to False.
if "setup_text_plots" not in globals():
    from astroML.plotting import setup_text_plots
setup_text_plots(fontsize=8, usetex=True)

data = fetch_LINEAR_geneva()

attributes = [('gi', 'logP'),
              ('gi', 'logP', 'ug', 'iK', 'JK', 'amp', 'skew')]
labels = ['$u-g$', '$g-i$', '$i-K$', '$J-K$',
          r'$\log(P)$', 'amplitude', 'skew']
cls = 'LCtype'
Ntrain = 3000

#------------------------------------------------------------
# Create attribute arrays
X = []
y = []

for attr in attributes:
    X.append(np.vstack([data[a] for a in attr]).T)
    LCtype = data[cls].copy()

    # there is no #3.  For a better color scheme in plots,
    # we'll set 6->3
    LCtype[LCtype == 6] = 3
    y.append(LCtype)

#@pickle_results('LINEAR_SVM.pkl')
def compute_SVM_results(i_train, i_test):
    classifiers = []
    predictions = []
    Xtests = []
    ytests = []
    Xtrains = []
    ytrains = []

    for i in range(len(attributes)):
        Xtrain = X[i][i_train]
        Xtest = X[i][i_test]
        ytrain = y[i][i_train]
        ytest = y[i][i_test]

        clf = SVC(kernel='linear', class_weight=None)
        clf.fit(Xtrain, ytrain)
        y_pred = clf.predict(Xtest)

        classifiers.append(clf)
        predictions.append(y_pred)

    return classifiers, predictions

i = np.arange(len(data))
i_train, i_test = train_test_split(i, random_state=0, train_size=2000)
clfs, ypred = compute_SVM_results(i_train, i_test)

#------------------------------------------------------------
# Plot the results
fig = plt.figure(figsize=(5, 5))
fig.subplots_adjust(hspace=0.1, wspace=0.1)

class_labels = []

for i in range(2):
    Xtest = X[i][i_test]
    ytest = y[i][i_test]
    amp = data['amp'][i_test]

    # Plot the resulting classifications
    ax1 = fig.add_subplot(221 + 2 * i)
    ax1.scatter(Xtest[:, 0], Xtest[:, 1],
                c=ypred[i], edgecolors='none', s=4, linewidths=0)

    ax1.set_ylabel(r'$\log(P)$')

    ax2 = plt.subplot(222 + 2 * i)
    ax2.scatter(amp, Xtest[:, 1],
                c=ypred[i], edgecolors='none', s=4, lw=0)

    #------------------------------
    # set axis limits
    ax1.set_xlim(-0.6, 2.1)
    ax2.set_xlim(0.1, 1.5)
    ax1.set_ylim(-1.5, 0.5)
    ax2.set_ylim(-1.5, 0.5)

    ax2.yaxis.set_major_formatter(plt.NullFormatter())
    if i == 0:
        ax1.xaxis.set_major_formatter(plt.NullFormatter())
        ax2.xaxis.set_major_formatter(plt.NullFormatter())
    else:
        ax1.set_xlabel(r'$g-i$')
        ax2.set_xlabel(r'$A$')

#------------------------------------------------------------
# Second figure
fig = plt.figure(figsize=(5, 5))
fig.subplots_adjust(left=0.11, right=0.95, wspace=0.3)

attrs = ['skew', 'ug', 'iK', 'JK']
labels = ['skew', '$u-g$', '$i-K$', '$J-K$']
ylims = [(-1.8, 2.2), (0.6, 2.9), (0.1, 2.6), (-0.2, 1.2)]

for i in range(4):
    ax = fig.add_subplot(221 + i)
    ax.scatter(data['gi'][i_test], data[attrs[i]][i_test],
               c=ypred[1], edgecolors='none', s=4, lw=0)
    ax.set_xlabel('$g-i$')
    ax.set_ylabel(labels[i])

    ax.set_xlim(-0.6, 2.1)
    ax.set_ylim(ylims[i])

#------------------------------------------------------------
# Save the results
#
# run the script as
#
#   >$ python fig_LINEAR_clustering.py --save
#
# to output the data file showing the cluster labels of each point
import sys
if len(sys.argv) > 1 and sys.argv[1] == '--save':
    filename = 'cluster_labels_svm.dat'

    print("Saving cluster labels to", filename)

    from astroML.datasets.LINEAR_sample import ARCHIVE_DTYPE
    new_data = np.zeros(len(data),
                        dtype=(ARCHIVE_DTYPE + [('2D_cluster_ID', 'i4'),
                                                ('7D_cluster_ID', 'i4')]))

    # switch the labels back 3->6
    for i in range(2):
        ypred[i][ypred[i] == 3] = 6

    # need to put labels back in order
    class_labels = [-999 * np.ones(len(data)) for i in range(2)]
    for i in range(2):
        class_labels[i][i_test] = ypred[i]

    for name in data.dtype.names:
        new_data[name] = data[name]
    new_data['2D_cluster_ID'] = class_labels[0]
    new_data['7D_cluster_ID'] = class_labels[1]

    fmt = ('%.6f   %.6f   %.3f   %.3f   %.3f   %.3f   %.7f   %.3f   %.3f   '
           '%.3f    %.2f     %i     %i      %s          %i              %i\n')

    F = open(filename, 'w')
    F.write('#    ra           dec       ug      gi      iK      JK     '
            'logP       Ampl    skew      kurt    magMed    nObs  LCtype  '
            'LINEARobjectID  2D_cluster_ID   7D_cluster_ID\n')
    for line in new_data:
        F.write(fmt % tuple(line[col] for col in line.dtype.names))
    F.close()

plt.show()
```

## Decision Trees

The hierarchical application of decision boundaries lead to _decision trees_

Tree structure:
- top node contains the entire data set
- at each branch the data are subdivided into two child nodes
- split is based on a predefined decision boundary (usually axis aligned)
- splitting repeats, recursively, until we reach a predefined stopping criteria

The "leaf nodes" record the fraction of points that have one classification or the other

Application of the tree to classification is simple (a series of binary decisions). The  fraction of points from the training set classified as one class or the other (in the leaf node) defines the class associated with that leaf node.

Decision trees are simple to interpret (a set of questions)

## Splitting Criteria

In order to build a decision tree we must choose the feature and
value on which we wish to split the data.

## Information content or entropy of the data

> $
    E(x) = -\sum_i p_i(x) \ln (p_i(x)),
$

where $i$ is the class and $p_i(x)$ is the probability of that class
given the training data.

Information gain is the reduction in entropy due to the partitioning of the data

For a binary split $IG(x)$ is

> $ IG(x|x_i) = E(x) - \sum_{i=0}^{1} \frac{N_i}{N}  E(x_i), $

where $N_i$ is the number of points, $x_i$, in the $i$th class,
and $E(x_i)$ is the entropy associated with that class

Search for the split is undertaken in a greedy fashion (one attribute at a time) where we sort the data on feature $i$ and maximize the information gain for a given split point, $s$,

>$
IG(x|s) = E(x) - \arg\max_s \left( \frac{N(x|x<s)}{N}  E(x|x<s) -
  \frac{N(x|x\ge s)}{N}  E(x|x\ge s)  \right ).
$

To determine the depth of the tree we use cross validation

```python
### Modeled after astroML book figure 9.13:
### https://www.astroml.org/book_figures/chapter9/fig_rrlyrae_decisiontree.html
from __future__ import print_function

from sklearn.tree import DecisionTreeClassifier
from astroML.datasets import fetch_rrlyrae_combined
from astroML.utils import split_samples
from astroML.utils import completeness_contamination
setup_text_plots(fontsize=16, usetex=True)

#----------------------------------------------------------------------
# get data and split into training & testing sets
X, y = fetch_rrlyrae_combined()
X = X[:, [1, 0, 2, 3]]  # rearrange columns for better 1-color results
(X_train, X_test), (y_train, y_test) = split_samples(X, y, [0.75, 0.25],
                                                     random_state=0)
N_tot = len(y)
N_st = np.sum(y == 0)
N_rr = N_tot - N_st
N_train = len(y_train)
N_test = len(y_test)
N_plot = 5000 + N_rr

#----------------------------------------------------------------------
# Fit Decision tree
Ncolors = np.arange(1, X.shape[1] + 1)

classifiers = []
predictions = []
Ncolors = np.arange(1, X.shape[1] + 1)
depths = [7, 12]

for depth in depths:
    classifiers.append([])
    predictions.append([])
    for nc in Ncolors:
        clf = DecisionTreeClassifier(random_state=0, max_depth=depth,
                                     criterion='entropy')
        clf.fit(X_train[:, :nc], y_train)
        y_pred = clf.predict(X_test[:, :nc])

        classifiers[-1].append(clf)
        predictions[-1].append(y_pred)

completeness, contamination = completeness_contamination(predictions, y_test)

print("completeness", completeness)
print("contamination", contamination)

#------------------------------------------------------------
# compute the decision boundary

clf = classifiers[1][1]
xlim = (0.7, 1.35)
ylim = (-0.15, 0.4)

xx, yy = np.meshgrid(np.linspace(xlim[0], xlim[1], 101),
                     np.linspace(ylim[0], ylim[1], 101))

Z = clf.predict(np.c_[yy.ravel(), xx.ravel()])
Z = Z.reshape(xx.shape)

#----------------------------------------------------------------------
# plot the results
fig = plt.figure(figsize=(15, 7.5))
fig.subplots_adjust(bottom=0.15, top=0.95, hspace=0.0,
                    left=0.1, right=0.95, wspace=0.2)

# left plot: data and decision boundary
ax = fig.add_subplot(121)
im = ax.scatter(X[-N_plot:, 1], X[-N_plot:, 0], c=y[-N_plot:],
                s=4, lw=0, cmap=plt.cm.binary, zorder=2)
im.set_clim(-0.5, 1)

ax.contour(xx, yy, Z, [0.5], colors='k')

ax.set_xlim(xlim)
ax.set_ylim(ylim)

ax.set_xlabel('$u-g$')
ax.set_ylabel('$g-r$')

ax.text(0.02, 0.02, "depth = %i" % depths[1],
        transform=ax.transAxes)

# plot completeness vs Ncolors
ax = fig.add_subplot(222)
ax.plot(Ncolors, completeness[0], 'o-k', ms=6, label="depth=%i" % depths[0])
ax.plot(Ncolors, completeness[1], '^--k', ms=6, label="depth=%i" % depths[1])

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.NullFormatter())

ax.set_ylabel('completeness')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

# plot contamination vs Ncolors
ax = fig.add_subplot(224)
ax.plot(Ncolors, contamination[0], 'o-k', ms=6, label="depth=%i" % depths[0])
ax.plot(Ncolors, contamination[1], '^--k', ms=6, label="depth=%i" % depths[1])
ax.legend(loc='lower right',
          bbox_to_anchor=(1.0, 0.79))

ax.xaxis.set_major_locator(plt.MultipleLocator(1))
ax.yaxis.set_major_locator(plt.MultipleLocator(0.2))
ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%i'))

ax.set_xlabel('N colors')
ax.set_ylabel('contamination')
ax.set_xlim(0.5, 4.5)
ax.set_ylim(-0.1, 1.1)
ax.grid(True)

plt.show()
```

## Alright, alright but what the @#%! should I use?

A convenient cop-out: no single model can be known in advance to be the best classifier!

In general the level of accuracy increases for parametric models as:
- naive Bayes,
- linear discriminant analysis (LDA),
- logistic regression,
- linear support vector machines,
- quadratic discriminant analysis (QDA),
- linear ensembles of linear models.

For non-parametric models accuracy increases as:
- decision trees
- $K$-nearest-neighbor,
- neural networks
- kernel discriminant analysis,
-  kernelized support vector machines
-  random forests
- boosting

Naive Bayes and its variants are by far the easiest to compute. Linear support vector machines are more expensive, though several fast algorithms exist. Random forests can be easily parallelized.

```python

```


# Econometrics HW_10

**11510691 程远星$\DeclareMathOperator*{\argmin}{argmin}
\DeclareMathOperator*{\argmax}{argmax}
\DeclareMathOperator*{\plim}{plim}
\newcommand{\ffrac}{\displaystyle \frac}
\newcommand{\d}[1]{\displaystyle{#1}}
\newcommand{\space}{\text{ }}
\newcommand{\bspace}{\;\;\;\;}
\newcommand{\bbspace}{\;\;\;\;\;\;\;\;}
\newcommand{\QQQ}{\boxed{?\:}}
\newcommand{\void}{\left.\right.}
\newcommand{\Tran}[1]{{#1}^{\mathrm{T}}}
\newcommand{\CB}[1]{\left\{ #1 \right\}}
\newcommand{\SB}[1]{\left[ #1 \right]}
\newcommand{\P}[1]{\left( #1 \right)}
\newcommand{\abs}[1]{\left| #1 \right|}
\newcommand{\norm}[1]{\left\| #1 \right\|}
\newcommand{\given}[1]{\left. #1 \right|}
\newcommand{\using}[1]{\stackrel{\mathrm{#1}}{=}}
\newcommand{\asim}{\overset{\text{a}}{\sim}}
\newcommand{\RR}{\mathbb{R}}
\newcommand{\EE}{\mathbb{E}}
\newcommand{\II}{\mathbb{I}}
\newcommand{\NN}{\mathbb{N}}
\newcommand{\ZZ}{\mathbb{Z}}
\newcommand{\QQ}{\mathbb{Q}}
\newcommand{\PP}{\mathbb{P}}
\newcommand{\AcA}{\mathcal{A}}
\newcommand{\FcF}{\mathcal{F}}
\newcommand{\AsA}{\mathscr{A}}
\newcommand{\FsF}{\mathscr{F}}
\newcommand{\dd}{\mathrm{d}}
\newcommand{\I}[1]{\mathrm{I}\left( #1 \right)}
\newcommand{\N}[1]{\mathcal{N}\left( #1 \right)}
\newcommand{\Exp}[1]{\mathrm{E}\left[ #1 \right]}
\newcommand{\Var}[1]{\mathrm{Var}\left[ #1 \right]}
\newcommand{\Avar}[1]{\mathrm{Avar}\left[ #1 \right]}
\newcommand{\Cov}[1]{\mathrm{Cov}\left( #1 \right)}
\newcommand{\Corr}[1]{\mathrm{Corr}\left( #1 \right)}
\newcommand{\ExpH}{\mathrm{E}}
\newcommand{\VarH}{\mathrm{Var}}
\newcommand{\AVarH}{\mathrm{Avar}}
\newcommand{\CovH}{\mathrm{Cov}}
\newcommand{\CorrH}{\mathrm{Corr}}
\newcommand{\ow}{\text{otherwise}}
\newcommand{\FSD}{\text{FSD}}
\void^\dagger$**

## Question 10.e1

$\bspace$Let the three middle estimated equations be

$$
\begin{align}
y_t &= \hat\alpha_0 + \hat\alpha_1 t + \ddot y_t \\
x_{t1} &= \hat\eta_0 + \hat\eta_1 t +\ddot x_{t1}\\
x_{t2} &= \hat\xi_0 + \hat\xi_1 t + \ddot x_{t2}
\end{align}$$

$\bspace$Plug these into the overall estimated model: $y_t = \hat\beta_0 + \hat\beta_1 x_{t1} + \hat\beta_2 x_{t2} + \hat\beta_3 t + \hat u_t$ and we obtain

$$\begin{align}
\hat\alpha_0 + \hat\alpha_1 t + \ddot y_t &= \hat\beta_0 + \hat\beta_1 \P{\hat\eta_0 + \hat\eta_1 t +\ddot x_{t1}} + \hat\beta_2 \P{\hat\xi_0 + \hat\xi_1 t + \ddot x_{t2}} + \hat\beta_3 t + \hat u_t\\
&= \P{\hat\beta_0 + \hat\beta_1\hat\eta_0 + \hat\beta_2 \hat\xi_0} + \hat\beta_1 \ddot x_{t1} + \hat\beta_2 \ddot x_{t2} + \P{\hat\beta_1\hat\eta_1+\hat\beta_2\hat\xi_1 + \hat\beta_3} t + \hat u_t
\end{align}$$

$\bspace$So now we tend to prove $\left\{\begin{align}
\hat\alpha_1 &= \hat\beta_1\hat\eta_1+\hat\beta_2\hat\xi_1 + \hat\beta_3\\
\hat\alpha_0 &= \hat\beta_0 + \hat\beta_1\hat\eta_0 + \hat\beta_2 \hat\xi_0
\end{align}\right.$

$$\begin{align}
\hat\alpha_1 &= \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}y_t}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}}\\
&= \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\P{\hat\beta_0 + \hat\beta_1 x_{t1} + \hat\beta_2 x_{t2} + \hat\beta_3 t + \hat u_t}}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}}\\
&= \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\hat\beta_0}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}} + \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\hat\beta_1 x_{t1}}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}} + \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\hat\beta_2 x_{t2}}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}} + \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\hat\beta_3 t}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}} +  \ffrac{\d{\sum_{t=1}^n \P{t - \bar t}\hat u_t}}{\d{\sum_{t=1}^n \P{t - \bar t}^2}}\\
&= \hat\beta_0 \cdot 0 + \hat\beta_1 \hat\eta_1 + \hat\beta_2 \hat\xi_1 +\hat\beta_3 \cdot 1 + 0 = \hat\beta_1\hat\eta_1+\hat\beta_2\hat\xi_1 + \hat\beta_3
\end{align}$$

$\bspace$For the other one, we use the average of dependent variable to derive the equation.

$$\begin{align}
\hat\alpha_0 &= \bar y - \P{\hat\beta_1 \hat\eta_1 +\hat\beta_2 \hat\xi_1 +\hat\beta_3}\bar t\\
&= \hat\beta_0 + \hat\beta_1\P{\bar x_1 - \hat\eta_1 \bar t} + \hat\beta_2\P{\bar x_2 - \xi_1 \bar t} + \bar{ \hat u}\\
&= \hat\beta_0 + \hat\beta_1\P{\bar x_1 - \P{\bar x_1 - \hat\eta_0}} + \hat\beta_2\P{\bar x_2 -\P{\bar x_2-\xi_0}} + 0 = \hat\beta_0 + \hat\beta_1\hat\eta_0 + \hat\beta_2 \hat\xi_0
\end{align}$$

## Question 10.2

$\bspace$$\text{TS}.2$ is violated, the reason accounting for this is the following. From the model for $\text{gGDP}_t$ we have

$$\begin{align}
\text{int}_t &= \gamma_0 + \gamma_1 \P{\text{gGDP}_{t-1} - 3} + v_t\\
&= \gamma_0 + \gamma_1 \P{\alpha_0 + \delta_0 \text{int}_{t-1} + \delta_1 \text{int}_{t-2}+ u_{t-1} - 3} + v_t\\
&= \P{\gamma_0 + \gamma_1 \alpha_0 - 3\gamma_1}+ \gamma_1\delta_0 \text{int}_{t-1} + \gamma_1\delta_1 \text{int}_{t-2} + \gamma_1 u_{t-1} + v_t
\end{align}$$

$\bspace$Given that $u_t$ is uncorrelated with $\text{int}_t$ and $\text{int}_{t-1}$, we claim that $u_{t-1}$ is uncorrelated with $\text{int}_{t-1}$ and $\text{int}_{t-2}$
$$\begin{align}
\Cov{\text{int}_t,u_{t-1}} &= \Cov{\P{\gamma_0 + \gamma_1 \alpha_0 - 3\gamma_1}+ \gamma_1\delta_0 \text{int}_{t-1} + \gamma_1\delta_1 \text{int}_{t-2} + \gamma_1 u_{t-1} + v_t , u_{t-1}}\\
&= \gamma_1 \Cov{u_{t-1},u_{t-1}}>0
\end{align}$$

$\bspace$So the two are correlated.

## Question 10.3

$\bspace$The conclusion is obvious. Since **LRP** is equalto $\sum_i \delta_i = \delta_0 + \delta_1 + \delta_2$, w have

$$y^* = \alpha_0 +\P{\delta_0 + \delta_1 + \delta_2}z^* = \alpha_0 + \text{LRP}\cdot z^*\Rightarrow \Delta y^* = \text{LRP}\cdot \Delta z^*$$

$\bspace$Following this we interprete **LRP** as *equilibrium propensity*.

## Question 11.6

$\P{1}$

$\bspace$We first calculate $t = \ffrac{1.104 - 1}{0.039} \approx 2.667$. From the statistic table with $df = 120$, the critical value for two-sided hypothesis is $2.62$, so we assert that we rejuct $H_0$ against $H_1:\beta_1\neq 1$ at %$7%$ significant level.

$\bspace$Pratically, however, the difference is more than $10\%$, so the estimator is different.

$\P{2}$

$\bspace$Still, we calculate $t = \ffrac{1.053 - 1}{0.039} \approx 1.359$. Under this we can't reject $H_0$. As for the lagged term, the $t$ statistic is calculated as $t = \ffrac{0.480}{0.109} = 4.404$, so we conclude that this term is significant. And judging from the coefficient of the estimated model, we'd better invest in six-month T-bills, since

$$\widehat{\text{hy6}_t} - \text{hy3}_{t-1} = -0.123 + 0.053\text{hy3}_{t-1} + 0.480\P{\text{r6}_{t-1} - \text{r3}_{t-1}}$$

$\P{3}$

$\bspace$It violates $\text{TS}.2$ and consequently $t$ test fails. There's unit root behavior.

$\P{4}$

$\bspace$We put $3$ seasonal dummy variables out of $4$ in the model. Then do $F$ test.

## Question 11.7

$\P{1}$

$$\begin{align}
y_t &= y_{t-1} + \lambda\P{\gamma_0 + \gamma_1 x_t + e_{t} - y_{t-1}} + a_t\\
&= \lambda \gamma_0 + \P{1-\lambda}y_{t-1} + \lambda \gamma_1 x_t + a_t +  \lambda e_t \\
&:= \beta_0 + \beta_1 y_{t-1} + \beta_2 x_t + u_t
\end{align}$$

$\bspace$where $\beta_0 := \lambda \gamma_0$, $\beta_1 := 1-\lambda$, $\beta_2 := \lambda \gamma_1$ and $u_t := a_t +  \lambda e_t$.

$\P{2}$

$\bspace$We could use OLS to do regression of $y_t$ on $x_t$ and $y_{t-1}$. And Given that $\Exp{e_t\mid \P{\mathbf x,\mathbf y}} = \Exp{a_t\mid \P{\mathbf x,\mathbf y}} = 0$, we java

$$\Exp{u_t\mid \P{\mathbf x,\mathbf y}} = 0$$

$\bspace$This shows that $\text{TS}.5$ is satisfied. Thus we have Gauss-Markov Assumptions satisfied so that after estimation we can find the $t$ statistic and test certain hypothesis. From the weak dependency and stationaryness we also claim the consistency of thie estimation.

$\P{3}$

$$\hat\beta_1 = 0.7\Rightarrow \hat\lambda = 1-\hat\beta_1 = 0.3 \Rightarrow \hat\gamma_1 = \ffrac{\hat\beta_2}{\hat\lambda} = 0.6667$$

***


# Reproducing Vitaly Feldman MNIST Results

The goal of this notebook is to reproduce the results in https://arxiv.org/abs/2008.11193 which are based on the MNIST model from https://github.com/pytorch/opacus/blob/master/examples/mnist.py

```python
import numpy as np
import syft as sy
from syft.core.tensor.tensor import Tensor
from syft.core.adp.entity import Entity
from syft.core.adp.adversarial_accountant import AdversarialAccountant
from collections import defaultdict

data_batch = np.random.rand(4,2)
label_batch = np.random.rand(4,10)

class DataSubjectGuardianOfTheGalaxy():
    """One of these per domain"""

    def __init__(self, enforce_unique_attrs=['name', 'email']):
        """"""
        self.enforce_unique_attrs = enforce_unique_attrs
        self.entity_id2obj = {}

        self.unique_attr2entity_id = defaultdict(dict)

        # verfiy_key-to-accountant mapping
        self.accountants = {}

        # the idea with the symbol factory is that a session with a data scientist
        # will have a unique SymbolFactory for the length of that session (this is
        # different from an accountant which a data scientist is stuck with for life).
        # A Symbol Factory is an object which produces symbols (mapped to prime numbers)
        # whenever a PhiTensor is turned into a GammaTensor. Thus, the Symbol Factory
        # can be re-initialized every time a data scientist creates a new session on a worker. However,
        # if ever the data scientist were to save some intermediate variables across sessions or,
        # to transfer a variable from one of their workers to another,
        # they would also need to bring across the symbol factory associated with those variables.
        # The main issue is that a symbol factory is what allows the privacy logic in gammatensor to
        # know when it's dealing with the same symbol twice or two different symbols.

    def register_data_subject(self, **attributes):
        """"""

        for attr in self.enforce_unique_attrs:
            if attr not in attributes.keys():
                raise Exception("The DataSubjectGuardian for your node requires you to "+\
                                "specify a unique " + str(attr) + " for every data subject. "+\
                               "Please specify email=<their email> in register_entity() method.")

        ent = Entity(**attributes)

        for attr in self.enforce_unique_attrs:
            try:
                self.unique_attr2entity_id[attr][ent.attributes[attr]] = ent.id
            except KeyError as e:
                print(e)
                raise KeyError("'" + str(attr) + "' attribute must be unique '" + str(attributes[attr]) + \
                               "' has already been claimed by another entity.")

        return ent

    @property
    def symbol_factory(self):
        return self._symbol_factory

import sympy as sp
class PrimeFactory:

    """IMPORTANT: it's very important that two tensors be able to tell that
    they are indeed referencing the EXACT same PrimeFactory. At present this is done
    by ensuring that it is literally the same python object. In the future, we will probaby
    need to formalize this. However, the main way this could go wrong is if we created some
    alternate way for checking to see if two prime factories 'sortof looked the same' but which
    in fact weren't the EXACT same object. This could lead to security leaks wherein two tensors
    think two different symbols in fact are the same symbol."""

    def __init__(self):
        self.prev_prime = 1

    def next(self):
        self.prev_prime = sp.nextprime(self.prev_prime)
        return self.prev_prime

from syft.core.adp.scalar import GammaScalar

class VirtualMachinePrivateScalarManager:

    def __init__(self):
        self.prime_factory = PrimeFactory()
        self.prime2symbol = {}

    def get_symbol(self,min_val, value, max_val, entity):
        gs = GammaScalar(min_val=min_val,
                        value=value,
                        max_val=max_val,
                        entity=entity)
        gs.prime = self.prime_factory.next()
        self.prime2symbol[gs.prime] = gs
        return gs.prime

dsg = DataSubjectGuardianOfTheGalaxy()
bob = dsg.register_data_subject(name="Bob", email="bob@openmined.org")
kritika = dsg.register_data_subject(name="Kritika", email="kritika@openmined.org")
madhava = dsg.register_data_subject(name="Madhava", email="madhava@openmined.org")
tudor = dsg.register_data_subject(name="Tudor", email="tudor@openmined.org")

entities = [bob,kritika,madhava,tudor]

# needs to come from the VirtualWorker/Session/etc.
scalar_manager = VirtualMachinePrivateScalarManager()

data = Tensor(data_batch).private(0.01,1,entities=entities, scalar_manager=scalar_manager).autograd(requires_grad=True)
data2 = Tensor(data_batch).private(0.02,2,entities=[bob,bob,bob,bob], scalar_manager=scalar_manager).autograd(requires_grad=True)
```

```python

```

```python

```

```python
y = data.sum(0)
y2 = data2.sum(0)
```

```python
acc = AdversarialAccountant(20)
```

```python
y.child.child.flat_scalars
```

    [<IntermediateGammaScalar: (0.04 < 3.278920885978709 < 4.0)>,
     <IntermediateGammaScalar: (0.04 < 2.3434082360030635 < 4.0)>]

```python
_y = y.publish(acc=acc, sigma=1.5)
# _y2 = y2.publish(acc=acc, sigma=1.5)
acc.print_ledger()
```

    <Entity:Tudor>	16.754948874946987
    <Entity:Kritika>	16.754948874946987
    <Entity:Madhava>	16.754948874946987
    <Entity:Bob>	16.754948874946987

```python

```

```python

```

```python

```

```python

```

```python
from syft.core.tensor.passthrough import HANDLED_FUNCTIONS
```

```python

data_batch = np.random.rand(4,28*28)
label_batch = np.random.rand(4,10)

bob = Entity(unique_name="Bob")

data = Tensor(data_batch).autograd(requires_grad=True)
target = Tensor(label_batch).autograd(requires_grad=True)

weights = Tensor(np.random.rand(28*28,10)).autograd(requires_grad=True)

for i in range(10):
    pred = data.dot(weights)
    diff = target - pred
    pre_loss = np.square(diff)
    loss = np.mean(pre_loss)
    extraneous_thing = -diff
    loss.backward()

    weights = weights - (weights.grad * 0.01)
    print(loss)
```

    Tensor(child=AutogradTensor(child=[39593.85306277]))
    Tensor(child=AutogradTensor(child=[12606.85187116]))
    Tensor(child=AutogradTensor(child=[4015.70540841]))
    Tensor(child=AutogradTensor(child=[1280.66337406]))
    Tensor(child=AutogradTensor(child=[409.85003185]))
    Tensor(child=AutogradTensor(child=[132.50026923]))
    Tensor(child=AutogradTensor(child=[44.08106506]))
    Tensor(child=AutogradTensor(child=[15.81375355]))
    Tensor(child=AutogradTensor(child=[6.70270769]))
    Tensor(child=AutogradTensor(child=[3.69698355]))

```python
import pymbolic as pmbl
import numpy as np

vs = list()
for i in range(2*28*28):
    x = pmbl.var("s"+str(i))
    vs.append(x)

ws = list()
for i in range(10*28*28):
    w = pmbl.var("w"+str(i))
    ws.append(x)

data = np.array(vs).reshape(2,28*28)
weights = np.array(ws).reshape(28*28,10)
```

```python

```

```python

```

```python
%%timeit
pred = data.dot(weights)
```

    58.3 ms ± 270 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

```python
from syft.lib.adp.tensor import Tensor
from syft.lib.adp.tensor import make_entities

data = Tensor(np.random.rand(2,28*28))
target = Tensor(np.random.rand(28*28,10))
entities = make_entities(n=len(data))
data = data.private(min_val=-0.42421296, max_val=2.8214867, entities=entities)
data = data.reshape(2,28*28)
weights = np.random.rand(28 * 28, 10)
```

    making entities

```python
%%timeit -n1 -r1
out = data.dot(weights)
```

    467 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
import cocos.device as cd
from cocos.symbolic import (
    LambdifiedMatrixExpression, \
    find_length_of_state_vectors
)
cd.info()
from cocos import numerics as cn
```

    Cocos running on ArrayFire v3.5.1 (OpenCL 64bit)
    [0] Apple: AMD Radeon Pro Vega 56 Compute Engine | OpenCL | compute version 1.2

```python
data = cn.random.rand(500000,28*28)
weights = cn.random.rand(28*28,10)
```

```python
%%timeit -n1 -r1
out = data.dot(weights)
```

    297 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

##### out

```python

```

```python

```

```python

```

```python
out2 = np.dot(data,weights)
```

```python
sp = out2[0][0].value.poly
```

```python
class ScalarTensor(np.ndarray):
    def __new__(
        cls,
        data
    ):
        obj = np.asarray(data).view(cls)
        return obj

    def __array_finalize__(self, obj):
        print("finalizing")
        if obj is None:
            return

    def __array_wrap__(self, out_arr, context=None):

        print(context)

        # out_arr.view calls __array_finalize__
        output = out_arr.view(self.__class__)

        return output
```

```python
import cocos.device as cd
from cocos.symbolic import (
    LambdifiedMatrixExpression, \
    find_length_of_state_vectors
)
cd.info()
```

    Cocos running on ArrayFire v3.5.1 (OpenCL 64bit)
    [0] Apple: AMD Radeon Pro Vega 56 Compute Engine | OpenCL | compute version 1.2

```python
import sympy as sym
import numpy as np
```

```python
# x1, x2, x3, t = sym.symbols('x1, x2, x3, t')
# argument_symbols = (x1, x2, x3)
# g = sym.Function('g')
# f = sym.Matrix([[x1 + x2], [(g(t) * x1 + x3) ** 2], [sym.exp(x1 + x2 + g(t))]])
# jacobian_f = f.jacobian([x1, x2, x3])

# def numeric_time_function(t: float):
#     return np.log(t)

# jacobian_f_lambdified \
#     = LambdifiedMatrixExpression(
#         argument_symbols=argument_symbols,
#         time_symbol=t,
#         symbolic_matrix_expression=jacobian_f,
#         symbolic_time_function_name_to_numeric_time_function_map={'g': numeric_time_function})

# n = 10000000
# X_gpu = cn.random.rand(n, 3)
# X_cpu = np.array(X_gpu)
```

```python

```

```python
# %%timeit
# result = X_gpu.sum()
```

```python
# %%timeit
# result2 = X_cpu.sum()
```

```python
single_poly = np.array([1,0,1])

single_poly.shape
```

    (3,)

```python
n_polys = (np.random.rand(10,3) > 0.5).astype(int)
n_polys
```

    array([[1, 0, 0],
           [0, 1, 1],
           [0, 1, 1],
           [0, 1, 1],
           [0, 1, 1],
           [0, 1, 0],
           [0, 0, 1],
           [0, 1, 1],
           [0, 0, 0],
           [1, 1, 0]])

```python
x = data.flatten()[0:5]
```

```python
import autograd.numpy as np   # Thinly-wrapped version of Numpy
from autograd import grad

def taylor_sine(x):  # Taylor approximation to sine function
    ans = currterm = x
    i = 0
    while np.abs(currterm) > 0.001:
        currterm = -currterm * x**2 / ((2 * i + 3) * (2 * i + 2))
        ans = ans + currterm
        i += 1
    return ans

grad_sine = grad(taylor_sine)
print("Gradient of sin(pi) is", grad_sine(x))
```

```python

```

```python

```

```python
# %%timeit
# jacobian_f_numeric_gpu = \
#     (jacobian_f_lambdified
#      .evaluate_with_kwargs(x1=X_gpu[:, 0],
#                            x2=X_gpu[:, 1],
#                            x3=X_gpu[:, 2],
#                            t=1.0))
```

```python
# %%timeit
# jacobian_f_numeric_cpu = \
#     (jacobian_f_lambdified
#      .evaluate_with_kwargs(x1=X_cpu[:, 0],
#                            x2=X_cpu[:, 1],
#                            x3=X_cpu[:, 2],
#                            t=1.0))
```

```python
print(f'numerical results from cpu and gpu match: '
      f'{np.allclose(jacobian_f_numeric_gpu, jacobian_f_numeric_cpu)}')
```

    numerical results from cpu and gpu match: True

```python

```

```python
import symengine
from symengine import var
```

```python
import uuid
```

```python
syms = ""
for i in range(784):
    id = str(uuid.uuid4()).replace("-","")
    syms += 's'+id+str(i)+' '
```

```python
v = var(syms)
```

```python
%%timeit -n1 -r1
result = np.sum(v)
```

    63.8 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python

```

```python

```

```python

```

```python

```

```python

```

    58 ms ± 291 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

```python

```

```python

```

```python
%%timeit -n1 -r1
out = np.sum(vs)
```

    2.12 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python

```

```python
syms = ""
for i in range(784):
    id = str(uuid.uuid4()).replace("-","")
    syms += 's'+id+str(i)+','
```

```python
from sympy import symbols
```

```python
symbols = symbols(syms[:-1])
```

```python
%%timeit -n1 -r1
out = np.sum(symbols)
```

    2.01 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
%%timeit -n1 -r1
out = np.sum([[x.value.poly for x in row] for row in data[0].tolist()])
```

    2.05 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
# %%timeit -n1 -r1
# result = data[0].sum()
```

```python
%%timeit -n1 -r1
result = data[1].sum()
```

    2.06 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
%%timeit -n1 -r1
result = data[0].sum()
```

    2.07 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
%%timeit -n1 -r1
result = data[1].sum()
```

    2.06 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python

```

```python
acc = AdversarialAccountant(max_budget=7000)
```

```python
out = result.publish(acc=acc,sigma=1.5)
acc.print_ledger()
```

```python
acc.print_ledger()
```

    <Entity:Constance_Weddle>	2.365443354618569

```python
=

x = Tensor(np.array([[1,1],[1,0],[0,1],[0,0]])).private(min_val=0, max_val=1, entities=entities, is_discrete=True)
y = Tensor(np.array([[1],[1],[0],[0]])).private(min_val=0, max_val=1, entities=entities, is_discrete=False)

_weights = Tensor(np.random.uniform(size=(2,1)))
```

```python
weights = _weights + 0
# acc = AdversarialAccountant(max_budget=7)  # causes error at end of budget

for i in range(3):
    batch_loss = 0

    pred = x.dot(weights)
    pre_loss = np.square(y-pred)
    loss = np.mean(pre_loss, axis=1)
#     loss = np.mean(pre_loss)
    ledger = loss.backward(accumulate_grads=True)
    weight_grad = (weights.grad*0.3)
    weight_grad_noise = weight_grad.publish(acc=acc, sigma=0.005)
    weights = weights - weight_grad_noise
    ledger.zero_grads()
    batch_loss += loss.value
    print(np.sum([loss.value]))

acc.print_ledger()

```

    0.17169736596498625
    0.07441205306476974
    0.04001946718047936
    <Entity:George>	602.4247883171074
    <Entity:Kritika>	602.4247883171074
    <Entity:Madhava>	602.4247883171074
    <Entity:Tudor>	602.4247883171074

```python

```

```python

```

```python
x = PhiScalar(0,0.01,1)
```

```python
out = x * x
```

```python
out.publish(acc=acc)
```

    <IntermediatePhiScalar: (0.0 < 0.000100000000000000 < 1.0)>
    found one!

    [0.703353940208477]

```python
acc.print_ledger()
```

    <Entity:George>	0.0
    <Entity:Richard_Alvarez>	0.0002737137892581691

```python
x.entity
```

```python
import sympy as sym
```

```python
a,b = sym.symbols('a,b')
```

```python
y = a + b
```

```python
from sympy.core.numbers import Number
class Float2(Number):

    def __init__(self, val):
        self.val = val

    def __add__(self, other):
        print("adding")
        return Float(self.val - other.val)
```

```python
out = y.subs({a:Float2(3)})
out
```

$\displaystyle b + 3$

```python
out.subs({b:Float2(2)})
```

$\displaystyle 5$

```python
# loss[0].value.poly
```

```python

```

```python

```

```python

```

```python

```

$\displaystyle 24ffce1fe8ab44a583abecf0b2e1ca69_{c06e7e291fdb4cb6b5ec0b9abbabf1bf} + 3 \cdot 3175e5ebc4944304bb4a1a010fa5003d_{162afbde9a87417a9a49c14568ea1c11} + 4 \cdot 5ec1e3788c314340a6ea45342375281e_{162afbde9a87417a9a49c14568ea1c11} + 6 \cdot 68ed3172c1f6417faa4f5ca24611e12d_{d59083b02ef74bdf8122e0c51454aa71} + 6 \cdot 98d310c205b743be9af907ff4646238b_{b63effdec6ba47bf93a126d0b5cc7086} + d082ac7660e84d0c806997c2dbe4f600_{b63effdec6ba47bf93a126d0b5cc7086} + 5 eeffa282abe44326ae5fc1b8a6f8c6a0_{d59083b02ef74bdf8122e0c51454aa71} + 2 fdefb3e070394d5cbf22820ac0ab7b00_{c06e7e291fdb4cb6b5ec0b9abbabf1bf}$

```python

```

```python
## first-pass methods to try to get into AutogradTensor

ops = ['T',
 '__abs__',
#  '__add__',
#  '__and__',
#  '__array__',
#  '__array_finalize__',
#  '__array_function__',
#  '__array_interface__',
#  '__array_prepare__',
#  '__array_priority__',
#  '__array_struct__',
#  '__array_ufunc__',
#  '__array_wrap__',
#  '__bool__',
#  '__class__',
#  '__complex__',
#  '__contains__',
#  '__copy__',
#  '__deepcopy__',
#  '__delattr__',
#  '__delitem__',
#  '__dir__',
 '__divmod__',
#  '__doc__',
 '__eq__',
#  '__float__',
 '__floordiv__',
#  '__format__',
 '__ge__',
#  '__getattribute__',
 '__getitem__',
 '__gt__',
#  '__hash__',
#  '__iadd__',
#  '__iand__', # trigger an error for inline
#  '__ifloordiv__',
#  '__ilshift__',
#  '__imatmul__',
#  '__imod__',
#  '__imul__',
 '__index__',
#  '__init__',
#  '__init_subclass__',
#  '__int__',
 '__invert__',
#  '__ior__',
#  '__ipow__',
#  '__irshift__',
#  '__isub__',
 '__iter__',
#  '__itruediv__',
#  '__ixor__',
 '__le__',
 '__len__',
 '__lshift__',
 '__lt__',
 '__matmul__',
#  '__mod__',
 '__mul__',
 '__ne__',
 '__neg__',
#  '__new__',
#  '__or__',
#  '__pos__',
 '__pow__',
 '__radd__',
#  '__rand__',
#  '__rdivmod__',
#  '__reduce__',
#  '__reduce_ex__',
 '__repr__',
 '__rfloordiv__',
 '__rlshift__',
 '__rmatmul__',
#  '__rmod__',
 '__rmul__',
#  '__ror__',
 '__rpow__',
 '__rrshift__',
 '__rshift__',
 '__rsub__',
 '__rtruediv__',
#  '__rxor__',
#  '__setattr__',
#  '__setitem__',
#  '__setstate__',
 '__sizeof__',
 '__str__',
 '__sub__',
#  '__subclasshook__',
 '__truediv__',
#  '__xor__',
#  'all',
#  'any',
 'argmax',
 'argmin',
#  'argpartition',
 'argsort',
#  'astype',
#  'base',
#  'byteswap',
 'choose',
 'clip',
#  'compress',
#  'conj',
#  'conjugate',
 'copy',
#  'ctypes',
 'cumprod',
 'cumsum',
#  'data',
 'diagonal',
 'dot',
#  'dtype',
#  'dump',
#  'dumps',
#  'fill',
#  'flags',
 'flat',
 'flatten',
#  'getfield',
#  'imag',
 'item',
 'itemset',
 'itemsize',
 'max',
 'mean',
 'min',
#  'nbytes',
 'ndim',
#  'newbyteorder',
#  'nonzero',
#  'partition',
 'prod',
#  'ptp',
#  'put',
#  'ravel',
#  'real',
 'repeat',
 'reshape',
 'resize',
#  'round',
#  'searchsorted',
#  'setfield',
#  'setflags',
#  'shape',
#  'size',
 'sort',
 'squeeze',
 'std',
#  'strides',
 'sum',
 'swapaxes',
 'take',
#  'tobytes',
#  'tofile',
#  'tolist',
#  'tostring',
#  'trace',
 'transpose',
#  'var',
#  'view'
      ]
```


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

## _*Relaxation and Decoherence*_

The latest version of this notebook is available on https://github.com/qiskit/qiskit-tutorial.

***
### Contributors
Martin Sandberg, Hanhee Paik, Antonio Córcoles, Doug McClure, and Jay Gambetta

## Introduction

In an ideal world, quantum systems would be well-isolated from their environment, which prevents unwanted dynamics of the quantum information we encode in them. For example, suppose we prepared a qubit in the $|1\rangle$ state, but through interaction with the environment, the state is flipped to $|0\rangle$. That flip could affect the outcome of a quantum algorithm that's being run using that qubit, meaning the answers we get out of the quantum device would change. For this reason, we seek to isolate quantum computers from the surrounding environment.

However, perfect isolation is not possible: after all, we have to be able to control the quantum computer, which means coupling it to external systems to manipulate quantum information. This tradeoff is sometimes referred to as the "Tao of quantum computing". Because our controls introduce coupling between qubits and the environment, we expect some unwanted interactions can occur.

These unwanted interactions introduce _noise_ into the qubits, which affects their behavior. The rate of these interactions sets characteristic timescales over which information encoded in qubits can be reliably stored and manipulated. (If the interaction has a rate $\Gamma$, the characteristic timescale is $\sim 1/\Gamma$.) In this tutorial, we discuss two timescales that arise from energy relaxation and decoherence -- usually referred to as $T_{1}$ and $T_{2}$, respectively -- and show how they can be measured.

**Contents**

[Measuring $T_{1}$ time](#t1)

Measuring $T_{2}^{\star}$ time

Code imports
==============

```python
import qiskit as qk
import numpy as np
from scipy.optimize import curve_fit
from qiskit.tools.qcvv.fitters import exp_fit_fun, osc_fit_fun, plot_coherence
from qiskit.tools.monitor import job_monitor
```

```python
# Load saved IBMQ accounts
qk.IBMQ.load_accounts()
```

```python
# backend and token settings
backend = qk.IBMQ.get_backend('ibmq_16_melbourne') # the device to run on
shots = 1024    # the number of shots in the experiment
```

```python
# This function is called in code below.
def pad_QId(circuit,N,qr):
    r'''A function for padding a circuit with identity gates.

    Args:
        circuit: The quantum circuit that the gates should be appended to
        N: The number of identity gates to add
        qr: The qubit register that the gates should be added to

    Returns:
        circuit: The original circuit object, but with N identity
                gates added to the qubit register qr
    '''
    for j in range(N):
        circuit.barrier(qr)
        circuit.iden(qr)
    return circuit
```

# Measuring $T_1$ time

**Theory**

The $T_{1}$ time is the characteristic timescale over which the state of a qubit damps toward the $|0\rangle$ state. Given an arbitrary initial single-qubit state $\rho(0)$, represented by a $2\times 2$ matrix as
$$\rho(0) = \begin{pmatrix}\rho_{00} & \rho_{01} \\ \rho_{01}^{\star} & \rho_{11}\end{pmatrix},$$
under amplitude damping noise, the state of the changes as
$$\rho(t) = \begin{pmatrix}\rho_{00} + (1-e^{-\Gamma_{1}t})\rho_{11} & e^{-\Gamma_{1}t/2}\rho_{01} \\ e^{-\Gamma_{1}t/2}\rho_{01}^{\star} & e^{-\Gamma_{1} t}\rho_{11}\end{pmatrix} \underset{t\rightarrow \infty}{\longrightarrow} |0\rangle\langle 0|.$$

Notice that amplitude damping noise also removes any coherences between $|0\rangle$ and $|1\rangle$ of the state (the off-diagonal elements.) The rate at which this _decoherence_ occurs is half that of $\Gamma_{1}$.

The time evolution of the state under amplitude damping noise can be derived as the continuous-time limit of an amplitude damping channel
$$\mathcal{E}[\rho] = M_{0} \rho M_{0}^{\dagger} + M_{1}\rho M_{1}^{\dagger},$$
where
$$M_{0} = \begin{pmatrix} 1 & 0 \\0& \sqrt{1-p}\end{pmatrix}~,~M_{1} = \begin{pmatrix} 0 & \sqrt{p} \\ 0 & 0 \end{pmatrix},$$
and the probability of decay $p$ is $\Gamma_{1}\Delta t$.

The decay rate $\Gamma_{1}$ sets a natural time scale for the decay process; namely, $\Gamma_{1}^{-1}$. This number is often called the $T_{1}$ time.

Notice that the probability of the qubit remaining in the $|1\rangle$ state is given by

$$P_{1}(t) = \mathrm{Tr}\left[|1\rangle\langle 1| \rho(t)\right] = e^{-\Gamma_{1} t}\rho_{11}.$$

If the qubit was prepared in the $|1\rangle$ state, then $P_{1}(t) =e^{-\Gamma_{1} t}$.

A simple way of estimating the $T_{1}$ time is to collect statistics about the decay curve for $P_{1}(t)$ when the qubit is initialized to $|1\rangle$. This can be done by choosing a variety of times $t_{1}, t_{2}, \cdots t_{N}$, and then running the following experiment many times:
* Prepare the qubit in $|1\rangle$.
* Wait a delay time $t_{j}$.
* Measure the qubit in the $|0\rangle, |1\rangle$ basis.

An estimate of $P_{1}(t_{j})$ is the number of times the qubit was observed to be in $|1\rangle$, divided by the total number of times the experiment was repeated. Given several estimated values of $P_{1}$ for a variety of $(t_{j})$, we can fit the resulting decay curve is fit to an exponential and extract an estimate of $\Gamma_{1}$, and hence, the $T_{1}$ time.

The IBM Q Experience does not currently support delays of arbitrary length, so for now, we just append a series of identity operations after the initial excitation pulse. Each identity operation has the same duration of a single-qubit gate and is followed by a -shorter- buffer time. These parameters are backend-dependent.

**Code**

The code blocks below walk through constructing the requisite experiments to estimate the $T_{1}$ time of a qubit, sending those experiments to an IBMQ backend, and then fitting the data the backend sends back.

```python
# Select qubit whose T1 is to be measured
qubit = 1

# Creating registers
qr = qk.QuantumRegister(5)
cr = qk.ClassicalRegister(5)

# the delay times are all set in terms of single-qubit gates
# so we need to calculate the time from these parameters
pulse_length=100 # single-qubit gate time
buffer_length=10 # spacing between pulses
unit = 'ns'

steps = 10
gates_per_step = 120
max_gates = (steps-1)*gates_per_step+1
tot_length = buffer_length+pulse_length
time_per_step = gates_per_step*tot_length
qc_dict = {}
for ii in range(steps):
    step_num = 'step_%s'%(str(ii))
    qc_dict.update({step_num:qk.QuantumCircuit(qr, cr)})
    qc_dict[step_num].x(qr[qubit])
    qc_dict[step_num]=pad_QId(qc_dict[step_num],gates_per_step*ii,qr[qubit])
    qc_dict[step_num].barrier(qr[qubit])
    qc_dict[step_num].measure(qr[qubit], cr[qubit])

circuits=list(qc_dict.values())
```

```python
# run the program
t1_job = qk.execute(circuits, backend, shots=shots)
job_monitor(t1_job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
# arrange the data from the run

result_t1 = t1_job.result()
keys_0_1=list(result_t1.get_counts(qc_dict['step_0']).keys())# get the key of the excited state '00010'
data=np.zeros(len(qc_dict.keys())) # numpy array for data
sigma_data = np.zeros(len(qc_dict.keys()))

# change unit from ns to microseconds
plot_factor=1
if unit.find('ns')>-1:
    plot_factor=1000
    punit='$\\mu$s'
xvals=time_per_step*np.linspace(0,len(qc_dict.keys()),len(qc_dict.keys()))/plot_factor # calculate the time steps in microseconds

for ii,key in enumerate(qc_dict.keys()):
    # get the data in terms of counts for the excited state normalized to the total number of counts
    data[ii]=float(result_t1.get_counts(qc_dict[key])[keys_0_1[0]])/shots
    sigma_data[ii] = np.sqrt(data[ii]*(1-data[ii]))/np.sqrt(shots)

# fit the data to an exponential
fitT1, fcov = curve_fit(exp_fit_fun, xvals, data, bounds=([-1,2,0], [1., 500, 1]))
ferr = np.sqrt(np.diag(fcov))

plot_coherence(xvals, data, sigma_data, fitT1, exp_fit_fun, punit, 'T$_1$ ', qubit)

print("a: " + str(round(fitT1[0],2)) + u" \u00B1 " + str(round(ferr[0],2)))
print("T1: " + str(round(fitT1[1],2))+ " µs" + u" \u00B1 " + str(round(ferr[1],2)) + ' µs')
print("c: " + str(round(fitT1[2],2)) + u" \u00B1 " + str(round(ferr[2],2)))
```

The last calibration of $T_1$ was measured to be

```python
print(backend.properties().qubits[qubit][0].value, backend.properties().qubits[qubit][0].unit)
```

    72.38826037862532 µs

# Measuring $T_2^*$ time

**Theory**

Amplitude damping noise affects the off-diagonal elements of the density matrix in addition to the on-diagonal elements. However, there are other noise processes that only affect the off-diagonal elements, while keeping the on-diagonal elements the same. These kinds of noise processes cause _decoherence_.

As a simple example of decoherence, consider the pure superposition state
$$|\psi(\theta)\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle + e^{i\theta}|1\rangle\right).$$
Expressed as a density matrix, this state is
$$\rho(\theta) = |\psi(\theta)\rangle \langle\psi(\theta)| = \frac{1}{2}\begin{pmatrix}1 & e^{-i\theta} \\ e^{i\theta} & 1\end{pmatrix}.$$

This state has _coherence_ between $|0\rangle$ and $|1\rangle$, which manifests itself in the non-zero off-diagonal terms. If the state had _decohered_, those off-diagonal terms would be zero:
$$\rho_{\mathrm{decohered}} =  \frac{1}{2}\begin{pmatrix}1 & 0 \\ 0 & 1\end{pmatrix}.$$
When the state has decohered, it can be written as a classical _mixture_:
$$\rho_{\mathrm{decohered}} = \frac{1}{2}\left(|0\rangle \langle 0| + |1\rangle \langle 1|\right).$$

One mechanism by which decoherence happens is _dephasing_. Under dephasing noise, the state of the qubit evolves as
$$\rho(t) = \begin{pmatrix}\rho_{00} & e^{-\Gamma_{2}t}\rho_{01} \\ e^{-\Gamma_{2}t}\rho_{01}^{\star} & \rho_{11}\end{pmatrix} \underset{t\rightarrow \infty}{\longrightarrow} \begin{pmatrix}\rho_{00} & 0\\ 0& \rho_{11}\end{pmatrix}.$$

The time evolution of $\rho$ under dephasing noise can be derived as the continuous-time limit of the following noise channel:
$$\mathcal{E}[\rho] = M_{0}\rho M_{0}^{\dagger} + M_{1} \rho M_{1}^{\dagger} + M_{2}\rho M_{2}^{\dagger},$$
where
$$M_{0} =\sqrt{1-p}I~,~M_{1} = \sqrt{p}\begin{pmatrix}1 &0 \\ 0 & 0 \end{pmatrix}~,~M_{2} = \sqrt{p}\begin{pmatrix}0 & 0 \\ 0 & 1\end{pmatrix}.$$

The rate of decay in the coherences can be measured by the following experiment:

* Prepare the qubit in the $|+\rangle$ state, which can be done by initializing the qubit to $|0\rangle$ and applying a Hadamard gate, $H$.
* Wait a delay time $t_{j}$.
* Measure the qubit in the $|\pm\rangle$ basis, which can be done by applying a Hadamard and then measuring in the computational basis.

If decoherence processes are present, then after a delay time $t_{j}$, the state of the qubit is

$$\rho(t_{j}) = \frac{1}{2}\begin{pmatrix}1 & e^{-\Gamma_{2}t_{j}} \\ e^{-\Gamma_{2}t_{j}} & 1\end{pmatrix}.$$

Measuring in the $|\pm\rangle$ basis, the probability of observing the outcome $|+\rangle$ is given by

$$P_{+}(t_{j}) = \mathrm{Tr}\left(|+\rangle \langle + | \rho(t_{j})\right) = \frac{1}{2}\left(1 + e^{-\Gamma_{2}t_{j}}\right).$$

Again, by estimating $P_{+}(t_{j})$ for a variety of $t_{j}$, we can then fit a decay curve to extract an estimate of $\Gamma_{2}$.

In the actual experiment, we change the phase of the pulse before the measurement in order to create oscillations in the observed dynamics of $P_{+}(t_{j})$. If we just did two Hadamard gates separated by a delay, we would observe a decay of characteristic time $T^*_2$, but with a strong dependence on any deviation of the calibrated qubit frequency from the actual one. By implementing the qubit pulses with different phases, we shift the frequency dependence into the oscillating feature of the dynamics, and can fit the decaying envelope for a more faithful measure of the coherence time.

There is one subtle point of note. In the discussion of $T_{1}$ time above, we saw that amplitude damping noise also causes the off-diagonal elements to decay (at a rate $\Gamma_{1}/2$). Suppose a qubit was affected by only amplitude damping noise, and dephasing noise was absent. In this scenaro, _the rate of decay of coherences can be non-zero, even in the absense of dephasing noise_.

For this reason, it's important to recognize there are many noise processes contributing to the rate at which coherences decay. In this tutorial, we assume that the total rate of decoherence, $\Gamma$, can be decomposed into a sum of independent rates:

$$\Gamma = \Gamma_{T_{1}} + \Gamma_{2} + \Gamma_{\mathrm{other}}. $$

Phenomenologically, the rate $\Gamma_{\mathrm{other}}$ quantifies the rate of decoherence due to other noise processes in addition to pure amplitude damping and pure dephasing about the $Z$-axis. Note that because general noise can cause dephasing about the $Z$-axis -- in addition to doing other things to the qubit -- echo sequences are typically used to help mitigate the effects of those kind(s) of noise on $T_{2}^{\star}$. (Echo sequences are discussed below, in the sections $T_{2}$ echo  and CPMG measurement.)

If decoherence at a rate $\Gamma$ is taking place, then the state of the qubit changes as
$$\begin{equation}
\rho(t) = \begin{pmatrix}\rho_{00} & e^{-\Gamma t}\rho_{01} \\ e^{-\Gamma t}\rho_{01}^{\star} & 1-\rho_{00}\end{pmatrix}.
\end{equation}$$

The timescale associated with this decay rate is called $T_{2}$, and is given by $T_{2} = 1/\Gamma$.

$T_{2}$ relates to the other timescales introduced previously as
$$T_{2} = \left(\frac{2}{T_{1}} + \frac{1}{T_{2}} + \frac{1}{T_{\mathrm{other}}}\right)^{-1} = T_{2}^{\star}\left( 1 + \frac{2T_{2}^{\star}}{T_{1}} + \frac{T^{\star}_{2}}{T_{\mathrm{other}}}\right)^{-1} \leq T_{2}^{\star},$$
where we've defined $T_{\mathrm{other}}  = 1 /\Gamma_{\mathrm{other}}$.

```python
# Select qubit on which to measure T2*
qubit = 1

# Creating registers
qr = qk.QuantumRegister(5)
cr = qk.ClassicalRegister(5)

steps = 35
gates_per_step = 20
max_gates = (steps-1)*gates_per_step+2

num_osc = 5
tot_length = buffer_length+pulse_length
time_per_step = gates_per_step*tot_length
qc_dict = {}
for ii in range(steps):
    step_num = 'step_%s'%(str(ii))
    qc_dict.update({step_num:qk.QuantumCircuit(qr, cr)})
    qc_dict[step_num].h(qr[qubit])
    qc_dict[step_num]=pad_QId(qc_dict[step_num],gates_per_step*ii,qr[qubit])
    qc_dict[step_num].u1(2*np.pi*num_osc*ii/(steps-1),qr[qubit])
    qc_dict[step_num].h(qr[qubit])
    qc_dict[step_num].barrier(qr[qubit])
    qc_dict[step_num].measure(qr[qubit], cr[qubit])
circuits = list(qc_dict.values())

```

```python
# run the program
t2star_job = qk.execute(circuits, backend, shots=shots)
job_monitor(t2star_job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
# arrange the data from the run

result_t2star = t2star_job.result()
keys_0_1 = list(result_t2star.get_counts(qc_dict['step_0']).keys())# get the key of the excited state '00010'

# change unit from ns to microseconds
plot_factor = 1
if unit.find('ns') > -1:
    plot_factor = 1000
    punit = '$\\mu$s'
xvals = time_per_step*np.linspace(0,len(qc_dict.keys()),len(qc_dict.keys()))/plot_factor # calculate the time steps

data = np.zeros(len(qc_dict.keys())) # numpy array for data
sigma_data = np.zeros(len(qc_dict.keys()))

for ii,key in enumerate(qc_dict.keys()):
    # get the data in terms of counts for the excited state normalized to the total number of counts
    data[ii] = float(result_t2star.get_counts(qc_dict[key])[keys_0_1[0]])/shots
    sigma_data[ii] = np.sqrt(data[ii]*(1-data[ii]))/np.sqrt(shots)

fitT2s, fcov = curve_fit(osc_fit_fun, xvals, data, p0=[0.5, 100, 1/10, np.pi, 0],
                         bounds=([0.3,0,0,0,0], [0.5, 200, 1/2,2*np.pi,1]))
ferr = np.sqrt(np.diag(fcov))

plot_coherence(xvals, data, sigma_data, fitT2s, osc_fit_fun, punit, '$T_2^*$ ', qubit)

print("a: " + str(round(fitT2s[0],2)) + u" \u00B1 " + str(round(ferr[0],2)))
print("T2*: " + str(round(fitT2s[1],2))+ " µs"+ u" \u00B1 " + str(round(ferr[1],2)) + ' µs')
print("f: " + str(round(10**3*fitT2s[2],3)) + 'kHz' + u" \u00B1 " + str(round(10**6*ferr[2],3)) + 'kHz')
print("phi: " + str(round(fitT2s[3],2)) + u" \u00B1 " + str(round(ferr[3],2)))
print("c: " + str(round(fitT2s[4],2)) + u" \u00B1 " + str(round(ferr[4],2)))
```

# Measurement of $T_2$ Echo

We have referred to the previous experiment's characteristic time as $T^*_2$ and not $T_2$ by analogy to nuclear magnetic resonance (NMR). Indeed, one can isolate different frequency components to the decoherence process by devising increasingly elaborated pulse sequences. To illustrate the analogy with NMR, one can think about an ensemble of nuclear spins precessing in an external DC magnetic field. Due to field inhomogeneities, each spin might precess with a slightly different Larmor frequency. This certainly will affect the observed coherence time of the ensemble. However, it is possible to echo away this low-frequency decoherence process by applying a pi-pulse to the system halfway through the delay. The effect of this pi-pulse is to reverse the direction of the precession of each individual spin due to field inhomogeneities. Thus, the spins that had precessed more now start precessing in the opposite direction faster than the spins that had precessed less, and after an equal delay, all the spins in the system recover the initial coherence, except for other, higher-frequency, decoherence mechanisms.

Here, we are measuring only a single qubit rather than an ensemble of spins. Consequently coherence measurements require averaging an ensemble of measurements in order to eliminate projection noise, and run-to-run fluctuations in the qubit's frequency which will similarly manifest themselves as decoherence if they are not canceled out. By running this $T_2$ echo sequence, we can therefore remove low-frequency components of the decoherence.

```python
# Select qubit to measure T2 echo on
qubit = 1

# Creating registers
qr = qk.QuantumRegister(5)
cr = qk.ClassicalRegister(5)

steps = 18
gates_per_step = 28
tot_length = buffer_length+pulse_length
max_gates = (steps-1)*2*gates_per_step+3
time_per_step = (2*gates_per_step)*tot_length
qc_dict = {}
for ii in range(steps):
    step_num = 'step_%s'%(str(ii))
    qc_dict.update({step_num:qk.QuantumCircuit(qr, cr)})
    qc_dict[step_num].h(qr[qubit])
    qc_dict[step_num]=pad_QId(qc_dict[step_num],gates_per_step*ii,qr[qubit])
    qc_dict[step_num].x(qr[qubit])
    qc_dict[step_num]=pad_QId(qc_dict[step_num],gates_per_step*ii,qr[qubit])
    qc_dict[step_num].h(qr[qubit])
    qc_dict[step_num].barrier(qr[qubit])
    qc_dict[step_num].measure(qr[qubit], cr[qubit])
circuits = list(qc_dict.values())

```

```python
# run the program
t2echo_job = qk.execute(circuits, backend, shots=shots)
job_monitor(t2echo_job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
# arrange the data from the run

result_t2echo = t2echo_job.result()
keys_0_1 = list(result_t2echo.get_counts(qc_dict['step_0']).keys())# get the key of the excited state '00010'

# change unit from ns to microseconds
plot_factor=1
if unit.find('ns')>-1:
    plot_factor=1000
    punit='$\\mu$s'
xvals=time_per_step*np.linspace(0,len(qc_dict.keys()),len(qc_dict.keys()))/plot_factor # calculate the time steps

data=np.zeros(len(qc_dict.keys())) # numpy array for data
sigma_data = np.zeros(len(qc_dict.keys()))

for ii,key in enumerate(qc_dict.keys()):
    # get the data in terms of counts for the excited state normalized to the total number of counts
    data[ii]=float(result_t2echo.get_counts(qc_dict[key])[keys_0_1[0]])/shots
    sigma_data[ii] = np.sqrt(data[ii]*(1-data[ii]))/np.sqrt(shots)

fitT2e, fcov = curve_fit(exp_fit_fun, xvals, data, bounds=([-1,10,0], [1, 150, 1]))
ferr = np.sqrt(np.diag(fcov))

plot_coherence(xvals, data, sigma_data, fitT2e, exp_fit_fun, punit, '$T_{2echo}$ ', qubit)

print("a: " + str(round(fitT2e[0],2)) + u" \u00B1 " + str(round(ferr[0],2)))
print("T2: " + str(round(fitT2e[1],2))+ ' µs' + u" \u00B1 " + str(round(ferr[1],2)) + ' µs')
print("c: " + str(round(fitT2e[2],2)) + u" \u00B1 " + str(round(ferr[2],2)))
```

The last calibration of $T_2$ was measured to be

```python
print(backend.properties().qubits[qubit][1].value, backend.properties().qubits[qubit][1].unit)
```

    111.62868245151734 µs

## CPMG measurement

As explained above, the echo sequence removes low-frequency decoherence mechanisms. This noise-filtering procedure can be extended with increased number of pi-pulses within the delay. In the following experiment, we implement an echo experiment with seven pi-pulses during the delay between the initial and final pulses. This kind of echo with several pi-pulses is referred to as a CPMG experiment, after Carr, Purcell, Meiboom, and Gill.

```python
# Select qubit for CPMG measurement of T2
qubit = 1

# Creating registers
qr = qk.QuantumRegister(5)
cr = qk.ClassicalRegister(5)

steps = 10
gates_per_step = 18
num_echo = 5 # has to be odd number to end up in excited state at the end
tot_length = buffer_length+pulse_length
time_per_step = ((num_echo+1)*gates_per_step+num_echo)*tot_length
max_gates = num_echo*(steps-1)*gates_per_step+num_echo+2
qc_dict = {}
for ii in range(steps):
    step_num='step_%s'%(str(ii))
    qc_dict.update({step_num:qk.QuantumCircuit(qr, cr)})
    qc_dict[step_num].h(qr[qubit])
    for iii in range(num_echo):
        qc_dict[step_num]=pad_QId(qc_dict[step_num], gates_per_step*ii, qr[qubit])
        qc_dict[step_num].x(qr[qubit])
    qc_dict[step_num]=pad_QId(qc_dict[step_num], gates_per_step*ii, qr[qubit])
    qc_dict[step_num].h(qr[qubit])
    qc_dict[step_num].barrier(qr[qubit])
    qc_dict[step_num].measure(qr[qubit], cr[qubit])
circuits=list(qc_dict.values())

```

```python
# run the program
t2cpmg_job = qk.execute(circuits, backend, shots=shots)
job_monitor(t2cpmg_job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
# arrange the data from the run

result_t2cpmg = t2cpmg_job.result()
keys_0_1 = list(result_t2cpmg.get_counts(qc_dict['step_0']).keys())# get the key of the excited state '00001'

# change unit from ns to microseconds
plot_factor = 1
if unit.find('ns') > -1:
    plot_factor = 1000
    punit = '$\\mu$s'
xvals = time_per_step*np.linspace(0,len(qc_dict.keys()),len(qc_dict.keys()))/plot_factor # calculate the time steps

data = np.zeros(len(qc_dict.keys())) # numpy array for data
sigma_data = np.zeros(len(qc_dict.keys()))

for ii,key in enumerate(qc_dict.keys()):
    # get the data in terms of counts for the excited state normalized to the total number of counts
    data[ii] = float(result_t2cpmg.get_counts(qc_dict[key])[keys_0_1[0]])/shots
    sigma_data[ii] = np.sqrt(data[ii]*(1-data[ii]))/np.sqrt(shots)

fitT2cpmg, fcov = curve_fit(exp_fit_fun, xvals, data, bounds=([-1,10,0], [1, 150, 1]))
ferr = np.sqrt(np.diag(fcov))

plot_coherence(xvals, data, sigma_data, fitT2cpmg, exp_fit_fun, punit, '$T_{2cpmg}$ ', qubit)

print("a: " + str(round(fitT2cpmg[0],2)) + u" \u00B1 " + str(round(ferr[0],2)))
print("T2: " + str(round(fitT2cpmg[1],2))+ ' µs' + u" \u00B1 " + str(round(ferr[1],2)) + ' µs')
print("c: " + str(round(fitT2cpmg[2],2)) + u" \u00B1 " + str(round(ferr[2],2)))
```

```python

```