```python

# Un commentaire commence par un #
# Importation d'un module
import numpy as np
# Les modules contiennent tout le materiel specialise

# boucle for
for i in range(0,10):
    print ("Le chiffre est :", i)
    # L'indentation est obligatoire en python, elle sert d'element syntaxique
    # print sert a afficher a l'ecran.

animaux=["canards", "ours", "chevres", "loups","alpagas"]
# Fin de l'indentation, la boucle for est finie
# On peut boucler sur un peu n'importe quoi,..
for ani in animaux:
    print ("Bruno aime les ", ani)
```

    Le chiffre est : 0
    Le chiffre est : 1
    Le chiffre est : 2
    Le chiffre est : 3
    Le chiffre est : 4
    Le chiffre est : 5
    Le chiffre est : 6
    Le chiffre est : 7
    Le chiffre est : 8
    Le chiffre est : 9
    Bruno aime les  canards
    Bruno aime les  ours
    Bruno aime les  chevres
    Bruno aime les  loups
    Bruno aime les  alpagas

```python
b=range(0,10)
# b vaut [0,1,2,...,9]
# La numerotation des tableaux en python commence a 0 et fini a n-1 comme en C
print (b[0], b[9])
# imprime le premier element et le dernier
# On aurait aussi pu faire:
print (b[0],b[-1])
# -1 est le dernier indice, -2 l'avant dernier, etc.
a=15
# Test logiques
if (a<10):
    print("a est inferieur a 10")
elif (a>10):
    print("a est superieur a 10")
else:
    print("a est egal a 10")
```

    0 9
    0 9
    a est superieur a 10

```python
# Multiplication
c = 1. * 2.

# Exposant
d = 3. ** 3.

# Division
e = 4. / 2.

# Division entiere
f = 5 // 2

# Modulo
g = 2 % 6
```

```python
# Une liste vide
avide=[]

# Une liste en python est en fait un objet. Elle possede donc ses methodes
avide.append(10)
# avide contient maintenant un element
print (avide)

#Definir une fonction en python est particulierement simple
def f(x):
    y = max(x,0.) + 10
    return y**2.
```

    [10]

```python
class Employe:
   #Classe generale pour stocker l'information sur un employe
   def __init__(self, nom, salaire):
      self.nom = nom
      self.salaire = salaire
   def imprime(self):
      print ("Nom : ", self.nom,  ", Salaire: ", self.salaire)

# Python permet aussi l'heritage
class Ingenieur(Employe):
    def __init__(self,nom,salaire,responsabilite):
        Employe.__init__(self,nom,salaire)
        self.responsabilite=responsabilite
    #Polymorphisme simple
    def imprime(self):
        print ("Nom : ", self.nom,  ", Salaire: ", self.salaire, " est ingenieur responsable de ", self.responsabilite)

```

```python
Jean = Employe("Jean",10000)
Bruno = Ingenieur("Bruno",10000,"Simulation")
Jean.imprime()
Bruno.imprime()
```

    Nom :  Jean , Salaire:  10000
    Nom :  Bruno , Salaire:  10000  est ingenieur responsable de  Simulation

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

dx=np.array([0.1,0.02,0.01,0.005])
error= 0.5*dx**2
a,b = np.polyfit(np.log(dx),np.log(error),1)

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(dx,error,'ko',label='$\Vert e_{\mathbf{u}}\Vert_{2}$')

ax.plot(dx,np.exp(b)*dx**a,'-k',label='$\Vert e_{\mathbf{u}}\Vert_{2}=%3.2f  \Delta x^{%3.2f}$' %(np.exp(b),a))

ax.set_yscale('log')
ax.set_xscale('log')
ax.grid(b=True, which='minor', color='grey', linestyle='--')
ax.grid(b=True, which='major', color='k', linestyle='-')
plt.ylabel('$\Vert e \Vert_2 $')
plt.xlabel('$\Delta x$')
plt.legend()
plt.show()
```

```python
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
n=20
dx=1/(n-1)
mat = -2*np.eye(n)
mat.flat[1::n+1]=1
mat.flat[n::n+1]=1
mat[0,:]=0
mat[-1,:]=0
mat[0,0]=1
mat[-1,-1]=1
b = - 20*np.ones(n) * dx**2.
b[0]=1
b[-1]=2
T=np.linalg.solve(mat,b)
plt.plot(np.linspace(0,1,n),T)
plt.show()
```

```python
import numpy as np
from scipy.integrate import simps

def f(x):
    return x**2.

x=np.linspace(0,1,11)
# x est maintenant [0, 0.01, 0.02,...,1.]
y = f(x)
integral=np.trapz(y,x) # methode du trapeze
integralS=simps(y,x) # methode de Simpson
print("L'integrale trapz : ",integral )
print("L'integrale simps : ",integralS )
print("La solution : ", 1./3.)

```

    L'integrale trapz :  0.335
    L'integrale simps :  0.333333333333
    La solution :  0.3333333333333333

```python
import numpy as np
from scipy.interpolate import interp1d
import matplotlib.pyplot as plt

x = np.linspace(0, 10, num=10, endpoint=True)
y = np.cos(x)
f = interp1d(x, y)
f2 = interp1d(x, y, kind='cubic')

xnew = np.linspace(0, 10, num=41, endpoint=True)

plt.plot(x, y, 'o', xnew, f(xnew), '-', xnew, f2(xnew), '--')
plt.legend(['data', 'linear', 'cubic'], loc='best')
plt.show()
```

```python
from sympy import *

#define symbolic variables
x,y = symbols("x y")
expr = cos(x) + 1

# On peut aussi definir une formule par un "string"
str_expr = "x**2 + 3*x - 1/2"
expr2 = sympify(str_expr)
expr2
```

    x**2 + 3*x - 1/2

```python
expr.subs(x,1.)
```

    1.54030230586814

```python
expr.subs(x,y)
```

    cos(y) + 1

```python
from sympy import *

#define symbolic variables
x,y,a = symbols("x y a")

def laplacian(u):
	return diff(u,x,x)+diff(u,y,y)

u=sin(a*x)*cos(a*y)
laplacian(u)
```

    -2*a**2*sin(a*x)*cos(a*y)

```python
from  sympy import *

#define symbolic variables
x,y,a = symbols("x y a")

u=sin(x)*exp(x)
#Integrale symbolique sans borne
integrate(u,x)

```

    exp(x)*sin(x)/2 - exp(x)*cos(x)/2

```python
#Integrale symbolique avec bornes
v=exp(-x)
integrate(v,(x,0,oo))
```

    1

```python

```


```python
# Standard Imports
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
import chart_studio.plotly as py
#Import plotly.express, naming convention as px
import plotly.express as px
import plotly.graph_objs as go #importing graphical objects
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
# Plot Settings
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
plt.rcParams['agg.path.chunksize'] = 10000
import sys
HELPERS_PATH = os.path.join("..", "Functions and Packages")
sys.path.append(HELPERS_PATH)
from helpers import visualization, preprocessing
%load_ext autoreload
%autoreload 2
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

## Types and Estimators

Let's work with our happiness dataset again.

```python
happiness_data = pd.read_csv("../data/happiness.csv")
happiness_data.head()
```

      Overall rank
      Country or region
      Score
      GDP per capita
      Social support
      Healthy life expectancy
      Freedom to make life choices
      Generosity
      Perceptions of corruption

      0
      1
      Finland
      7.769
      1.340
      1.587
      0.986
      0.596
      0.153
      0.393

      1
      2
      Denmark
      7.600
      1.383
      1.573
      0.996
      0.592
      0.252
      0.410

      2
      3
      Norway
      7.554
      1.488
      1.582
      1.028
      0.603
      0.271
      0.341

      3
      4
      Iceland
      7.494
      1.380
      1.624
      1.026
      0.591
      0.354
      0.118

      4
      5
      Netherlands
      7.488
      1.396
      1.522
      0.999
      0.557
      0.322
      0.298

### OLS

**OLS** stands for Ordinary Least Squares.  This is the most common method, and follows the standard Sklearn API for construction.  Use `LinearRegression` as the estimator.

```python
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score

X = happiness_data[['GDP per capita', 'Generosity']]
y = happiness_data[['Score']]

# Split
X_train, X_test, y_train, y_test = train_test_split(X, y)

# Construct lin_reg object
lin_reg = LinearRegression()

# Fit to data
lin_reg.fit(X_train, y_train)

# Predict
y_pred = lin_reg.predict(X_test)
```

#### Visualizing

```python
def plot_multiple_happiness_regression():
    fig = px.scatter_3d(data_frame = happiness_data, x = 'Generosity', y = 'GDP per capita', z = 'Score')
    #I like smaller markers
    fig.update_traces(marker = dict(size = 5))
    # Get the min and max of our x and y axes
    x_min, x_max = X_test['Generosity'].min(), X_test['Generosity'].max()
    y_min, y_max = X_test['GDP per capita'].min(), X_test['GDP per capita'].max()

    # Create the mesh (interval) size
    mesh_size = 0.03

    # Create the ranges of the axes
    xrange = np.arange(x_min, x_max, mesh_size)
    yrange = np.arange(y_min, y_max, mesh_size)

    # Create MeshGrid
    xx, yy = np.meshgrid(xrange, yrange)

    # Predict the model on the raveled (flattened) versions of the xx and yy gen'd by MeshGrid function
    y_pred = lin_reg.predict(np.c_[xx.ravel(), yy.ravel()])

    #Reshape to the xx so all our dimensions are the same for our goSurface
    y_pred = y_pred.reshape(xx.shape)

    # Add traces, with our z being the y_preds
    fig.add_traces(go.Surface(x= xrange, y = yrange, z=y_pred, name="predictions"))
    fig.show()
```

