# Construcción Curva OIS

```python
from IPython.display import Image
```

En el notebook anterior vimos que el valor de un derivado perfectamente colateralizado se obtiene descontando su payoff con un factor de descuento que proviene de la curva asociada a la tasa de remuneración del colateral.

Dicha tasa de remuneración es, en la gran mayoría de los casos, una tasa overnight. En el caso USD, para los CSA se utiliza *Effective Fed Funds* mientras que las contrapartes centrales ya migraron a *SOFR* y se espera que esto también ocurra para los CSAs.

A continuación se verá cómo construir la curva asociada a *SOFR* a partir de cotizaciones de swaps de *SOFR* versus tasa fija. El procedimiento se conoce como **bootstrapping** por el hecho que la construcción de la curva se hace de forma incremental, partiendo de una solución parcial de fácil obtención.

## Bootstrapping para OIS

Tenemos que (notebook 5):

$$
\begin{equation}
\Pi\left(t,X\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T\right)\Pi\left(T,X\right)\right]
\end{equation}
$$

$$
\begin{equation}
D_c\left(t,T\right)=\exp\left[-\int_t^Tr_c\left(u\right)du\right]
\end{equation}
$$

$$
\begin{equation}
\mathbb{E}_t^{Q_f}\left[D_c\left(t,T\right)\right]=P_c\left(t,T\right)
\end{equation}
$$

$$
\begin{equation}
D_c\left(t,T\right)={B_c\left(t,T\right)}^{-1}
\end{equation}
$$

Consideremos el valor de la pata fija de un OIS. Para fijar las ideas, supongamos una pata a 2Y con cupones anuales. Suponemos, adicionalmente que el valor del nocional es 1 y que éste se paga al vencimiento. El valor de la pata fija es:

$$
\begin{equation}
\Pi^{fija}\left(t\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_1\right)\cdot r\cdot\frac{T_1-T_0}{360}+D_c\left(t,T_2\right)\left(1+r \cdot \frac{T_2-T_1}{360}\right)\right]
\end{equation}
$$

$$
\begin{equation}
\Pi^{fija}\left(t\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_1\right)\right]\cdot r\cdot\frac{T_1-T_0}{360}+\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_2\right)\right]\left(1+r \cdot \frac{T_2-T_1}{360}\right)
\end{equation}
$$

$$
\begin{equation}
\Pi^{fija}\left(t\right)=P_c\left(t,T_1\right)\cdot r\cdot\frac{T_1-T_0}{360}+P_c\left(t,T_2\right)\left(1+r \cdot \frac{T_2-T_1}{360}\right)
\end{equation}
$$

Vemos que la expresión (7) corresponde a la suma de:

- primer cupón de intereses traído a valor presente con el factor de descuento entre $t$ y $T_1$
- segundo cupón de intereses y nocional traído a valor presente con el factor de descuento entre $t$ y $T_2$

Donde $T_0$ representa la fecha en que el primer cupón del swap comienza a devengar intereses (usualmente 2 días hábiles después de la fecha de celebración del contrato).

Por otro lado, el valor de la pata flotante es:

$$
\begin{equation}
\Pi^{flot}\left(t\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_1\right) \cdot \left(\frac{B_c\left(t,T_1\right)}{B_c\left(t,T_0\right)}-1\right)\right]+\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_2\right) \cdot \frac{B_c\left(t,T_2\right)}{B_c\left(t,T_1\right)}\right]
\end{equation}
$$

Aplicando (4) se obtiene:

$$
\begin{equation}
\Pi^{flot}\left(t\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_0\right)-D_c\left(t,T_1\right)\right]+\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_1\right)\right]
\end{equation}
$$

Utilizando el hecho que el valor esperado es un operador lineal se llega a:

$$
\begin{equation}
\Pi^{flot}\left(t\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_0\right)\right]=P_c\left(t,T_0\right)
\end{equation}
$$

Si se supone que $t=T_0$ tenemos que:

$$
\begin{equation}
P_c\left(t,T_0\right)=P_c\left(T_0,T_0\right)=1
\end{equation}
$$

Y por lo tanto, utilizando (7) y (10) se deduce que:

$$
\begin{equation}
\Pi^{fija}\left(T_0\right)=P_c\left(t_0,T_1\right)\cdot r\cdot\frac{T_1-T_0}{360}+P_c\left(t_0,T_2\right)\left(1+r \cdot \frac{T_2-T_1}{360}\right)=1
\end{equation}
$$

O sea, el valor presente de los flujos de la pata fija, considerando que el nocional se paga al vencimiento y utilizando los factores de descuento de la curva OIS es 1 (más generalmente es igual al nocional si se considera un nocional distinto de 1).

La fórmula (12) se puede extender fácilmente a los OIS de cualquier vencimiento y la podemos escribir, en forma general, de la siguiente manera:

$$
\begin{equation}
\Pi^{fija,tenor}\left(T_0\right)=\sum\limits_{j=1}^{m-1}P_c\left(T_0,T_j\right)\cdot r\cdot\frac{T_j-T_{j-1}}{360}+P_c\left(T_m,T_{m-1}\right)\cdot\left(1+r\cdot\frac{T_m-T_{m-1}}{360}\right)=1
\end{equation}
$$

Donde $tenor$ representa el plazo del swap (6M, 1Y, 5Y, ...) y $m$ es el número de cupones del swap. Por ejemplo, en un swap con $tenor=5Y$ y periodicidad anual, $m=5$. Para el mismo $tenor$, si la periodicidad es semestral, entonces $m=10$.

La ecuación (13), por si sola, no sería muy útil si no existiera toda una familia de OIS a distintos plazos para los cuales puedo observar su tasa fija en un momento dado del tiempo. Por ejemplo, para los OIS de *SOFR*, se dispone del siguiente panel de precios durante las horas en que estos productos se transan (hay incluso más plazos, pero no se ven todos en esta imagen).

```python
Image(url="img/20201015_sofr_swaps.gif", width=900, height=720)
```

Los primeros 13 swaps son cero cupón, y por lo tanto, las ecuaciones correspondientes son muy sencillas:

$$
\begin{equation}
\Pi^{fija,tenor}\left(T_0\right)=P_c\left(T_0,T_{tenor}\right)\cdot\left(1+r_{tenor}\cdot\frac{T_{tenor}-T_0}{360}\right)=1
\end{equation}
$$

donde $tenor\in\ \left\{ 1W, 2W, 3W, 1M, 2M, 3M, 4M, 5M, 6M, 9M, 10M, 11M,1Y \right\}$.

Al swap cuyo $tenor=18M$ le corresponde la siguiente ecuación:

$$
\begin{equation}
\Pi^{fija,18M}\left(T_0\right)=P_c\left(T_0,T_1\right)\cdot r_{18M}\cdot\frac{T_1-T_{0}}{360}+P_c\left(T_0,T_{2}\right)\cdot\left(1+r_{18M}\cdot\frac{T_2-T_1}{360}\right)=1
\end{equation}
$$

En (15), gracias a las ecuaciones (14), el único término desconocido es $P_c\left(T_0,T_2\right)$ y por lo tanto, se puede resolver. Esto mismo se repite para los swaps cuyo $tenor \in \left\{ 2Y,3Y,4Y,5Y \right\}$.

Al swap cuyo $tenor=10Y$ le corresponde la siguiente ecuación:

$$
\begin{equation}
\Pi^{fija,10Y}\left(T_0\right)=\sum\limits_{j=1}^{9}P_c\left(T_0,T_j\right)\cdot r_{10Y}\cdot\frac{T_j-T_{j-1}}{360}+P_c\left(T_0,T_{10}\right)\cdot\left(1+r_{10Y}\cdot\frac{T_{10}-T_9}{360}\right)=1
\end{equation}
$$

En (16), los términos $P_c\left(T_0,T_j\right)$ con $j\in \left\{6,7,8,9,10\right\}$y son desconocidos, lo que nos fuerza a imponer alguna condición adicional que reduzca el número de incógnitas a 1. La condición que se impone es la siguiente:

$$
\begin{equation}
P_c\left(T_0,T_j\right)=g\left(T_j, P_c\left(T_0,T_5\right),  P_c\left(T_0,T_{10}\right)\right)
\end{equation}
$$

Donde $j\in\left\{6,7,8,9\right\}$ y $g$ es algún tipo de interpolación. Un razonamiento similar debe aplicarse al swap cuyo $tenor=20Y$.

### Ejercicio

Demuestre que, para valorizar, el valor esperado en $t$ de un flujo entre $T_1$ y $T_2$ de la pata flotante se puede calcular como:

$$
\begin{equation}
nominal\cdot r_{T_1,T_2}\cdot\frac{T_2-T_1}{360}
\end{equation}
$$

donde

$$
\begin{equation}
r_{T_1,T_2}=\left( \frac{P_c\left(t, T_1\right)}{P_c\left(t, T_2\right)}-1\right)\cdot\frac{360}{T_2-T_1}
\end{equation}
$$

Por una parte tenemos que (considerando $nominal=1$):

$$
\begin{equation}
\Pi^{flot}\left(t,T_1,T_2\right)=\mathbb{E}_t^{Q_f}\left[D_c\left(t,T_2\right)\left(\frac{B_c\left(t,T_2\right)}{B_c\left(t,T_1\right)}-1\right)\right]=P_c\left(t,T_1\right)-P_c\left(t,T_2\right)
\end{equation}
$$

Por otra parte también sabemos que (siempre con $nominal=1$):

$$
\begin{equation}
\Pi^{flot}\left(t,T_1,T_2\right)=P_c\left(t,T_2\right)\cdot\mathbb{E}_t^{Q_f^{T_2}}\left[ON\left(T_1,T_2\right)\cdot\frac{T_2-T_1}{360}\right]
\end{equation}
$$

Donde $ON\left(T_1,T_2\right)$ es la tasa equivalente al producto de los factores de capitalización de la tasa overnight asociada al OIS entre $T_1$ y $T_2$.

Juntando (20) y (21) llegamos a:

$$
\begin{equation}
\frac{T_2-T_1}{360}\cdot\mathbb{E}_t^{Q_f^{T_2}}\left[ON\left(T_1,T_2\right)\right]=\frac{P_c\left(t,T_1\right)-P_c\left(t,T_2\right)}{P_c\left(t,T_2\right)}
\end{equation}
$$

Por lo tanto, si denotamos $\mathbb{E}_t^{Q_f^{T_2}}\left[ON\left(T_1,T_2\right)\right]=r_{T_1,T_2}$ (22) implica (18) utilizando $nominal=1$. Es evidente que el resultado se generaliza para cualquier $nominal>0$.

## Implementación

En lo que sigue, se implementa el **bootstrapping** para swaps de *SOFR* versus tasa fija.

### Librerías

```python
from finrisk import QC_Financial_3 as Qcf
from scipy.optimize import root_scalar
import modules.auxiliary as aux
import pandas as pd
import numpy as np
import math
```

### Variables Globales

Se da de alta un objeto de tipo `Qcf.BusinessCalendar`y se agregan los feriados relevantes para la implementación.

