# Bayesian optimization

*Selected Topics in Mathematical Optimization*

**Michiel Stock** ([email](michiel.stock@ugent.be))

```python
import numpy as np
import sympy as sp
sp.init_printing()
from numpy import sin, exp, cos
from sklearn.preprocessing import normalize
from scipy.stats import norm
from scipy.integrate import odeint
import matplotlib.pyplot as plt
from sklearn.gaussian_process import GaussianProcessRegressor
%matplotlib inline
```

```python
np.random.seed(3)
```

```python
blue = '#264653'
green = '#2a9d8f'
yellow = '#e9c46a'
orange = '#f4a261'
red = '#e76f51'
black = '#50514F'
```

## Motivation

**Goal**:

Solve

$$
\mathbf{x}^* = \text{arg min}_\mathbf{x}\, f(\mathbf{x})
$$

when every evaluation of $f(\mathbf{x})$ is *expensive*.

**Example 1**:

*Dark art* of selecting the right hyperparameters for a machine learning model. Ditto for many optimization methods!

**Example 2**:

*Model calibration* to find the right parameters or structure of a mechanistic model.

**Example 3**:

Experimentation for improvement (drug design, product development, new cooking recipes...):

1. Gather data
2. Make model
3. Select new experiments for testing in the lab
4. Repeat

## Leading example

Damped spring mass system.

Model:

$$
m\ddot{y}(t) + \gamma \dot{y}(t) + k y(t) = 0\,,
$$

with:
- $m$: the mass (1 kg)
- $\gamma$: friction parameter (unknown)
- $k$: the spring constant (unknown)

Can be simulated (here exact solution known).

```python
def damped_oscillator(t_vals, g, k, m=1, sigma=0, x0=1):
    if g**2 > 4*k*m:
        l1 = (-g**2 + (g**2 - 4*k*m)**0.5) / (2 * m)
        l2 = (-g**2 - (g**2 - 4*k*m)**0.5) / (2 * m)
        c1, c2 = np.linalg.solve([[1, 1], [l1, l2]], [[x0], [0]]).flatten()
        y_vals = c1 * exp(t_vals * l1) + c2 * exp(t_vals * l2)
    elif g**2 == 4*k*m:
        l1 = - g / (2 * m)
        c1, c2 = np.linalg.solve([[1, 0], [l1, 1]], [[x0], [0]]).flatten()
        y_vals = c1 * exp(t_vals * l1) + c2 * t_vals * exp(t_vals * l1)
    else:
        alpha = - g / (2 * m)
        beta = (4 * m * k - g**2)**0.5 / (2 * m)
        c1, c2 = np.linalg.solve([[1, 0], [alpha, beta]], [[x0], [0]]).flatten()
        y_vals = exp(alpha * t_vals) * (c1 * cos(beta * t_vals) + c2 * sin(beta * t_vals))
    return y_vals + np.random.randn(len(t_vals)) * sigma
```

```python
def plot_ode_examples():
    fig, ax = plt.subplots()

    t_vals = np.linspace(0, 5, num = 1000)

    for g in np.logspace(-2, 2, num=4):
        for k in np.logspace(-2, 2, num=4):
            y_vals = damped_oscillator(t_vals, g=g, k=k)
            ax.plot(t_vals, y_vals)
    ax.set_xlabel('$t$')
    ax.set_ylabel('$y(t)$')
```

```python
plot_ode_examples()
```

## Finding the best model parameters

We perform some experiments, and have 20 noisy measurements.

```python
# parameters
m=1  # known
kstar=7  # unknown
gstar=1  # unknown
```

```python
# noisy observations
t_meas = np.linspace(0, 5, num=20)
y_obs = damped_oscillator(t_meas, g=gstar, k=kstar, m=m, sigma=0.15)
```

```python
# true function
t_vals = np.linspace(0, 5, num = 1000)
y_vals = damped_oscillator(t_vals, g=gstar, k=kstar, m=m)
```

```python
def show_experiments():
    fig, ax = plt.subplots()

    ax.plot(t_vals, y_vals, c=blue, label='unknown parametrized function')
    ax.scatter(t_meas, y_obs, c=red, label='noisy observations')
    ax.set_xlabel('$t$')
    ax.set_ylabel('$y(t)$')
    ax.legend(loc=0)
```

```python
show_experiments()
```

For a given $\gamma$ and $k$, compute the mean squared error:

$$
MSE(\gamma, k) = \frac{1}{20} \sum_{i=1}^{20} (y(t_i) - \hat{y}(t_i, \gamma, k))^2
$$

Best parameters obtained by:

$$
\gamma^*, k^* = \text{arg min}_{\gamma, k} MSE(\gamma, k)
$$

```python
def get_mse(g, k):
    """
    Computes the mean squared error for a given gamma and k.
    """
    y_sim = damped_oscillator(t_meas, g=g, k=k)
    return np.log10(np.mean((y_sim - y_obs)**2))
```

Since gamma and kappa are positive values for which we do not know the scale, it makes sense to work with the logarithm of these parameters.

```python
g_vals = np.logspace(-2, 2, num=100)
k_vals = np.logspace(-2, 2, num=100)

mse_pars = np.zeros((len(g_vals), len(k_vals)))
for i, g in enumerate(g_vals):
    for j, k in enumerate(k_vals):
        mse_pars[i, j] = get_mse(g=g, k=k)
```

Because the example is simple, we can search the complete space for the best parameters.

```python
G, K = np.meshgrid(g_vals, k_vals)

def plot_objective():

    fig, ax = plt.subplots(figsize=(6, 6))
    ax.contourf(K, G, mse_pars.T)
    ax.set_xlabel('$k$')
    ax.set_ylabel('$g$')
    ax.scatter(kstar, gstar, c='y')
    ax.loglog()
```

```python
plot_objective()
```

In general, it is *not* possible to sample the whole parameter space. If the model is really complex, even a single evalutation might be expensive! What if we only can test a limited number of parameter combinations?

## Grid search vs. random search

```python
param_grid = np.array([[g, k] for k in np.logspace(-1.5, 1.5, num=4) for g in np.logspace(-1.5, 1.5, num=4)])
param_random = 10**np.random.uniform(-2, 2, size=(16, 2))
```

```python

def show_grid_random_search():

    fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(12, 5))

    ax0.scatter(param_grid[:,0], param_grid[:,1], c=red, label='samples')
    ax0.set_title('Grid')

    ax1.scatter(param_random[:,0], param_random[:,1], c=red, label='samples')
    ax1.set_title('Random')

    for ax in (ax1, ax0):
        ax.scatter(kstar, gstar, c=yellow, label='optimal parameters')
        ax.loglog()
        ax.set_xlabel('$k$')
        ax.set_ylabel('$\gamma$')
        ax.set_ylim([1e-2, 1e2])
        ax.set_xlim([1e-2, 1e2])
        ax.legend(loc=0)
```

```python
show_grid_random_search()
```

> **Don't use grid search!**

```python
# evaluate instances
simulated_parameters = param_random
#simulated_parameters = param_grid
mse_obs = np.array([get_mse(g=g, k=k) for g, k in simulated_parameters])
mse_obs
```

    array([-0.8542471 , -0.46689791, -0.33187908, -0.85548296, -0.94725825,
           -0.27965313, -0.8541473 , -0.18839734, -0.85414733, -0.29718357,
           -0.24694029, -0.8541491 , -0.95694289, -0.01830113, -0.22508889,
           -0.9303206 ])

## Surrogate modelling with Gaussian processes

Predict the performance of *new* parameter combinations using a surrogate model.

(From here on onwards, $\mathbf{x}\in\mathcal{X}$ is used to denote the parameters)

Gaussian process:
- learn a function of the form $f: \mathcal{X}\rightarrow \mathbb{R}$
- nonlinear model, uses a positive-definite covariance function $K:\mathcal{X} \times \mathcal{X} \rightarrow \mathbb{R}$
- Bayesian method: prior on function and use Bayes' theorem to condition on observed data
- can be updated if more date becomes available (online learning)
- fully probabilistic, in theory no tuning!

The model return for a given instance $\mathcal{x}$:

- $\mu(\mathbf{x})$: expected value;
- $\sigma(\mathbf{x})$: standard deviation.