```python
plot_multiple_happiness_regression()
```

We can look at multiple OLS regressions with seaborn using `sns.pairplot(data, x_vars, y_vars, kind = 'reg'`

```python
happiness_data.head()
```

      Overall rank
      Country or region
      Score
      GDP per capita
      Social support
      Healthy life expectancy
      Freedom to make life choices
      Generosity
      Perceptions of corruption

      0
      1
      Finland
      7.769
      1.340
      1.587
      0.986
      0.596
      0.153
      0.393

      1
      2
      Denmark
      7.600
      1.383
      1.573
      0.996
      0.592
      0.252
      0.410

      2
      3
      Norway
      7.554
      1.488
      1.582
      1.028
      0.603
      0.271
      0.341

      3
      4
      Iceland
      7.494
      1.380
      1.624
      1.026
      0.591
      0.354
      0.118

      4
      5
      Netherlands
      7.488
      1.396
      1.522
      0.999
      0.557
      0.322
      0.298

```python
sns.set_style("whitegrid")
sns.pairplot(data = happiness_data, x_vars=['GDP per capita', 'Generosity', 'Healthy life expectancy'], y_vars='Score', height = 4, aspect=1, kind = 'reg')
```

#### Evaluating

Let's take a look at our intercept and coefficients.

```python
# Check coef
print('Coefficients: ', lin_reg.coef_)

# MSE
print('Mean squared error: %.2f'
      % mean_squared_error(y_test, y_pred))

# The coefficient of determination: 1 is perfect prediction
print('Coefficient of determination: %.2f'
      % r2_score(y_test, y_pred))
```

    Coefficients:  [[2.27861606 2.09810539]]
    Mean squared error: 0.61
    Coefficient of determination: 0.38

#### Statsmodels