```python
bus_cal = Qcf.BusinessCalendar(Qcf.QCDate(1, 1, 2020), 20)

for agno in range(2020, 2071):
    f = Qcf.QCDate(12, 10, agno)
    if f.week_day() == Qcf.WeekDay.SAT:
        bus_cal.add_holiday(Qcf.QCDate(14, 10, agno))
        # pass
    elif f.week_day() == Qcf.WeekDay.SUN:
        bus_cal.add_holiday(Qcf.QCDate(13, 10, agno))
    elif f.week_day() == Qcf.WeekDay.MON:
        bus_cal.add_holiday(Qcf.QCDate(12, 10, agno))
    elif f.week_day() == Qcf.WeekDay.TUE:
        bus_cal.add_holiday(Qcf.QCDate(11, 10, agno))
    elif f.week_day() == Qcf.WeekDay.WED:
        bus_cal.add_holiday(Qcf.QCDate(10, 10, agno))
    elif f.week_day() == Qcf.WeekDay.THU:
        bus_cal.add_holiday(Qcf.QCDate(9, 10, agno))
    else:
        bus_cal.add_holiday(Qcf.QCDate(8,10, agno))

bus_cal.add_holiday(Qcf.QCDate(15, 2, 2021))
```

### Funciones

```python
# Estamos usando type annotations
def make_fixed_leg(start_date: str, tenor: str, rate_value: float) -> Qcf.Leg:
    """
    Construye un pata fija con algunos parámetros prefijados:

    - recibo o pago: R
    - nocional: 1,000,000
    - moneda: USD
    - periodicidad: 1Y
    - business adjustment rule: MODFOLLOW
    - stub period: SHORTFRONT
    - lag de pago: 0

    params:

    - start_date: fecha inicial de la pata en formato ISO (YYYY-MM-DD)
    - tenor: plazo estructurado de la pata (1Y, 2Y, 18M, ...)
    - rate_value: valor de la tasa fija

    return:

    - objeto `Qcf.Leg` con cashflows de tipo `FixedRateCashflow`
    """
    # Recibo o pago los flujos de esta pata
    rp = Qcf.RecPay.RECEIVE

    # Periodicidad de pago
    periodicidad = Qcf.Tenor('1Y')

    # Tipo de período irregular (si lo hay)
    periodo_irregular = Qcf.StubPeriod.SHORTFRONT

    # Regla para ajustes de días feriados
    bus_adj_rule = Qcf.BusyAdjRules.MODFOLLOW

    # Número de días después de la fecha final de devengo en que se paga el flujo
    lag_pago = 0

    # Nocional del contrato
    nocional = 10000000.0

    # Considera amortización
    amort_es_flujo = True

    # Moneda
    moneda = Qcf.QCUSD()

    # Es un bono de RF
    es_bono = False

    # Fecha de inicio de devengo del primer cupón
    fecha_inicial = Qcf.build_qcdate_from_string(start_date)

    # Fecha final de devengo, antes de ajustes, del último cupón
    qc_tenor = Qcf.Tenor(tenor)
    meses = qc_tenor.get_years()*12 + qc_tenor.get_months()
    if meses > 0:
        fecha_final = fecha_inicial.add_months(meses)
    else:
        dias = qc_tenor.get_days()
        fecha_final = fecha_inicial.add_days(dias)

    tasa_cupon = Qcf.QCInterestRate(
        rate_value, Qcf.QCAct360(), Qcf.QCLinearWf())

    # Se da de alta el objeto y se retorna
    return Qcf.LegFactory.build_bullet_fixed_rate_leg(
        rp,
        fecha_inicial,
        fecha_final,
        bus_adj_rule,
        periodicidad,
        periodo_irregular,
        bus_cal,
        lag_pago,
        nocional,
        amort_es_flujo,
        tasa_cupon,
        moneda,
        es_bono)
```

```python
def error_with_rate(rate: float, *args) -> float:
    """
    Calcula el error entre el valor presente de una pata fija calculado con una curva cero y el nocional
    de la pata fija. Esta función está diseñada para servir de apoyo al proceso de bootstrapping de una curva
    cero cupón de OIS.

    params:

    - rate: valor a utilizar para la última tasa de la curva
    - args: iterable con plazos de la curva, tasas de la curva, objeto Qcf.Leg a valorizar y fecha de
    valorización.

    return:

    - float con el monto del error.
    """
    # Para usar la librería, los plazos y tasas deben estar en estos formatos.
    # Recordar que la librería está escrita en C++.
    plazos = Qcf.long_vec()
    for p in args[0]:
        plazos.append(p)

    tasas = Qcf.double_vec()
    for t in args[1]:
        tasas.append(t)

    pata = args[2]

    fecha_val = args[3]

    # Se construye una curva cero Qcf
    curva = Qcf.QCCurve(plazos, tasas)
    cuantos = curva.get_length()
    par = curva.get_values_at(cuantos - 1)
    # La variable rate se transforma en la última tasa de la curva
    curva.set_pair(par.tenor, rate)
    curva = Qcf.QCLinearInterpolator(curva)
    curva = Qcf.ZeroCouponCurve(
        curva,
        Qcf.QCInterestRate(
            0.0,
            Qcf.QCAct365(),
            Qcf.QCContinousWf()
        ))

    # Se da de alta el objeto que calcula valores presentes
    vp = Qcf.PresentValue()

    # Se retorna la diferencia entre el vp calculado con la curva y el nominal
    return vp.pv(fecha_val, pata, curva) - pata.get_cashflow_at(0).get_nominal()
```

### Formatos `DataFrame`

```python
frmt = {
    'rate_value': '{:.8%}',
    'valor_tasa': '{:.8%}',
    'nominal': '{:,.2f}',
    'amortizacion': '{:,.2f}',
    'interes': '{:,.2f}',
    'flujo': '{:,.2f}'
}
```

### Obtiene Data

```python
swaps = pd.read_excel('data/20201012_sofr_zero.xlsx', sheet_name='upload')
swaps['start_date'] = swaps['start_date'].astype(str).str[:10]
```

```python
swaps.style.format(frmt)
```

        	        start_date	        tenor	        rate_value	        rate_type	        periodicity	        stub_period

                        0
                        2020-10-14
                        1D
                        0.08000000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        1
                        2020-10-14
                        7D
                        0.08290000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        2
                        2020-10-14
                        14D
                        0.07690000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        3
                        2020-10-14
                        21D
                        0.07630005%
                        LINACT360
                        1Y
                        SHORTFRONT

                        4
                        2020-10-14
                        1M
                        0.07700000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        5
                        2020-10-14
                        2M
                        0.07700000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        6
                        2020-10-14
                        3M
                        0.08000000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        7
                        2020-10-14
                        4M
                        0.07700000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        8
                        2020-10-14
                        5M
                        0.07500000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        9
                        2020-10-14
                        6M
                        0.07400000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        10
                        2020-10-14
                        7M
                        0.07260000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        11
                        2020-10-14
                        8M
                        0.07149995%
                        LINACT360
                        1Y
                        SHORTFRONT

                        12
                        2020-10-14
                        9M
                        0.07100000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        13
                        2020-10-14
                        10M
                        0.07040000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        14
                        2020-10-14
                        11M
                        0.06990000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        15
                        2020-10-14
                        12M
                        0.06929995%
                        LINACT360
                        1Y
                        SHORTFRONT

                        16
                        2020-10-14
                        18M
                        0.06330000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        17
                        2020-10-14
                        2Y
                        0.05600000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        18
                        2020-10-14
                        3Y
                        0.07400000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        19
                        2020-10-14
                        4Y
                        0.12400000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        20
                        2020-10-14
                        5Y
                        0.19100000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        21
                        2020-10-14
                        6Y
                        0.27300000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        22
                        2020-10-14
                        7Y
                        0.35400000%
                        LINACT360
                        1Y
                        SHORTFRONT

                        23
                        2020-10-14
                        8Y
                        0.43000001%
                        LINACT360
                        1Y
                        SHORTFRONT

                        24
                        2020-10-14
                        9Y
                        0.50099999%
                        LINACT360
                        1Y
                        SHORTFRONT

                        25
                        2020-10-14
                        10Y
                        0.56800002%
                        LINACT360
                        1Y
                        SHORTFRONT

                        26
                        2020-10-14
                        12Y
                        0.67400002%
                        LINACT360
                        1Y
                        SHORTFRONT

                        27
                        2020-10-14
                        15Y
                        0.78299999%
                        LINACT360
                        1Y
                        SHORTFRONT

                        28
                        2020-10-14
                        20Y
                        0.89499998%
                        LINACT360
                        1Y
                        SHORTFRONT

                        29
                        2020-10-14
                        25Y
                        0.93455002%
                        LINACT360
                        1Y
                        SHORTFRONT

                        30
                        2020-10-14
                        30Y
                        0.95700002%
                        LINACT360
                        1Y
                        SHORTFRONT

                        31
                        2020-10-14
                        40Y
                        0.92200002%
                        LINACT360
                        1Y
                        SHORTFRONT

                        32
                        2020-10-14
                        50Y
                        0.84599999%
                        LINACT360
                        1Y
                        SHORTFRONT

### Construye Objetos `Qcf`

```python
qc_swaps = []
for s in swaps.itertuples():
    # Se da de alta la pata fija
    leg = make_fixed_leg(s.start_date, s.tenor, s.rate_value)

    # Se calcula el plazo del último cupón de cada pata
    cuantos_flujos = leg.size()
    last_pmt_date = leg.get_cashflow_at(cuantos_flujos - 1).get_settlement_date()
    start_date = Qcf.build_qcdate_from_string(s.start_date) # Transforma de str a Qcf.QCDate
    plazo = start_date.day_diff(last_pmt_date) # Se calcula el número de días entre ambas fechas
    qc_swaps.append((leg, plazo))
```

```python
aux.show_leg(qc_swaps[18][0], 'FixedRateCashflow', '').style.format(frmt)
```

        	        fecha_inicial	        fecha_final	        fecha_pago	        nominal	        amortizacion	        interes	        amort_es_flujo	        flujo	        moneda	        valor_tasa	        tipo_tasa

                        0
                        2020-10-14
                        2021-10-14
                        2021-10-14
                        10,000,000.00
                        0.00
                        7,502.78
                        True
                        7,502.78
                        USD
                        0.07400000%
                        LinAct360

                        1
                        2021-10-14
                        2022-10-14
                        2022-10-14
                        10,000,000.00
                        0.00
                        7,502.78
                        True
                        7,502.78
                        USD
                        0.07400000%
                        LinAct360

                        2
                        2022-10-14
                        2023-10-16
                        2023-10-16
                        10,000,000.00
                        10,000,000.00
                        7,543.89
                        True
                        10,007,543.89
                        USD
                        0.07400000%
                        LinAct360

### Construye Curva Cero Cupón

Demuestra función objetivo.

```python
plazos = []
tasas = []

plazos.append(1) # Los plazos se calculan desde start_date (2020-10-14)
tasa = math.log(1 + .08 / 100 * 1 / 360)* 365.0 / 1
# 1 + .08% * 1/360 = exp(r * 1/365) -> log(1 + .08% * 1/360) * 365/1
tasas.append(tasa)

plazos.append(int(7))
tasas.append(.0)

# Puedo valorizar cualquiera de los swaps ya dado de alta.
error_with_rate(.0008, plazos, tasas, qc_swaps[1][0], Qcf.QCDate(14, 10, 2020))
```

    7.768488567322493