For a brief, but well-founded overview of Gaussian processes, consult the [chapter](http://www.inference.org.uk/mackay/gpB.pdf) in the book of MacKay.

```python
gaussian_process = GaussianProcessRegressor(alpha=1e-3)
# take logarithm of MSE and the parameters to obtain better scaling
gaussian_process.fit(np.log10(simulated_parameters), mse_obs)
```

    GaussianProcessRegressor(alpha=0.001, copy_X_train=True, kernel=None,
                 n_restarts_optimizer=0, normalize_y=False,
                 optimizer='fmin_l_bfgs_b', random_state=None)

```python
instance = np.log10([[3, 0.1]])

mu, sigma = gaussian_process.predict(instance, return_std=True)
print('Predicted MSE: {} ({})'.format(mu[0], sigma[0]))
```

    Predicted MSE: -0.8392304324239794 (0.34574779040748693)

Explore whole parameter space.

```python
mu_mse = np.zeros_like(mse_pars)
std_mse = np.zeros_like(mse_pars)

for i, g in enumerate(g_vals):
    for j, k in enumerate(k_vals):
        instance = np.log10([[g, k]])
        mu, sigma = gaussian_process.predict(instance, return_std=True)
        mu_mse[i, j] = mu[:]
        std_mse[i, j] = sigma[:]
```

```python
def show_gp_preds():
    fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(12, 5))

    ax0.contourf(K, G, mu_mse.T)
    ax0.set_title('Estimated MSE')

    ax1.contourf(K, G, std_mse.T)
    ax1.set_title('Std MSE')

    for ax in (ax0, ax1):
        ax.set_xlabel('$k$')
        ax.set_ylabel('$\gamma$')
        ax.loglog()
        ax.scatter(kstar, gstar, c=yellow)
        ax.scatter(simulated_parameters[:,0], simulated_parameters[:,1], c=red)
```

```python
show_gp_preds()
```

Slice for $k=1$.

```python
k_slice = np.column_stack((np.logspace(-2, 2, num=100), [1]*100))

# expected mse, std
mu, sigma = gaussian_process.predict(np.log10(k_slice), return_std=True)

def show_slice():

    fig, ax = plt.subplots()
    ax.plot(k_slice[:,0], mu, c=green, label='expected value')
    ax.plot(k_slice[:,0], mu-2*sigma, c=orange, ls='--', label='95% interval')
    ax.plot(k_slice[:,0], mu+2*sigma, c=orange, ls='--')
    ax.semilogx()
    ax.set_xlabel('$\gamma$')
    ax.set_ylabel('predicted MSE')
    ax.legend(loc=0)
```

```python
show_slice()
```

## Acquisition functions

How to choose points to simulate/test?

Acquisition function $a:\mathcal{X}\rightarrow\mathbb{R}^+$, determines how 'interesting' a point is to explore.

Choose

$$
\mathbf{x}_\text{next} = \text{arg max}_\mathbf{x} a(\mathbf{x})\,.
$$

Trade-off between *exploration* and *exploitation*.

**Probability of Improvement**

$$
a_\text{PI}(\mathbf{x}) = P(f(\mathbf{x})<f(\mathbf{x}_\text{best}) ) = \Phi(\gamma(\mathbf{x}))\,,
$$
with $\Phi(\cdot)$ the CDF of a standard normal distirbution and
$$
\gamma(\mathbf{x})= \frac{f(x_\text{best}) - \mu(\mathbf{x})}{\sigma(\mathbf{x})}\,.
$$

```python
def calculate_gamma(mu, sigma, fbest):
    return (fbest - mu) / sigma

def probability_improvement(mu, sigma, fbest=np.min(mse_obs)):
    """
    Calculates probability of improvement.
    """
    gamma_values = calculate_gamma(mu, sigma, fbest)
    return norm.cdf(gamma_values)
```

**Expected Improvement**

$$
a_\text{EI}(\mathbf{x}) = E[\max(f(\mathbf{x}) - f(\mathbf{x}_\text{best}), 0)] = \sigma(\mathbf{x})(\gamma(\mathbf{x})\Phi(\gamma(\mathbf{x})) + \phi(\gamma(\mathbf{x}))\,,
$$
with $\phi(\cdot)$ the PDF of a standard normal distirbution.

```python
def expected_improvement(mu, sigma, fbest=np.min(mse_obs)):
    """
    Calculates expected improvement.
    """
    gamma_values = calculate_gamma(mu, sigma, fbest)
    return sigma * (gamma_values * norm.cdf(gamma_values) + norm.pdf(gamma_values))
```

**GP Lower Confidence Limit**

$$
a_\text{LCB} = \mu(\mathbf{x}) - \kappa \sigma(\mathbf{x})\,,
$$
with $\kappa$ a parameter determining the tightness of the bound.

```python
def lower_confidence_bound(mu, sigma, kappa=2):
    """
    Calculates lower confidence bound.

    Made negative: maximizing acquisition function!
    """
    return - (mu - kappa * sigma)
```

```python
def plot_acquisitions():
    fig, (ax0, ax1, ax2, ax3) = plt.subplots(nrows=4, sharex=True, figsize=(8, 10))

    # plot mu and sigma
    ax0.plot(k_slice[:,0], mu, c=green, label='expected value')
    ax0.plot(k_slice[:,0], mu-2*sigma, c=orange, ls='--', label='95% interval')
    ax0.plot(k_slice[:,0], mu+2*sigma, c=orange, ls='--')
    ax0.semilogx()
    ax0.set_ylabel('predicted\nMSE')

    # plot information gains
    ax1.plot(k_slice[:,0], probability_improvement(mu, sigma), c=blue, label='PI')
    ax1.set_ylabel('PI')
    ax2.plot(k_slice[:,0], expected_improvement(mu, sigma), c=blue, label='EI')
    ax2.set_ylabel('EI')
    ax3.plot(k_slice[:,0], lower_confidence_bound(mu, sigma), c=blue, label='LCB')
    ax3.set_ylabel('LCB')
    ax3.set_xlabel('$\gamma$')

```

```python
plot_acquisitions()
```

```python
PI = np.zeros_like(mse_pars)
EI = np.zeros_like(mse_pars)
LCB = np.zeros_like(mse_pars)

for i, g in enumerate(g_vals):
    for j, k in enumerate(k_vals):
        instance = np.log10([[g, k]])
        mu, sigma = gaussian_process.predict(instance, return_std=True)
        mu = mu[:]
        sigma = sigma[:]
        PI[i,j] = probability_improvement(mu, sigma)
        EI[i,j] = expected_improvement(mu, sigma)
        LCB[i,j] = lower_confidence_bound(mu, sigma)
```

```python
def show_acquisition_contourf(ax0, ax1, ax2):
    ax0.contourf(K, G, PI.T)
    ax0.set_title('PI')
    ax0.scatter(simulated_parameters[:,0], simulated_parameters[:,1], c=red)

    ax1.contourf(K, G, EI.T)
    ax1.set_title('EI')
    ax1.scatter(simulated_parameters[:,0], simulated_parameters[:,1], c=red)

    ax2.contourf(K, G, LCB.T)
    ax2.set_title('LCB')
    ax2.scatter(simulated_parameters[:,0], simulated_parameters[:,1], c=red)

    for ax in (ax0, ax1, ax2):
        ax.set_xlabel('$k$')
        ax.set_ylabel('$\gamma$')
        ax.loglog()
        ax.scatter(kstar, gstar, c=yellow)
        ax.scatter(simulated_parameters[:,0], simulated_parameters[:,1], c=red)
```

```python
fig, (ax0, ax1, ax2) = plt.subplots(ncols=3, figsize=(13, 5))
show_acquisition_contourf(ax0, ax1, ax2)
```

Use Gaussian process model to select new points to evaluate!

## Gradient-based optimization for finding next point

If we assume that evaluating points from the Gaussian process surogate model is much cheaper than evaluating the true function, we can either use an exhaustive sampling of this space (if the dimension is not too large) or use a gradient-based optimizer to find a maximizer of the acquisition function.

```python
from scipy.optimize import minimize
```

```python
neg_acquisition = lambda parameter : -probability_improvement(*gaussian_process.predict(parameter.reshape((1,2)),
                                                                                return_std=True))
```

```python
# start from best previous point
f0, x0 = min(zip(mse_obs, simulated_parameters))

result = minimize(neg_acquisition, x0=x0,
                  bounds=[(-2, 2), (-2, 2)])

xnew = result.x

result
```

          fun: array([-0.36629826])
     hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>
          jac: array([-1.83551246e-01, -1.22679644e-06])
      message: b'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
         nfev: 18
          nit: 5
       status: 0
      success: True
            x: array([2.        , 1.06184257])

```python
print('New parameters to try: {}'.format(10**xnew))
```

    New parameters to try: [100.          11.53035202]

## Concluding remarks

**Remark 1**

Gaussian process model usually continuous and differentiable w.r.t. $\mathbf{x}$:

- $\nabla \mu(\mathbf{x})$
- $\nabla \sigma(\mathbf{x})$

gradient-based optimization!

**Ramark 2**

Use of correlation between instances:

```python
param_random = 10**np.random.uniform(-2, 2, size=(20, 2))

mu, cov = gaussian_process.predict(np.log10(param_random), return_cov=True)

fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(8, 4))
ax0.plot(10**mu)
ax0.set_ylabel('estimated MSE')

ax1.imshow(cov, interpolation='nearest', cmap='hot')
ax1.set_title('Covariance')
```

Use coviance to select a *set* of informative instances to explore!

**Remark 3**

Some instances will differ in execution times:

- regularization size (machine learning)
- learning rate (machine learning)
- number of parameters
- grid size approximation (mechanistic modelling)

Better optimize *expected improvement per second*. Use a model of expected duration (second Gaussian process).

## References

Shahriari et al. *Taking the human out of the loop: a review of Bayesian optimization*. Proceedings of the IEEE 104(1) p. 148-175

Snoek, J., Larochelle, H., Adams, R. '*Practical Bayesian optimization of machine learning algorithms*'. Advances in Neural Information Processing Systems (2012)

Rasmussen, C., Williams, C., '*Gaussian Processes for Machine Learning*'. The MIT Press (2006)

```python

```


CompEcon Toolbox:

DemDdp02

Asset replacement model

Randall Romero Aguilar, PhD

This demo is based on the original Matlab demo accompanying the  Computational Economics and Finance 2001 textbook by Mario Miranda and Paul Fackler.

Last updated: 2020-Sep-09

## About

At the beginning of each year, a manufacturer must decide whether to continue to operate an aging physical asset or replace it with a new one. An asset that is $a$ years old yields a profit contribution $p(a)$ up to $n$ years, at which point the asset becomes unsafe and must be
replaced by law. The cost of a new asset is $c$. What replacement policy maximizes profits?

This is an infinite horizon, deterministic model with time $t$ measured in years.

## Initial tasks

```python
if 'google.colab' in str(get_ipython()):
    print("This notebook is running on Google Colab. Installing the compecon package.")
    !pip install compecon
```

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from compecon import DDPmodel
%matplotlib inline
```

## Model Parameters

Assume a maximum asset age of 5 years, asset replacement cost $c = 75$, and annual discount factor $\delta = 0.9$.

```python
maxage  = 5
repcost = 75
delta   = 0.9
```

### State Space

The state variable $a \in \{1, 2, 3, \dots, n\}$ is the age of the asset in years.

```python
S = np.arange(1, 1 + maxage)  # machine age
n = S.size                    # number of states
```

### Action Space

The action variable $x \in \{\text{keep, replace}\}$ is the hold-replacement decision.

```python
X = np.array(['keep', 'replace'])     # list of actions
m = len(X)                  # number of actions
```

### Reward Function

The reward function is
\begin{equation}
f(a, x) = \begin{cases} p(a), &x = \text{keep} \\
    p(0) - c, &x = \text{replace}
\end{cases}
\end{equation}

Assuming a profit contribution $p(a) = 50 − 2.5a − 2.5a^2$ that is a function of the asset age $a$ in years:

```python
f = np.zeros((m, n))
f[0] = 50 - 2.5 * S - 2.5 * S ** 2
f[1] = 50 - repcost
f[0, -1] = -np.inf
```

### State Transition Function

The state transition function is
\begin{equation}
g(a, x) = \begin{cases} a + 1, &x = \text{keep} \\
    1, &x = \text{replace}
\end{cases}
\end{equation}

```python
g = np.zeros_like(f)
g[0] = np.arange(1, n + 1)
g[0, -1] = n - 1  # adjust last state so it doesn't go out of bounds
```

## Model Structure

The value of an asset of age a satisfies the Bellman equation
\begin{equation}
V(a) = \max\{p(a) + \delta V(a + 1),\quad p(0) − c + \delta V(1)\}
\end{equation}

where we set $p(n) = -\infty$ to enforce replacement of an asset of age $n$. The Bellman equation asserts that if the manufacturer keeps an asset of age $a$, he earns $p(a)$ over the coming year and begins the subsequent year with an asset that is one year older and worth $V(a+1)$; if he replaces the asset, however, he starts the year with a new asset, earns $p(0)−c$ over the year, and begins the subsequent year with an asset that is one year old and worth $V(1)$. Actually, our language is a little loose here. The value $V(a)$ measures not only the current and future net earnings of an asset of age $a$, but also the net earnings of all future assets that replace it.

To solve and simulate this model, use the CompEcon class ```DDPmodel```.

```python
model = DDPmodel(f, g, delta)
model.solve()
```

```python
solution = pd.DataFrame({
    'Age of Machine': S,
    'Action': X[model.policy],
    'Value': model.value}).set_index('Age of Machine')

solution['Action'] = solution['Action'].astype('category')
```

## Analysis

### Plot Optimal Value
This Figure gives the value of the firm at
the beginning of the period as a function of the asset’s age.

```python
ax = solution['Value'].plot();
ax.set(title='Optimal Value Function', ylabel='Value', xticks=S);
```

### Plot Optimal Policy
This Figure gives the optimal action as a function of the asset’s age.

```python
ax = solution['Action'].cat.codes.plot(marker='o')
ax.set(title='Optimal Replacement Policy', yticks=[0, 1], yticklabels=X, xticks=S);
```

### Simulate Model
The path was computed by performing a deterministic simulation of 12 years in duration using the ```simulate()``` method.

```python
sinit, nyrs = S.min() - 1, 12
t = np.arange(1 + nyrs)
spath, xpath = model.simulate(sinit, nyrs)
```

```python
simul = pd.DataFrame({
    'Year': t,
    'Age of Machine': S[spath],
    'Action': X[xpath]}).set_index('Year')

simul['Action'] = pd.Categorical(X[xpath], categories=X)
simul
```

### Plot State and Action Paths
Next Figure gives the age of the asset along the optimal path. As can be seen in this figure, the asset is replaced every four years.

```python
fig, axs = plt.subplots(2, 1, sharex=True)
simul['Age of Machine'].plot(ax=axs[0])
axs[0].set(title='Age Path');

simul['Action'].cat.codes.plot(marker='o', ax=axs[1])
axs[1].set(title='Replacement Path', yticks=[0, 1], yticklabels=X);
```

notebooks/ddp/02 Asset replacement model.ipynb


Solution to: [Day 4: Geometric Distribution I](https://www.hackerrank.com/challenges/s10-geometric-distribution-1/problem)

Table of Contents

- Table of Contents
- Notes
    - Negative Binomial Experiment
    - Negative Binomial Distribution
    - Geometric Distribution
    - Example
- Math Solution
- Solution
    - Imports
    - Input
    - Geometric Distribution
    - Format
    - Main

```javascript
%%javascript
$.getScript('https://kmahelona.github.io/ipython_notebook_goodies/ipython_notebook_toc.js')
```

    <IPython.core.display.Javascript object>

# Notes
## Negative Binomial Experiment
The negative binomial experiment has the following properties:
- n, independent trials
- binary outcome: success (s), or failure (f)
- P(s) is the same for every trial
- experiment continues until `x` successes are observed.

If X is the number of experiments until the `x`th success, then X is a discrete random variable called a negative binomial.

## Negative Binomial Distribution
Consider the following *probability mass function*
\begin{equation}
\large
b^{*}\text{(x, n, p)} = {n - 1 \choose x - 1} * p^{x} * q^{n-x}
\end{equation}

This is a negative binomial with the following properties:
- number of successes to be observed is x
- n, number of trials
- p, p(x) in 1 trial
- q, p(not x) in 1 trial
- b*(x, n, p) is the negative binomial probability, MEANING: the probability of having x - 1 successes after n - 1 trials and having x successes after n trials

## Geometric Distribution
*Geometric distribution* is the special case of a negative binomial distribution to determine the number of Bernoulli trials required for *a success*.

Recall, X is the number of successes in n independent trials, so for each i, 1 <= i <= n

\begin{equation}
\large
X_{i}
\begin{cases}
1  \text{, if the ith trial is a success} \\
0  \text{, otherwise}
\end{cases}
\end{equation}

The geometric distribution is a negative binomial distribution where the number of successes is 1. We express this with the following formula:

\begin{equation}
\large
\text{g(n, p)} = q^{n - 1} * p
\end{equation}

## Example
Bob is a high school basketball player. He is a 70% free throw shooter, meaning his probability of making a free throw is 0.70. What is the probability that Bob makes his first free throw on his fifth shot?

- n = 5
- p = 0.7
- q = 0.3

\begin{equation}
\large
\text{ g(5, 0.7)} = 0.3^{4} * 0.7
\end{equation}

\begin{equation}
\large
0.00567
\end{equation}

# Math Solution

The probability that a machine produces a defective product is 1/3. What is the probability that the 1st defect is found during the 5th inspection?

- n = 5
- p = 1/3
- q = 2/3

\begin{equation}
\large
\text{g(n, p)} = q^{n-1} * p
\end{equation}

\begin{equation}
\large
(\frac{2}{3})^{4} * \frac{1}{3}
\end{equation}

\begin{equation}
\large
0.066
\end{equation}

# Solution

## Imports

```python
from typing import Tuple
```

## Input

```python
def get_input() -> Tuple[float, int]:
    """Returns input for Day 4: Geometric distribution i.

    Returns:
        Tuple[float, int]: Geometric distribution p and n, respectively
    """
    num, denom = [int(x) for x in input().split()]
    p = num / denom
    n = int(input())
    return (p, n)
```

## Geometric Distribution

```python
def calc_geom_dist(n: int, p: float) -> float:
    """Returns geometric distribution of n & p

    g(n, p) = q ** (n-1) * p

    Args:
        n (int): number of trials
        p (float): probability of x

    Returns:
        float: Geometric distribution, given n and p
    """
    q = 1 - p
    return q ** (n-1) * p
```

## Format

```python
def format_scale(num: float) -> float:
    """Returns number formatted to scale

    Args:
        num (float): Number to format

    Returns:
        float: Number formatted to scale
    """
    return "{:.3f}".format(num)
```

## Main

```python
def main():
    p, n = get_input()

    geom_dist = calc_geom_dist(n, p)
    print( format_scale(geom_dist))
```

```python
if __name__ == "__main__":
    main()
```

    1 3
    5
    0.066


```python
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import clear_output
```

# Baby Equation

# $$ \frac{dx}{dt} = \lambda x$$

# Setup

### Generator takes input $t$ and proposes solutions: $x(t)$

```python
from pde_nn.utils import draw_neural_net
fig, ax = plt.subplots(1,1,figsize=(10,8))
draw_neural_net(ax, .1, .9, .1, .9, [1,20, 20, 20,1])
ax.set_title('Generator Architecture', fontsize=15);
```

### Autograd used to calculate derivative $\frac{dx}{dt}$.

### Discriminator classifies real for $\lambda x$ and fake for $\frac{dx}{dt}$. Generator tries to fool discriminator to predict real on fake.

```python
from pde_nn.utils import draw_neural_net
fig, ax = plt.subplots(1,1,figsize=(10,8))
draw_neural_net(ax, .1, .9, .1, .9, [1, 10, 10,1])
ax.set_title('Discriminator Architecture', fontsize=15);
```

### They play this game until, hopefully, the generator produces solution x(t) indistinguishable from true.

## Analytic Solutionn

```python
L = -1
analytic = lambda t: np.exp(L*t)
```

```python
t = np.linspace(0,10,100)
plt.figure(figsize=(10,7))
plt.xlabel('$t$', fontsize=20)
plt.ylabel('$x$', fontsize=20)
plt.plot(t, analytic(t), 'o', alpha=0.5, label='analytic')
plt.title('Analytic solution for L=-1 (n=100)', fontsize=20);
```

## GAN

```python
import torch
import torch.nn as nn
from torch import tensor, autograd
from collections import OrderedDict
torch.manual_seed(1337)
```

    <torch._C.Generator at 0x113560d90>

```python
from pde_nn.gan import Generator, Discriminator, train_GAN, train_Lagaris
from pde_nn.gan import plot_loss, plot_preds, plot_losses_and_preds
```

## GAN Training

```python
%%time
G,D,G_loss,D_loss = train_GAN(500,
                              g_hidden_units=20,
                              g_hidden_layers=3,
                              d_hidden_units=10,
                              d_hidden_layers=2,
                              logging=False,
                              G_iters=9,
                              D_iters=1,
                              n=100)
plot_losses_and_preds(G_loss, D_loss, G, t, analytic) # (with fix of freezing parameters!)
```

```python
%%time
G,D,G_loss,D_loss = train_GAN(500,
                              g_hidden_units=20,
                              g_hidden_layers=3,
                              d_hidden_units=10,
                              d_hidden_layers=2,
                              logging=False,
                              G_iters=9,
                              D_iters=1,
                              n=100)
plot_losses_and_preds(G_loss, D_loss, G, t, analytic) # no freezing!
```

## Lagaris Training

```python
%%time
F,F_loss = train_Lagaris(500,
                         g_hidden_units=20,
                         g_hidden_layers=3,
                         G_iters=10,
                         n=100)
plot_losses_and_preds(F_loss, F_loss, F, t, analytic)
```

## Compare MSE (pred vs. actual)

```python
t_torch = tensor(t, dtype=torch.float, requires_grad=True).reshape(-1,1)
gan_pred = G.predict(t_torch)
lag_pred = F.predict(t_torch)
```

```python
actual = tensor(analytic(t), dtype=torch.float).reshape(-1,1)
```

```python
mse = nn.MSELoss()
gan_mse = mse(gan_pred, actual)
lag_mse = mse(lag_pred, actual)
```

```python
print('GAN MSE: {}'.format(gan_mse))
print('Lagaris MSE: {}'.format(lag_mse))
```

    GAN MSE: 2.847582196083276e-12
    Lagaris MSE: 3.588611008353837e-08

## Compare MSE (many trials)

```python
n = 10
ep = 500
gan_mses = []
lag_mses = []
lags = []
gans = []
F_losses = []
G_losses = []
D_losses = []
for i in range(n):

    G,D,G_loss,D_loss = train_GAN(ep,
                              g_hidden_units=20,
                              g_hidden_layers=3,
                              d_hidden_units=10,
                              d_hidden_layers=2,
                              logging=False,
                              G_iters=9,
                              D_iters=1,
                              n=100)

    F,F_loss = train_Lagaris(ep,
                         g_hidden_units=20,
                         g_hidden_layers=3,
                         G_iters=10,
                         n=100)

    gans.append(G)
    lags.append(F)

    gan_mses.append(mse(G.predict(t_torch), actual).item())
    lag_mses.append(mse(F.predict(t_torch), actual).item())

    G_losses.append(G_loss)
    D_losses.append(D_loss)
    F_losses.append(F_loss)
```

```python
for i in range(n):
    spaces = ' '*(25-len(str(gan_mses[i])))
    print("GAN: {} {} LAG: {}".format(gan_mses[i], spaces, lag_mses[i]))
```

    GAN: 2.422474113666917e-12      LAG: 2.4981332558127178e-08
    GAN: 5.922702971372087e-15      LAG: 1.7654894435281676e-08
    GAN: 8.48264438291918e-13       LAG: 2.5764915534409738e-08
    GAN: 2.968019485473633          LAG: 5.872946218232755e-08
    GAN: 6.313779252024865e-13      LAG: 6.642136174406232e-09
    GAN: 6.263148888407954e-10      LAG: 1.047880804350143e-08
    GAN: 3.9885811667361715e-12     LAG: 3.5024996236643346e-08
    GAN: 6.675635813735425e-05      LAG: 3.094884704069045e-08
    GAN: 1.535515726342699e-14      LAG: 3.003405968371453e-09
    GAN: 7.890315191581149e-16      LAG: 1.9871622569667124e-08

```python
fig, ax = plt.subplots(1,1)
ax.hist(np.log(gan_mses), bins=10, label='GAN')
ax.hist(np.log(lag_mses), bins=10, label='Lagaris')
ax.legend()
ax.set_xlabel('log MSE')
ax.set_ylabel('Frequency')
ax.set_title('MSE Comparison of GAN vs Lagaris');
```

## Worst GAN

```python
mess = np.argmax(gan_mses)
plot_losses_and_preds(G_losses[mess], D_losses[mess], gans[mess], t, analytic)
```

## Best GAN

```python
best = np.argmin(gan_mses)
plot_losses_and_preds(G_losses[best], D_losses[best], gans[best], t, analytic)
```

## All GAN Losses

```python
join_losses = list(zip(G_losses, D_losses, gan_mses))
join_losses = sorted(join_losses, key=lambda x: x[2])
```

```python
fig, ax = plt.subplots(3,4,figsize=(15,8))
fig.tight_layout(pad=1.5)
ax = ax.ravel()
for i, a_ in enumerate(ax):
    if i>len(G_losses)-1:
        fig.delaxes(a_)
    else:
        plot_loss(join_losses[i][0], join_losses[i][1], a_)
        a_.set_title("MSE={}".format(join_losses[i][2]))
        a_.set_xlabel('')
        a_.set_ylabel('')
```

## Extrapolate??

```python
fig, ax = plt.subplots(1,1, figsize=(10,6))
t_ext = np.linspace(0,100,1000)
plot_preds(gans[best], t_ext, analytic, ax)
```

```python
fig, ax = plt.subplots(1,1, figsize=(10,6))
t_ext = np.linspace(0,100,1000)
plot_preds(lags[best], t_ext, analytic, ax)
```

Same as GAN in this case.

# New Equation: Simple Harmonic Oscillator

# $$ m \frac{d^2 x}{d t^2} = -kx $$

**Set up:**

## $ x = -\frac{m}{k} \frac{d^2 x}{d t^2} = -\frac{d^2 x}{d t^2} $

- Real: $x(t)$

- Fake: $-\frac{m}{k} \frac{d^2 x}{d t^2}$

- Constants: $m=1, k=1 \implies \omega = 1$

- Initial condition: $x(0) = 0, \frac{dx}{dt}(0)=-.5$

**Analytic Solution:**

$ x(t) = A \text{cos}(\omega t + \phi), \text{ where } \omega = \sqrt{\frac{k}{m}}$

or

$ x(t) = x_0 \cos{\omega t} + \frac{v_0}{\omega} \sin(\omega t) $

```python
analytic_oscillator = lambda t: 0*np.cos(t) + .5*np.sin(t)
t = np.linspace(0,2*np.pi,100)
plt.plot(t, analytic_oscillator(t), '-o', alpha=0.3)
plt.title('Solution of Harmonic Oscillator')
plt.xlabel('time (t)')
plt.ylabel('position (x)');
```

** Note: ** it lies within the same range as `tanh` which is helpful.

```python
from pde_nn.gan import train_GAN_SHO
import numpy as np
import torch.nn as nn
import torch
```

## Unsupervised

### Demo

```python
# %%time
# torch.manual_seed(42)
# G,D,G_loss,D_loss = train_GAN_SHO(2000,
#                               g_hidden_units=20,
#                               g_hidden_layers=2,
#                               d_hidden_units=30,
#                               d_hidden_layers=2,
#                               d_lr=0.001,
#                               g_lr=0.001,
#                               t_low=0,
#                               t_high=2*np.pi,
#                               logging=False,
#                               G_iters=9,
#                               D_iters=1,
#                               n=100,
#                               x0=0.,
#                               dx_dt0=.5,
#                               realtime_plot=True,
#                               activation=nn.Tanh(),
#                               wgan=False,
#                               soft_labels=False,
#                               real_data=False)
```

- noise to x and derivative (independelty white nosie)
- two eqns (two generators?)

#### 15 Gen : 1 Disc | 20x2 Gen | 30x2 Disc

#### 9 Gen : 1 Disc | 20x2 Gen | 30x2 Disc

## System of ODE

$$\begin{equation}
\frac{dx}{dt} = u
\end{equation}$$

$$\begin{equation}
\frac{du}{dt} = -x
\end{equation}$$

### Demo

```python
%%time
torch.manual_seed(42)
G,D1,D2,G_loss,D_loss = train_GAN_SHO(5000,
                              g_hidden_units=40,
                              g_hidden_layers=4,
                              d_hidden_units=20,
                              d_hidden_layers=2,
                              d_lr=0.001,
                              g_lr=0.001,
                              t_low=0,
                              t_high=2*np.pi,
                              logging=False,
                              G_iters=15,
                              D_iters=1,
                              n=100,
                              x0=0.,
                              dx_dt0=.5,
                              realtime_plot=False,
                              activation=nn.Tanh(),
                              wgan=False,
                              soft_labels=False,
                              real_data=False,
                              systemOfODE=True)
```

#### 20x4 Gen | 2x 40x2 Disc | 4G : 1D

### Using data

```python
# %%time
# G,D,G_loss,D_loss = train_GAN_SHO(5000,
#                               g_hidden_units=20,
#                               g_hidden_layers=2,
#                               d_hidden_units=30,
#                               d_hidden_layers=2,
#                               d_lr=0.001,
#                               g_lr=0.001,
#                               t_low=0,
#                               t_high=2*np.pi,
#                               logging=False,
#                               G_iters=9,
#                               D_iters=1,
#                               n=100,
#                               clip=.1,
#                               max_while=10,
#                               x0=0.,
#                               dx_dt0=.5,
#                               realtime_plot=True,
#                               activation=nn.Tanh(),
#                               wgan=False,
#                               soft_labels=False,
#                               real_data=True)
```

### WGAN

```python
# %%time
# ## do it with data on (0,5) maybe
# G,D,G_loss,D_loss = train_GAN_SHO(2000,
#                               g_hidden_units=20,
#                               g_hidden_layers=1,
#                               d_hidden_units=60,
#                               d_hidden_layers=1,
#                               d_lr=0.01,
#                               g_lr=0.01,
#                               t_low=0,
#                               t_high=1,
#                               logging=False,
#                               G_iters=1,
#                               D_iters=1,
#                               n=100,
#                               clip=.1,
#                               max_while=10,
#                               x0=0,
#                               dx_dt0=-.5,
#                               realtime_plot=True,
#                               activation=nn.Tanh(),
#                               wgan=True,
#                               soft_labels=False,
#                               real_data=True,
#                               gradient_penalty=True,
#                               gp_hyper=1)
```

#### Literally fit the function?

```python
g=Generator(n_hidden_units=40, n_hidden_layers=4, activation=nn.Tanh(), output_tan=False)
t_torch = torch.tensor(t, dtype=torch.float).reshape(-1,1)
mse = torch.nn.MSELoss()
torch_analytical = 1*torch.cos(t_torch) + -1*torch.sin(t_torch)
opti_ = torch.optim.Adam(g.parameters(), lr=0.001, betas=(0.9, 0.999))

for e in range(1000):
    pred=g(t_torch)
    loss=mse(pred, torch_analytical)

    opti_.zero_grad()
    loss.backward()
    opti_.step()

plt.plot(t, g(t_torch).detach().numpy(), label='nn')
plt.plot(t, analytic_oscillator(t), label='analytic')
plt.legend()
plt.grid();
```

```python
class Swish(torch.nn.Module):
    """
    Swish activation function
    """
    def __init__(self, beta=1.0):
        super(Swish, self).__init__()
        self.beta = beta

    def forward(self, input):
        return input * torch.sigmoid(self.beta * input)

    def extra_repr(self):
        return 'beta={}'.format(self.beta)
```

```python
sigmoid = torch.nn.Sigmoid()
swish = lambda x: x * torch.sigmoid(1 * x)
x_in = torch.linspace(-5,5,100)
dx = x_in[1] - x_in[0]
swish_out = swish(x_in).detach().numpy()
x_in = x_in.detach().numpy()

plt.plot(x_in, swish_out, label='f')
swish_d1 = np.gradient(swish_out, x_in)
plt.plot(x_in, swish_d1, label='d1')
swish_d2 = np.gradient(swish_d1, x_in)
plt.plot(x_in, swish_d2, label='d2')
plt.legend()
plt.grid();
```

# Another Equation: Burgers' Equation

# $$ \frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = \nu \frac{\partial^2 u}{\partial x^2} $$

**Set up:**

## $ u = \left( \nu \frac{\partial^2 u}{\partial x^2} - \frac{\partial u}{\partial t} \right) \left(\frac{\partial u}{\partial x}\right) ^{-1} $

- Real: $u(x,t)$

- Fake: $\left( \nu \frac{\partial^2 u}{\partial x^2} - \frac{\partial u}{\partial t} \right) \left(\frac{\partial u}{\partial x}\right) ^{-1}$

- Constant: $\nu = 1$

- Initial condition: $u(x,0)=f(x)$

**Numerical Solution:**

```python

```


# Quantization of Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Quantization Error of a Linear Uniform Quantizer

The quantization results in two different types of distortions, as illustrated in the [preceding notebook](linear_uniform_characteristic.ipynb). Overload distortions are a consequence of exceeding the maximum amplitude of the quantizer. Granular distortions are a consequence of the quantization process when no clipping occurs. Various measures are used to quantify the distortions of a given quantizer. We limit ourselves to the Signal-to-Noise ratio.

### Signal-to-Noise Ratio

A quantizer can be evaluated by its [signal-to-noise ratio](https://en.wikipedia.org/wiki/Signal-to-noise_ratio) (SNR), which is defined as the power of the unquantized signal $x[k]$ divided by the power of the quantization error $e[k]$. Under the assumption that both signals are drawn from a zero-mean weakly stationary process, the average SNR is given as

\begin{equation}
SNR = 10 \cdot \log_{10} \left( \frac{\sigma_x^2}{\sigma_e^2} \right) \quad \text{ in dB}
\end{equation}

where $\sigma_x^2$ and $\sigma_e^2$ denote the variances of the signals $x[k]$ and $e[k]$. The statistical properties of the signal $x[k]$ and the quantization error $e[k]$ are required in order to evaluate the SNR of a quantizer.

### Model for the Quantization Error

The statistical properties of the quantization error $e[k]$ have been derived for instance in [[Zölzer](../index.ipynb#Literature)]. We only summarize the results here. We focus on the non-clipping case first, hence on granular distortions. Here the quantization error is in general bounded $|e[k]| < \frac{Q}{2}$.

Under the assumption that the average magnitude of the input signal is much larger that the quantization step size $Q$, the quantization error $e[k]$ can be approximated by the following statistical model and assumptions

1. The quantization error $e[k]$ is not correlated with the input signal $x[k]$

2. The quantization error is [white](../random_signals/white_noise.ipynb)

    $$ \Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \sigma_e^2 $$

2. The probability density function (PDF) of the quantization error is given by the zero-mean [uniform distribution](../random_signals/important_distributions.ipynb#Uniform-Distribution)

    $$ p_e(\theta) = \frac{1}{Q} \cdot \text{rect} \left( \frac{\theta}{Q} \right) $$

The variance of the quantization error is then [calculated from its PDF](../random_signals/important_distributions.ipynb#Uniform-Distribution) as

\begin{equation}
\sigma_e^2 = \frac{Q^2}{12}
\end{equation}

Let's assume that the quantization index is represented as binary or [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) with $w$-bits. The common notation for the mid-tread quantizer is that $x_\text{min}$ can be represented exactly. The quantization step is then given as

\begin{equation}
Q = \frac{ x_\text{max}}{2^{w-1} - 1} = \frac{ |x_\text{min}|}{2^{w-1}}
\end{equation}

Using this, the variance of the quantization error can be related to the word length $w$

\begin{equation}
\sigma_e^2 = \frac{x^2_\text{max}}{3 \cdot 2^{2w}}
\end{equation}

From this result it can be concluded that the power of the quantization error decays by 6 dB per additional bit. This holds only for the assumptions stated above.

### Uniformly Distributed Signal

In order to calculate the average SNR of a linear uniform quantizer, a model for the input signal $x[k]$ is required. Let's assume that the signal is modeled by a zero-mean uniform distribution

\begin{equation}
p_x(\theta) = \frac{1}{2 x_\text{max}} \text{rect}\left( \frac{\theta}{2 x_\text{max}} \right)
\end{equation}

Hence, all amplitudes between $-x_\text{max}$ and $x_\text{max}$ occur with the same probability. The variance of the signal is then calculated to

\begin{equation}
\sigma_x^2 = \frac{4 x_\text{max}^2}{12}
\end{equation}

Introducing $\sigma_x^2$ and $\sigma_e^2$ into the definition of the SNR yields

\begin{equation}
SNR = 10 \cdot \log_{10} \left( 2^{2 w} \right) \approx 6.02 \, w \quad \text{in dB}
\end{equation}

This is often referred to as the 6 dB/bit rule of thumb for quantization. Note that in the derivation above it has been assumed that the signal $x[k]$ uses the full amplitude range of the quantizer. If this is not the case, the SNR will be lower since $\sigma_x^2$ is lower.

#### Example

In this example the linear uniform quantization of a random signal drawn from a uniform distribution is evaluated. The amplitude range of the quantizer is $x_\text{min} = -1$ and $x_\text{max} = 1 - Q$.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import scipy.signal as sig

w = 8  # wordlength of the quantized signal
A = 1  # amplitude of input signal
N = 8192  # number of samples
K = 30  # maximum lag for cross-correlation

def uniform_midtread_quantizer(x, Q):
    # limiter
    x = np.copy(x)
    idx = np.where(x <= -1)
    x[idx] = -1
    idx = np.where(x > 1 - Q)
    x[idx] = 1 - Q
    # linear uniform quantization
    xQ = Q * np.floor(x/Q + 1/2)

    return xQ

def analyze_quantizer(x, e):
    # estimated PDF of error signal
    pe, bins = np.histogram(e, bins=20, normed=True, range=(-Q, Q))
    # estimate cross-correlation between input and error
    ccf = 1/len(x) * np.correlate(x, e, mode='full')
    # estimate PSD of error signal
    nf, Pee = sig.welch(e, nperseg=128)
    # estimate SNR
    SNR = 10*np.log10((np.var(x)/np.var(e)))
    print('SNR = %f in dB' %SNR)

    # plot statistical properties of error signal
    plt.figure(figsize=(10,6))

    plt.subplot(121)
    plt.bar(bins[:-1]/Q, pe*Q, width = 2/len(pe))
    plt.title('Estimated histogram of quantization error')
    plt.xlabel(r'$\theta / Q$')
    plt.ylabel(r'$\hat{p}_x(\theta) / Q$')
    plt.axis([-1, 1, 0, 1.2])

    plt.subplot(122)
    plt.plot(nf*2*np.pi, Pee*6/Q**2)
    plt.title('Estimated PSD of quantization error')
    plt.xlabel(r'$\Omega$')
    plt.ylabel(r'$\hat{\Phi}_{ee}(e^{j \Omega}) / \sigma_e^2$')
    plt.axis([0, np.pi, 0, 2]);

    plt.figure(figsize=(10,6))
    ccf = ccf[N-K-1:N+K-1]
    kappa = np.arange(-len(ccf)//2,len(ccf)//2)
    plt.stem(kappa, ccf)
    plt.title('Cross-correlation function between input signal and error')
    plt.xlabel(r'$\kappa$')
    plt.ylabel(r'$\varphi_{xe}[\kappa]$')

# quantization step
Q = 1/(2**(w-1))
# compute input signal
x = np.random.uniform(size=N, low=-A, high=(A-Q))
# quantize signal
xQ = uniform_midtread_quantizer(x, Q)
e = xQ - x
# analyze quantizer
analyze_quantizer(x, e)
```

**Exercise**

* Change the number of bits `w` and check if the derived SNR holds
* Change the amplitude `A` of the input signal. What happens if you make the amplitude very small? Why?

### Harmonic Signal

For a harmonic input signal $x[k] = x_\text{max} \cdot \cos[\Omega_0 k]$ the variance $\sigma_x^2$ is given by its squared [root mean square](https://en.wikipedia.org/wiki/Root_mean_square) (RMS) value

\begin{equation}
\sigma_x^2 = \frac{x_\text{max}^2}{2}
\end{equation}

Introducing this into the definition of the SNR together with the variance $\sigma_e^2$ of the quantization error yields

\begin{equation}
SNR = 10 \cdot \log_{10} \left(2^{2 w} \cdot \frac{3}{2} \right) \approx 6.02 \, w + 1.76 \quad \text{in dB}
\end{equation}

The gain of 1.76 dB with respect to the case of a uniformly distributed input signal is due to the fact that the amplitude distribution of a harmonic signal is not uniform

\begin{equation}
p_x(\theta) = \frac{1}{\pi \sqrt{1 - (\frac{\theta}{x_\text{max}})^2}}
\end{equation}

for $|\theta| < x_\text{max}$. High amplitudes are more likely to occur. The relative power of the quantization error is lower for higher amplitudes which results in an increase of the average SNR.

### Normally Distributed Signal

So far, we did not consider clipping of the input signal $x[k]$, e.g. by ensuring that its minimum/maximum values do not exceed the limits of the quantizer. However, this cannot always be ensured for practical signals. Moreover, many practical signals cannot be modeled as a uniform distribution. For instance a [normally distributed](../random_signals/important_distributions.ipynb#Normal-Distribution) random signal exceeds a given maximum value with non-zero probability. Hence, clipping will occur for such an input signal. Clipping results in overload distortions whose amplitude can be much higher that $\frac{Q}{2}$. For the overall average SNR both granular and overload distortions have to be included.

For a normally distributed signal with a given probability that clipping occurs $\Pr \{ |x[k]| > x_\text{max} \} = 10^{-5}$ the SNR can be calculated to [[Zölzer](../index.ipynb#Literature)]

\begin{equation}
SNR \approx 6.02 \, w - 8.5 \quad \text{in dB}
\end{equation}

The reduction of the SNR by 8.5 dB results from the fact that small signal values are more likely to occur for a normally distributed signal. The relative quantization error for small signals is higher, which results in a lower average SNR. Overload distortions due to clipping result in a further reduction of the average SNR.

### Laplace Distributed Signal

The [Laplace distribution](../random_signals/important_distributions.ipynb#Laplace-Distribution) is a commonly applied model for speech and music signals. As for the normal distribution, clipping will occur with a non-zero probability. For a Laplace distributed signal with a given probability that clipping occurs $\Pr \{ |x[k]| > x_\text{max} \} = 10^{-4}$ the SNR can be calculated to [[Vary et al.](../index.ipynb#Literature)]

\begin{equation}
SNR \approx 6.02 \, w - 9 \quad \text{in dB}
\end{equation}

Even though the probability of clipping is higher as for the normally distributed signal above, the SNR is in the same range. The reason for this is, that the Laplace distribution features low signal values with a higher and large values with a lower probability in comparison to the normal distribution.

#### Example

The following example evaluates the SNR of a linear uniform quantizer with $w=8$ for a Laplace distributed signal $x[k]$. The SNR is computed for various probabilities that clipping occurs.

```python
w = 8  # wordlength of the quantized signal
Pc = np.logspace(-20, np.log10(.5), num=500)  # probabilities for clipping
N = int(1e6)  # number of samples

def compute_SNR(Pc):
    # compute input signal
    sigma_x = - np.sqrt(2) / np.log(Pc)
    x = np.random.laplace(size=N, scale=sigma_x/np.sqrt(2) )
    # quantize signal
    xQ = uniform_midtread_quantizer(x, Q)
    e = xQ - x
    # compute SNR
    SNR = 10*np.log10((np.var(x)/np.var(e)))

    return SNR

# quantization step
Q = 1/(2**(w-1))
# compute SNR for given probabilities
SNR = [compute_SNR(P) for P in Pc]

# plot results
plt.figure(figsize=(8,4))
plt.semilogx(Pc, SNR)
plt.xlabel('Probability for clipping')
plt.ylabel('SNR in dB')
plt.grid()
```

**Exercise**

* Can you explain the specific shape of the curve? What effect dominates for clipping probabilities below/obove the maximum?

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2017*.


# EVALUATING LINEARIZATIONS OF CLOSED LOOP SYSTEMS

This notebook is a companion to the slides for understanding closed loop systems.

# Preliminaries

```python
!pip install -q control
!pip install -q tellurium
!pip install -q controlSBML

import collections
import control
import controlSBML as ctl
from IPython.display import HTML, Math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sympy
import tellurium as te
```

```python
TIMES = ctl.makeSimulationTimes(0, 5, 500)
```

# Helpers

```python
def plotStep(tf, is_plot=True):
    """
    Plots a transfer function step response

    Parameters
    ----------
    tf: control.TransferFunction
    """
    result = control.forced_response(tf, T=TIMES, U=1)
    if is_plot:
        plt.plot(result.t, result.outputs)

# TESTS
Gs = control.TransferFunction([2], [1, 3])
plotStep(Gs, is_plot=False)
print("OK!")
```

    OK!

```python
def pp(obj):
    """
    Does a LaTex rendering of an object with a latex representation.

    Parameters
    ----------
    obj: object
    """
    display(Math(str(obj)))

# Tests
pp(r"e^{\alpha}")
```

$\displaystyle e^{\alpha}$

# Closed Loop Transfer Functions

**Transfer Functions**
\begin{eqnarray}
H_{RY}(s) & = & \frac{Y(s)}{R(s)} & = & \frac{C(s) G(s)}{1 + C(s) G(s) F(s)} \\
H_{RE}(s) & = & \frac{E(s)}{R(s)} & = & \frac{1}{1 + C(s) G(s) F(s)} \\
H_{NY}(s) & = & \frac{Y(s)}{N(s)} & = & -\frac{ F(s)}{1 + C(s) G(s) F(s)} \\
H_{DY}(s) & = & \frac{Y(s)}{D(s)} & = & \frac{ C(s)}{1 + C(s) G(s) F(s)} \\
\end{eqnarray}

# Problem Statement

We want to evaluate the transfer functions for a variety of systems $C(s)$ and filters $F(s)$.
We will use $G(s) = \frac{2}{s + 3}$, $C(s) = k_p$, and $F(s) = \frac{k_f}{s + k_f}$.

1. Create an experimental infrastructure, codes that creates a plot with a line for each of the transfer functions. You wil need to create the transfer functions C(s), G(s), F(s). The plot should have a legend.
    1. Inputs: G_tf (transfer function for G(s)), k_p, k_f
1. Construct plots such that $k_p \in \{1, 5, 10\}$, $k_f \in \{1, 5, 10\}$.
1. Discuss the tradeoffs between large and small values of $k_p$, $k_f$? Some of the simulations have oscillations. Why?

# Experimental Infrastructure

```python
# Initial start on function. Needs to be completed.
def plotTFs(Gs, k_p, k_f, times=TIMES, ylim=None, title=None, is_plot=True):
    """
    Constructs the transfer functions for the proportional controller, and filter.
    Calculates the transfer functions HRYs, HREs, HNYs, HDYs and plots them.

    Parameters
    ----------
    Gs: control.TransferFunction
    k_pf: float
    k_f: float
    times: list-float
    ylim: (float, float)
        limits of y-values
    title: str

    Returns
    -------
    dct
        key: name of transfer function
        value: control.TransferFunction
    """
    Cs = control.TransferFunction([k_p], [1])
    Fs = control.TransferFunction([k_f], [1, k_f])
    denom = 1 + Cs*Gs*Fs
    # Construct the transfer functions
    tf_dct = {
        "HRYs": Cs*Gs/denom,
        "HREs": None,  # TODO
        "HNYs": None,  # TODO
        "HDYs": None,  # TODO
    }
    # Construct the plots
    _, ax = plt.subplots(1)
    for tf in tf_dct.values():
        result = control.forced_response(tf, T=TIMES, U=1)
        # TODO: plot
    # Do plots
    # TODO: other plot stuff
    if not is_plot:
        plt.close()
    return tf_dct

# Tests
Gs = control.TransferFunction([2], [1, 3])
try:
    dct = plotTFs(Gs, 10, 10, ylim=[0, 3], title="Example", is_plot=False)
    assert(len(dct) == 4)
    assert("TransferFunction" in str(type(dct["HRYs"])))
    print("OK!")
except:
    print("Tests failed!")
```

# Plots

```python
for k_p in [1, 5, 10]:
    for k_f in [1, 5, 10]:
        title = "k_p: %2.1f k_f: %2.1f" % (k_p, k_f)
        _ = plotTFs(Gs, k_p, k_f, title=title, ylim=[-1, 3])
```

# Discussion


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

### Section 2.5 Écoulement de deux fluides visqueux adjacents sur un plan.
> Pour cet exemple on ira directement à l'équation diférentielle puisqu'on a déjà fait le même type de bilans à la section 2.2 en détails.

>

```python
#
# Pierre Proulx
#
# Préparation de l'affichage et des outils de calcul symbolique
#
import sympy as sp
from IPython.display import *
sp.init_printing(use_latex=True)
import matplotlib.pyplot as plt
```

```python
# définition des variables symboliques et des lois de Newton, voir figure ci-haut
x,p_0,p_L,mu1,mu2,L,B = sp.symbols('x,p_0,p_L,mu_I,mu_II,L,B')
tau_xz1 = sp.symbols('tau_xz^I')
tau_xz2 = sp.symbols('tau_xz^II')
C11,C12,C21,C22,C1,C2 = sp.symbols('C_11,C_12,C_21,C_22,C1,C2')
vz1 = sp.Function('v_z^I')(x)
vz2 = sp.Function('v_z^II')(x)
#
# Lois de Newton
#
newton1 = -mu1*sp.Derivative(vz1, x)
newton2 = -mu2*sp.Derivative(vz2, x)
```

#### Le bilan est identique à celui fait dans la section 2, sans la gravité, alors le résultat est simple, pour chacun des deux fluides on a le résultat du bilan montré sur la figure ci-haut:

```python
# Bilan des forces
eq1 = sp.Eq(tau_xz1(x)-(p_0-p_L)*x/L+C11)
eq2 = sp.Eq(tau_xz2(x)-(p_0-p_L)*x/L+C12)

eq1 = eq1.subs(tau_xz1(x), newton1)
eq2 = eq2.subs(tau_xz2(x), newton2)

display(eq1)
display(eq2)
```

```python
vz1=sp.dsolve(eq1,vz1)
vz2=sp.dsolve(eq2,vz2)
#
# Chaque dsolve génère une nouvelle constante par intégration, On les renomme ici pour respecter notre
# convention. Vous pouvez vérifier en faisant display des vz avant ces remplacements. On veut avoir
# la même notation que celle de Transport Phenomena autant que possible.
#
vz1=vz1.subs(C1,C21)
vz2=vz2.subs(C1,C22)

display(vz1)
display(vz2)
```

```python
# On peut solutionner directement les 4 constantes avec les 4 conditions aux limites
#
# Ici étant donné qu'on travaille avec sympy qui fera la solution automatiquement, on ne se préocuppe
# pas de décomposer le problème en plusieurs morceaux, on va directement à 4 équations et 4 inconnues
# en fonction des 4 conditions aux limites
#
vz1=vz1.rhs                 # prendre la partie de droite nous simplifiera les manips après
vz2=vz2.rhs

conditions = [
    sp.Eq(vz1.subs(x,0), vz2.subs(x,0)),                               # vitesses égales à l'interface
    sp.Eq(mu1*sp.diff(vz1,x).subs(x,0), mu2*sp.diff(vz2,x).subs(x,0)), # cisaillement égaux
    sp.Eq(vz1.subs(x,B),0),                                            # non glissement
    sp.Eq(vz2.subs(x,-B),0)]                                           # non glissement

display(*conditions)
```

```python
constantes = sp.solve(conditions, sp.symbols('C_11,C_12,C_21,C_22'))
display(constantes)
```

```python
vz1=vz1.subs(constantes)
vz2=vz2.subs(constantes)

display(vz1)
display(vz2)
```

### Maintenant on tracera les profils en variant les viscosités.

```python
dico = {
    'mu_I':0.001,
    'mu_II':0.002,
    'B':0.01,
    'L':10,
    'p_0':100,
    'p_L':0}
B=B.subs(dico)  # B est le même dans les 3 cas...

#Chaque cas a une valeur pour Mu_II et une couleur pour être représenté sur le graph
cas = [
    (0.002,'red'),
    (0.005,'blue'),
    (0.010,'green')]

vzPlot=[]
for muII,color in cas:
    dico['mu_II'] = muII
    vzPlot.append( (vz1.subs(dico), (x,0,B))  )
    vzPlot.append( (vz2.subs(dico), (x,-B,0)) )

plts = sp.plot(*vzPlot, ylabel='V', legend=True, show=False)

for ii, (muII, color) in enumerate(cas):
    plts[ii*2].label  = '$\mu_{{II}}$={}'.format(muII)
    plts[ii*2].line_color = color
    plts[ii*2+1].label= ''
    plts[ii*2+1].line_color = color

plts.show()
```

```python

```


# Session 8: the Kernel trick in Logistic Regression

------------------------------------------------------
Introduction to Data Science & Machine Learning

*Pablo M. Olmos olmos@tsc.uc3m.es*

------------------------------------------------------

Just as we did for linear regression, we can optimize a Kernel Logistic Classifier in the dual space and exploit the kernel trick to derive classifiers that are able to find decision boundaries in a possibly infinite feature dimensional space.

## Logistic Regression

[Logistic Regression](https://onlinecourses.science.psu.edu/stat504/node/149) (LR) is a particular class of **generalized linear model** in which we use a **linear combination of the input features** to adjust the parameter of a particular probability distribution that explains the variable that we want to predict.

Given our **trainin database** of $N$ entries of the form $(\mathbf{x}^{(i)},y^{(i)})$, imagine that each of the input vectors is transformed to include **additional features** (such as polynomial expansions or any other transformation). We denote the **feature vector** as

\begin{align}
\phi(\mathbf{x}): \mathbb{R}^L \rightarrow \mathbb{R}^D,
\end{align}
where $D\geq L$. In LR, we interpolate the following probabilistic function:
\begin{align}
P(y=1|\mathbf{x}) =\frac{1}{1+\text{e}^{-\mathbf{\theta}^T\phi(\mathbf{x})}}=\sigma\left(\mathbf{\theta}^T\phi(\mathbf{x})\right),
\end{align}
where $\sigma(\cdot): \mathbb{R}\rightarrow [0,1]$ is the [sigmoid function](http://mathworld.wolfram.com/SigmoidFunction.html).

----

### The cross entropy objective function

While in standard regression the most common loss function is the MSE (mean squared error), in classification with probabilistic ouputs the most common choice is the [**cross entropy loss**](https://rdipietro.github.io/friendly-intro-to-cross-entropy-loss/):
\begin{align}
L(\theta)&=\frac{1}{N}\left[-\sum_{i=0}^{N}y^{(i)}\log\left(\sigma(\mathbf{\theta}^T\phi(\mathbf{x}^{(i)})\right)+(1-y^{(i)})\left(1-\sigma(\mathbf{\theta}^T\phi(\mathbf{x}^{(i)})\right)\right].
\end{align}
Note that this loss is always positives and grows when $y^{(i)}=1$ and $\sigma(\mathbf{\theta}^T\phi(\mathbf{x}^{(i)})\rightarrow 0$ and viceversa. I.e., penalizes predicted probabilities far from the right label. As in linear regression, we can use $L_2$ or $L_1$ regularization to control overfitting:

\begin{align}
L(\theta,\lambda)&=\frac{1}{N}\left[-\sum_{i=0}^{N}y^{(i)}\log\left(\sigma(\mathbf{\theta}^T\phi(\mathbf{x}^{(i)})\right)+(1-y^{(i)})\left(1-\sigma(\mathbf{\theta}^T\phi(\mathbf{x}^{(i)})\right)+\lambda \mathbf{\theta}^T \mathbf{\theta}\right].
\end{align}

Unlike linear regression with MSE loss, minimizing the cross entropy loss function w.r.t. $\mathbf{\theta}$ **does not have closed form solution**. However, the function is still **convex** and can be easily optimized by **gradient descent**.

----

## The kernel trick

Instead of minimizing the cross entropy loss function in the primal space, we will reformulate the problem by assuming that the optimal solution belongs to the vector subspace of the transformed datapoints, i.e., that there exists $\boldsymbol{a}\in\mathbb{R}^N$ such that

\begin{align}
\boldsymbol{\theta}_\lambda=\sum_{i=0}^N a^i \phi(\mathbf{x}^{(i)})
\end{align}

Note that, given $\mathbf{a}$, the class probability estimate for a new vector $\mathbf{x}^{*}$ is:

\begin{align}
P(y^*=1|\mathbf{x}^*) &= \sigma\left(\boldsymbol{\theta}_\lambda^T \phi(\mathbf{x}^*)\right) = \sigma\left(\left(\sum_{i=1}^{N} a^i \phi(\mathbf{x}^{(i)})\right)^T \phi(\mathbf{x}^*)\right) = \sigma\left(\sum_{i=1}^{N} a^i \phi(\mathbf{x}^{(i)})^T \phi(\mathbf{x}^*)\right) \\ &= \sigma\left(\sum_{i=1}^{N} a^i_\lambda k(\mathbf{x}^{(i)},\mathbf{x}^*)\right) = \sigma\left(\mathbf{a}^T \mathbf{k}(\mathbf{x}^*)\right),
\end{align}
where

\begin{align}
\mathbf{k}(\mathbf{x}^*) = \left[\begin{array}{c} k(\mathbf{x}^{(1)},\mathbf{x}^*) \\ k(\mathbf{x}^{(2)},\mathbf{x}^*) \\ \vdots \\ k(\mathbf{x}^{(N)},\mathbf{x}^*)\end{array}\right] = \left[\begin{array}{c} \phi(\mathbf{x}^{(1)})^T \phi(\mathbf{x}^*) \\ \phi(\mathbf{x}^{(2)})^T \phi(\mathbf{x}^*) \\ \vdots \\ \phi(\mathbf{x}^{(N)})^T \phi(\mathbf{x}^*)\end{array}\right]
\end{align}

and $k(\mathbf{x}^{(i)},\mathbf{x}^*)$ is the **kernel function** between points $\mathbf{x}^{(i)}$ and $\mathbf{x}^{*}$.

## Cross entropy in Kernel Logistic Regression

If we substitute $\mathbf{\theta} = \mathbf{\Phi}^T\mathbf{a}$ in the LR cross entropy function we get the dual loss function:

\begin{align}
L(\boldsymbol{a},\lambda) = \frac{1}{N}\left[-\sum_{i=0}^{N}y^{(i)}\log\left(\sigma(\mathbf{a}^T \mathbf{k}(\mathbf{x}^{(i)})\right)+(1-y^{(i)})\left(1-\sigma(\mathbf{a}^T \mathbf{k}(\mathbf{x}^{(i)})\right)+\lambda\mathbf{a}^T\mathbf{K}\mathbf{a}\right]
\end{align}
where $\mathbf{K}$ is the $(N\times N)$ symmetric **kernel matrix** such that

\begin{align}
K_{nm} = k(\mathbf{x}^{(m)},\mathbf{x}^{(n)}).
\end{align}

The KLR optimization problem is solved by **gradient descend**. This function is **not included in sklearn** as for kernel methods in classification, sparse kernel machines like [**Support Vector Machines**](http://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html) are preferred.

---

```python
import numpy as np
import sklearn
import functions    #Some useful functions
import matplotlib.pyplot as plt
%matplotlib inline

```

### Loading the example database

```python
data = np.loadtxt('ex2data2.txt', delimiter=',')
```

```python
## Dimensions
dims=np.shape(data)
N=dims[0]

## Create X and Y
X=data[:,0:2]
Y=data[:,2]

### Splitting in training and validation data sets

np.random.seed(0)

X_train,X_test,Y_train,Y_test = functions.split_set(X,Y,0.8)

min1=np.min(X[:,0])
max1=np.max(X[:,0])
min2=np.min(X[:,1])
max2=np.max(X[:,1])

## Plot Training Data
index=(Y_train==1)
plt.plot(X_train[index,0],X_train[index,1],'s',label="Class 1")
index=(Y_train==0)
plt.plot(X_train[index,0],X_train[index,1],'ro',label="Class 0")
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.grid(True)
plt.legend()

plt.title('Training Data')
```

```python
# Data Normalization

X_tnorm,mx,stdx=functions.normalize(X_train,0,1,True)
X_tnorm = functions.add_interfit(X_tnorm)
```

### Training the Kernel LR with RBF kernel

```python
## Setting the model parameters and computing the Kernel matrix
N_train = Y_train.shape[0]

a=0.1*np.ones([N_train,1])   #Initialization

bw = 0.01

K = functions.compute_Kernel_matrix(X_tnorm,N_train,bw,kernel='rbf')

l = 20

#Optimizer
tolerance=1e-03
max_iter=2e3
step=1e-02
```

```python
# Training the model

a_opt=functions.optimize_KLR(N_train,K,Y_train.reshape([-1,1]),l,a,max_iter,step,tolerance,verbose=True)

L_opt,grad_opt=functions.cost(a_opt,N_train,K,Y_train.reshape([-1,1]),l)

```

    Iteration 0, cost L=0.945169, grad_norm =0.542846
    Iteration 100, cost L=0.774187, grad_norm =0.299250
    Iteration 200, cost L=0.719832, grad_norm =0.173675
    Iteration 300, cost L=0.700974, grad_norm =0.104165
    Iteration 400, cost L=0.694062, grad_norm =0.063782
    Iteration 500, cost L=0.691439, grad_norm =0.039593
    Iteration 600, cost L=0.690419, grad_norm =0.024823
    Iteration 700, cost L=0.690015, grad_norm =0.015692
    Iteration 800, cost L=0.689853, grad_norm =0.009998
    Iteration 900, cost L=0.689786, grad_norm =0.006428
    Iteration 1000, cost L=0.689759, grad_norm =0.004182
    Iteration 1100, cost L=0.689747, grad_norm =0.002769
    Iteration 1200, cost L=0.689742, grad_norm =0.001881
    Iteration 1300, cost L=0.689739, grad_norm =0.001327
    Iteration 1395, cost L=0.689738, grad_norm =0.000999

Lets plot the decision boundary, looking for points with predicted label probability around 0.5

```python
#Plotting the decision boundary
x1,x2 = np.mgrid[min1:max1:(max1-min1)/50, min2:max2:(max2-min2)/50]
grid = np.transpose(np.row_stack([x1.ravel(), x2.ravel()]))
grid_norm,_,_ = functions.normalize(grid,mx,stdx,False)
grid_norm = functions.add_interfit(grid_norm)

num_points=np.shape(grid_norm)[0]
probs = np.zeros([num_points,1])
```

```python
#Computing the KLR class prob. for all points in the grid

for i in range(num_points):
    k = functions.compute_kernel_vector(X_tnorm,grid_norm[i,:],N_train,bw,kernel='rbf')
    probs[i] = functions.sigmoid(np.transpose(k)@a_opt)
```

```python
## Plot Training Data
index=(Y_train==1)
plt.plot(X_train[index,0],X_train[index,1],'s',label="Class 1")
index=(Y_train==0)
plt.plot(X_train[index,0],X_train[index,1],'ro',label="Class 0")
plt.contour(x1,x2,np.reshape(probs[:,0],np.shape(x1)),[0.5],cmap=plt.cm.gray)
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.grid(True)
plt.legend()
plt.title('Training Data')

```

```python

```

Notebooks/Session 8 Kernel Logistic Regression/Session 8 Kernel Logistic Regression.ipynb


# Nearest Neighbor Used-based Collaborative Filtering

This notebook is a toy implementation of the classic collaborative filtering algorithm. The purpose of this implementation is to provide a small and clear numerical example.

### Detailed Description
See [Introduction to Algorithmic Marketing](https://algorithmicweb.wordpress.com/ ) book

### Data
We use a small rating matrix defined inline. No external datasets are used.

```python
%matplotlib inline
import sympy as sy
import numpy as np
import matplotlib.pyplot as plt
```

```python
# Input rating matrix
# X stands for unknown ratings
X = np.nan
R = np.array([
    [5, 4, X, 1, 2, 1], #
    [4, X, 3, 1, 1, 2], #
    [X, 5, 5, X, 3, 3], # users
    [2, X, 1, 4, 5, 4], #
    [2, 2, 2, X, 4, X], #
    [1, 2, 1, X, 5, 4]  #
   #       items
])
```

```python
# Define common varibales and helper functions
n, m = R.shape

def known(r):
    return not np.isnan(r)

def known_user_ratings(R, u):
    return [r for r in R[u, :] if known(r)]

def user_common_ratings(R, u, v):
    return np.array(list(filter(
        lambda r: all(known(val) for val in r),
        np.column_stack((R[u, :], R[v, :]))
    ))).T

def mean_r(R, u):
    return np.mean(known_user_ratings(R, u))
```

```python
# Calculate the matrix of user similarities
def similarity(R, u, v):
    I_uv = user_common_ratings(R, u, v)
    mu_u = mean_r(R, u)
    mu_v = mean_r(R, v)
    return sum([
          ((I_uv[0, i] - mu_u)*(I_uv[1, i] - mu_v)) /
          (np.linalg.norm(I_uv[0, :] - mu_u) * np.linalg.norm(I_uv[1, :] - mu_v))
          for i in range(I_uv.shape[1])])

user_similarity = np.array([[
    similarity(R, u, v)
    for u in range(n)] for v in range(n)])
```

```python
print(user_similarity)
```

    [[ 1.          0.87489889  0.94087507 -0.79660185 -0.5939994  -0.78571429]
     [ 0.87489889  1.          0.87235674 -0.84016805 -0.81047483 -0.88236074]
     [ 0.94087507  0.87235674  1.         -0.93847426 -0.87038828 -0.91970901]
     [-0.79660185 -0.84016805 -0.93847426  1.          0.85993942  0.95257577]
     [-0.5939994  -0.81047483 -0.87038828  0.85993942  1.          0.94715031]
     [-0.78571429 -0.88236074 -0.91970901  0.95257577  0.94715031  1.        ]]

```python
# Predict ratings based on the user similarities
k = 2 # neighborhood size

def predict_rating(R, u, i):
    # neighbors sorted by similarity
    all_neighbors = np.argsort(user_similarity[u])[::-1]

    # remove neighbors without ratings for i and select top k
    neighbors = list(filter(lambda v: known(R[v,i]) and not v==u, all_neighbors))[:k]
    mu_u = mean_r(R, u)
    score = 0
    norm = 0
    print("user %s, item %s <- user neighbors %s" % (u, i, neighbors))
    for v in neighbors:
        mu_v = mean_r(R, v)
        score = score + user_similarity[u,v]*(R[v,i] - mu_v)
        norm = norm + abs(user_similarity[u,v])
    return mu_u + score/norm

ratings = np.array([[ R[u,i] if known(R[u,i]) else predict_rating(R, u, i)
   for i in range(m)] for u in range(n)])

print("\nComplete rating matrix:")
np.set_printoptions(precision=2)
print(ratings)
```

    user 0, item 2 <- user neighbors [2, 1]
    user 1, item 1 <- user neighbors [0, 2]
    user 2, item 0 <- user neighbors [0, 1]
    user 2, item 3 <- user neighbors [0, 1]
    user 3, item 1 <- user neighbors [5, 4]
    user 4, item 3 <- user neighbors [3, 0]
    user 4, item 5 <- user neighbors [5, 3]
    user 5, item 3 <- user neighbors [3, 0]

    Complete rating matrix:
    [[5.   4.   3.5  1.   2.   1.  ]
     [4.   3.4  3.   1.   1.   2.  ]
     [6.11 5.   5.   2.59 3.   3.  ]
     [2.   2.65 1.   4.   5.   4.  ]
     [2.   2.   2.   3.63 4.   3.61]
     [1.   2.   1.   3.76 5.   4.  ]]

```python

```