For more advanced techniques and hypothesis testing, see [this](http://localhost:8889/notebooks/MLProjects/Notes/Statsmodels.ipynb) notebook.

### Ridge Regression

Plots the sum of the squared residuals vs slope values, constantly changing the line through the data value.

Imposes a penalty, which is added to the sum of the squared residuals.  This **prevents overfitting**:
    \begin{equation}
  \lambda * \mbox{Slope}^2
\end{equation}

The whole model:

In this case, alpha is lambda, and w is slope.

```python
# Import Ridge
from sklearn.linear_model import Ridge

# Construct Estimator - alpha = 1, meaning linear, nothing is changing, same effect as LinearRegression
ridge = Ridge(alpha = 1)

#Let's make this one feature and one y value
X = happiness_data[['GDP per capita']]
y = happiness_data[['Score']]

#Split
X_train, X_test, y_train, y_test = train_test_split(X, y)

#Fit model
ridge.fit(X_train, y_train)

#Predict
y_pred = ridge.predict(X_test)
```

Let's view the coefficient, w, which represents the slope

```python
w = ridge.coef_
w
```

    array([[2.08713558]])

```python
from visualization import plot_lin_reg
```

```python
plot_lin_reg(ridge, X, y)
```

Finally, exporting this to chart-studio, and adding a little formatting . . .

```python
from IPython.display import HTML
HTML('')
```

    /Users/lukeanglin/opt/anaconda3/lib/python3.8/site-packages/IPython/core/display.py:717: UserWarning:

    Consider using IPython.display.IFrame instead

Let's see what happens if we increase $\alpha$ (also known as $\lambda$) to 1000.

```python
y_range.shape
```

    (100, 1)

```python
# Construct Estimator - alpha = 1000, higher penalty, lower bias.
ridge = Ridge(alpha = 1000)

#Fit and predict
ridge.fit(X_train, y_train)
y_range = ridge.predict(X_range.reshape(-1, 1))

#Save to CSV for Chart-Studio Formatting
np.savetxt('../data/ridge_predictions_for_plotly.csv', y_range, delimiter = ',')

#Show new chart
HTML('')
```

    /Users/lukeanglin/opt/anaconda3/lib/python3.8/site-packages/IPython/core/display.py:717: UserWarning:

    Consider using IPython.display.IFrame instead

### Lasso Regression

Similar to ridge regression, imposes a penalty to **prevent overfitting**

$$\lvert \lambda * \mbox{Slope} \rvert$$

The whole model:

```python
# Import Lasso
from sklearn.linear_model import Lasso

# Construct Estimator - alpha = 1, meaning linear, nothing is changing, same effect as LinearRegression
lasso = Lasso(alpha = 1)

#Let's make this one feature and one y value
X = happiness_data[['GDP per capita']]
y = happiness_data[['Score']]

#Split
X_train, X_test, y_train, y_test = train_test_split(X, y)

#Fit model
lasso.fit(X_train, y_train)

#Predict
y_pred = lasso.predict(X_test)

#Show coef
l
```

    array([0.])

### Ridge vs. Lasso

Both are considered **regularizing** estimators.  This means that they introduce some bias for a significant drop in variance.

Differences
* Ridge can only shrink features asymptotically close to zero
* Lasso can shrink features all the way to zero
* Lasso will therefore **completely exclude** useless variables, while ridge will make them **less effective**

Summary:  If you have mostly **useful variables**, use ridge, otherwise, use lasso.

### Elastic Net

What if we have a model with **tons of variables** and we don't know whether or not most of them are useful?  We use elastic net in this scenario, another regularization method.

Combines the elastic penalty and the ridge penalty:

$$\lvert \lambda_{1} * \mbox{Slope} \rvert + \lambda_{2} * \mbox{Slope}^2$$

Notice the subscripts on the $\lambda\mbox{'s}$.

Elastic Net is best at dealing with **multiple, correlated parameters.**

## Choosing

For a [table format](https://airtable.com/shrQ5rfksG64QobIy/tblkAQbukd3Al0OT6)


# Table of Contents

1  Exponentiality of the lifetimes distributions

1.1  load data

1.2  Calculate first and second moments of lifetime distributions

2  Visual check of the lifetime distribution

```python
%matplotlib inline
```

# Exponentiality of the lifetimes distributions

## load data

```python
! ls -lrt  ../lifetime_ar/*/ | tail
```

    -rw-r--r-- 1 lustelzl tb 66000 Jan 27 13:26 lifetime_dt10_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 48600 Jan 27 13:26 lifetime_dt50_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 72750 Jan 27 13:26 lifetime_dt1_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 71475 Jan 27 13:26 lifetime_dt2_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 69225 Jan 27 13:26 lifetime_dt5_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 57900 Jan 27 13:26 lifetime_dt25_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb 35775 Jan 27 13:26 lifetime_dt100_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb  4575 Jan 27 13:26 lifetime_dt1000_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb  9750 Jan 27 13:26 lifetime_dt500_ala_c_md.txt
    -rw-r--r-- 1 lustelzl tb  2100 Jan 27 13:26 lifetime_dt2500_ala_c_md.txt

```python
lag_time_l = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000, 2500, 5000]
```

```python
remd_dt_hdw_d = {}
remd_dt_cdw_d = {}

for k in lag_time_l:
    #print k
    #wa_tau_a_ar = np.column_stack((v.weight.values, v.weight.values / sum(v.weight.values),  v.wait_T.values))
    w_tau_a_ar = np.genfromtxt("../lifetime_ar/helix/lifetime_dt{}_ala_h.txt".format(k))
    remd_dt_hdw_d[k] = w_tau_a_ar

    w_tau_a_ar = np.genfromtxt("../lifetime_ar/coil/lifetime_dt{}_ala_h.txt".format(k))
    #np.savetxt("lifetime_ar/coil/lifetime_dt{}_ala_h.txt".format(k), wa_tau_a_ar)
    remd_dt_cdw_d[k] = w_tau_a_ar
```

```python
md_dt_hdw_d = {}
md_dt_cdw_d = {}

for k in lag_time_l:
    w_tau_a_ar = np.genfromtxt("../lifetime_ar/helix/lifetime_dt{}_ala_h_md.txt".format(k))
    md_dt_hdw_d[k] = w_tau_a_ar

    ar = np.genfromtxt("../lifetime_ar/coil/lifetime_dt{}_ala_c_md.txt".format(k))
    md_dt_cdw_d[k] = ar
```

should also load the lifetimes distributions presented in Figure $.

## Calculate first and second moments of lifetime distributions

The lifetimes are scaled by $w_i$, to constitute a the wait for a single transition event.

\begin{equation}
\tau_i = \frac{\tilde{\tau}_i}{w_i}
\end{equation}

$\tau_i$ enters the lifetime distribution with weight $w_i$

\begin{equation}
\langle \tau \rangle = \frac{\sum_i w_i \tau_i}{\sum_i w_i} = \frac{\sum_i \tilde{\tau}_i}{\sum_i w_i}
\end{equation}

This is equivalent to the definition of $k_{mn} = N_{mn}/t_n $. Entering the lifetimes with a weight $w_i$ thus ensures that the mean lifetime is equal to the inverse rate coefficient.

\begin{equation}
\langle \tau^2 \rangle = \frac{\sum_i \tau_i^2}{\sum_i w_i} =  \frac{\sum_i \frac{\tilde{\tau}^2}{w_i}}{\sum_i{w_i}}
\end{equation}

Calculating the variance.

\begin{equation}
\langle \tau^2 \rangle - \langle \tau \rangle^2 = \langle (\tau - \langle \tau \rangle )^2 \rangle = \frac{\sum w_i (\tau_i - \langle \tau \rangle)^2}{\sum_i w_i} =   \frac{\sum_i w_i ( \frac{\tilde{\tau_i}}{w_i} -  \langle \tau \rangle)^2}{\sum_i w_i}
\end{equation}

```python
def check_exp_unw(tau_a, w_a, return_mom=False):
    """
    variance is not weighted. This should give the wrong result
    """
    #_av = np.average( tau_a / w_a, weights=w_a)
    _av = np.sum(tau_a) / np.sum(w_a)
    _var = np.sum( (tau_a - _av)**2 / (np.sum(w_a)) )
    exp = _var/ _av**2
    if return_mom:
        return exp, (_av, _var)
    else:
         return exp

def check_exp_w(tau_a, w_a, return_mom=False):
    """
    Calculate weighted mean and variance for the lifetimes
    """
    #_av = np.average( tau_a / w_a, weights=w_a)
    _av = np.sum(tau_a) / np.sum(w_a)
    _var = np.sum( w_a*(tau_a/w_a - _av)**2) / (np.sum(w_a) )
    exp = _var/ _av**2
    if return_mom:
        return exp, (_av, _var)
    else:
         return exp
```

```python
tf = 1.0/ 1000.0
```

```python
fig, ax = plt.subplots(1,2, figsize=(6,3))

for k, v in remd_dt_hdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0])
    lg1_remd, = ax[0].plot(int(k)*tf, _exp, "s", c=cl[1])

for k, v in remd_dt_cdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0])

    lg2_remd, = ax[1].plot(int(k)*tf, _exp, "s", c=cl[3])

for k, v in md_dt_hdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0])
    ax[0].plot(int(k)*tf, _exp, "o", c=cl[0])

for k, v in md_dt_cdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0])
    ax[1].plot(int(k)*tf, _exp, "o", c=cl[2])

ax[0].legend([lg1_remd], [r'REMD $\mathregular{\tau_h}$'],
               loc=3, handletextpad=-0.1, borderaxespad=0.1,
              frameon=True)
ax[1].legend([ lg2_remd], [r'REMD $\mathregular{\tau_c}$'],
               loc=3,handletextpad=-0.1, borderaxespad=0.1,
              frameon=True)

for a in ax.flat:
    a.loglog()
    #a.set_ylim(10**-1, 10**1)
    a.plot([10**-3, 10**1], [1]*2, "-")
    a.set_xlim(10**-3.1, 10**1)
    a.set_ylabel(r"$\mathregular{< \tau^2> - <\tau>^2 / <\tau>^2}$",
                fontsize=12)
    a.set_xlabel("$\mathregular{\Delta t \, [ns]}$", fontsize=12)
    a.tick_params(axis='both', which='major', labelsize=12)

fig.tight_layout()
```

```python
fig, ax = plt.subplots(1,2, figsize=(6,3))

for k, v in remd_dt_hdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0], return_mom=True)
    lg1_remd, = ax[0].plot(int(k)*tf, _exp[1][1], "s", c=cl[1])

for k, v in remd_dt_cdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0], return_mom=True)

    lg2_remd, = ax[1].plot(int(k)*tf, _exp[1][1], "s", c=cl[3])

for k, v in md_dt_hdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0], return_mom=True)
    ax[0].plot(int(k)*tf, _exp[1][1], "o", c=cl[0])

for k, v in md_dt_cdw_d.items():
    _exp = check_exp_w(v[:,2], v[:,0], return_mom=True)
    ax[1].plot(int(k)*tf, _exp[1][1], "o", c=cl[2])

ax[0].legend([lg1_remd], [r'REMD $\mathregular{\tau_h}$'],
               loc=3, handletextpad=-0.1, borderaxespad=0.1,
              frameon=True)
ax[1].legend([ lg2_remd], [r'REMD $\mathregular{\tau_c}$'],
               loc=3,handletextpad=-0.1, borderaxespad=0.1,
              frameon=True)

for a in ax.flat:
    a.loglog()
    a.set_ylim(10**3, 10**9)
    #a.plot([10**-3, 10**1], [1]*2, "-")
    a.set_xlim(10**-3.1, 10**1)
    a.set_ylabel(r"$\mathregular{\langle \tau^2 \rangle - \langle \tau^2 \rangle \, [ns^2]}$",
                fontsize=12)
    a.set_xlabel("$\mathregular{\Delta t \, [ns]}$", fontsize=12)
    a.tick_params(axis='both', which='major', labelsize=12)

fig.tight_layout()
```

# Visual check of the lifetime distribution

* need to compare to MD

```python
from kinetics.ala_kinetics import *
from kinetics.waiting_time import *
```

```python
ala_t_factor = 1.0/ 1000.0
_blog = np.logspace(-3,1, 5000)
```

```python
mh_1 = remd_dt_hdw_d[1][:,2] > 0
mh_50 = remd_dt_hdw_d[50][:,2] > 0
```

```python
fit_plot_cdf??
```

```python
print np.max(remd_dt_hdw_d[1][:,2][mh_1] / remd_dt_hdw_d[1][:,0][mh_1])
print np.var(remd_dt_hdw_d[1][:,2][mh_1] / remd_dt_hdw_d[1][:,0][mh_1]), \
check_exp_w(remd_dt_hdw_d[1][:,2][mh_1], remd_dt_hdw_d[1][:,0][mh_1], return_mom=True)[1][1]
```

    2977.0
    91677.5419681 43746.9958621

```python
np.max(md_dt_hdw_d[1][:,2])
print np.var(md_dt_hdw_d[1][:,2])
```

    31651.5224742

```python
fig, ax = plt.subplots(1,2, figsize=(15,3))
ax[0], b, l = fit_plot_cdf(ax[0], remd_dt_hdw_d[1][:,2][mh_1] / remd_dt_hdw_d[1][:,0][mh_1] * ala_t_factor ,
                 weights=remd_dt_hdw_d[1][:,0][mh_1],
                bins=_blog)

ax[1], b, l = fit_plot_cdf(ax[1], md_dt_hdw_d[1][:,2] * ala_t_factor ,
                bins=_blog)

for a in ax:
    a.semilogx()
```

```python
mc_1 = remd_dt_cdw_d[1][:,2] > 0
```

```python
print np.max(remd_dt_cdw_d[1][:,2][mc_1] / remd_dt_cdw_d[1][:,0][mc_1])
print np.var(remd_dt_cdw_d[1][:,2][mc_1] / remd_dt_cdw_d[1][:,0][mc_1]), \
check_exp_w(remd_dt_cdw_d[1][:,2][mc_1], remd_dt_cdw_d[1][:,0][mc_1], return_mom=True)[1][1]
```

    4527.0
    237747.650485 97716.1784696

```python
np.max(md_dt_cdw_d[1][:,2])
print np.var(md_dt_cdw_d[1][:,2])
```

    68072.3526156

```python
fig, ax = plt.subplots(1,2, figsize=(18,4))
ax[0], b, l = fit_plot_cdf(ax[0], remd_dt_cdw_d[1][:,2][mc_1] / remd_dt_cdw_d[1][:,0][mc_1] * ala_t_factor ,
                 weights=remd_dt_cdw_d[1][:,0][mc_1],
                bins=_blog)

ax[1], b, l = fit_plot_cdf(ax[1], md_dt_cdw_d[1][:,2] * ala_t_factor ,
                bins=_blog)

for a in ax:
    a.semilogx()
```

```python
fig, ax = plt.subplots(figsize=(10,3))
_ = fit_plot_cdf(ax, remd_dt_hdw_d[50][:,2][mh_50] / remd_dt_hdw_d[50][:,0][mh_50] * ala_t_factor ,
                 weights=remd_dt_hdw_d[50][:,0][mh_50],
                bins=_blog)
ax.semilogx()
```

```python

```

```python

```


# Neuromatch Academy: Week 2, Day 1, Tutorial 2
# Deep Learning: Encoding Neural Responses

**Content creators**: Jorge A. Menendez, Carsen Stringer

**Content reviewers**: Roozbeh Farhoodi, Ella Batty, Kshitij Dwivedi, Spiros Chavlis, Michael Waskom

---
#Tutorial Objectives

In this tutorial, we'll use deep learning to build an encoding model from stimuli to neural activity. Specifically, we'll be looking at the activity of ~20,000 neurons in mouse primary visual cortex responding to oriented gratings recorded in [this study](https://www.biorxiv.org/content/10.1101/679324v2.abstract).

Because the stimuli are 1D and the neurons respond with smooth tuning curves, we will model the neural responses as a 1D convolutional operation on the stimulus.

In this tutorial, we will
* Understand the basics of convolution
* Build and train a convolutional neural network to predict neural responses using PyTorch
* Visualize and analyze its internal representations

---
# Setup

```python
import os
import numpy as np
import torch
from torch import nn
from torch import optim
from matplotlib import pyplot as plt
```

```python
#@title Figure settings
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
#@title Data retrieval and loading
import hashlib
import requests

fname = "W3D4_stringer_oribinned6_split.npz"
url = "https://osf.io/p3aeb/download"
expected_md5 = "b3f7245c6221234a676b71a1f43c3bb5"

if not os.path.isfile(fname):
  try:
    r = requests.get(url)
  except requests.ConnectionError:
    print("!!! Failed to download data !!!")
  else:
    if r.status_code != requests.codes.ok:
      print("!!! Failed to download data !!!")
    elif hashlib.md5(r.content).hexdigest() != expected_md5:
      print("!!! Data download appears corrupted !!!")
    else:
      with open(fname, "wb") as fid:
        fid.write(r.content)
```

```python
#@title Helper Functions
# Some helper functions
def load_data_split(data_name=fname):
  """Load mouse V1 data from Stringer et al. (2019)

  Data from study reported in this preprint:
  https://www.biorxiv.org/content/10.1101/679324v2.abstract

  These data comprise time-averaged responses of ~20,000 neurons
  to ~4,000 stimulus gratings of different orientations, recorded
  through Calcium imaginge. The responses have been normalized by
  spontaneous levels of activity and then z-scored over stimuli, so
  expect negative numbers. The repsonses were split into train and
  test and then each set were averaged in bins of 6 degrees.

  This function returns the relevant data (neural responses and
  stimulus orientations) in a torch.Tensor of data type torch.float32
  in order to match the default data type for nn.Parameters in
  Google Colab.

  It will hold out some of the trials when averaging to allow us to have test
  tuning curves.

  Args:
    data_name (str): filename to load

  Returns:
    resp_train (torch.Tensor): n_stimuli x n_neurons matrix of neural responses,
        each row contains the responses of each neuron to a given stimulus.
        As mentioned above, neural "response" is actually an average over
        responses to stimuli with similar angles falling within specified bins.
    resp_test (torch.Tensor): n_stimuli x n_neurons matrix of neural responses,
        each row contains the responses of each neuron to a given stimulus.
        As mentioned above, neural "response" is actually an average over
        responses to stimuli with similar angles falling within specified bins
    stimuli: (torch.Tensor): n_stimuli x 1 column vector with orientation
        of each stimulus, in degrees. This is actually the mean orientation
        of all stimuli in each bin.

  """
  with np.load(data_name) as dobj:
    data = dict(**dobj)
  resp_train = data['resp_train']
  resp_test = data['resp_test']
  stimuli = data['stimuli']

  # Return as torch.Tensor
  resp_train_tensor = torch.tensor(resp_train, dtype=torch.float32)
  resp_test_tensor = torch.tensor(resp_test, dtype=torch.float32)
  stimuli_tensor = torch.tensor(stimuli, dtype=torch.float32)

  return resp_train_tensor, resp_test_tensor, stimuli_tensor

def plot_tuning(ax, stimuli, respi_train, respi_test, neuron_index, linewidth=2):
  """Plot the tuning curve of a neuron"""

  ax.plot(stimuli, respi_train, 'y', linewidth=linewidth)  # plot its responses as a function of stimulus orientation
  ax.plot(stimuli, respi_test, 'm', linewidth=linewidth)  # plot its responses as a function of stimulus orientation
  ax.set_title('neuron %i' % neuron_index)
  ax.set_xlabel('stimulus orientation ($^o$)')
  ax.set_ylabel('neural response')
  ax.set_xticks(np.linspace(0, 360, 5))
  ax.set_ylim([-0.5, 2.4])

# from bayes day!
def my_gaussian(x_points, mu, sigma):
  """
  Returns normalized Gaussian estimated at points `x_points`, with parameters: mean `mu` and std `sigma`

  Args:
    x_points (numpy array of floats): points at which the gaussian is evaluated
    mu (scalar): mean of the Gaussian
    sigma (scalar): std of the gaussian

  Returns:
    (numpy array of floats) : un-normalized Gaussian (i.e. without constant) evaluated at `x`
  """
  px = np.exp(- 1/2/sigma**2 * (mu - x_points) ** 2)
  px = px / px.sum()
  return px

def plot_conv(pad, stimulus, filter, conv_out):
  """ plot 1D convolution """
  # plot stimulus
  ax = fig.add_subplot(1,3,1)
  ax.plot(np.arange(0, 360), stimulus, 'k')
  ax.set_title('stimulus')
  ax.set_xlabel('orientation ($^o$)')
  ax.set_ylabel('stimulus')

  # plot convolutional filter
  ax = fig.add_subplot(1,3,2)
  ax.plot(np.arange(-pad, pad), filter)
  ax.set_xlabel('orientation ($^o$)')
  ax.set_ylabel('magnitude')
  ax.set_title('convolutional filter')

  # plot convolutional output
  ax = fig.add_subplot(1,3,3)
  n_units = (~np.isnan(conv_out)).sum()
  ax.scatter(np.arange(0,n_units),
             conv_out[~np.isnan(conv_out)], s=30,
             cmap='hsv', c=np.arange(0,n_units))
  ax.set_xlabel('convolutional unit')
  ax.set_ylabel('activation')
  ax.set_title('activations of\nconvolutional units')

def plot_example_activations(act):
  """ plot activations act and corresponding stimulus
  Args:
        act: activations of convolutional layer (n_bins x conv_channels x n_bins)
  """
  ns = [10,25,40]
  fig, axs = plt.subplots(1,3,figsize=(12,4))
  for k, (n, ax) in enumerate(zip(ns, axs.flatten())):
    ax.plot(n * np.ones(2), [act.min()*1.15, act.max()*1.15], 'k', linewidth=4)
    ax.plot(act[n].T, '.', linewidth=2)
    ax.set_xlabel('convolutional unit')
    ax.set_ylabel('activation')
    ax.set_title('stim id %d'%n)
    leg = ['chan%d'%i for i in range(act.shape[1])]
    leg.insert(0, 'stim')
    n_units = act.shape[0]
    for k,s in enumerate(leg):
      if k==0:
        ax.text(((n+15)%n_units)/n_units, .9-k*.1, s, transform=ax.transAxes, color='k', ha='center')
      else:
        ax.text(((n+15)%n_units)/n_units, .9-k*.1, s, transform=ax.transAxes, color='C%d'%(k-1), ha='center')

def train(net, custom_loss, train_data, train_labels,
          test_data=None, test_labels=None,
          learning_rate=10, n_iter=500, L2_penalty=0., L1_penalty=0.):
  """Run gradient descent for network without batches

  Args:
    net (nn.Module): deep network whose parameters to optimize with SGD
    custom_loss: loss function for network
    train_data: training data (n_train x input features)
    train_labels: training labels (n_train x output features)
    test_data: test data (n_train x input features)
    test_labels: test labels (n_train x output features)
    learning_rate (float): learning rate for gradient descent
    n_epochs (int): number of epochs to run gradient descent
    L2_penalty (float): magnitude of L2 penalty
    L1_penalty (float): magnitude of L1 penalty

  Returns:
    train_loss: training loss across iterations
    test_loss: testing loss across iterations

  """
  optimizer = optim.SGD(net.parameters(), lr=learning_rate, momentum=0.5) # Initialize PyTorch SGD optimizer
  train_loss = np.nan * np.zeros(n_iter)  # Placeholder for train loss
  test_loss = np.nan * np.zeros(n_iter)  # Placeholder for test loss

  # Loop over epochs
  for i in range(n_iter):
    y_pred = net(train_data) # Forward pass: compute predicted y by passing train_data to the model.

    if L2_penalty>0 or L1_penalty>0:
      weights = net.out_layer.weight
      loss = custom_loss(y_pred, train_labels, weights, L2_penalty, L1_penalty)
    else:
      loss = custom_loss(y_pred, train_labels)

    ### Update parameters
    optimizer.zero_grad() # zero out gradients
    loss.backward() # Backward pass: compute gradient of the loss with respect to model parameters
    optimizer.step() # step parameters in gradient direction
    train_loss[i] = loss.item()  # .item() transforms the tensor to a scalar and does .detach() for us

    # Track progress
    if (i+1) % (n_iter // 10) == 0 or i==0:
      if test_data is not None and test_labels is not None:
        y_pred = net(test_data)
        if L2_penalty>0 or L1_penalty>0:
          loss = custom_loss(y_pred, test_labels, weights, L2_penalty, L1_penalty)
        else:
          loss = custom_loss(y_pred, test_labels)
        test_loss[i] = loss.item()
        print(f'iteration {i+1}/{n_iter} | train loss: {train_loss[i]:.4f} | test loss: {test_loss[i]:.4f}')
      else:
        print(f'iteration {i+1}/{n_iter} | train loss: {train_loss[i]:.4f}')

  return train_loss, test_loss

def plot_pred_weights(y_pred, y_train, y_test, weights):
  """ plot example neural response prediction + weights """
  fig = plt.figure(figsize=(12,4))
  ax =fig.add_subplot(1,3,1)
  ax.plot(y_train, 'y', linewidth=1)
  ax.plot(y_test, 'm', linewidth=1)
  ax.plot(y_pred, 'g', linestyle='-', linewidth=3)
  ax.set_xlabel('stimulus bin')
  ax.set_ylabel('response')
  ax.text(0.1, 1.0, 'train', color='y', transform=ax.transAxes)
  ax.text(0.1, 0.9, 'test', color='m', transform=ax.transAxes)
  ax.text(0.1, 0.8, 'pred', color='g', transform=ax.transAxes)

  ax=fig.add_subplot(1,3,2)
  ax.plot(y_train, y_train, 'k', lw=1)
  ax.scatter(y_train, y_pred, s=8, color='y')
  ax.scatter(y_test, y_pred, s=8, color='m')
  ax.set_xlabel('neural response')
  ax.set_ylabel('predicted response', color='g')
  ax.text(0.1, 1.0, 'train', color='y', transform=ax.transAxes)
  ax.text(0.1, 0.9, 'test', color='m', transform=ax.transAxes)
  plt.axis('square')

  ### plot weights of fully-connected layer for first 300 neurons
  plt.subplot(1,3,3)
  plt.imshow(weights, aspect='auto', cmap='bwr', vmin=-0.01,vmax=0.01)
  plt.title('out_layer weights')
  plt.ylabel('neurons')
  plt.xlabel('convolutional units')
  plt.colorbar()
  plt.tight_layout()
  plt.show()

def plot_prediction(ax, y_pred, y_test):
  """ plot prediction of neural response + test neural response """
  ax.plot(y_test,color='m')
  ax.plot(y_pred, 'g', linewidth=3)
  ax.set_xlabel('stimulus bin')
  ax.set_ylabel('response')

def plot_training_curves(train_loss, test_loss):
  f, ax = plt.subplots()
  ax.plot(train_loss, 'y', label="Train loss")
  ax.plot(test_loss, '.', markersize=10, color='m', label="Test loss")
  ax.set(xlabel="Gradient descent iteration", ylabel="Mean squared error")
  plt.legend()
```

---
# Section 1: Neural Tuning Curves

In the next cell, we plot the turning curves of a random subset of neurons. We have binned the stimuli orientations more than in Tutorial 1.

Rerun the cell to look at different example neurons and observe the diversity of tuning curves in the population. How can we fit these neural responses with an encoding model?

```python
#@title
#@markdown Execute this cell to load data and plot neural tuning curves

### Load data and bin at 8 degrees
# responses are split into test and train
resp_train, resp_test, stimuli = load_data_split()
n_stimuli, n_neurons = resp_train.shape
print('resp_train contains averaged responses of %i neurons to %i binned stimuli' % (n_neurons, n_stimuli))
#print(resp_train.shape)

# also make stimuli into array of 0's and 1's
n_bins = len(stimuli)
stim_binary = torch.eye(n_bins, dtype=torch.float32)

# Visualize tuning curves
fig, axs = plt.subplots(3, 5, figsize=(15,7))
for k, ax in enumerate(axs.flatten()):
  neuron_index = np.random.choice(n_neurons)  # pick random neuron
  plot_tuning(ax, stimuli, resp_train[:, neuron_index], resp_test[:, neuron_index], neuron_index, linewidth=2)
  if k==0:
    ax.text(1.0, 0.9, 'train', color='y', transform=ax.transAxes)
    ax.text(1.0, 0.65, 'test', color='m', transform=ax.transAxes)
fig.tight_layout()
plt.show()
```

---
# Section 2: Introduction to convolutions

```python
#@title Video 1: Intro to convolutions
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="vPNu8CNg9i4", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

## Section 2.1: 1d convolution in numpy

We provide an example function below, described in the video, which performs a 1D convolution of a stimulus input $s$ with **filter** $f$ of size $K$ (these filters are also called *kernels*). In particular, it computes:

$$a_x = \sum^{K/2}_{i=-K/2} f_i \, s_{x-i}$$

where $a_x$ is the convolutional output at position $x$.

There is no exercise in this section but make sure you understand what is happening in the function below (i.e. what a convolution is). It can be helpful to write or draw this out on paper to clarify! You could even make your own short stimulus and filter and calculate what you think the convolutional output should be by hand, and then compare to the function output.

```python
def convolve1d(stimulus, f, pad, stride):
  """ Pads stimulus and performs 1d convolution

  Args:
    stimulus (ndarray): the 1D input for the convolution
    f (ndarray): the 1D filter for the convolution
    pad (scalar): the amount of zero padding for the stimulus
    stride (scalar): how far the filter moves every step

  Returns:
    (ndarray): convolutional output, same size as stimulus
  """

  # Pad the stimulus
  zero_pads = np.zeros(pad)
  padded_stimulus = np.concatenate((zero_pads, stimulus, zero_pads))

  # Initialize convolutional output
  a = np.nan * np.zeros(360)

  # Compute the convolution
  for x in np.arange(0+pad, 360+pad, stride, int): # loop over positions x

    # Compute element-wise multiplication between filter and stimulus
    a[x - pad] = (f * padded_stimulus[x-pad : x+pad]).sum()

  return a
```

```python
#@title
#@markdown Execute this cell to call convolve1d with a Gaussian filter and plot results

# Convolutional parameters
K = 49 # size of convolutional filter
stride = 1 # how often to compute the convolution along the stim axis
pad = K // 2 # we will need to pad stimulus with zeros to perform convolution

# Create stimulus
ori = 135
stimulus = np.zeros(360)
stimulus[ori] = 1.0

# Create Gaussian filter
# we will use the code from W2D1 (bayes day) to create this!
# mean of gaussian mu=0
i = np.arange(-pad, pad)
f = my_gaussian(i, 0.0, sigma=10)

# Call function
a = convolve1d(stimulus, f, pad, stride)

# Plot results
fig = plt.figure(figsize=(15,4))
plot_conv(pad, stimulus, f, a)
plt.show()
```

## Section 2.2: Convolutional layer

You have just learned how to compute what is called a single convolutional **channel**: a single filter applied to the input resulting in several units, where the number of units depends on the *stride* you set.

(Note if filter size *K* is odd and you set the *pad=K//2* and *stride=1* (as is the default above), you get a **channel** of units that is the same size as the input.)

*Contemplation:* How does a neuron potentially combine those activation units and create the tuning curves they have? Will we need more than one convolutional filter to recreate all the responses we see?

```python
# to_remove explanation

"""
We can think of all neurons being composed of the same shaped peak but at
different/multiple locations for each neuron. However, it appears that different
neurons have peaks that are shaped differently, or even the same neuron has two
peaks with different shapes. Therefore we may want multiple different convolutional
filters.
""";
```

Let's add more convolutional channels and implement this operation efficiently using pytorch. A *layer* of convolutional channels can be implemented with one line of code using the PyTorch class `nn.Conv1d()`, which requires the following arguments for initialization:
  * $C^{in}$: the number of input channels
  * $C^{out}$: the number of output channels (number of different convolutional filters)
  * $K$: the size of the $C^{out}$ different convolutional filters

When you run the network, you can input a stimulus of arbitrary length ($H^{in}$), but it needs to be shaped as a 2D input $C^{in} \times H^{in}$. In our case, $C^{in}=1$ because there is only one orientation input and $H^{in}$ is the number of stimulus bins $B$.

```python
class ConvolutionalLayer(nn.Module):
  """Deep network with one convolutional layer
     Attributes: conv (nn.Conv1d): convolutional layer
  """
  def __init__(self, c_in=1, c_out=8, K=9):
    """Initialize layer

    Args:
        c_in: number of input stimulus channels
        c_out: number of output convolutional channels
        K: size of each convolutional filter

    """
    super().__init__()
    self.conv = nn.Conv1d(c_in, c_out, kernel_size=K,
                          padding=K//2, stride=1)

  def forward(self, s):
    """Run stimulus through convolutional layer

    Args:
        s (torch.Tensor): n_stimuli x h tensor with stimuli

    Returns:
        (torch.Tensor): n_stimuli x c_out x h tensor with convolutional layer unit activations.

    """
    s = s.unsqueeze(1)  # n_stimuli x 1 x h, add a singleton dimension for the single channel
    a = self.conv(s)  # output of convolutional layer

    return a
```

### Exercise 1: 1D convolution in pytorch

We will now run the convolutional layer on our stimulus. In particular, we will use the binary stimuli (`stim_binary`), which is a 60 x 60 tensor where each row contains the binary stimuli for one orientation (all zeros except for a one at that orientation). This tensor is size 60 instead of 360 because we have binned the orientations. Each row of this matrix is a different example orientation that we want to convolve - see cell below to visualize three rows of this tensor.

```python
# @markdown Execute this cell to visualize stim_binary
row_inds = [10, 25, 40]
fig = plt.figure(figsize=(15,4))

for j, row_ind in enumerate(row_inds):
  ax = fig.add_subplot(1, 3, j+1)
  ax.plot(np.arange(0, 60), stim_binary[row_ind,:], 'k')
  ax.set_title('stim_binary row '+str(row_ind))
  ax.set_xlabel('orientation bin')
  ax.set_ylabel('stimulus')

plt.show()
```

`nn.Conv1d` takes in a tensor of size $(N, C^{in}, H^{in}$) where $N$ is the number of examples, $C^{in}$ is the number of input channels, and $H^{in}$ is the number of stimulus bins $B$. Since our stimulus has only one input channel,  the `ConvolutionalLayer` class adds the $C^{in}$ dimension for us: we need to input an $(N, H^{in})$ stimulus, which `stim_binary` is!

We will plot the outputs of the convolution. `convout` is a tensor of size $(N, C^{out}, H^{in})$ where $N$ is the number of examples and $C^{out}$ are the number of convolutional channels. In the plot, the activations for a single channel are shown in one color.

```python
# Convolution layer parameters
K = 9 # filter size, now that we've binned let's make this smaller than for the numpy conv
conv_channels = 8 # how many convolutional channels to have in our layer

convout = np.zeros(0) # assign convolutional activations to convout

################################################################################
## TODO for students: compute convolution activations from stim_binary using pytorch
# Complete and uncomment
################################################################################

# Initialize conv layer
# convLayer = ConvolutionalLayer(...)

# Call conv layer on stimulus
# convout = convLayer(...)
# convout = convout.detach() # detach gradients
# print(convout.shape) # can you identify what each of these dimensions are?

# Plot results
# plot_example_activations(convout)
```

```python
# to_remove solution

# Convolution layer parameters
K = 9 # filter size, now that we've binned let's make this smaller than for the numpy conv
conv_channels = 8 # how many convolutional channels to have in our layer

convout = np.zeros(0) # assign convolutional activations to convout

# Initialize conv layer
convLayer = ConvolutionalLayer(c_in=1, c_out=conv_channels, K=K)

# Call conv layer on stimulus
convout = convLayer(stim_binary)
convout = convout.detach() # detach gradients
print(convout.shape) # can you identify what each of these dimensions are?

# Plot results
with plt.xkcd():
  plot_example_activations(convout)
```

#### Think!
  - Why are the convolutional activations for a given channel the same for many units?
  - What is the width of the non-constant activations (i.e. how many units in a given channel would differ from the constant)?
  - How many weights does this convLayer have?
  - How many would it have if it were a fully connected layer?

```python
# to_remove explanation

"""
1. As seen above, the stimulus is zero everywhere except for in one bin, and so
the convolutional activations are constant everywhere except for around the stimulus.
The constant offset is from a bias term that each convolutional channel has.
2. The width of non-constant activations is the kernel size K.
3. The convLayer has K*C_out weights.
4. A fully connected layer would have (B^2)*C_out weights.
""";
```

---
# Section 3: Encoding model using convolutions

```python
#@title Video 2: Encoding model using convolutions
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="Me8X3Kro0EE", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

## Section 3.1: Convolutional layer & fully connected layer

We will now build an encoding model by hooking this convolutional layer up to a fully connected layer, like the one that we used in Tutorial 1 (`nn.Linear`). We will use this model to predict neural responses.

This linear layer will have weights $W^{out}$ and we will get an output vector $\mathbf{y}$ of predicted neural responses.

### Exercise 2: Implement encoding model

In this exercise, you will create the encoding model described above. In particular, you will:

* Add a fully connected layer to `__init__` method of network.
* Add a fully connected layer to `forward` method of network.

We will then train the network using the helper function `train`. Full training will take a few minutes: if you want to train for just a few steps to speed up the code while iterating on your code, you can decrease the `n_iter` input from 500.

```python
class ConvFC(nn.Module):
  """Deep network with one convolutional layer + one fully connected layer

  Attributes:
    conv (nn.Conv1d): convolutional layer
    dims (tuple): shape of convolutional layer output
    out_layer (nn.Linear): linear layer

  """

  def __init__(self, n_neurons, c_in=1, c_out=8, K=9, b=60):
    """ initialize layer
    Args:
        c_in: number of input stimulus channels
        c_out: number of convolutional channels
        K: size of each convolutional filter
        h: number of stimulus bins, n_bins
    """
    super().__init__()
    self.conv = nn.Conv1d(c_in, c_out, kernel_size=K, padding=K//2)
    self.dims = (c_out, b)  # dimensions of conv layer output
    M = np.prod(self.dims) # number of hidden units

    ################################################################################
    ## TO DO for students: add fully connected layer to network (self.out_layer)
    # Fill out function and remove
    raise NotImplementedError("Student exercise: add fully connected layer to initialize network")
    ################################################################################
    self.out_layer = nn.Linear(M, ...)

    nn.init.normal_(self.out_layer.weight, std=0.01) # initialize weights to be small

  def forward(self, s):
    """ Predict neural responses to stimuli s

    Args:
        s (torch.Tensor): p x L tensor with stimuli

    Returns:
        torch.Tensor: p x N tensor with convolutional layer unit activations.

    """
    s = s.unsqueeze(1)  # p x 1 x L, add a singleton dimension for the single channel
    a = self.conv(s)  # output of convolutional layer
    a = a.view(-1, np.prod(self.dims))  # flatten each convolutional layer output into a vector

    ################################################################################
    ## TO DO for students: add fully connected layer to forward pass of network (self.out_layer)
    # Fill out function and remove
    raise NotImplementedError("Student exercise: add fully connected layer to network")
    ################################################################################
    y = ...

    return y

# Choose loss function
MSE_loss = nn.MSELoss()

## Initialize network
# net = ConvFC(n_neurons)

## Run GD on training set data
## ** this time we are also providing the test data to estimate the test loss
# train_loss, test_loss = train(net, MSE_loss, stim_binary, resp_train,
#                               test_data=stim_binary, test_labels=resp_test,
#                               n_iter=500, learning_rate=20)

## Plot the training loss over iterations of GD
# plot_training_curves(train_loss, test_loss)
```

```python
# to_remove solution
class ConvFC(nn.Module):
  """Deep network with one convolutional layer + one fully connected layer

  Attributes:
    conv (nn.Conv1d): convolutional layer
    dims (tuple): shape of convolutional layer output
    out_layer (nn.Linear): linear layer

  """

  def __init__(self, n_neurons, c_in=1, c_out=8, K=9, b=60):
    """ initialize layer
    Args:
        c_in: number of input stimulus channels
        c_out: number of convolutional channels
        K: size of each convolutional filter
        h: number of stimulus bins, n_bins
    """
    super().__init__()
    self.conv = nn.Conv1d(c_in, c_out, kernel_size=K, padding=K//2)
    self.dims = (c_out, b)  # dimensions of conv layer output
    M = np.prod(self.dims) # number of hidden units

    self.out_layer = nn.Linear(M, n_neurons)

    nn.init.normal_(self.out_layer.weight, std=0.01) # initialize weights to be small

  def forward(self, s):
    """ Predict neural responses to stimuli s

    Args:
        s (torch.Tensor): p x L tensor with stimuli

    Returns:
        torch.Tensor: p x N tensor with convolutional layer unit activations.

    """
    s = s.unsqueeze(1)  # p x 1 x L, add a singleton dimension for the single channel
    a = self.conv(s)  # output of convolutional layer
    a = a.view(-1, np.prod(self.dims))  # flatten each convolutional layer output into a vector

    y = self.out_layer(a)

    return y

# Choose loss function
MSE_loss = nn.MSELoss()

# Initialize network
net = ConvFC(n_neurons)

# Run GD on training set data
# ** this time we are also providing the test data to estimate the test loss
train_loss, test_loss = train(net, MSE_loss, stim_binary, resp_train,
                              test_data=stim_binary, test_labels=resp_test,
                              n_iter=500, learning_rate=20)

# Plot the training loss over iterations of GD
with plt.xkcd():
  plot_training_curves(train_loss, test_loss)
```

We trained this network to predict the neural responses -- see the yellow curve for the training loss. We also computed the test loss every 50 iterations. The training loss goes down throughout training but the testing loss doesn’t -- why is this? *We are overfitting to the NOISE in the training set.*

Let’s look at a prediction for a single neuron (below). The yellow curve is the training data, the pink curve is the testing data and the prediction is in green. You can barely see the yellow curve because the prediction has fit so well to the training data. However, some of what it has fit is noise.

If we look at the weight matrix, we see that the weights are all positive or negative. Did we expect this to happen? Or did we think that this tuning curve is the sum of only a few filters and positions?

```python
# to_remove explanation

"""
We see many sharp peaks in the training set, but these peaks are not always present
in the test set, suggesting that they are due to noise. Therefore, ignoring this
noise, we might have expected the neural responses to be a sum of only a few
different filters and only at a few different positions. This would mean that we
would expect the `out_layer` weight matrix to be sparse, not dense like the one
shown here.
""";
```

```python
#@title
#@markdown Execute this cell to examine prediction for example neuron and see weights

# Input stimuli to network
y_pred = net(stim_binary)
print('output shape: ', y_pred.shape) # what are the two dimensions of this network output?

# Plot example neural response prediction and some fully-connected layer weights

# Look at the weights of the out_layer of the network
weights = net.out_layer.weight.detach()
print('output weights shape: ', weights.shape) # what are these two dimensions of the fully connected layer weights?

# Plot prediction + neuron + weights
neuron_index = np.random.choice(n_neurons)
plot_pred_weights(y_pred[:,neuron_index].detach(), resp_train[:,neuron_index],
                  resp_test[:,neuron_index], weights[:20])
```

*Comprehension check*: what does each dimension of the output and output weights correspond to?

```python
# to_remove explanation

"""
The output is shape 60 by 23589 where 60 is the number of stimulus bins and 23589
is the number of predicted neurons. The out_layer weights have shape 23589 by 480
where 23589 is the number of predicted neurons and 480 is the number of convolutional
units in the conv layer. There are 480 convolutional units because the stimulus
is length 60 and we used 8 $C^{out}$ convolutional channels.
""";
```

We can reduce overfitting using L2 regularization as we learned on W1D4.

Additionally there is another type of regularization you might want... If we think of a neuron as a sum of a few convolutional filters, we might expect the weight matrix of the fully-connected layer to be sparse. Therefore, we can also apply an L1 regularization penalty to enforce sparsity.

---
# (Bonus) Section 4: Regularization

```python
#@title Video 3: Regularization
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="Qnn5OPHKo5w", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

As discussed in the lecture, it is often important to incorporate regularization terms into the loss function to avoid overfitting. In particular, in this case, we want to use these terms to enforce sparsity in the output layer.

Here we'll consider the classic L2 regularization penalty $\mathcal{R}_{L2}$, which is the sum of squares of each weight in the network $\sum_{ij} {\mathbf{W}^{out}_{ij}}^2$ times a constant that we call `L2_penalty`.

We will also add an L1 regularization penalty $\mathcal{R}_{L1}$ to enforce sparsity of the weights, which is the sum of the absolute values of the weights $\sum_{ij} |{\mathbf{W}^{out}_{ij}}|$ times a constant that we call `L1_penalty`.

We will add both of these to the loss function:
\begin{equation}
    L = (y - \tilde{y})^2 + \mathcal{R}_{L2} + \mathcal{R}_{L1}
\end{equation}

The parameters `L2_penalty` and `L1_penalty` are inputs to the train function.

### (Bonus) Exercise 3: Add regularization to training

We will create a new loss function that adds L1 and L2 regularization.
In particular, you will:
* add L2 loss penalty to the weights
* add L1 loss penalty to the weights

We will then train the network using this loss function. Full training will take a few minutes: if you want to train for just a few steps to speed up the code while iterating on your code, you can decrease the n_iter input from 500.

Hint: since we are using `torch` instead of `np`, we will use `torch.abs` instead of `np.absolute`. You can use `torch.sum` or `.sum()` to sum over a tensor.

```python
def regularized_MSE_loss(output, target, weights=None, L2_penalty=0, L1_penalty=0):
  """loss function for MSE

  Args:
    output (torch.Tensor): output of network
    target (torch.Tensor): neural response network is trying to predict
    weights (torch.Tensor): fully-connected layer weights (net.out_layer.weight)
    L2_penalty : scaling factor of sum of squared weights
    L1_penalty : scalaing factor for sum of absolute weights

  Returns:
    (torch.Tensor) mean-squared error with L1 and L2 penalties added

  """

  loss_fn = nn.MSELoss()
  loss = loss_fn(output, target)

  ##############################################################################
  # TO DO: add L1 and L2 regularization to the loss function and remove the error
  raise NotImplementedError("Student exercise: complete regularized_MSE_loss")
  ##############################################################################

  if weights is not None:
    L2 = L2_penalty * ...
    L1 = L1_penalty * ...
    loss += L1 + L2

  return loss

# Initialize network
net = ConvFC(n_neurons)

# Uncomment below to test your function

# Train network
# train_loss, test_loss = train(net, regularized_MSE_loss, stim_binary, resp_train,
#                               test_data=stim_binary, test_labels=resp_test,
#                               learning_rate=10, n_iter=500,
#                               L2_penalty=1e-4, L1_penalty=1e-6)

# Plot the training loss over iterations of GD
# plot_training_curves(train_loss, test_loss)
```

```python
# to_remove solution
def regularized_MSE_loss(output, target, weights=None, L2_penalty=0, L1_penalty=0):
  """loss function for MSE

  Args:
    output (torch.Tensor): output of network
    target (torch.Tensor): neural response network is trying to predict
    weights (torch.Tensor): fully-connected layer weights of network (net.out_layer.weight)
    L2_penalty : scaling factor of sum of squared weights
    L1_penalty : scalaing factor for sum of absolute weights

  Returns:
    (torch.Tensor) mean-squared error with L1 and L2 penalties added

  """

  loss_fn = nn.MSELoss()
  loss = loss_fn(output, target)

  if weights is not None:
    L2 = L2_penalty * torch.square(weights).sum()
    L1 = L1_penalty * torch.abs(weights).sum()
    loss += L1 + L2

  return loss

# Initialize network
net = ConvFC(n_neurons)

# Train network
train_loss, test_loss = train(net, regularized_MSE_loss, stim_binary, resp_train,
                              test_data=stim_binary, test_labels=resp_test,
                              learning_rate=10, n_iter=500,
                              L2_penalty=1e-4, L1_penalty=1e-6)

# Plot the training loss over iterations of GD
with plt.xkcd():
  plot_training_curves(train_loss, test_loss)
```

If we now train the network with these regularization penalties we find that the train and test loss are similar throughout training: both continue decreasing.

We will now look at the predictions after using the regularized loss function. We can see below that the prediction is much smoother than before! This is because the weight matrix is in fact sparser (zero is represented by white in this color map)

```python
#@title
#@markdown Execute this cell to examine prediction for example neuron and see weights

# Plot prediction + neuron + weights
weights = net.out_layer.weight.detach()
y_pred = net(stim_binary)
neuron_index = np.random.choice(n_neurons)
plot_pred_weights(y_pred[:,neuron_index].detach(), resp_train[:,neuron_index],
                  resp_test[:,neuron_index], weights[:20])
```

Now let's look at what the predictions look like for many neurons

```python
#@title
#@markdown Execute this cell to examine predictions for random subsets of neurons

# Visualize tuning curves & plot neural predictions
fig, axs = plt.subplots(2, 5, figsize=(15,6))
for k, ax in enumerate(axs.flatten()):
  ineur = np.random.choice(n_neurons)
  plot_prediction(ax, y_pred[:,ineur].detach(), resp_test[:,ineur])
  if k==0:
    ax.text(.1, 1., 'test', color='m', transform=ax.transAxes)
    ax.text(.1, .9, 'prediction', color='g', transform=ax.transAxes)

fig.tight_layout()
plt.show()
```

---
# Summary

In this notebook, we built and evaluated a neural network based encoding model to predict neural responses from stimuli. To do so we :
* implemented a basic convolution filter
* implemented and trained a convolutional neural network with multiple filters to predict neural responses using PyTorch
* learned about and implemented L2/L1 regularization to avoid overfitting

What can this tell us about the representation of oriented gratings in mouse visual cortex? Maybe we can think of interpreting each of the convolutional channels as a computation performed by a single group of neurons in thalamus, and each visual cortical neuron combines various groups of thalamic neurons. But we'd have to test hypotheses like these by, for instance, recording thalamic neurons.

Tutorial 3 is bonus, although we recommend watching the videos if possible!

---
# Appendix

```python
#@title Video 4: Some practical advice for fitting neural networks
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="eU74NFroIHk", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

## Why CNN's?

CNN models are particularly [well-suited](https://www.nature.com/articles/nn.4244) to modeling the visual system for a number of reasons:

1. **Distributed computation**: like any other neural network, CNN's use distributed representations to compute -- much like the brain seems to do. Such models, therefore, provide us with a vocabulary with which to talk and think about such distributed representations. Because we know the exact function the model is built to perform (e.g. orientation discrimination), we can analyze its internal representations with respect to this function and begin to interpret why the representations look the way they do. Most importantly, we can then use these insights to analyze the structure of neural representations observed in recorded population activity. We can qualitatively and quantitatively compare the representations we see in the model and in a given population of real neurons to hopefully tease out the computations it performs.

2. **Hierarchical architecture**: like in any other deep learning architecture, each layer of a deep CNN comprises a non-linear transformation of the previous layer. Thus, there is a natural hierarchy whereby layers closer to the network output represent increasingly more abstract information about the input image. For example, in a network trained to do object recognition, the early layers might represent information about edges in the image, whereas later layers closer to the output might represent various object categories. This resembles the [hierarchical structure of the visual system](https://pubmed.ncbi.nlm.nih.gov/1822724/), where [lower-level areas](https://www.jneurosci.org/content/25/46/10577.short) (e.g. retina, V1) represent visual features of the sensory input and [higher-level areas](https://www.sciencedirect.com/science/article/pii/S089662731200092X) (e.g. V4, IT) represent properties of objects in the visual scene. We can then naturally use a single CNN to model multiple visual areas, using early CNN layers to model lower-level visual areas and late CNN layers to model higher-level visual areas.

  Relative to fully connected networks, CNN's, in fact, have further hierarchical structure built-in through the max pooling layers. Recall that each output of a convolution + pooling block is the result of processing a local patch of the inputs to that block. If we stack such blocks in a sequence, then the outputs of each block will be sensitive to increasingly larger regions of the initial raw input to the network: an output from the first block is sensitive to a single patch of these inputs, corresponding to its "receptive field"; an output from the second block is sensitive to a patch of outputs from the first block, which together are sensitive to a larger patch of raw inputs comprising the union of their receptive fields. Receptive fields thus get larger for deeper layers (see [here](http://colah.github.io/posts/2014-07-Conv-Nets-Modular/) for a nice visual depiction of this). This resembles primate visual systems, where neurons in higher-level visual areas respond to stimuli in wider regions of the visual field than neurons in lower-level visual areas.

3. **Convolutional layers**: through the weight sharing constraint, the outputs of each channel of a convolutional layer process different parts of the input image in exactly the same way. This architectural constraint effectively builds into the network the assumption that objects in the world typically look the same regardless of where they are in space. This is useful for modeling the visual system for two (largely separate) reasons:
  * Firstly, this assumption is generally valid in mammalian visual systems, since mammals tend to view the same object from many perspectives. Two neurons at a similar hierarchy in the visual system with different receptive fields could thus end up receiving statistically similar synaptic inputs, so that the synaptic weights developed over time may end up being similar as well.
  * Secondly, this architecture significantly improves object recognition ability. Object recognition was essentially an unsolved problem in machine learning until the [advent](https://en.wikipedia.org/wiki/AlexNet) of techniques for effectively training *deep* convolutional neural networks. Fully connected networks on their own can't achieve object recognition abilities anywhere close to human levels, making them bad models of human object recognition. Indeed, it is generally the case that [the better a neural network model is at object recognition, the closer the match between its representations and those observed in the brain](https://www.pnas.org/content/111/23/8619.short). That said, it is worth noting that our much simpler orientation discrimination task (in Tutorial 3) can be solved by relatively simple networks.


# Applied Stochastic Processes HW_08

**11510691 程远星$\DeclareMathOperator*{\argmin}{argmin}
\DeclareMathOperator*{\argmax}{argmax}
\DeclareMathOperator*{\plim}{plim}
\newcommand{\using}[1]{\stackrel{\mathrm{#1}}{=}}
\newcommand{\ffrac}{\displaystyle \frac}
\newcommand{\asim}{\overset{\text{a}}{\sim}}
\newcommand{\space}{\text{ }}
\newcommand{\bspace}{\;\;\;\;}
\newcommand{\QQQ}{\boxed{?\:}}
\newcommand{\void}{\left.\right.}
\newcommand{\Tran}[1]{{#1}^{\mathrm{T}}}
\newcommand{\d}[1]{\displaystyle{#1}}
\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{\dd}{\mathrm{d}}
\newcommand{\Exp}{\mathrm{E}}
\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{\Var}[2][\,\!]{\mathrm{Var}_{#1}\left[#2\right]}
\newcommand{\Avar}[2][\,\!]{\mathrm{Avar}_{#1}\left[#2\right]}
\newcommand{\Cov}[2][\,\!]{\mathrm{Cov}_{#1}\left(#2\right)}
\newcommand{\Corr}[2][\,\!]{\mathrm{Corr}_{#1}\left(#2\right)}
\newcommand{\I}[1]{\mathrm{I}\left( #1 \right)}
\newcommand{\N}[1]{\mathcal{N} \left( #1 \right)}
\newcommand{\ow}{\text{otherwise}}
\void^\dagger$**

## Question 1

$\P{1}$

$$\pi_0 = 0.25 \times \pi_0^0 + 0.75 \times \pi_0^2 \Rightarrow \pi_0 = \ffrac{1}{3} < 1$$

$\P{2}$

$$\mu = 0.5\times 1 + 0.25 \times 2 = 1 \Rightarrow \pi_0 = 1 $$

$\P{3}$

$$\pi_0 = \ffrac{1}{6} + 0.5 \pi_0 + \ffrac{1}{3}\pi_0^3 \Rightarrow \pi_0 = \ffrac{\sqrt 3-1} {2} < 1$$

## Question 2

$\P{1}$

$$\begin{align}
P\CB{\tau \leq 2} &= P\CB{\tau = 1} + P\CB{\tau = 2 \mid \tau \neq 1} \\
&= P_0 + P\CB{X_1 = j,X_2 = 0,j\geq1}\\
&= p + \sum_{j=1}^{\infty} p\P{1-p}^j \cdot p^j\\
&= p\P{\sum_{j=0}^{\infty} \P{p\P{1-p}}^j} \\
&= \ffrac{p}{1-p+p^2}
\end{align}$$

$$\begin{align}
P\CB{\tau = 2} &= P\CB{X_1 = j,X_2 = 0,j\geq1} \\
&= P\CB{\tau \leq 2} - p\\
&= \ffrac{p^2 - p^3}{1-p+p^2}
\end{align}$$

$\P{2}$

$\bspace$First we need to find the $\mu$

$$\begin{align}
\mu &= \sum_{j=0}^{\infty} j \cdot p\P{1-p}^j \\
&= p\P{\sum_{j=1}^{\infty} j\cdot\P{1-p}^j }\\
\P{1-p}\mu &= p\P{\sum_{j=2}^{\infty} \P{j-1}\cdot\P{1-p}^j}
\end{align}$$

$$\begin{align}
\mu &= \ffrac{1}{p}\P{\mu - \P{1-p}\mu}\\
&= \ffrac{1}{p}\cdot p\cdot\P{ \sum_{j=1}^{\infty}\P{1-p}^j }\\
&= \ffrac{1-p} {p}
\end{align}$$

$\bspace$So that when $p\geq0.5$, $\mu \leq 1$ and consequently $\pi_0 = 1$. Then when $0<p <0.5$, we have the equation for $\pi_0$

$$\begin{align}\pi_0 &= p + p\P{1-p}\pi_0 + p\P{1-p}^2\pi_0^2 + \cdots + p\P{1-p}^n\pi_0^n+\cdots\\
&= p\P{\sum_{j=0}^{\infty}\P{\P{1-p}\pi_0}^j}\\
&\bspace \text{given that }\P{1-p}\pi_0<1,\\
\pi_0 &=  \ffrac{p} {1-\P{1-p}\pi_0}\\
\Rightarrow \pi_0 &= \ffrac{p} {1-p} < 1
\end{align}$$

And $\pi_0 = \ffrac{p} {1-p}$ is not contradictory to the assumption that $\P{1-p}\pi_0<1$. So that's the answer when $0<p < 0.5$.

## Question 3

$\bspace$We can write the distribution of $P_j$ first

$$\begin{array}{c|c|c} \hline
P_0 & P_1 & P_2 \\ \hline
\P{1-p}^2 & 2p\P{1-p} & p^2 \\ \hline
\end{array}$$

$\bspace$Then $\mu = 2p\P{1-p} + 2p^2 = 2p$, greater than $1$ only when $p > 0.5$.

$\P{1.a}$

$\bspace$As discussed, when $0
 0.5$, we have the equation

$$\pi_0 = \P{1-p}^2 + 2p\P{1-p} \pi_0 + p^2\pi_0^2 \Rightarrow \pi_0 = \P{\ffrac{1-p} {p}}^2 <1$$

$\P{1.b}$

$\bspace$We first focus on the second generation, where we have the probability table of the individuals left.

$$\begin{array}{c|c|c|c} \hline
4 & 3 & 2 & 1 \\ \hline
p^6 & 2p^5\P{1-p} & 2p^5\P{1-p}^3 + 6p^4\P{1-p^2} & 4p^2\P{1-p}^2 + 6p^3\P{1-p}^3 \\ \hline
\end{array}$$

$\bspace$Then the probability of first extinction in the third generation can be obtained by

$$\begin{align}
&\P{1-p}^8\cdot p^6 + \P{1-p}^6\cdot 2p^5\P{1-p} + \P{1-p}^4\cdot \P{2p^5\P{1-p}^3 + 6p^4\P{1-p^2}}\\
&\bspace+ \P{1-p}^2\cdot \P{4p^2\P{1-p}^2 + 6p^3\P{1-p}^3}\\
=&\;\P{1-p}^8\cdot p^6 + 4\P{1-p}^7\cdot p^5 + 6\P{1-p}^6\cdot p^4 + 6\P{1-p}^5\cdot p^3 + 4\P{1-p}^4\cdot p^2
\end{align}$$

$\P{2}$

$\bspace$Here when $0
0.5$, we can calculate the extinction probability condition on $\lambda$:

$$\begin{align}
&P\CB{\text{extinction}}\\
=&\; \sum_{j = 0}^{\infty} P\CB{\text{extinction}\mid \lambda = j}\cdot P\CB{\lambda = j}\\
=&\; \sum_{j = 0}^{\infty} \P{\ffrac{1-p} {p}}^{2j} e^{-\lambda} \ffrac{\lambda^j} {j!}\\
=&\; e^{-\lambda} \sum_{j = 0}^{\infty} \left.\P{\P{\ffrac{1-p}{p}}^2\lambda}^j\right/ {j!}
\end{align}$$

$\bspace$Since $e^x = 1+x+\ffrac{x^2} {2!} + \cdots+\ffrac{x^n} {n!}+\cdots$, we have the preceding results simplified as

$$P\CB{\text{extinction}} = \exp\CB{\P{\ffrac{1-p}{p}}^2\lambda - \lambda}$$

***

Probability and Statistics/Applied Random Process/HW/HW_08.ipynb