```python
plazos = []
tasas = []

plazos.append(1) # Los plazos se calculan desde start_date (2020-10-14)
tasa = math.log(1 + .08 / 100 * 1 / 360) * 365.0 / 1
# 1 + .08% * 1/360 = exp(r * 1/365) -> log(1 + .08% * 1/360) * 365/1
tasas.append(tasa)

for s in qc_swaps[1:]:
    plazos.append(s[1])
    tasas.append(0.0)

    x = root_scalar(
        error_with_rate,
        method='bisect',
        bracket=[0.0, .02],
        x0=.0008,
        args=(plazos, tasas, s[0], Qcf.QCDate(14, 10, 2020)),
        xtol=.00000000000000001
    )

    tasas[-1] = x.root
```

```python
plazos
```

    [1,
     7,
     14,
     21,
     33,
     61,
     92,
     125,
     152,
     182,
     212,
     243,
     273,
     306,
     335,
     365,
     547,
     730,
     1097,
     1462,
     1826,
     2191,
     2556,
     2924,
     3288,
     3653,
     4383,
     5479,
     7306,
     9133,
     10957,
     14610,
     18262]

```python
tasas
```

    [0.0008111102098831921,
     0.0008405071032302658,
     0.0007796688760092165,
     0.0007735805246738891,
     0.000780666927269067,
     0.0007806435151746883,
     0.0008110282284667393,
     0.0007805900951461808,
     0.0007602962852209315,
     0.0007501374788097336,
     0.00073592602409974,
     0.0007247551823124353,
     0.0007196673738400249,
     0.0007135642967602961,
     0.0007084779642283934,
     0.0007023777801444453,
     0.0006416085984074015,
     0.0005675783969944348,
     0.0007500989440060123,
     0.0012575197717080045,
     0.0019386402831874962,
     0.002774632944838871,
     0.0036034027848490164,
     0.004384125923198167,
     0.005116828317946096,
     0.0058116574729007355,
     0.006917483831122234,
     0.008063525939196518,
     0.00925324111720689,
     0.009657189205339875,
     0.009881027327780387,
     0.00940832436133798,
     0.008464263389120666]

```python
for_df = []

for p, t in zip(plazos, tasas):
    for_df.append((p,t))

df_curva = pd.DataFrame(for_df, columns=['plazo', 'tasa'])
# df_curva['df'] = df_curva.apply(lambda row: math.exp(-row['plazo']*row['tasa']/365), axis=1)
df_curva['df'] = np.exp(-df_curva['plazo'] * df_curva['tasa'] / 365.0)
df_curva.style.format({'tasa':'{:.8%}', 'df':'{:.8%}'})
```

        	        plazo	        tasa	        df

                        0
                        1
                        0.08111102%
                        99.99977778%

                        1
                        7
                        0.08405071%
                        99.99838808%

                        2
                        14
                        0.07796689%
                        99.99700953%

                        3
                        21
                        0.07735805%
                        99.99554936%

                        4
                        33
                        0.07806669%
                        99.99294216%

                        5
                        61
                        0.07806435%
                        99.98695448%

                        6
                        92
                        0.08110282%
                        99.97955973%

                        7
                        125
                        0.07805901%
                        99.97327104%

                        8
                        152
                        0.07602963%
                        99.96834336%

                        9
                        182
                        0.07501375%
                        99.96260288%

                        10
                        212
                        0.07359260%
                        99.95726494%

                        11
                        243
                        0.07247552%
                        99.95176081%

                        12
                        273
                        0.07196674%
                        99.94618731%

                        13
                        306
                        0.07135643%
                        99.94019579%

                        14
                        335
                        0.07084780%
                        99.93499645%

                        15
                        365
                        0.07023778%
                        99.92978688%

                        16
                        547
                        0.06416086%
                        99.90389281%

                        17
                        730
                        0.05675784%
                        99.88654873%

                        18
                        1097
                        0.07500989%
                        99.77481323%

                        19
                        1462
                        0.12575198%
                        99.49756947%

                        20
                        1826
                        0.19386403%
                        99.03483661%

                        21
                        2191
                        0.27746329%
                        98.34825349%

                        22
                        2556
                        0.36034028%
                        97.50820167%

                        23
                        2924
                        0.43841259%
                        96.54885343%

                        24
                        3288
                        0.51168283%
                        95.49526595%

                        25
                        3653
                        0.58116575%
                        94.34948791%

                        26
                        4383
                        0.69174838%
                        92.02897968%

                        27
                        5479
                        0.80635259%
                        88.59974057%

                        28
                        7306
                        0.92532411%
                        83.09240116%

                        29
                        9133
                        0.96571892%
                        78.53377535%

                        30
                        10957
                        0.98810273%
                        74.33261856%

                        31
                        14610
                        0.94083244%
                        68.61968516%

                        32
                        18262
                        0.84642634%
                        65.47567760%

Tasa fwd entre t1 y t2 días.

```python
from scipy.interpolate import interp1d
```

```python
crv = interp1d(df_curva['plazo'], df_curva['tasa'], 'linear')
```

```python
t1 = 98
t2 = 456
r1 = crv(t1)
r2 = crv(t2)
df1 = math.exp(-r1 * t1 / 365)
df2 = math.exp(-r2 * t2 / 365)
fwd12 = (df1 / df2 - 1.0) * 360.0 / (t2 - t1)
print(f'{fwd12:.8%}')
```

    0.06269389%

Se exporta a Excel este resultado.

```python
df_curva.to_excel('data/20201012_built_sofr_zero.xlsx', index=False)
```

### Comprobación

Con el resultado (raíces) se construye un objeto `Qcf.ZeroCouponCurve`.

```python
qc_plazos = Qcf.long_vec()
qc_tasas = Qcf.double_vec()

for p, t in zip(plazos, tasas):
    qc_plazos.append(p)
    qc_tasas.append(t)

curva_final = Qcf.QCCurve(qc_plazos, qc_tasas)
curva_final = Qcf.QCLinearInterpolator(curva_final)

# Se construye la curva cero cupón
curva_final = Qcf.ZeroCouponCurve(
    curva_final,
    Qcf.QCInterestRate(
        0.0,
        Qcf.QCAct365(),
        Qcf.QCContinousWf()
    )
)
```

```python
curva_final
```

    <finrisk.QC_Financial_3.ZeroCouponCurve at 0x7fb537b5dd50>

Se calcula el valor presente de cada una de las patas fijas de los swaps.

```python
vp = Qcf.PresentValue()
for i, s in enumerate(qc_swaps):
    pv = vp.pv(Qcf.QCDate(14, 10, 2020), s[0], curva_final)
    print(f'{i}: {pv:,.8f}')
```

    0: 10,000,000.00000000
    1: 10,000,000.00000000
    2: 10,000,000.00000000
    3: 10,000,000.00000000
    4: 10,000,000.00000000
    5: 10,000,000.00000000
    6: 10,000,000.00000000
    7: 10,000,000.00000000
    8: 10,000,000.00000000
    9: 10,000,000.00000000
    10: 10,000,000.00000000
    11: 10,000,000.00000000
    12: 10,000,000.00000000
    13: 10,000,000.00000000
    14: 10,000,000.00000000
    15: 10,000,000.00000000
    16: 10,000,000.00000000
    17: 10,000,000.00000000
    18: 10,000,000.00000000
    19: 10,000,000.00000000
    20: 10,000,000.00000000
    21: 10,000,000.00000000
    22: 10,000,000.00000000
    23: 10,000,000.00000000
    24: 10,000,000.00000000
    25: 10,000,000.00000000
    26: 10,000,000.00000000
    27: 10,000,000.00000000
    28: 10,000,000.00000000
    29: 10,000,000.00000000
    30: 10,000,000.00000000
    31: 10,000,000.00000000
    32: 10,000,000.00000000


# Neuromatch Academy: Week 1, Day 3, Tutorial 1
# Model Fitting: Linear regression with MSE

**Content creators**: Pierre-Étienne Fiquet, Anqi Wu, Alex Hyafil with help from Byron Galbraith

**Content reviewers**: Lina Teichmann, Saeed Salehi, Patrick Mineault,  Ella Batty, Michael Waskom

___
#Tutorial Objectives

This is Tutorial 1 of a series on fitting models to data. We start with simple linear regression, using least squares optimization (Tutorial 1) and Maximum Likelihood Estimation (Tutorial 2). We will use bootstrapping to build confidence intervals around the inferred linear model parameters (Tutorial 3). We'll finish our exploration of regression models by generalizing to multiple linear regression and polynomial regression (Tutorial 4). We end by learning how to choose between these various models. We discuss the bias-variance trade-off (Tutorial 5) and Cross Validation for model selection (Tutorial 6).

In this tutorial, we will learn how to fit simple linear models to data.
- Learn how to calculate the mean-squared error (MSE)
- Explore how model parameters (slope) influence the MSE
- Learn how to find the optimal model parameter using least-squares optimization

---

**acknowledgements:**
- we thank Eero Simoncelli, much of today's tutorials are inspired by exercises asigned in his mathtools class.

---
# Setup

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

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

```python
#@title Helper functions

def plot_observed_vs_predicted(x, y, y_hat, theta_hat):
  """ Plot observed vs predicted data

  Args:
      x (ndarray): observed x values
  y (ndarray): observed y values
  y_hat (ndarray): predicted y values

  """
  fig, ax = plt.subplots()
  ax.scatter(x, y, label='Observed')  # our data scatter plot
  ax.plot(x, y_hat, color='r', label='Fit')  # our estimated model
  # plot residuals
  ymin = np.minimum(y, y_hat)
  ymax = np.maximum(y, y_hat)
  ax.vlines(x, ymin, ymax, 'g', alpha=0.5, label='Residuals')
  ax.set(
      title=fr"$\hat{{\theta}}$ = {theta_hat:0.2f}, MSE = {mse(x, y, theta_hat):.2f}",
      xlabel='x',
      ylabel='y'
  )
  ax.legend()

```

---
# Section 1: Mean Squared Error (MSE)

```python
#@title Video 1: Linear Regression & Mean Squared Error
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="HumajfjJ37E", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video

```

    Video available at https://youtube.com/watch?v=HumajfjJ37E

**Linear least squares regression** is an old but gold  optimization procedure that we are going to use for data fitting. Least squares (LS) optimization problems are those in which the objective function is a quadratic function of the
parameter(s) being optimized.

Suppose you have a set of measurements, $y_{n}$ (the "dependent" variable) obtained for different input values, $x_{n}$ (the "independent" or "explanatory" variable). Suppose we believe the measurements are proportional to the input values, but are corrupted by some (random) measurement errors, $\epsilon_{n}$, that is:

$$y_{n}= \theta x_{n}+\epsilon_{n}$$

for some unknown slope parameter $\theta.$ The least squares regression problem uses **mean squared error (MSE)** as its objective function, it aims to find the value of the parameter $\theta$ by minimizing the average of squared errors:

\begin{align}
\min _{\theta} \frac{1}{N}\sum_{n=1}^{N}\left(y_{n}-\theta x_{n}\right)^{2}
\end{align}

We will now explore how MSE is used in fitting a linear regression model to data. For illustrative purposes, we will create a simple synthetic dataset where we know the true underlying model. This will allow us to see how our estimation efforts compare in uncovering the real model (though in practice we rarely have this luxury).

First we will generate some noisy samples $x$ from [0, 10) along the line $y = 1.2x$ as our dataset we wish to fit a model to.

```python
# @title

# @markdown Execute this cell to generate some simulated data

# setting a fixed seed to our random number generator ensures we will always
# get the same psuedorandom number sequence
np.random.seed(121)

# Let's set some parameters
theta = 1.2
n_samples = 30

# Draw x and then calculate y
x = 10 * np.random.rand(n_samples)  # sample from a uniform distribution over [0,10)
noise = np.random.randn(n_samples)  # sample from a standard normal distribution
y = theta * x + noise

# Plot the results
fig, ax = plt.subplots()
ax.scatter(x, y)  # produces a scatter plot
ax.set(xlabel='x', ylabel='y');
```

Now that we have our suitably noisy dataset, we can start trying to estimate the underlying model that produced it. We use MSE to evaluate how successful a particular slope estimate $\hat{\theta}$ is for explaining the data, with the closer to 0 the MSE is, the better our estimate fits the data.

## Exercise 1: Compute MSE

In this exercise you will implement a method to compute the mean squared error for a set of inputs $x$, measurements $y$, and slope estimate $\hat{\theta}$. We will then compute and print the mean squared error for 3 different choices of theta

```python
def mse(x, y, theta_hat):
  """Compute the mean squared error

  Args:
    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.
    theta_hat (float): An estimate of the slope parameter

  Returns:
    float: The mean squared error of the data with the estimated parameter.
  """
  ####################################################
  ## TODO for students: compute the mean squared error
  # Fill out function and remove
  #raise NotImplementedError("Student exercise: compute the mean squared error")
  ####################################################

  # Compute the estimated y
  y_hat = theta_hat*x

  # Compute mean squared error
  mse = np.mean((y-y_hat)**2)

  return mse

# Uncomment below to test your function
theta_hats = [0.75, 1.0, 1.5]
for theta_hat in theta_hats:
  print(f"theta_hat of {theta_hat} has an MSE of {mse(x, y, theta_hat):.2f}")
```

    theta_hat of 0.75 has an MSE of 9.08
    theta_hat of 1.0 has an MSE of 3.01
    theta_hat of 1.5 has an MSE of 4.52

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D3_ModelFitting/solutions/W1D3_Tutorial1_Solution_e5ee9b3d.py)

The result should be:

theta_hat of 0.75 has an MSE of 9.08\
theta_hat of 1.0 has an MSE of 3.0\
theta_hat of 1.5 has an MSE of 4.52

We see that $\hat{\theta} = 1.0$ is our best estimate from the three we tried. Looking just at the raw numbers, however, isn't always satisfying, so let's visualize what our estimated model looks like over the data.

```python
#@title

#@markdown Execute this cell to visualize estimated models

fig, axes = plt.subplots(ncols=3, figsize=(18, 4))
for theta_hat, ax in zip(theta_hats, axes):

  # True data
  ax.scatter(x, y, label='Observed')  # our data scatter plot

  # Compute and plot predictions
  y_hat = theta_hat * x
  ax.plot(x, y_hat, color='r', label='Fit')  # our estimated model

  ax.set(
      title= fr'$\hat{{\theta}}$= {theta_hat}, MSE = {mse(x, y, theta_hat):.2f}',
      xlabel='x',
      ylabel='y'
  );

axes[0].legend()
```

## Interactive Demo: MSE Explorer

Using an interactive widget, we can easily see how changing our slope estimate changes our model fit. We display the **residuals**, the differences between observed and predicted data, as line segments between the data point (observed response) and the corresponding predicted response on the model fit line.

```python
#@title

#@markdown Make sure you execute this cell to enable the widget!

@widgets.interact(theta_hat=widgets.FloatSlider(1.0, min=0.0, max=2.0))
def plot_data_estimate(theta_hat):
  y_hat = theta_hat * x
  plot_observed_vs_predicted(x, y, y_hat, theta_hat)

```

    interactive(children=(FloatSlider(value=1.0, description='theta_hat', max=2.0), Output()), _dom_classes=('widg…

While visually exploring several estimates can be instructive, it's not the most efficient for finding the best estimate to fit our data. Another technique we can use is choose a reasonable range of parameter values and compute the MSE at several values in that interval. This allows us to plot the error against the parameter value (this is also called an **error landscape**, especially when we deal with more than one parameter). We can select the final $\hat{\theta}$ ($\hat{\theta}_{MSE}$) as the one which results in the lowest error.

```python
# @title

# @markdown Execute this cell to loop over theta_hats, compute MSE, and plot results

# Loop over different thetas, compute MSE for each
theta_hat_grid = np.linspace(-2.0, 4.0)
errors = np.zeros(len(theta_hat_grid))
for i, theta_hat in enumerate(theta_hat_grid):
  errors[i] = mse(x, y, theta_hat)

# Find theta that results in lowest error
best_error = np.min(errors)
theta_hat = theta_hat_grid[np.argmin(errors)]

# Plot results
fig, ax = plt.subplots()
ax.plot(theta_hat_grid, errors, '-o', label='MSE', c='C1')
ax.axvline(theta, color='g', ls='--', label=r"$\theta_{True}$")
ax.axvline(theta_hat, color='r', ls='-', label=r"$\hat{{\theta}}_{MSE}$")
ax.set(
  title=fr"Best fit: $\hat{{\theta}}$ = {theta_hat:.2f}, MSE = {best_error:.2f}",
  xlabel=r"$\hat{{\theta}}$",
  ylabel='MSE')
ax.legend();
```

We can see that our best fit is $\hat{\theta}=1.18$ with an MSE of 1.45. This is quite close to the original true value $\theta=1.2$!

---
# Section 2: Least-squares optimization

While the approach detailed above (computing MSE at various values of $\hat\theta$) quickly got us to a good estimate, it still relied on evaluating the MSE value across a grid of hand-specified values. If we didn't pick a good range to begin with, or with enough granularity, we might miss the best possible estimator. Let's go one step further, and instead of finding the minimum MSE from a set of candidate estimates, let's solve for it analytically.

We can do this by minimizing the cost function. Mean squared error is a convex objective function, therefore we can compute its minimum using calculus. Please see video or appendix for this derivation! After computing the minimum, we find that:

\begin{align}
\hat\theta = \frac{\vec{x}^\top \vec{y}}{\vec{x}^\top \vec{x}}
\end{align}

This is known as solving the normal equations. For different ways of obtaining the solution, see the notes on [Least Squares Optimization](https://www.cns.nyu.edu/~eero/NOTES/leastSquares.pdf) by Eero Simoncelli.

### Exercise 2: Solve for the Optimal Estimator

In this exercise, you will write a function that finds the optimal $\hat{\theta}$ value using the least squares optimization approach (the equation above) to solve MSE minimization. It shoud take arguments $x$ and $y$ and return the solution $\hat{\theta}$.

We will then use your function to compute $\hat{\theta}$ and plot the resulting prediction on top of the data.

```python
def solve_normal_eqn(x, y):
  """Solve the normal equations to produce the value of theta_hat that minimizes
    MSE.

    Args:
    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.

  Returns:
    float: the value for theta_hat arrived from minimizing MSE
  """

  ################################################################################
  ## TODO for students: solve for the best parameter using least squares
  # Fill out function and remove
#   raise NotImplementedError("Student exercise: solve for theta_hat using least squares")
  ################################################################################

  # Compute theta_hat analytically
  theta_hat = np.dot(x,y)/np.dot(x,x)

  return theta_hat

# Uncomment below to test your function
theta_hat = solve_normal_eqn(x, y)
y_hat = theta_hat * x
plot_observed_vs_predicted(x, y, y_hat, theta_hat)

```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D3_ModelFitting/solutions/W1D3_Tutorial1_Solution_cb582fbb.py)

*Example output:*

We see that the analytic solution produces an even better result than our grid search from before, producing $\hat{\theta} = 1.21$ with MSE = 1.43!

---
# Summary

- Linear least squares regression is an optimization procedure that can be used for data fitting:
    - Task: predict a value for $y$ given $x$
    - Performance measure: $\textrm{MSE}$
    - Procedure: minimize $\textrm{MSE}$ by solving the normal equations
- **Key point**: We fit the model by defining an *objective function* and minimizing it.
- **Note**: In this case, there is an *analytical* solution to the minimization problem and in practice, this solution can be computed using *linear algebra*. This is *extremely* powerful and forms the basis for much of numerical computation throughout the sciences.

---
# Appendix

## Least Squares Optimization Derivation

We will outline here the derivation of the least squares solution.

We first set the derivative of the error expression with respect to $\theta$ equal to zero,

\begin{align}
\frac{d}{d\theta}\frac{1}{N}\sum_{i=1}^N(y_i - \theta x_i)^2 = 0 \\
\frac{1}{N}\sum_{i=1}^N-2x_i(y_i - \theta x_i) = 0
\end{align}

where we used the chain rule. Now solving for $\theta$, we obtain an optimal value of:

\begin{align}
\hat\theta = \frac{\sum_{i=1}^N x_i y_i}{\sum_{i=1}^N x_i^2}
\end{align}

Which we can write in vector notation as:

\begin{align}
\hat\theta = \frac{\vec{x}^\top \vec{y}}{\vec{x}^\top \vec{x}}
\end{align}

This is known as solving the *normal equations*. For different ways of obtaining the solution, see the notes on [Least Squares Optimization](https://www.cns.nyu.edu/~eero/NOTES/leastSquares.pdf) by Eero Simoncelli.

```python

```


# Programming Exercise 4:  Neural Networks Learning

## Introduction

In this exercise, you will implement the backpropagation algorithm for neural networks and apply it to the task of hand-written digit recognition. Before starting on the programming exercise, we strongly recommend watching the video lectures and completing the review questions for the associated topics.

All the information you need for solving this assignment is in this notebook, and all the code you will be implementing will take place within this notebook. The assignment can be promptly submitted to the coursera grader directly from this notebook (code and instructions are included below).

Before we begin with the exercises, we need to import all libraries required for this programming exercise. Throughout the course, we will be using [`numpy`](http://www.numpy.org/) for all arrays and matrix operations, [`matplotlib`](https://matplotlib.org/) for plotting, and [`scipy`](https://docs.scipy.org/doc/scipy/reference/) for scientific and numerical computation functions and tools. You can find instructions on how to install required libraries in the README file in the [github repository](https://github.com/dibgerge/ml-coursera-python-assignments).

```python
# used for manipulating directory paths
import os

# Scientific and vector computation for python
import numpy as np

# Plotting library
from matplotlib import pyplot

# Optimization module in scipy
from scipy import optimize

# will be used to load MATLAB mat datafile format
from scipy.io import loadmat

# library written for this exercise providing additional functions for assignment submission, and others
import utils

# define the submission/grader object for this exercise
grader = utils.Grader()

# tells matplotlib to embed plots within the notebook
%matplotlib inline
```

## Submission and Grading

After completing each part of the assignment, be sure to submit your solutions to the grader. The following is a breakdown of how each part of this exercise is scored.

| Section | Part                                             | Submission function | Points
| :-      |:-                                                | :-                  | :-:
| 1       | [Feedforward and Cost Function](#section1)                    | [`nnCostFunction`](#nnCostFunction)   | 30
| 2       | [Regularized Cost Function](#section2)                        | [`nnCostFunction`](#nnCostFunction)   | 15
| 3       | [Sigmoid Gradient](#section3)                                 | [`sigmoidGradient`](#sigmoidGradient) | 5
| 4       | [Neural Net Gradient Function (Backpropagation)](#section4)   | [`nnCostFunction`](#nnCostFunction)   | 40
| 5       | [Regularized Gradient](#section5)                             | [`nnCostFunction`](#nnCostFunction)   |10
|         | Total Points                                     |    | 100

You are allowed to submit your solutions multiple times, and we will take only the highest score into consideration.

At the end of each section in this notebook, we have a cell which contains code for submitting the solutions thus far to the grader. Execute the cell to see your score up to the current section. For all your work to be submitted properly, you must execute those cells at least once.

## Neural Networks

In the previous exercise, you implemented feedforward propagation for neural networks and used it to predict handwritten digits with the weights we provided. In this exercise, you will implement the backpropagation algorithm to learn the parameters for the neural network.

We start the exercise by first loading the dataset.

```python
#  training data stored in arrays X, y
data = loadmat(os.path.join('Data', 'ex4data1.mat'))
X, y = data['X'], data['y'].ravel()

# set the zero digit to 0, rather than its mapped 10 in this dataset
# This is an artifact due to the fact that this dataset was used in
# MATLAB where there is no index 0
y[y == 10] = 0

# Number of training examples
m = y.size
```

### 1.1 Visualizing the data

You will begin by visualizing a subset of the training set, using the function `displayData`, which is the same function we used in Exercise 3. It is provided in the `utils.py` file for this assignment as well. The dataset is also the same one you used in the previous exercise.

There are 5000 training examples in `ex4data1.mat`, where each training example is a 20 pixel by 20 pixel grayscale image of the digit. Each pixel is represented by a floating point number indicating the grayscale intensity at that location. The 20 by 20 grid of pixels is “unrolled” into a 400-dimensional vector. Each
of these training examples becomes a single row in our data matrix $X$. This gives us a 5000 by 400 matrix $X$ where every row is a training example for a handwritten digit image.

$$ X = \begin{bmatrix} - \left(x^{(1)} \right)^T - \\
- \left(x^{(2)} \right)^T - \\
\vdots \\
- \left(x^{(m)} \right)^T - \\
\end{bmatrix}
$$

The second part of the training set is a 5000-dimensional vector `y` that contains labels for the training set.
The following cell randomly selects 100 images from the dataset and plots them.

```python
# Randomly select 100 data points to display
rand_indices = np.random.choice(m, 100, replace=False)
sel = X[rand_indices, :]

utils.displayData(sel)
```

### 1.2 Model representation

Our neural network is shown in the following figure.

It has 3 layers - an input layer, a hidden layer and an output layer. Recall that our inputs are pixel values
of digit images. Since the images are of size $20 \times 20$, this gives us 400 input layer units (not counting the extra bias unit which always outputs +1). The training data was loaded into the variables `X` and `y` above.

You have been provided with a set of network parameters ($\Theta^{(1)}, \Theta^{(2)}$) already trained by us. These are stored in `ex4weights.mat` and will be loaded in the next cell of this notebook into `Theta1` and `Theta2`. The parameters have dimensions that are sized for a neural network with 25 units in the second layer and 10 output units (corresponding to the 10 digit classes).

```python
# Setup the parameters you will use for this exercise
input_layer_size  = 400  # 20x20 Input Images of Digits
hidden_layer_size = 25   # 25 hidden units
num_labels = 10          # 10 labels, from 0 to 9

# Load the weights into variables Theta1 and Theta2
weights = loadmat(os.path.join('Data', 'ex4weights.mat'))

# Theta1 has size 25 x 401
# Theta2 has size 10 x 26
Theta1, Theta2 = weights['Theta1'], weights['Theta2']

# swap first and last columns of Theta2, due to legacy from MATLAB indexing,
# since the weight file ex3weights.mat was saved based on MATLAB indexing
Theta2 = np.roll(Theta2, 1, axis=0)

# Unroll parameters
nn_params = np.concatenate([Theta1.ravel(), Theta2.ravel()])
```

### 1.3 Feedforward and cost function

Now you will implement the cost function and gradient for the neural network. First, complete the code for the function `nnCostFunction` in the next cell to return the cost.

Recall that the cost function for the neural network (without regularization) is:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right]$$

where $h_\theta \left( x^{(i)} \right)$ is computed as shown in the neural network figure above, and K = 10 is the total number of possible labels. Note that $h_\theta(x^{(i)})_k = a_k^{(3)}$ is the activation (output
value) of the $k^{th}$ output unit. Also, recall that whereas the original labels (in the variable y) were 0, 1, ..., 9, for the purpose of training a neural network, we need to encode the labels as vectors containing only values 0 or 1, so that

$$ y =
\begin{bmatrix} 1 \\ 0 \\ 0 \\\vdots \\ 0 \end{bmatrix}, \quad
\begin{bmatrix} 0 \\ 1 \\ 0 \\ \vdots \\ 0 \end{bmatrix}, \quad \cdots  \quad \text{or} \qquad
\begin{bmatrix} 0 \\ 0 \\ 0 \\ \vdots \\ 1 \end{bmatrix}.
$$

For example, if $x^{(i)}$ is an image of the digit 5, then the corresponding $y^{(i)}$ (that you should use with the cost function) should be a 10-dimensional vector with $y_5 = 1$, and the other elements equal to 0.

You should implement the feedforward computation that computes $h_\theta(x^{(i)})$ for every example $i$ and sum the cost over all examples. **Your code should also work for a dataset of any size, with any number of labels** (you can assume that there are always at least $K \ge 3$ labels).

**Implementation Note:** The matrix $X$ contains the examples in rows (i.e., X[i,:] is the i-th training example $x^{(i)}$, expressed as a $n \times 1$ vector.) When you complete the code in `nnCostFunction`, you will need to add the column of 1’s to the X matrix. The parameters for each unit in the neural network is represented in Theta1 and Theta2 as one row. Specifically, the first row of Theta1 corresponds to the first hidden unit in the second layer. You can use a for-loop over the examples to compute the cost.

```python
def nnCostFunction(nn_params,
                   input_layer_size,
                   hidden_layer_size,
                   num_labels,
                   X, y, lambda_=0.0):
    """
    Implements the neural network cost function and gradient for a two layer neural
    network which performs classification.

    Parameters
    ----------
    nn_params : array_like
        The parameters for the neural network which are "unrolled" into
        a vector. This needs to be converted back into the weight matrices Theta1
        and Theta2.

    input_layer_size : int
        Number of features for the input layer.

    hidden_layer_size : int
        Number of hidden units in the second layer.

    num_labels : int
        Total number of labels, or equivalently number of units in output layer.

    X : array_like
        Input dataset. A matrix of shape (m x input_layer_size).

    y : array_like
        Dataset labels. A vector of shape (m,).

    lambda_ : float, optional
        Regularization parameter.

    Returns
    -------
    J : float
        The computed value for the cost function at the current weight values.

    grad : array_like
        An "unrolled" vector of the partial derivatives of the concatenatation of
        neural network weights Theta1 and Theta2.

    Instructions
    ------------
    You should complete the code by working through the following parts.

    - Part 1: Feedforward the neural network and return the cost in the
              variable J. After implementing Part 1, you can verify that your
              cost function computation is correct by verifying the cost
              computed in the following cell.

    - Part 2: Implement the backpropagation algorithm to compute the gradients
              Theta1_grad and Theta2_grad. You should return the partial derivatives of
              the cost function with respect to Theta1 and Theta2 in Theta1_grad and
              Theta2_grad, respectively. After implementing Part 2, you can check
              that your implementation is correct by running checkNNGradients provided
              in the utils.py module.

              Note: The vector y passed into the function is a vector of labels
                    containing values from 0..K-1. You need to map this vector into a
                    binary vector of 1's and 0's to be used with the neural network
                    cost function.

              Hint: We recommend implementing backpropagation using a for-loop
                    over the training examples if you are implementing it for the
                    first time.

    - Part 3: Implement regularization with the cost function and gradients.

              Hint: You can implement this around the code for
                    backpropagation. That is, you can compute the gradients for
                    the regularization separately and then add them to Theta1_grad
                    and Theta2_grad from Part 2.

    Note
    ----
    We have provided an implementation for the sigmoid function in the file
    `utils.py` accompanying this assignment.
    """
    # Reshape nn_params back into the parameters Theta1 and Theta2, the weight matrices
    # for our 2 layer neural network
    Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                        (hidden_layer_size, (input_layer_size + 1)))

    Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                        (num_labels, (hidden_layer_size + 1)))

    # Setup some useful variables
    m = y.size

    # You need to return the following variables correctly
    J = 0
    Theta1_grad = np.zeros(Theta1.shape)
    Theta2_grad = np.zeros(Theta2.shape)

    # ====================== YOUR CODE HERE ======================

    # ================================================================
    # Unroll gradients
    # grad = np.concatenate([Theta1_grad.ravel(order=order), Theta2_grad.ravel(order=order)])
    grad = np.concatenate([Theta1_grad.ravel(), Theta2_grad.ravel()])

    return J, grad
```

Use the following links to go back to the different parts of this exercise that require to modify the function `nnCostFunction`.

Back to:
- [Feedforward and cost function](#section1)
- [Regularized cost](#section2)
- [Neural Network Gradient (Backpropagation)](#section4)
- [Regularized Gradient](#section5)

Once you are done, call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`. You should see that the cost is about 0.287629.

```python
lambda_ = 0
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                   num_labels, X, y, lambda_)
print('Cost at parameters (loaded from ex4weights): %.6f ' % J)
print('The cost should be about                   : 0.287629.')
```

*You should now submit your solutions.*

```python
grader = utils.Grader()
grader[1] = nnCostFunction
grader.grade()
```

### 1.4 Regularized cost function

The cost function for neural networks with regularization is given by:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right] + \frac{\lambda}{2 m} \left[ \sum_{j=1}^{25} \sum_{k=1}^{400} \left( \Theta_{j,k}^{(1)} \right)^2 + \sum_{j=1}^{10} \sum_{k=1}^{25} \left( \Theta_{j,k}^{(2)} \right)^2 \right] $$

You can assume that the neural network will only have 3 layers - an input layer, a hidden layer and an output layer. However, your code should work for any number of input units, hidden units and outputs units. While we
have explicitly listed the indices above for $\Theta^{(1)}$ and $\Theta^{(2)}$ for clarity, do note that your code should in general work with $\Theta^{(1)}$ and $\Theta^{(2)}$ of any size. Note that you should not be regularizing the terms that correspond to the bias. For the matrices `Theta1` and `Theta2`, this corresponds to the first column of each matrix. You should now add regularization to your cost function. Notice that you can first compute the unregularized cost function $J$ using your existing `nnCostFunction` and then later add the cost for the regularization terms.

[Click here to go back to `nnCostFunction` for editing.](#nnCostFunction)

Once you are done, the next cell will call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`, and $\lambda = 1$. You should see that the cost is about 0.383770.

```python
# Weight regularization parameter (we set this to 1 here).
lambda_ = 1
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                      num_labels, X, y, lambda_)

print('Cost at parameters (loaded from ex4weights): %.6f' % J)
print('This value should be about                 : 0.383770.')
```

*You should now submit your solutions.*

```python
grader[2] = nnCostFunction
grader.grade()
```

## 2 Backpropagation

In this part of the exercise, you will implement the backpropagation algorithm to compute the gradient for the neural network cost function. You will need to update the function `nnCostFunction` so that it returns an appropriate value for `grad`. Once you have computed the gradient, you will be able to train the neural network by minimizing the cost function $J(\theta)$ using an advanced optimizer such as `scipy`'s `optimize.minimize`.
You will first implement the backpropagation algorithm to compute the gradients for the parameters for the (unregularized) neural network. After you have verified that your gradient computation for the unregularized case is correct, you will implement the gradient for the regularized neural network.

### 2.1 Sigmoid Gradient

To help you get started with this part of the exercise, you will first implement
the sigmoid gradient function. The gradient for the sigmoid function can be
computed as

$$ g'(z) = \frac{d}{dz} g(z) = g(z)\left(1-g(z)\right) $$

where

$$ \text{sigmoid}(z) = g(z) = \frac{1}{1 + e^{-z}} $$

Now complete the implementation of `sigmoidGradient` in the next cell.

```python
def sigmoidGradient(z):
    """
    Computes the gradient of the sigmoid function evaluated at z.
    This should work regardless if z is a matrix or a vector.
    In particular, if z is a vector or matrix, you should return
    the gradient for each element.

    Parameters
    ----------
    z : array_like
        A vector or matrix as input to the sigmoid function.

    Returns
    --------
    g : array_like
        Gradient of the sigmoid function. Has the same shape as z.

    Instructions
    ------------
    Compute the gradient of the sigmoid function evaluated at
    each value of z (z can be a matrix, vector or scalar).

    Note
    ----
    We have provided an implementation of the sigmoid function
    in `utils.py` file accompanying this assignment.
    """

    g = np.zeros(z.shape)

    # ====================== YOUR CODE HERE ======================

    # =============================================================
    return g
```

When you are done, the following cell call `sigmoidGradient` on a given vector `z`. Try testing a few values by calling `sigmoidGradient(z)`. For large values (both positive and negative) of z, the gradient should be close to 0. When $z = 0$, the gradient should be exactly 0.25. Your code should also work with vectors and matrices. For a matrix, your function should perform the sigmoid gradient function on every element.

```python
z = np.array([-1, -0.5, 0, 0.5, 1])
g = sigmoidGradient(z)
print('Sigmoid gradient evaluated at [-1 -0.5 0 0.5 1]:\n  ')
print(g)
```

*You should now submit your solutions.*

```python
grader[3] = sigmoidGradient
grader.grade()
```

## 2.2 Random Initialization

When training neural networks, it is important to randomly initialize the parameters for symmetry breaking. One effective strategy for random initialization is to randomly select values for $\Theta^{(l)}$ uniformly in the range $[-\epsilon_{init}, \epsilon_{init}]$. You should use $\epsilon_{init} = 0.12$. This range of values ensures that the parameters are kept small and makes the learning more efficient.

One effective strategy for choosing $\epsilon_{init}$ is to base it on the number of units in the network. A good choice of $\epsilon_{init}$ is $\epsilon_{init} = \frac{\sqrt{6}}{\sqrt{L_{in} + L_{out}}}$ where $L_{in} = s_l$ and $L_{out} = s_{l+1}$ are the number of units in the layers adjacent to $\Theta^{l}$.

Your job is to complete the function `randInitializeWeights` to initialize the weights for $\Theta$. Modify the function by filling in the following code:

```python
# Randomly initialize the weights to small values
W = np.random.rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init
```
Note that we give the function an argument for $\epsilon$ with default value `epsilon_init = 0.12`.

```python
def randInitializeWeights(L_in, L_out, epsilon_init=0.12):
    """
    Randomly initialize the weights of a layer in a neural network.

    Parameters
    ----------
    L_in : int
        Number of incomming connections.

    L_out : int
        Number of outgoing connections.

    epsilon_init : float, optional
        Range of values which the weight can take from a uniform
        distribution.

    Returns
    -------
    W : array_like
        The weight initialiatized to random values.  Note that W should
        be set to a matrix of size(L_out, 1 + L_in) as
        the first column of W handles the "bias" terms.

    Instructions
    ------------
    Initialize W randomly so that we break the symmetry while training
    the neural network. Note that the first column of W corresponds
    to the parameters for the bias unit.
    """

    # You need to return the following variables correctly
    W = np.zeros((L_out, 1 + L_in))

    # ====================== YOUR CODE HERE ======================

    # ============================================================
    return W
```

*You do not need to submit any code for this part of the exercise.*

Execute the following cell to initialize the weights for the 2 layers in the neural network using the `randInitializeWeights` function.

```python
print('Initializing Neural Network Parameters ...')

initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size)
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels)

# Unroll parameters
initial_nn_params = np.concatenate([initial_Theta1.ravel(), initial_Theta2.ravel()], axis=0)
```

### 2.4 Backpropagation

Now, you will implement the backpropagation algorithm. Recall that the intuition behind the backpropagation algorithm is as follows. Given a training example $(x^{(t)}, y^{(t)})$, we will first run a “forward pass” to compute all the activations throughout the network, including the output value of the hypothesis $h_\theta(x)$. Then, for each node $j$ in layer $l$, we would like to compute an “error term” $\delta_j^{(l)}$ that measures how much that node was “responsible” for any errors in our output.

For an output node, we can directly measure the difference between the network’s activation and the true target value, and use that to define $\delta_j^{(3)}$ (since layer 3 is the output layer). For the hidden units, you will compute $\delta_j^{(l)}$ based on a weighted average of the error terms of the nodes in layer $(l+1)$. In detail, here is the backpropagation algorithm (also depicted in the figure above). You should implement steps 1 to 4 in a loop that processes one example at a time. Concretely, you should implement a for-loop `for t in range(m)` and place steps 1-4 below inside the for-loop, with the $t^{th}$ iteration performing the calculation on the $t^{th}$ training example $(x^{(t)}, y^{(t)})$. Step 5 will divide the accumulated gradients by $m$ to obtain the gradients for the neural network cost function.

1. Set the input layer’s values $(a^{(1)})$ to the $t^{th }$training example $x^{(t)}$. Perform a feedforward pass, computing the activations $(z^{(2)}, a^{(2)}, z^{(3)}, a^{(3)})$ for layers 2 and 3. Note that you need to add a `+1` term to ensure that the vectors of activations for layers $a^{(1)}$ and $a^{(2)}$ also include the bias unit. In `numpy`, if a 1 is a column matrix, adding one corresponds to `a_1 = np.concatenate([np.ones((m, 1)), a_1], axis=1)`.

1. For each output unit $k$ in layer 3 (the output layer), set
$$\delta_k^{(3)} = \left(a_k^{(3)} - y_k \right)$$
where $y_k \in \{0, 1\}$ indicates whether the current training example belongs to class $k$ $(y_k = 1)$, or if it belongs to a different class $(y_k = 0)$. You may find logical arrays helpful for this task (explained in the previous programming exercise).

1. For the hidden layer $l = 2$, set
$$ \delta^{(2)} = \left( \Theta^{(2)} \right)^T \delta^{(3)} * g'\left(z^{(2)} \right)$$
Note that the symbol $*$ performs element wise multiplication in `numpy`.

1. Accumulate the gradient from this example using the following formula. Note that you should skip or remove $\delta_0^{(2)}$. In `numpy`, removing $\delta_0^{(2)}$ corresponds to `delta_2 = delta_2[1:]`.
$$ \Delta^{(l)} = \Delta^{(l)} + \delta^{(l+1)} (a^{(l)})^{(T)} $$

1. Obtain the (unregularized) gradient for the neural network cost function by dividing the accumulated gradients by $\frac{1}{m}$:
$$ \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)}$$

**Python/Numpy tip**: You should implement the backpropagation algorithm only after you have successfully completed the feedforward and cost functions. While implementing the backpropagation alogrithm, it is often useful to use the `shape` function to print out the shapes of the variables you are working with if you run into dimension mismatch errors.

[Click here to go back and update the function `nnCostFunction` with the backpropagation algorithm](#nnCostFunction).

**Note:** If the iterative solution provided above is proving to be difficult to implement, try implementing the vectorized approach which is easier to implement in the opinion of the moderators of this course. You can find the tutorial for the vectorized approach [here](https://www.coursera.org/learn/machine-learning/discussions/all/threads/a8Kce_WxEeS16yIACyoj1Q).

After you have implemented the backpropagation algorithm, we will proceed to run gradient checking on your implementation. The gradient check will allow you to increase your confidence that your code is
computing the gradients correctly.

### 2.4  Gradient checking

In your neural network, you are minimizing the cost function $J(\Theta)$. To perform gradient checking on your parameters, you can imagine “unrolling” the parameters $\Theta^{(1)}$, $\Theta^{(2)}$ into a long vector $\theta$. By doing so, you can think of the cost function being $J(\Theta)$ instead and use the following gradient checking procedure.

Suppose you have a function $f_i(\theta)$ that purportedly computes $\frac{\partial}{\partial \theta_i} J(\theta)$; you’d like to check if $f_i$ is outputting correct derivative values.

$$
\text{Let } \theta^{(i+)} = \theta + \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
\quad \text{and} \quad \theta^{(i-)} = \theta - \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
$$

So, $\theta^{(i+)}$ is the same as $\theta$, except its $i^{th}$ element has been incremented by $\epsilon$. Similarly, $\theta^{(i−)}$ is the corresponding vector with the $i^{th}$ element decreased by $\epsilon$. You can now numerically verify $f_i(\theta)$’s correctness by checking, for each $i$, that:

$$ f_i\left( \theta \right) \approx \frac{J\left( \theta^{(i+)}\right) - J\left( \theta^{(i-)} \right)}{2\epsilon} $$

The degree to which these two values should approximate each other will depend on the details of $J$. But assuming $\epsilon = 10^{-4}$, you’ll usually find that the left- and right-hand sides of the above will agree to at least 4 significant digits (and often many more).

We have implemented the function to compute the numerical gradient for you in `computeNumericalGradient` (within the file `utils.py`). While you are not required to modify the file, we highly encourage you to take a look at the code to understand how it works.

In the next cell we will run the provided function `checkNNGradients` which will create a small neural network and dataset that will be used for checking your gradients. If your backpropagation implementation is correct,
you should see a relative difference that is less than 1e-9.

**Practical Tip**: When performing gradient checking, it is much more efficient to use a small neural network with a relatively small number of input units and hidden units, thus having a relatively small number
of parameters. Each dimension of $\theta$ requires two evaluations of the cost function and this can be expensive. In the function `checkNNGradients`, our code creates a small random model and dataset which is used with `computeNumericalGradient` for gradient checking. Furthermore, after you are confident that your gradient computations are correct, you should turn off gradient checking before running your learning algorithm.

**Practical Tip:** Gradient checking works for any function where you are computing the cost and the gradient. Concretely, you can use the same `computeNumericalGradient` function to check if your gradient implementations for the other exercises are correct too (e.g., logistic regression’s cost function).

```python
utils.checkNNGradients(nnCostFunction)
```

*Once your cost function passes the gradient check for the (unregularized) neural network cost function, you should submit the neural network gradient function (backpropagation).*

```python
grader[4] = nnCostFunction
grader.grade()
```

### 2.5 Regularized Neural Network

After you have successfully implemented the backpropagation algorithm, you will add regularization to the gradient. To account for regularization, it turns out that you can add this as an additional term *after* computing the gradients using backpropagation.

Specifically, after you have computed $\Delta_{ij}^{(l)}$ using backpropagation, you should add regularization using

$$ \begin{align}
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} & \qquad \text{for } j = 0 \\
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} + \frac{\lambda}{m} \Theta_{ij}^{(l)} & \qquad \text{for } j \ge 1
\end{align}
$$

Note that you should *not* be regularizing the first column of $\Theta^{(l)}$ which is used for the bias term. Furthermore, in the parameters $\Theta_{ij}^{(l)}$, $i$ is indexed starting from 1, and $j$ is indexed starting from 0. Thus,

$$
\Theta^{(l)} = \begin{bmatrix}
\Theta_{1,0}^{(i)} & \Theta_{1,1}^{(l)} & \cdots \\
\Theta_{2,0}^{(i)} & \Theta_{2,1}^{(l)} & \cdots \\
\vdots &  ~ & \ddots
\end{bmatrix}
$$

[Now modify your code that computes grad in `nnCostFunction` to account for regularization.](#nnCostFunction)

After you are done, the following cell runs gradient checking on your implementation. If your code is correct, you should expect to see a relative difference that is less than 1e-9.

```python
#  Check gradients by running checkNNGradients
lambda_ = 3
utils.checkNNGradients(nnCostFunction, lambda_)

# Also output the costFunction debugging values
debug_J, _  = nnCostFunction(nn_params, input_layer_size,
                          hidden_layer_size, num_labels, X, y, lambda_)

print('\n\nCost at (fixed) debugging parameters (w/ lambda = %f): %f ' % (lambda_, debug_J))
print('(for lambda = 3, this value should be about 0.576051)')
```

```python
grader[5] = nnCostFunction
grader.grade()
```

### 2.6 Learning parameters using `scipy.optimize.minimize`

After you have successfully implemented the neural network cost function
and gradient computation, the next step we will use `scipy`'s minimization to learn a good set parameters.

```python
#  After you have completed the assignment, change the maxiter to a larger
#  value to see how more training helps.
options= {'maxiter': 100}

#  You should also try different values of lambda
lambda_ = 1

# Create "short hand" for the cost function to be minimized
costFunction = lambda p: nnCostFunction(p, input_layer_size,
                                        hidden_layer_size,
                                        num_labels, X, y, lambda_)

# Now, costFunction is a function that takes in only one argument
# (the neural network parameters)
res = optimize.minimize(costFunction,
                        initial_nn_params,
                        jac=True,
                        method='TNC',
                        options=options)

# get the solution of the optimization
nn_params = res.x

# Obtain Theta1 and Theta2 back from nn_params
Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                    (hidden_layer_size, (input_layer_size + 1)))

Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                    (num_labels, (hidden_layer_size + 1)))
```

After the training completes, we will proceed to report the training accuracy of your classifier by computing the percentage of examples it got correct. If your implementation is correct, you should see a reported
training accuracy of about 95.3% (this may vary by about 1% due to the random initialization). It is possible to get higher training accuracies by training the neural network for more iterations. We encourage you to try
training the neural network for more iterations (e.g., set `maxiter` to 400) and also vary the regularization parameter $\lambda$. With the right learning settings, it is possible to get the neural network to perfectly fit the training set.

```python
pred = utils.predict(Theta1, Theta2, X)
print('Training Set Accuracy: %f' % (np.mean(pred == y) * 100))
```

## 3 Visualizing the Hidden Layer

One way to understand what your neural network is learning is to visualize what the representations captured by the hidden units. Informally, given a particular hidden unit, one way to visualize what it computes is to find an input $x$ that will cause it to activate (that is, to have an activation value
($a_i^{(l)}$) close to 1). For the neural network you trained, notice that the $i^{th}$ row of $\Theta^{(1)}$ is a 401-dimensional vector that represents the parameter for the $i^{th}$ hidden unit. If we discard the bias term, we get a 400 dimensional vector that represents the weights from each input pixel to the hidden unit.

Thus, one way to visualize the “representation” captured by the hidden unit is to reshape this 400 dimensional vector into a 20 × 20 image and display it (It turns out that this is equivalent to finding the input that gives the highest activation for the hidden unit, given a “norm” constraint on the input (i.e., $||x||_2 \le 1$)).

The next cell does this by using the `displayData` function and it will show you an image with 25 units,
each corresponding to one hidden unit in the network. In your trained network, you should find that the hidden units corresponds roughly to detectors that look for strokes and other patterns in the input.

```python
utils.displayData(Theta1[:, 1:])
```

### 3.1 Optional (ungraded) exercise

In this part of the exercise, you will get to try out different learning settings for the neural network to see how the performance of the neural network varies with the regularization parameter $\lambda$ and number of training steps (the `maxiter` option when using `scipy.optimize.minimize`). Neural networks are very powerful models that can form highly complex decision boundaries. Without regularization, it is possible for a neural network to “overfit” a training set so that it obtains close to 100% accuracy on the training set but does not as well on new examples that it has not seen before. You can set the regularization $\lambda$ to a smaller value and the `maxiter` parameter to a higher number of iterations to see this for youself.


Massimo Nocentini

March and April 2018: cleanup

November 2016: splitting from "big" notebook

Abstract

Theory of matrix functions, with applications to Pascal array $\mathcal{P}$.

```python
from sympy import *
from sympy.abc import n, i, N, x, lamda, phi, z, j, r, k, a, alpha, beta

init_printing()
```

```python
import functions_catalog
```

```python

from matrix_functions import *
from commons import *
from sequences import *
```

```python
%run ../../src/commons.py
%run ../../src/matrix_functions.py
%run ../../src/functions_catalog.py
```

# Pascal array $\mathcal{P}$

```python
m=8
```

```python
P_ = Matrix(m,m, lambda n,k: binomial(n, k, evaluate=k >= n or not k))
P_ # not usable in the framework because not a definition in equality style
```

```python
P = define(Symbol(r'\mathcal{{P}}_{{ {} }}'.format(m)), Matrix(m,m,binomial))
P
```

```python
eigendata = spectrum(P)
eigendata
```

```python
data, eigenvals, multiplicities = eigendata.rhs # unpacking to use `eigenvals` in `subs`
```

```python
Phi_polynomials = component_polynomials(eigendata, early_eigenvals_subs=True)
Phi_polynomials
```

```python
cmatrices = component_matrices(P, Phi_polynomials)
list(cmatrices.values())
```

## `power` function

```python
f_power, g_power = functions_catalog.power(eigendata, Phi_polynomials)
```

```python
P_power = g_power(P)
P_power
```

```python
production_matrix(P_power.rhs)
```

```python
assert P_power.rhs == (P.rhs**r).applyfunc(simplify)
```

## `inverse` function

```python
f_inverse, g_inverse = functions_catalog.inverse(eigendata, Phi_polynomials)
```

```python
P_inverse = g_inverse(P)
P_inverse, g_inverse(P_inverse)
```

```python
production_matrix(P_inverse.rhs)
```

```python
assert (P_inverse.rhs * P.rhs) == Matrix(m, m, identity_matrix())
assert P_inverse.rhs == P.rhs**(-1)
assert P_inverse.rhs == P_power.rhs.subs({r:-1})
```

## `sqrt` function

```python
f_sqrt, g_sqrt = functions_catalog.square_root(eigendata, Phi_polynomials)
```

```python
P_sqrt = g_sqrt(P)
P_sqrt
```

```python
production_matrix(P_sqrt.rhs)
```

```python
assert P_sqrt.rhs == P.rhs**(S(1)/2)
assert P_sqrt.rhs * P_sqrt.rhs == P.rhs
assert P_sqrt.rhs == P_power.rhs.subs({r:S(1)/2})
```

```python
P_power.rhs.subs({r:S(1)/3})
```

```python
P_power.rhs.subs({r:2})
```

```python
inspect(_)
```

    nature(is_ordinary=True, is_exponential=True)

## `expt` function

```python
f_exp, g_exp = functions_catalog.exp(eigendata, Phi_polynomials)
```

```python
P_exp = g_exp(P)
P_exp
```

```python
inspect(P_exp.rhs)
```

    nature(is_ordinary=False, is_exponential=True)

```python
production_matrix(P_exp.rhs.subs({alpha:1})) # faster than `production_matrix(P_exp.rhs).subs({alpha:1})`
```

```python
simp_P_expt = define(P_exp.lhs,
                     Mul(exp(alpha), P_exp.rhs.applyfunc(lambda c: (c/exp(alpha)).expand()), evaluate=False))
simp_P_expt
```

```python
from sympy.functions.combinatorial.numbers import stirling
```

```python
S = Matrix(m, m, lambda n,k: stirling(n,k, kind=2))
define(Symbol('S'), S), production_matrix(S), production_matrix(S, exp=True)
```

```python
S*P.rhs*S**(-1), S**(-1)*P.rhs*S
```

## `log` function

```python
f_log, g_log, = functions_catalog.log(eigendata, Phi_polynomials)
```

```python
P_log = g_log(P)
P_log
```

```python
inspect(P_log.rhs[1:,:-1])
```

    nature(is_ordinary=False, is_exponential=True)

```python
production_matrix(P_log.rhs[1:,:-1])
```

```python
P_exp_dirty = define(P_exp.lhs, P_exp.rhs.subs({alpha:1}))
P_exp_dirty
```

```python
P_exp_eigendata = spectrum(P_exp_dirty)
P_exp_eigendata
```

```python
P_exp_Phi_polynomials = component_polynomials(P_exp_eigendata, early_eigenvals_subs=True)
P_exp_Phi_polynomials
```

```python
f_log_dirty, g_log_dirty, = functions_catalog.log(P_exp_eigendata, P_exp_Phi_polynomials)
```

```python
g_log_dirty
```

```python
g_log_dirty(P_exp_dirty)
```

```python
P_log_eigendata = spectrum(P_log)
P_log_eigendata
```

```python
P_log_Phi_polynomials = component_polynomials(P_log_eigendata, early_eigenvals_subs=True)
P_log_Phi_polynomials
```

```python
f_exp_dirty, g_exp_dirty, = functions_catalog.exp(P_log_eigendata, P_log_Phi_polynomials)
```

```python
g_exp_dirty
```

```python
g_exp_dirty(P_log)
```

```python
_.rhs.subs({alpha:1})
```

## `sin` function

```python
f_sin, g_sin, G_sin = functions_catalog.sin(eigendata, Phi_polynomials)
```

```python
P_sin = G_sin(P)
P_sin
```

```python
production_matrix(P_sin.rhs).applyfunc(simplify) # takes long to evaluate
```

## `cos` function

```python
f_cos, g_cos, G_cos = functions_catalog.cos(eigendata, Phi_polynomials)
```

```python
P_cos = G_cos(P)
P_cos
```

```python
production_matrix(P_sin).applyfunc(simplify) # takes long to evaluate
```

```python
assert (P_sin.rhs**2 + P_cos.rhs**2).applyfunc(trigsimp) == Matrix(m,m, identity_matrix()) # sin^2 + cos^2 = 1
```

## `sin` function

```python
P2 = define(Symbol(r'2\,\mathcal{P}'), 2*P.rhs)
P2
```

```python
eigendata_P2 = spectrum(P2)
Phi_polynomials_P2 = component_polynomials(eigendata_P2, early_eigenvals_subs=True)
```

```python
f_sin2, g_sin2, G_sin2 = functions_catalog.sin(eigendata_P2, Phi_polynomials_P2)
```

```python
g_sin2
```

```python
P_sin2 = G_sin2(P2)
P_sin2
```

```python
PP = P_sin2.rhs.applyfunc(lambda i: i.subs({alpha:1}))
assert PP == (2*P_sin.rhs*P_cos.rhs).applyfunc(trigsimp)
```

```python
PP
```

---

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.


```
from sympy import *
```

```
x = Symbol("x")
```

```
limit(exp(x)*exp(x**2)*(erf(x+1/exp(x))-erf(x)), x, oo)
```

    2/sqrt(pi)

The examples here show the limit computation on exp-log expressions (from Gruntz' thesis pp. 122 to 123)

Eqn 8.1

```
exp(x)*(exp(1/x-exp(-x))-exp(1/x))
```

    (-exp(1/x) + exp(-exp(-x) + 1/x))*exp(x)

```
limit(_, x, oo)
```

    -1

Eqn 8.2

```
exp(x)*(exp(1/x+exp(-x)+exp(-x**2)) - exp(1/x-exp(-exp(x))))
```

    (-exp(-exp(-exp(x)) + 1/x) + exp(exp(-x**2) + exp(-x) + 1/x))*exp(x)

```
limit(_, x, oo)
```

    1

Eqn 8.3

```
exp(exp(x-exp(-x))/(1-1/x)) - exp(exp(x))
```

    exp(exp(x - exp(-x))/(1 - 1/x)) - exp(exp(x))

```
limit(_, x, oo)
```

    oo

Eqn 8.4

```
exp(exp(exp(x)/(1-1/x))) - exp(exp(exp(x)/(1-1/x-log(x)**(-log(x)))))
```

    exp(exp(exp(x)/(1 - 1/x))) - exp(exp(exp(x)/(1 - log(x)**(-log(x)) - 1/x)))

```
limit(_, x, oo)
```

    -oo

Eqn 8.5

```
exp(exp(exp(x+exp(-x)))) / exp(exp(exp(x)))
```

    exp(-exp(exp(x)))*exp(exp(exp(x + exp(-x))))

```
limit(_, x, oo)
```

    oo

Eqn 8.6

```
exp(exp(exp(x))) / exp(exp(exp(x-exp(-exp(x)))))
```

    exp(exp(exp(x)))*exp(-exp(exp(x - exp(-exp(x)))))

```
limit(_, x, oo)
```

    oo

Eqn 8.7

```
exp(exp(exp(x))) / exp(exp(exp(x-exp(-exp(exp(x))))))
```

    exp(exp(exp(x)))*exp(-exp(exp(x - exp(-exp(exp(x))))))

```
limit(_, x, oo)
```

    1

Eqn 8.8

```
exp(exp(x)) / exp(exp(x-exp(-exp(exp(x)))))
```

    exp(exp(x))*exp(-exp(x - exp(-exp(exp(x)))))

```
limit(_, x, oo)
```

    1

Eqn 8.9

```
log(x)**2 * exp(sqrt(log(x))*(log(log(x)))**2 * exp(sqrt(log(log(x))) * (log(log(log(x))))**3)) / sqrt(x)
```

    exp(exp(sqrt(log(log(x)))*log(log(log(x)))**3)*sqrt(log(x))*log(log(x))**2)*log(x)**2/sqrt(x)

```
limit(_, x, oo)
```

    0

Eqn 8.10

```
(x*log(x)*(log(x*exp(x)-x**2))**2) / (log(log(x**2+2*exp(exp(3*x**3*log(x))))))
```

    x*log(x)*log(-x**2 + x*exp(x))**2/log(log(x**2 + 2*exp(exp(3*x**3*log(x)))))

```
limit(_, x, oo)
```

    1/3

Eqn 8.11

```
(exp(x*exp(-x)/(exp(-x)+exp(-2*x**2/(x+1)))) - exp(x))/x
```

    (-exp(x) + exp(x*exp(-x)/(exp(-2*x**2/(x + 1)) + exp(-x))))/x

```
limit(_, x, oo)
```

    -exp(2)

Eqn 8.12

```
(3**x + 5**x)**(1/x)
```

    (3**x + 5**x)**(1/x)

```
limit(_, x, oo)
```

    5

Eqn 8.13

```
x/log(x**(log(x**(log(2)/log(x)))))
```

    x/log(x**log(x**(log(2)/log(x))))

```
limit(_, x, oo)
```

    oo

Eqn 8.14

```
exp(exp(2*log(x**5+x)*log(log(x)))) / exp(exp(10*log(x)*log(log(x))))
```

    exp(-exp(10*log(x)*log(log(x))))*exp(exp(2*log(x**5 + x)*log(log(x))))

```
limit(_, x, oo)
```

    oo

Eqn 8.15

```
4*exp(exp(S(5)/2*x**(-S(5)/7)+ S(21)/8*x**(S(6)/11)+2*x**(-8)+S(54)/17*x**(S(49)/45) ))**8 / (9*log(log(-log(S(4)/3*x**(-S(5)/14))))**(S(7)/6))
```

    4*exp(8*exp(54*x**(49/45)/17 + 21*x**(6/11)/8 + 2/x**8 + 5/(2*x**(5/7))))/(9*log(log(-log(4/(3*x**(5/14)))))**(7/6))

```
limit(_, x, oo)
```

    oo

Eqn 8.16

```
(exp(4*x*exp(-x)/(1/exp(x)+1/exp(2*x**2/(x+1)))) - exp(x)) / exp(x)**4
```

    (-exp(x) + exp(4*x*exp(-x)/(exp(-2*x**2/(x + 1)) + exp(-x))))*exp(-4*x)

```
limit(_, x, oo)
```

    1

Eqn 8.17

```
exp(x*exp(-x)/(exp(-x)+exp(-2*x**2/(x+1))))/exp(x)
```

    exp(-x)*exp(x*exp(-x)/(exp(-2*x**2/(x + 1)) + exp(-x)))

```
limit(_, x, oo)
```

    1

Eqn 8.18

```
(exp(exp(-x/(1+exp(-x))))*exp(-x/(1+exp(-x/(1+exp(-x)))))*exp(exp(-x+exp(-x/(1+exp(-x)))))) / (exp(-x/(1+exp(-x))))**2 - exp(x) + x
```

    x - exp(x) + exp(2*x/(1 + exp(-x)))*exp(-x/(1 + exp(-x/(1 + exp(-x)))))*exp(exp(-x/(1 + exp(-x))))*exp(exp(-x + exp(-x/(1 + exp(-x)))))

```
limit(_, x, oo)
```

    2

Eqn 8.19

```
log(x)*(log(log(x)+log(log(x))) - log(log(x))) / (log(log(x)+log(log(log(x)))))
```

    (log(log(x) + log(log(x))) - log(log(x)))*log(x)/log(log(x) + log(log(log(x))))

```
limit(_, x, oo)
```

    1

Eqn 8.20

```
exp((log(log(x+exp(log(x)*log(log(x)))))) / (log(log(log(exp(x)+x+log(x))))))
```

    exp(log(log(x + exp(log(x)*log(log(x)))))/log(log(log(x + exp(x) + log(x)))))

```
limit(_, x, oo)
```

    E

The following examples show limit computation on special functions (from Gruntz' thesis p. 126)

Eqn 8.21

```
exp(x)*(sin(1/x+exp(-x))-sin(1/x+exp(-x**2)))
```

    (sin(exp(-x) + 1/x) - sin(exp(-x**2) + 1/x))*exp(x)

```
limit(_, x, oo)
```

    1

Eqn 8.22

```
exp(exp(x)) * (exp(sin(1/x+exp(-exp(x)))) - exp(sin(1/x)))
```

    (-exp(sin(1/x)) + exp(sin(exp(-exp(x)) + 1/x)))*exp(exp(x))

```
limit(_, x, oo)
```

    1

Eqn 8.23

```
(erf(x-exp(-exp(x))) - erf(x)) * exp(exp(x)) * exp(x**2)
```

    (-erf(x) + erf(x - exp(-exp(x))))*exp(x**2)*exp(exp(x))

```
limit(_, x, oo)
```

    -2/sqrt(pi)

Eqn 8.24

```
(Ei(x-exp(-exp(x))) - Ei(x)) *exp(-x)*exp(exp(x))*x
```

    x*(-Ei(x) + Ei(x - exp(-exp(x))))*exp(-x)*exp(exp(x))

```
limit(_, x, oo)
```

    -1

Eqn 8.25

```
exp((log(2)+1)*x) * (zeta(x+exp(-x)) - zeta(x))
```

    (-zeta(x) + zeta(x + exp(-x)))*exp(x*(log(2) + 1))

```
#limit(_, x, oo)
```

Eqn 8.26

```
exp(x)*(gamma(x+exp(-x)) - gamma(x))
```

    (-gamma(x) + gamma(x + exp(-x)))*exp(x)

```
limit(_, x, oo)
```

    oo

Eqn 8.27

```
exp(gamma(x-exp(-x))*exp(1/x)) - exp(gamma(x))
```

    exp(exp(1/x)*gamma(x - exp(-x))) - exp(gamma(x))

```
#limit(_, x, oo)
```

Eqn 8.28

```
(gamma(x+1/gamma(x)) - gamma(x)) / log(x)
```

    (-gamma(x) + gamma(x + 1/gamma(x)))/log(x)

```
limit(_, x, oo)
```

    1

Eqn 8.29

```
x * (gamma(x-1/gamma(x)) - gamma(x) + log(x))
```

    x*(log(x) - gamma(x) + gamma(x - 1/gamma(x)))

```
limit(_, x, oo)
```

    1/2

Eqn 8.30

```
((gamma(x+1/gamma(x)) - gamma(x))/log(x) - cos(1/x))*x*log(x)
```

    x*((-gamma(x) + gamma(x + 1/gamma(x)))/log(x) - cos(1/x))*log(x)

```
limit(_, x, oo)
```

    -1/2

Eqn 8.31

```
gamma(x+1)/sqrt(2*pi) - exp(-x)*(x**(x+S(1)/2) + x**(x-S(1)/2)/12)
```

    -(x**(x - 1/2)/12 + x**(x + 1/2))*exp(-x) + sqrt(2)*gamma(x + 1)/(2*sqrt(pi))

```
limit(_, x, oo)
```

    oo

Eqn 8.32

```
log(gamma(gamma(x)))/exp(x)
```

    exp(-x)*log(gamma(gamma(x)))

```
limit(_, x, oo)
```

    oo

Eqn 8.33

```
exp(exp(digamma(digamma(x))))/x
```

    exp(exp(polygamma(0, polygamma(0, x))))/x

```
limit(_, x, oo)
```

    exp(-1/2)

Eqn 8.34

```
exp(exp(digamma(log(x))))/x
```

    exp(exp(polygamma(0, log(x))))/x

```
limit(_, x, oo)
```

    exp(-1/2)

Eqn 8.35

```
exp(exp(exp(digamma(digamma(digamma(x))))))/x
```

    exp(exp(exp(polygamma(0, polygamma(0, polygamma(0, x))))))/x

```
limit(_, x, oo)
```

    0

Eqn 8.36

```
besselj(2,x)*exp(x*(2*log(2+sqrt(3))-sqrt(3)))*sqrt(x)
```

    sqrt(x)*exp(x*(-sqrt(3) + 2*log(sqrt(3) + 2)))*besselj(2, x)

```
#limit(_, x, oo)
```

Eqn 8.37

```
Max(x, exp(x))/log(Min(exp(-x), exp(-exp(x))))
```

    Max(x, exp(x))/log(Min(exp(-x), exp(-exp(x))))

```
#limit(_, x, oo)
```

Some other examples

```
digamma(digamma(digamma(x)))
```

    polygamma(0, polygamma(0, polygamma(0, x)))

```
limit(_, x, oo)
```

    oo

```
loggamma(loggamma(x))
```

    loggamma(loggamma(x))

```
limit(_, x, oo)
```

    oo