Sascha Spors,
Professorship Signal Theory and Digital Signal Processing,
Institute of Communications Engineering (INT),
Faculty of Computer Science and Electrical Engineering (IEF),
University of Rostock,
Germany

# Tutorial Digital Signal Processing

**IIR Filter**,
Winter Semester 2021/22 (Master Course #24505)

- lecture: https://github.com/spatialaudio/digital-signal-processing-lecture
- tutorial: https://github.com/spatialaudio/digital-signal-processing-exercises

Feel free to contact lecturer frank.schultz@uni-rostock.de

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.markers import MarkerStyle
from matplotlib.patches import Circle
from scipy import signal

np.set_printoptions(precision=16)
```

```python
def zplane_plot(ax, z, p, k):
    """Plot pole/zero/gain plot of discrete-time, linear-time-invariant system.

    Note that the for-loop handling might be not very efficient
    for very long FIRs

    z...array of zeros in z-plane
    p...array of poles in z-zplane
    k...gain factor

    taken from own work
    URL = ('https://github.com/spatialaudio/signals-and-systems-exercises/'
           'blob/master/sig_sys_tools.py')

    currently we don't use the ax input parameter, we rather just plot
    in hope for getting an appropriate place for it from the calling function
    """
    # draw unit circle
    Nf = 2**7
    Om = np.arange(Nf) * 2*np.pi/Nf
    plt.plot(np.cos(Om), np.sin(Om), 'C7')

    try:  # TBD: check if this pole is compensated by a zero
        circle = Circle((0, 0), radius=np.max(np.abs(p)),
                        color='C7', alpha=0.15)
        plt.gcf().gca().add_artist(circle)
    except ValueError:
        print('no pole at all, ROC is whole z-plane')

    zu, zc = np.unique(z, return_counts=True)  # find and count unique zeros
    for zui, zci in zip(zu, zc):  # plot them individually
        plt.plot(np.real(zui), np.imag(zui), ms=8,
                 color='C0', marker='o', fillstyle='none')
        if zci > 1:  # if multiple zeros exist then indicate the count
            plt.text(np.real(zui), np.imag(zui), zci)

    pu, pc = np.unique(p, return_counts=True)  # find and count unique poles
    for pui, pci in zip(pu, pc):  # plot them individually
        plt.plot(np.real(pui), np.imag(pui), ms=8,
                 color='C0', marker='x')
        if pci > 1:  # if multiple poles exist then indicate the count
            plt.text(np.real(pui), np.imag(pui), pci)

    plt.text(0, +1, 'k={0:f}'.format(k))
    plt.text(0, -1, 'ROC for causal: white')
    plt.axis('square')
    plt.xlabel(r'$\Re\{z\}$')
    plt.ylabel(r'$\Im\{z\}$')
    plt.grid(True, which="both", axis="both",
             linestyle="-", linewidth=0.5, color='C7')

def bode_plot(b, a, N=2**10, fig=None):  # for IIR if length of b and a are the same
    if fig is None:
        fig = plt.figure()

    z, p, gain = signal.tf2zpk(b, a)
    W, Hd = signal.freqz(b, a, N, whole=True)

    # print('number of poles:', len(p), '\npole(s) at:', p,
    #      '\nnumber of zeros:', len(z), '\nzero(s) at:', z)

    gs = fig.add_gridspec(2, 2)
    # magnitude
    ax1 = fig.add_subplot(gs[0, 0])
    ax1.plot(W/np.pi, np.abs(Hd), "C0",
             label=r'$|H(\Omega)|$)',
             linewidth=2)
    ax1.set_xlim(0, 2)
    ax1.set_xticks(np.arange(0, 9)/4)
    ax1.set_xlabel(r'$\Omega \,/\, \pi$', color='k')
    ax1.set_ylabel(r'$|H|$', color='k')
    ax1.set_title("Magnitude response", color='k')
    ax1.grid(True, which="both", axis="both",
             linestyle="-", linewidth=0.5, color='C7')

    # phase
    ax2 = fig.add_subplot(gs[1, 0])
    ax2.plot(W/np.pi, (np.angle(Hd)*180/np.pi), "C0",
             label=r'$\mathrm{angle}(H('r'\omega))$',
             linewidth=2)
    ax2.set_xlim(0, 2)
    ax2.set_xticks(np.arange(0, 9)/4)
    ax2.set_xlabel(r'$\Omega \,/\, \pi$', color='k')
    ax2.set_ylabel(r'$\angle(H)$ / deg', color='k')
    ax2.set_title("Phase response", color='k')
    ax2.grid(True, which="both", axis="both",
             linestyle="-", linewidth=0.5, color='C7')

    # zplane
    ax3 = fig.add_subplot(gs[0, 1])
    zplane_plot(ax3, z, p, gain)

    # impulse response
    N = 2**4  # here specially chosen for the examples below
    k = np.arange(0, N)
    x = np.zeros(N)
    x[0] = 1
    h = signal.lfilter(b, a, x)
    ax4 = fig.add_subplot(gs[1, 1])
    ax4.stem(k, h, linefmt='C0', markerfmt='C0o',
             basefmt='C0:', use_line_collection=True)
    ax4.set_xlabel(r'$k$')
    ax4.set_ylabel(r'$h[k]$')
    ax4.set_title('Impulse Response')
    ax4.grid(True, which="both", axis="both", linestyle="-",
             linewidth=0.5, color='C7')

# some defaults for the upcoming code:
figsize = (12, 9)
```

```python
# taken from lecture's repository
# https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py

def bilinear_biquad(B, A, fs):
    """Get the bilinear transform of a 2nd-order Laplace transform.

    bilinear transform H(s)->H(z) with s=2*fs*(z-1)/(z+1)

    input:
    B[0] = B0   B[1] = B1   B[2] = B2
    A[0] = A0   A[1] = A1   A[2] = A2
    fs...sampling frequency in Hz
           Y(s)   B0*s^2+B1*s+B2   B[0]*s^2+B[1]*s+B[2]
    H(s) = ---- = -------------- = --------------------
           X(s)   A0*s^2+A1*s+A2   A[0]*s^2+A[1]*s+A[2]
    output:
    b[0] = b0   b[1] = b1   b[2] = b2
    a[0] = 1    a[1] = a1   a[2] = a2
           Y(z)   b2*z^-2+b1*z^-1+b0   b[2]*z^-2+b[1]*z^-1+b[0]
    H(z) = ---- = ------------------ = ------------------------
           X(z)   a2*z^-2+a1*z^-1+ 1   a[2]*z^-2+a[1]*z^-1+a[0]
    """
    A0, A1, A2 = A
    B0, B1, B2 = B
    fs2 = fs**2

    a0 = A2 + 2*A1*fs + 4*A0*fs2
    b0 = B2 + 2*B1*fs + 4*B0*fs2

    b1 = 2*B2 - 8*B0*fs2
    a1 = 2*A2 - 8*A0*fs2

    b2 = B2 - 2*B1*fs + 4*B0*fs2
    a2 = A2 - 2*A1*fs + 4*A0*fs2

    b = np.array([b0, b1, b2]) / a0
    a = np.array([a0, a1, a2]) / a0

    return b, a

def f_prewarping(f, fs):
    """Do the frequency prewarping for bilinear transform.

    input:
    f...analog frequency in Hz to be prewarped
    fs...sampling frequency in Hz
    output:
    prewarped angular frequency wpre in rad/s
    """
    return 2*fs*np.tan(np.pi*f/fs)
```

# Filter Fundamentals

The transfer function of digital filters can be generally expressed in the $z$-domain as
\begin{equation}
H(z)=\frac{Y(z)}{X(z)} = \frac{\sum\limits_{m=0}^M b_mz^{-m}}{\sum\limits_{n=0}^N a_nz^{-n}}
=\frac{b_0z^0+b_1z^{-1}+b_2z^{-2}+...+b_Mz^{-M}}{a_0z^0+a_1z^{-1}+a_2z^{-2}+...+a_Nz^{-N}}
\end{equation}
with input $X(z)$ and output $Y(z)$.
Real input signals $x[k]$ that should end up as real output signals $y[k]$ (in terms of signal processing fundamentals this is a special case, though most often needed in practice) require real coefficients $b,a\in\mathbb{R}$.
This is only achieved with

- single or multiple **real** valued
- single or multiple **complex conjugate** pairs

of zeros and poles.

Furthermore, in practice we most often aim at (i) causal and (ii) bounded input, bound output (BIBO) stable LTI systems, which requires (i) $M \leq N$ and (ii) poles inside the unit circle.
If all poles **and** zeros are **inside** the unit circle then the system is **minimum-phase** and thus $H(z)$ is straightforwardly **invertible**.

Further concepts related to the transfer function are:

- Analysis of the transfer characteristics is done by the DTFT
$H(z=\mathrm{e}^{\mathrm{j}\Omega})$, i.e. evaluation on the unit circle.

- We use $a_0=1$ according to convention in many textbooks.

- The convention for arraying filter coefficients is straightforward with Python index starting at zero:
$b_0=b[0]$, $b_1=b[1]$, $b_2=b[2]$, ..., $a_0=a[0]=1$, $a_1=a[1]$, $a_2=a[2]$.

## Filtering Process

- A **non-recursive** system with $a_1,a_2,...,a_N=0$ always exhibits a **finite
impulse response** (FIR), note: $a_0=1$ for output though. Due to the finite length impulse response, a non-recursive system is always stable.

- The output signal of a **non-recursive** system in practice can be calculated by **linear
convolution**
\begin{equation}
y[k] = \sum\limits_{m=0}^{M} h[m] x[-m+k]
\end{equation}
of the finite impulse response $h[m]=[b_0, b_1, b_2,...,b_M]$ and the input signal $x[k]$.

- A **recursive system** exhibits at least one $a_{n\geq1}\neq0$. Because
of the feedback of the output into the system, a potentially **infinite impulse
response** (IIR) and a potentially non-stable system results.

- For a **recursive** system, in practice the **difference equation**
\begin{equation}
y[k] = b_0 x[k] + b_1 x[k-1] + b_2 x[k-2] + ... + b_M x[k-M] -a_1 y[k-1] - a_2 y[k-2] - a_3 y[k-3] - ... - a_N y[k-N]
\end{equation}
needs to be implemented.

- A **pure non-recursive** system is obtained by ignoring the feedback paths, i.e. setting $a_1,a_2,...,a_N=0$.
- A **pure recursive** system is obtained by ignoring the forward paths, i.e. setting $b_0,b_1,...,b_M=0$. Then, the values of the state variables $z^{-1}, z^{-2}, ..., z^{-M}$ alone determine how the system starts to perform at $k=0$, since the system has no input actually. This system type can be used to generate (damped) oscillations.

Please note: A recursive system can have a finite impulse response, but this is very rarely the case.
Therefore, literature usually refers to
- an FIR filter when dealing with a non-recursive system
- an IIR filter when dealing with a recursive system

## Signal Flow Chart of Direct Form I

For example, the signal flow for a **second order** ($M=N=2$), system with
- a non-recursive part (feedforward paths, left $z^{-}$-path)
- a recursive part (feedback paths, left $z^{-}$-path)

is depicted below (graph taken from Wikimedia Commons) as straightforward **direct form I**, i.e. directly following the difference equation.

Such a second order section is usually termed a biquad.

# IIR Filter

In the following 2nd order IIR filters shall be discussed.
The transfer function is with usual convention $a_0=1$

\begin{equation}
H(z)=\frac{\sum\limits_{m=0}^2 b_mz^{-m}}{\sum\limits_{n=0}^2 a_nz^{-n}}
=\frac{b_0+b_1z^{-1}+b_2z^{-2}}{1+a_1z^{-1}+a_2z^{-2}}
\end{equation}

Very simple pole / zero placements are given in order to demonstrate the principle and the impact of zeros and poles.
Once this is understood, more complicated filter characteristics and filter design methods can be approached.

## Pole Zero Placement Examples for IIR Filters of 2nd Order

Let us define a small function where we can define a complex zero $z_0 = z_r \mathrm{e}^{\mathrm{j} z_a}$ and a complex pole $p_0 = p_r \mathrm{e}^{\mathrm{j} p_a}$ and the function finds the complex conjugates, the IIR filter coefficients $b$ and $a$ and plots magnitude, phase response, pole/zero map and impulse response.

```python
def pz_placement(zr, za, pr, pa):
    z = zr * np.exp(+1j*za)
    p = pr * np.exp(+1j*pa)

    b = np.poly([z, np.conj(z)])
    a = np.poly([p, np.conj(p)])

    bode_plot(b, a, fig=plt.figure(figsize=figsize))
```

Then we can play around with some pole/zero alignments.

```python
# no IIR actually, but rather FIR just to make a point:
# put poles into origin
pz_placement(zr=1/2, za=np.pi/2, pr=0, pa=0)
```

```python
# filter transfer function from above can be inverted
# this yields a stable IIR filter, since its poles are inside unit circle
# and thus white ROC includes the unit cirlce
pz_placement(zr=0, za=0, pr=1/2, pa=np.pi/2)
```

```python
# shift zeros closer to poles
# less ripple in magnitude response
# note that y-axis has changed in comparison to above example
pz_placement(zr=1/3, za=np.pi/2, pr=1/2, pa=np.pi/2)
```

```python
# 2nd order lowpass filter
# special here is:
# zero at -1 thus amplitude 0 at fs/2, phase -180 deg at fs/2
# two real poles at same location
pz_placement(zr=1, za=np.pi, pr=1/2, pa=0)
```

```python
# 2nd order lowpass filter
# zero at -1 thus amplitude 0 at fs/2
# this time complex conjugate pole pair to yield about the same magnitude
# at DC
# check the differences between the two filters
# you might create an own plot where both filters can be overlayed
pz_placement(zr=1, za=np.pi, pr=3/5, pa=np.pi/8)
```

# Filter Design with Bilinear Transform

A simple and analytically straightforward method to design digital filters from analog filter transfer functions is the so called bilinear transform method.

## Mapping

A Laplace transfer function can be transformed to $z$-domain transfer function by

\begin{equation}
s=2f_s\frac{z-1}{z+1}.
\end{equation}

This is known as bilinear transform.
There are several derivations for this mapping. One way is to linearize a function, namely applying the Taylor series and then taking only the linear term:
So, we know that the exact link between $s$ and $z$ is given by the definition
\begin{equation}
z = \mathrm{e}^{\,s T}
\end{equation}
using the sampling interval $T = \frac{1}{f_s}$. The inverse function is
\begin{equation}
s = \frac{1}{T} \cdot \ln (z).
\end{equation}
The Taylor series of the natural logarithm is known as
\begin{equation}
\ln(z) = 2 \left( \frac{z-1}{z+1} + \frac{(z-1)^3}{3(z+1)^3} + \frac{(z-1)^5}{5(z+1)^5} + \dots \right).
\end{equation}
Considering the linear term only yields the mapping introduced above.

## Characteristics of this Mapping
- left $s$-half plane mapped to the unit circle in $z$-domain
- right $s$-half plane mapped to outer unit circle in $z$-domain
- thus, stable analog filters ($s$-domain) yield stable discrete-time filters ($z$-domain)
- $s=0$ is mapped to $z=1$
- infinite, positive $\mathrm{j}\omega$ axis of $s$-domain is mapped to the upper unit circle
- infinite, negative $\mathrm{j}\omega$ axis of $s$-domain is mapped to the lower unit circle
- $n$-th order filter in $s$-domain yields also $n$-th filter in $z$-domain, due to same powers of $s$ and $z$ in the mapping. Question: what happens if one uses more terms of the Taylor approximation.

When rearranging the mapping to
\begin{equation}
z = \frac{2 + s T}{2 - s T}
\end{equation}
we can see, what happens for $s = \pm \mathrm{j}\omega$ with $\omega\rightarrow\infty$. In both cases the mapping $z=-1$ is achieved. This make the upper/lower semi-circle obvious.

## Non-Linear Mapping of Frequency Axis

Evaluating $s$- and $z$-plane along their frequency axis, i.e. setting $s=\mathrm{j}\omega$ and $z = \mathrm{e}^{\mathrm{j}\Omega}$, the mapping $s=2f_s\frac{z-1}{z+1}$ becomes

\begin{equation}
\mathrm{j}\omega=2f_s\frac{\mathrm{e}^{\mathrm{j}\Omega}-1}{\mathrm{e}^{\mathrm{j}\Omega}+1}.
\end{equation}

This can be rearranged to

\begin{equation}
\omega = 2 f_s \tan{(\frac{\Omega}{2})}
\end{equation}

and to its inverse function

\begin{equation}
\Omega = 2 \mathrm{atan}({\frac{\omega}{2 f_s}}).
\end{equation}

We see that the mapping between analog angular frequency $\omega$ and discrete-time frequency $\Omega$ is non-linear in the bilinear transform.

For the example given below (using $f_s=1$ Hz), consider the **tan-mapping** in the left plot:

A digital bandwidth from $0 < \Omega < 1$ maps approximately to $0 < \omega < 1$, the digital bandwidth
$1 < \Omega < 2$ however maps to $1 < \omega < 3$ in analog domain. For digital bandwidth $2 < \Omega < 3$ this bandwidth mismatch gets even worse and is not depicted in the graph anymore.

Digital to analog bandwidth mapping: the higher the digital frequency, the larger the bandwidth mismatch compared to analog domain becomes, analog bandwidth appears larger.

Now, consider the **atan-mapping** in the right plot:

An analog bandwidth from $0<\omega<1$ maps approximately to $0<\Omega<1$, we should expect this, since this is the region where tan() and atan() are approximately linear.
However, an analog bandwidth from $1<\omega<2$ maps approximately to $1<\Omega<\frac{3}{2}$, meaning that the digital bandwidth gets squeezed.

Thus, analog to digital bandwidth mapping: the higher the analog frequency, the larger the bandwidth mismatch compared to digital domain becomes, digital bandwidth appears smaller.

```python
plt.figure(figsize=(9, 3))

fs = 1  # sampling frequency in Hz
N = 2**8

plt.subplot(1, 2, 1)
W = np.arange(-N, N) * np.pi/N
w = 2*fs*np.tan(W/2)
plt.plot(W, W, 'C7', label='linear')
plt.plot(W, w, label='tan()')
plt.plot([-np.pi, np.pi], [2*np.pi*fs/2, 2*np.pi*fs/2],
         color='C3', label=r'$f_s/2$')
plt.xlim(-np.pi, +np.pi)
plt.ylim(-4, 4)
plt.xticks(np.arange(-4, 5, 1))
plt.yticks(np.arange(-4, 5, 1))
plt.xlabel(r'digital $\Omega$ / rad')
plt.ylabel(r'analog $\omega$ / (rad/s)')
plt.legend()
plt.grid(True)

plt.subplot(1, 2, 2)
w = np.arange(-N, N) * 10/N
W = 2*np.arctan(w/(2*fs))
plt.plot(w, w, 'C7', label='linear')
plt.plot(w, W, label='atan()')
plt.plot([2*np.pi*fs/2, 2*np.pi*fs/2], [-np.pi, np.pi],
         color='C3', label=r'$f_s/2$')
plt.xlim(-4, +4)
plt.ylim(-np.pi, +np.pi)
plt.xticks(np.arange(-4, 5, 1))
plt.yticks(np.arange(-4, 5, 1))
plt.xlabel(r'analog $\omega$ / (rad/s)')
plt.ylabel(r'digital $\Omega$ / rad')
plt.legend()
plt.grid(True)
```

There is no way to avoid these non-linear mapping effects, they come inherently with the application of the bilinear transform.
However, one can be cautious in filter design basically having three options:

1. Make sure that the bandwidth(s) under discussion are far away from half of the sampling frequency. Or in other words, try to work in the linear-like part of the tan/atan-mapping functions
2. Since 1. cannot be fulfilled always, one could select exactly one frequency where the non-linear mapping could be compensated for in the frequency design. A meaningful choice is the cut-off frequency of low- or highpass filters or the mid-frequency of bandpass or -stop filters. This technique is known as frequency pre-warping.
3. For bandpass/stop filters, which have a Q-factor / bandwidth parameter, one could do a bandwidth pre-warping as well. However, compared to option 2 (which is exact), this effect works only as an approximation. It can never compensate the mapping mismatch completely.

Let us first check one of the most simplest examples: the analog lowpass filter of first order and the design of discrete-time version with bilinear transform, in order to check the effects.

## Bilinear Transformation Example 1: 1st Order Lowpass Filter

The Laplace transfer function of a 1st order lowpass (consider the simple RC-circuit) with cut-off frequency $\omega_c = \frac{1}{R C} = \frac{1}{\tau}$ is given as

\begin{equation}
H(s) = \frac{1}{\frac{s}{\omega_c} + 1}.
\end{equation}

The discrete-time filter using bilinear transform is obtained by the mapping $s=2f_s\frac{z-1}{z+1}$.
Inserting this mapping to $H(s)$ yields

\begin{equation}
H(z) = \frac{1}{1+\frac{2f_s\frac{z-1}{z+1}}{\omega_c}} = \frac{1 + z^{-1}}{(1+\frac{2 f_s}{\omega_c}) + (1-\frac{2 f_s}{\omega_c}) z^{-1}}
\end{equation}

Normalizing such that $a_0=1$, yields
\begin{equation}
H(z) = \frac{\frac{1}{1+\frac{2 f_s}{\omega_c}}+\frac{1}{1+\frac{2 f_s}{\omega_c}}z^{-1}}{1+\frac{1-\frac{2 f_s}{\omega_c}}{1+\frac{2 f_s}{\omega_c}} z^{-1}} = \frac{b_0 + b_1 z^{-1}}{1 + a_1 z^{-1}},
\end{equation}
and thus a first order filter.

The single, real zero is always given at $z_0 = -\frac{b_1}{b_0} = -1$ independently from the chosen sampling frequency and cut-off-frequency. This is not by accident: since, a lowpass (in other words a high-cut) is intended, the bilinear transform forces the amplitude to zero at highest usable frequency, which is $f_s/2$. The single, real pole is given at $z_\infty = -a_1$. It is simple to show, that for $f_c = 0$, the pole is at $z_\infty = 1$. For $f_c = f_s/2$, the pole is at $z_\infty = - \frac{1-2/\pi}{1+2/\pi} \approx -0.22203094070331453$.
Thus, for $f_c \ll f_s/2$ the pole is close to the unit circle on the $\Re(z)$-axis and then moving to left along the axis when increasing $f_c$.
From this simple example (and although not proving it), we see that a stable Laplace transfer function yields a stable discrete-time version using bilinear transform. The pole is always within the unit circle (besides the special case of $f_c=0$ Hz).

Let's implement this little example to compare the analog and discrete-time level responses over frequency.

Play around with the cut-off frequency $\omega_c$. The closer it gets to $f_s/2$ the more the level responses deviate. Can you explain this? Pay special attention to the lowpass slope and where the cut-off frequency of the digital filter is.

```python
plt.figure(figsize=(9, 3))

fs = 10000  # sampling frequency in Hz
f = np.arange(1, fs)  # frequency in Hz
w = 2*np.pi*f  # angular frequency in rad/s
W = w/fs  # digital angular frequency in rad

wc = 2*np.pi*100  # cut-off frequency in rad/s
# wc = 2*np.pi*3000  # effect of frequency mismatch becomes obvious

# Laplace transfer
B = [0, 1]
A = [1/wc, 1]
[_, Hanalog] = signal.freqs(B, A, w)  # H

# z transfer via bilinear transform
n = 1 + 2*fs/wc
d = 1 - 2*fs/wc
b = [1/n, 1/n]
a = [1, d/n]
[_, Hbilinear] = signal.freqz(b, a, W)  # H1

plt.semilogx(f, 20*np.log10(np.abs(Hanalog)), label='analog filter')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear)),
             label='digital filter with bilinear transform')
plt.semilogx([f[0], f[-1]], [-3, -3], 'C7', label='-3 dB')
plt.ylim(-40, 10)
plt.xlim(1, fs/2)
plt.xlabel('f / Hz')
plt.ylabel('|H| / dB')
plt.legend()
plt.grid(which='both')

zero = -b[1]/b[0]
pole = -a[1]
print('zero at:', zero, 'pole at:', pole)
```

Suppose we want to have the cut-off frequency exactly as desired in the digital frequency domain.

Linear mapping of $\omega_c \rightarrow \Omega_c$ gives $\Omega_c = \frac{\omega_c}{f_s}$.

This $\Omega_c$ is now inserted to the mapping

\begin{equation}
\omega_{c,\mathrm{PRE}} = 2 f_s \tan{\frac{\Omega_c}{2}} = 2 f_s \tan{\frac{2\pi f_c}{2 f_s}}.
\end{equation}

We can easily verify that

\begin{equation}
\Omega_c = 2 \mathrm{atan}{\frac{\omega_{c,\mathrm{PRE}}}{2 f_s}}
\end{equation}

holds we when remap and thus we obtain our desired $\Omega_c$.

Thus, instead of using $\omega_c$ in the Laplace transfer function, we use the so called **pre-warped** cut-off frequency $\omega_{c,\mathrm{PRE}}$.

## Bilinear Transformation Example 2: 1st Order Lowpass Filter with Frequency Pre-Warping

Let's do this for our lowpass example. So, the code is the same with the pre-warping part being added.

```python
plt.figure(figsize=(9, 3))

fs = 10000  # sampling frequency in Hz
f = np.arange(1, fs)  # frequency in Hz
w = 2*np.pi*f  # angular frequency in rad/s
W = w/fs  # digital angular frequency in rad

wc = 2*np.pi*100  # cut-off frequency in rad/s
wc = 2*np.pi*3000  #

# Laplace transfer
B = [0, 1]
A = [1/wc, 1]
[_, Hanalog] = signal.freqs(B, A, w)  # H

# z transfer via bilinear transform
n = 1 + 2*fs/wc
d = 1 - 2*fs/wc
b = [1/n, 1/n]
a = [1, d/n]
[_, Hbilinear] = signal.freqz(b, a, W)  # H1

# z transfer via bilinear transform with frequency pre-warping
wc_pre = 2*fs*np.tan(wc/(2*fs))
n = 1 + 2*fs/wc_pre
d = 1 - 2*fs/wc_pre
b = [1/n, 1/n]
a = [1, d/n]
[_, Hbilinear_prewarping] = signal.freqz(b, a, W)  # H2

plt.semilogx(f, 20*np.log10(np.abs(Hanalog)), label='analog filter')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear)),
             label='digital filter with bilinear transform')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear_prewarping)),
             label='digital filter with bilinear transform and frequency prewaring')
plt.semilogx([f[0], f[-1]], [-3, -3], 'C7', label='-3 dB')
plt.ylim(-40, 10)
plt.xlim(1, fs/2)
plt.xlabel('f / Hz')
plt.ylabel('|H| / dB')
plt.legend()
plt.grid(which='both')

zero = -b[1]/b[0]
pole = -a[1]
print('zero at:', zero, 'pole at:', pole)
```

Again, play around with the cut-off frequency $\omega_c$. The closer it gets to $f_s/2$ the more the level responses deviate. What is the advantage of the frequency pre-warping?

## General Second Order Filter Bilinear Transform

The second order filter is very important in practical filter design, since higher-order filters are usually split into a cascade of second order sections (SOS). Thus, it is meaningful to explicitly derive the bilinear transform of a general SOS given in Laplace domain.

Let us denote the Laplace transfer function with real coefficients $B,A$ as
\begin{equation}
H(s)=\frac{B_0s^2+B_1s+B_2}{A_0s^2+A_1s+A_2}
\end{equation}
for a second order filter. This transfer function is to be transformed to a discrete-time filter via bilinear transform. Let us denote the $z$-transfer function with real coefficients $b,a$ as
\begin{equation}
H(z)=\frac{b_0+b_1z^{-1}+b_2z^{-2}}{1+a_1z^{-1}+a_2z^{-2}}.
\end{equation}

The aboved discussed mapping $s=2f_s\frac{z-1}{z+1}$ can be inserted to $H(s)$ to yield a representation for $H(z)$. The coefficients for this $H(z)$ can be derived to
\begin{equation}
\begin{split}
b_0=\frac{B_2+2B_1f_s+4B_0f_s^2}{A_2+2A_1f_s+4A_0f_s^2}\\
b_1=\frac{2B_2-8B_0f_s^2}{A_2+2A_1f_s+4A_0f_s^2}\\
b_2=\frac{B_2-2B_1f_s+4B_0f_s^2}{A_2+2A_1f_s+4A_0f_s^2}\\
a_1=\frac{2A_2-8A_0f_s^2}{A_2+2A_1f_s+4A_0f_s^2}\\
a_2=\frac{A_2-2A_1f_s+4A_0f_s^2}{A_2+2A_1f_s+4A_0f_s^2}.
\end{split}
\end{equation}

It is again worth to note that the bilinear transform is preserving the filter order, a $n$-th order analog filter is transformed to $n$-th order discrete-time filter. There are other design methods existing that do not have this characteristics, but rather design a $m>n$-th order discrete-time filter from $n$-th order analog filter.

## Bilinear Transformation Example 3: Digital Parametric Equalizer

This example was taken from Ex. 8.9 on p. 479 in Ife02. Filter parameters were modified to make the effects of pre-warping more obvious.

An analog parametric equalizer (PEQ, aka bell filter) with the Laplace transfer function

\begin{equation}
H(s)=\frac{s^2+(3+k)\frac{\omega_0}{Q}s+\omega_0^2}{s^2+(3-k)\frac{\omega_0}{Q}s+\omega_0^2} \qquad \text{with} \qquad k=3\frac{g-1}{g+1}
\end{equation}

and an amplitude factor $g$, center frequency $\omega_0=2\pi f_0$ and Q-factor
$Q$ is to be transformed into a discrete-time filter by means of the **bilinear
transform**. The filter shall exhibit a level of $G=6$ dB at center frequency $f_0=10$ kHz and a Q-factor of $Q=3$. The sampling frequency shall be $f_s = 48$ kHz. Note that $k$ here is obviously not the sample index variable.

1. Calculate the linear amplitude factor $g$ from $G$.

2. Plot the magnitude of $|H(s)|$ in dB over frequency.

3. Design $H_1(z)$ with the plain bilinear transform and plot its magnitude in dB over frequency.

4. Design $H_2(z)$ with bilinear transform and so-called frequency pre-warping
\begin{equation}
2f_s\tan{\left(\pi\frac{f_0}{f_s}\right)} \rightarrow \omega_0
\end{equation}
and plot its magnitude in dB over frequency

5. Design $H_3(z)$ with bilinear transform, frequency pre-warping and additional so-called Q-factor pre-warping (here with the tan()-function, there are other mappings as well, see [audio filters](https://nbviewer.jupyter.org/github/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.ipynb))
\begin{equation}
2f_s\tan{\left(\pi\frac{f_0}{f_s}\right)} \rightarrow \omega_{0}
\end{equation}
\begin{equation}
\frac{\frac{\pi f_0}{f_s}}{\tan\left(\frac{\pi f_0}{f_s}\right)}\cdot Q \rightarrow Q.
\end{equation}
Plot its magnitude in dB over frequency.

6. Discuss the differences of the level responses. Especially pay attention to what happens at $f_0$, $f_s/2$ and with the bandwidth of the filter.

Ife02...Ifeachor, E.C.; Jervis, B.W. (2002): Digital Signal Processing. 2nd ed. Prentice Hall.

```python
plt.figure(figsize=(9, 3))
# general parameter
fs = 48000  # sampling frequency in Hz
f = np.arange(100, fs)  # frequency in Hz
w = 2*np.pi*f  # angular frequency in rad/s
W = w/fs  # digital angular frequency in rad

# specific filter parameter
G = 6  # level in dB
f0 = 10000  # Hz
Q = 3  # quality

# some temp variables
g = 10**(G/20)  # level to linear gain
k = 3*(g-1)/(g+1)

# binlinear transform without any prewarping
w0 = 2*np.pi*f0
B = [1, (3+k)*w0/Q, w0**2]  # these are the coefficients of the analog filter
A = [1, (3-k)*w0/Q, w0**2]
[b, a] = bilinear_biquad(B, A, fs)  # function defined above
print('bilinear')
print('b', b)
print('a', a)
[b, a] = signal.bilinear(B, A, fs)
print('bilinear from scipy.signal')
print('b', b)
print('a', a)
[_, Hanalog] = signal.freqs(B, A, w)  # H
[_, Hbilinear] = signal.freqz(b, a, W)  # H1

# binlinear transform with frequency pre-warping
wpre = f_prewarping(f0, fs)
B = [1, (3+k)*wpre/Q, wpre**2]
A = [1, (3-k)*wpre/Q, wpre**2]
[b, a] = bilinear_biquad(B, A, fs)
print('bilinear with frequency prewarping')
print('b', b)
print('a', a)
[_, Hbilinear_wpre] = signal.freqz(b, a, W)  # H2

# binlinear transform with frequency and bandwidth pre-warping
wpre = f_prewarping(f0, fs)
Qpre = Q * (np.pi*f0 / fs) / np.tan(np.pi*f0 / fs)
B = [1, (3+k)*wpre/Qpre, wpre**2]
A = [1, (3-k)*wpre/Qpre, wpre**2]
[b, a] = bilinear_biquad(B, A, fs)
print('bilinear with frequency AND bandwidth pre-warping')
print('b', b)
print('a', a)
[_, Hbilinear_wpre_Qpre] = signal.freqz(b, a, W)  # H3

plt.semilogx(f, 20*np.log10(np.abs(Hanalog)), label='analog filter')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear)),
             label='digital filter with pure bilinear transform')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear_wpre)),
             label='bilinear, prewarping: f')
plt.semilogx(f, 20*np.log10(np.abs(Hbilinear_wpre_Qpre)),
             label='bilinear, prewarping: f, Q')
plt.xlim(4000, fs/2)

plt.xlabel(r'$f$ / Hz')
plt.ylabel(r'$20\mathrm{log}_{10}|H|$ / dB')
plt.legend()
plt.grid(which='both')
```

# **Copyright**

The notebooks are provided as [Open Educational Resources](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebooks 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: *Frank Schultz, Digital Signal Processing - A Tutorial Featuring Computational Examples* with the URL https://github.com/spatialaudio/digital-signal-processing-exercises


---

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

# Diagrama de Inmersión para el espacio-tiempo de Schwarzschild

### Resumen

En este cuaderno se presenta el diagrama de inmersión para el espacio-tiempo de Schwarzschild.

---

Comenzamos con el espacio plano 3-dimensional en cordenadas cilíndricas,

\begin{equation}
ds^2 = dz^2 + dr^2 + r^2 d\phi^2
\end{equation}

y consideramos una superficie (2-dimensional) dada por la ecuación

\begin{equation}
z= z(r).
\end{equation}

Se tiene entonces

\begin{equation}
dz= \frac{dz}{dr}dr
\end{equation}

por lo que

\begin{equation}
ds^2 = \left[ 1+ \left(\frac{dz}{dr}\right)^2\right]dr^2 + r^2 d\phi^2.
\end{equation}

Comparando esta relación con el elemento de línea de Schwarzschild en un instante determinado, $dt=0$, y para un angulo fijo  $d\theta =0$,

\begin{equation}
ds^2 = \left( 1- \frac{2M}{r}\right)^{-1}dr^2 + r^2 d\phi^2,
\end{equation}

se puede realizar la identificación

\begin{equation}
 1+ \left(\frac{dz}{dr}\right)^2 = \left( 1- \frac{2M}{r}\right)^{-1}.
\end{equation}

De esta relación se obtiene

\begin{align}
\left(\frac{dz}{dr}\right)^2 =& \left( 1- \frac{2M}{r}\right)^{-1} - 1 \\
\left(\frac{dz}{dr}\right)^2 =& \frac{r}{r-2M} - 1\\
\left(\frac{dz}{dr}\right)^2 =& \frac{2M}{r-2M} \\
\frac{dz}{dr} =& \sqrt{\frac{2M}{r-2M}}\\
dz =& \sqrt{2M} \frac{dr}{\sqrt{r-2M}}.
\end{align}

Integrado esta expresión se tiene

\begin{equation}
z(r) = \sqrt{8M}\sqrt{r-2M} .
\end{equation}

---
## Gráfico con  `matplotlib.pyplot`

```python
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np
import warnings
warnings.simplefilter('ignore')

# Mass of the central object
M=5.

# Surface definition
x = np.arange(-100, 100, 0.25)
y = np.arange(-100, 100, 0.25)
x, y = np.meshgrid(x, y)

r = np.sqrt(x**2 + y**2)
z = np.sqrt(8*M)*np.sqrt(r - 2*M)

# 3D-Plot environment
fig, ax = plt.subplots(1, 2, subplot_kw={"projection": "3d"}, figsize=(15,5))

# Plot the wireframe
ax[0].plot_wireframe(x, y, z, rstride=20, cstride=20)

# Plot the surface.
ax[1].plot_surface(x, y, z,  rstride=20, cstride=20)

plt.show()
```

## Plot using `EinsteinPy`

```python
from einsteinpy.hypersurface import SchwarzschildEmbedding
from einsteinpy.plotting import HypersurfacePlotter
from astropy import units as u
```

```python
surface_obj = SchwarzschildEmbedding(5. * u.M_sun)
surface = HypersurfacePlotter(embedding=surface_obj, plot_type='surface')
surface.plot()
surface.show()
```

```python

```

03. Embedding Diagram Schwarzschild.ipynb


# Método dos Mínimos Quadrados (MMQ)

## License

All content can be freely used and adapted under the terms of the
[Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).

## Imports

Coloque **todos** os `import` na célula abaixo. Não se esqueça do `%matplotlib inline` para que os gráficos apareçam no notebook.

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

## IMPORTANTE

Agora que vocês sabem técnicas de programação defensiva, eu espero que todo o código que vocês fizerem abaixo utilizem essas técnicas. Crie docstrings para suas funções, cheque as entradas (quando for possível) e cheque as saídas. **Não esqueçam dos comentários**.

## Fabricando dados para teste

Para saber se nosso código está funcionando, precisamos fabricar alguns dados com parâmetros conhecidos. Vamos gerar dados que seguem a equação da reta:

$$
d_i = a x_i + b
$$

**IMPORTANTE**: Eu vou usar a biblioteca numpy as gerar os dados abaixo.
Vocês **não podem** utilizar o numpy para calcular a sua solução.
Uso do numpy deve ser conquistado com ~~sangue~~esforço.
O código abaixo serve como exemplo do que vocês poderão fazer ao utilizar o Python no seu trabalho (fora da aula).

```python
a = 10
b = 50
N = 50
# Vou utilizar a função linspace do numpy para facilitar a vida
# Essa função cria N valores igualmente espaçados entre dois números (5 e 50)
x = np.linspace(5, 50, N)
# Agora podemos usar os valores de x, a e b acima para simular dados observados
dados_obs = a*x + b
# Vamos adicionar erro aleatório aos dados para ficar mais interessante
# O erro seguirá uma distribuição normal com os seguintes parâmetros
media_erro = 0
std_erro = 20
# A linha abaixo faz com que os valores aleatórios não sejam verdadeiramente aleatórios
# veja https://en.wikipedia.org/wiki/Pseudorandom_number_generator
np.random.seed(42)
# Gera a lista de numéros aleatórios
erro = np.random.normal(loc=media_erro, scale=std_erro, size=len(dados_obs))
# Agora podemos adicionar o erro aos dados observados
dados_obs += erro
```

Utilize a célula abaixo para gerar um gráfico de círculos pretos (`ok`) de seus dados.

```python
plt.figure() # Vamos criar um gráfico que consiga gerar círculos pretos com os nossos dados
plt.plot(x, dados_obs, "ok") # Plota os valores necessáriosno formato de bolas pretas.

plt.title("Gráfico de circulos pretos") # Vamos colocar um título no nosso gráfico.

plt.xlabel("Valores") # Dando nome ao eixo x.
plt.ylabel("Dados Observados") #dando nome ao eixo y
```

## Forma matricial da equação da reta e a matriz Jacobiana

Temos uma equação da reta para cada valor de $x_i$:

$$
\begin{align}
d_1 &= ax_1 + b \\
d_2 &= ax_2 + b \\
\vdots \\
d_N &= ax_N + b \\
\end{align}
$$

Esse sistema pode ser escrito de forma matricial com os parâmetros sendo $a$ e $b$:

$$
\begin{bmatrix}
d_1 \\ d_2 \\ \vdots \\ d_N
\end{bmatrix} =
\begin{bmatrix}
x_1 & 1 \\
x_2 & 1 \\
\vdots & \vdots \\
x_N & 1
\end{bmatrix}
\begin{bmatrix}
a \\ b
\end{bmatrix}
$$

$$
\bar{d} = \bar{\bar{A}}\bar{p}
$$

## Tarefa

Faça uma função chamada `jacobiana` que calcule e retorne a matrix Jacobiana ($\bar{\bar{A}}$).

**Para pensar**: o que essa função deve receber como argumento? (**Dica**: ela só precisa de 1)

```python
def jacobiana (x): #criando a função que retorna a matriz

    """
    Criando a matriz jacobiana
    """

    jacobiana = [] #criando uma lista vazia

    for i in range(N):
        jacobiana.append([x[i], 1]) #adicionando linhas da matriz (Xn, 1)
    return jacobiana

```

### Resultado esperado

A célula abaixo testa a sua Jacobiana contra uma produzida pelo numpy.

```python
assert np.allclose(jacobiana(x), np.transpose([x, np.ones_like(x)]))
```

## Tarefa

Calcule dados preditos para o vetor de parâmetros definido abaixo **utilizando a forma matricial da equação**. Guarde o resultado em uma variável chamada `preditos`.

Faça um gráfico dos dados observados (gerados acima) como pontos pretos e os dados preditos que você calculou como uma linha vermelha.

**Dica**: utilize as funções que você criou na aula passada.

```python
p = [5, 15]
```

```python
# Preciso criar uma função que consiga multiplicar uma matriz por um vetor, para isso uso o mvult.
def vmult(m, v):

    """
    Multiplica uma matriz por um vetor
    """
    assert len(m[0]) == len(v), 'Número de colunas da matriz diferente do número de linhas do vetor.'

    D = []  # Serve para criar uma lista vazia.

    for i in range(len(m)): # Serve para andar pelas linhas da matriz
        # Cria uma variavel que recebe o valor zero.
        soma = 0

        for k in range(len(m[0])): # Serve para andar pelas colunas da matriz

            soma = soma + (m[i][k] * v[k])  # Com isso, eu consigo multiplicar a matriz pelo vetor.

        D.append(soma) # Faz com que todos os meus resultados gerados fiquem na lista vazia.
    return D
```

```python
#para plotar o gráfico, tenho que definir as minhas variáveis

jac = jacobiana(x) #atribuindo jbn a função jacobiana
preditos = vmult(jac, p) #atribuindo uma variável para a multiplicação, que sao os dados preditos
```

```python
# Criando o gráfico.
plt.figure()

plt.plot(x, dados_obs, "ok") # Serve para plotar os valores das coordenadas do gráfico em pontos pretos.

plt.plot(x, preditos, "-r", linewidth=3) # Plota os valores das coordenadas x, y em uma linha vermelha.

plt.title("Dados observados e preditos") # Vou atribuir um título para o meu segundo gráfico.
```

### Resultado esperado

A célula abaixo testa seus resultados contra um calculado com o numpy.

```python
assert np.allclose(preditos, np.dot(jacobiana(x), p))
```

O gráfico deve ser parecido com o abaixo:

## Sistema de equações normais

A solução de mínimos quadrados é o vetor $\bar{p}$ que resolve o sistema linear abaixo (chamado de sistema de equações normais):

$$
\bar{\bar{A}}^T\bar{\bar{A}}\bar{p} = \bar{\bar{A}}^T\bar{d}^o
$$

Para resolver esse sistema, precisamos primeiramente calcular a matriz do sistema $\bar{\bar{A}}^T\bar{\bar{A}}$ e o vetor do lado direito $\bar{\bar{A}}^T\bar{d}^o$.

## Tarefa

Faça uma função chamada `eqnormais_sistema` que calcule e retorne a matriz $\bar{\bar{A}}^T\bar{\bar{A}}$ dada a matriz Jacobiana.

Utilize as funções criadas na aula anterior.

**Dica**: É possível saber quantas linhas e colunas o sistema deve conter. Cheque se o seu resulatdo possui esse número.

```python
#Para definição da matriz transposta usamos:

def mtrans (m):
    """
    Calcula a matriz transposta a partir de uma matriz qualquer.

    """

    for i in range(1, len(m[0]), 1):

        assert len(m[i]) == len(m[i - 1]), "Alguma linha da matriz não apresenta o mesmo número de elementos das outras"

    Tp = [] #criando uma lista vazia para colocar a Matriz Transposta
    for i in range(len(m[0])):#Serve para variar o número de colunas
        Y = []
        for j in range(len(m)):# serve para variar o numero de linhas
            Z = m[j][i] #serve para escolhem o elemento da coluna i, linha j
            Y.append(Z) #faz com que os resultados gerados fiquem na lista Y

        Tp.append(Y) #serve para os resultados gerados na lista Y fiquem na matriz transposta
    return(Tp)

# Para multiplicar as matrizes usamos:
def mult(m1, m2):

    """
    Multiplica duas matrizes
    """

    M = [] #criando outra lista vazia para guardar a multiplicação das matrizes

    for i in range(len(m1)):

        M.append([])
        for j in range(len(m2[0])):
            soma = 0
            for k in range(len(m1[0])):
                soma = soma + (m1[i][k]*m2[k][j])
            M[i].append(soma) #guardamos os resultados da multiplicação em C
    return M
```

```python
# Para definir a função eqnormais_sistema:

def eqnormais_sistema(x):

    """
    Matriz transposta multiplicada pela matriz jacobiana
    """

    return mult(mtrans(x), x)
```

```python
eqnormais_sistema(jacobiana(x))
```

    [[46594.387755102049, 1375.0], [1375.0, 50]]

### Resultado esperado

A célula abaixo testa seus resultados contra um calculado com o numpy.

```python
assert np.allclose(eqnormais_sistema(jacobiana(x)), np.transpose(jacobiana(x)).dot(jacobiana(x)))
```

## Tarefa

Faça uma função chamada `eqnormais_lado_direito` que calcule e retorne o vetor do lado direito do sistema de equações normais.

**Dicas**:

* Essa função deve receber 2 argumentos.
* O que essa função deve retornar é um vetor ou matriz?
* É possível saber o número de elementos que o resultado deve conter. Cheque esse número.

```python
# Para criar a função eqnormais_lado_direito devemos usar:

def eqnormais_lado_direito(x,y):

    return vmult(mtrans(x), y) #vetor do lado direito
```

```python
jaeq = mtrans(jac)
```

```python
result = eqnormais_lado_direito(jac, dados_obs)
```

```python

```

### Resultado esperado

A célula abaixo testa seus resultados contra um calculado com o numpy.

```python
assert np.allclose(eqnormais_lado_direito(jacobiana(x), dados_obs), np.transpose(jacobiana(x)).dot(dados_obs))
```

```python
#faltou checar o numero de elementos
```

## Solução de mínimos quadrados

Agora que temos o sistema de equações normais, podemos resolvê-lo numericamente para encontrar os valores de $a$ e $b$ que produzem a reta que melhor ajusta nossos dados.

## Tarefa

Faça uma função chamada `elim_gauss` que resolve um sistema de equações utilizando a eliminação de Gauss. Essa função deve receber como argumento a matriz do sistema e o vetor lado direito e retornar o vetor de solução.

**Dicas**:

* Cheque o número de elementos na matriz e no vetor.
* A matriz deve ser quadrada.

```python
# Para definir a função devemos
def elim_gauss(A, x):
    """
    Faz o escalonamento para resolver o sistema linear
    """
    for k in range (0, len(A)-1, 1): # serve para variar dentro das linhas, ignorando a ultima linha.
        for i in range (k+1, len(A[0]), 1): #serve para variar dentro das colunas

            temp = -A[i][k]/A[k][k] # Criamos uma variável para guardar os resultados do escalonamento.

            for j in range (k, len(A), 1): #Serve para fazer o escalonamento

                A[i][j] = A[i][j] + A[k][j] * temp  # Escalona a matriz, multiplicando todos os elementos dela pela temp,
                                                    #e somando o resultado na proxima parte do matriz.

            x[i] = x[i] + x[k] * temp
# Para calcular o resultado do vetor do lado direito fazemos:
        vetor_dir = [0]*len(A)

    for k in range(len(A)-1, -1, -1): #serve para variar entre as linhas da matriz

        for i in range(len(A[0])-1, k, -1): #serve para variar entre as colunas da mesma matriz
               vetor_dir[k] = vetor_dir[k] + (vetor_dir[i]*A[k][i]) #chamamos o somatório de cada resultado de vetor_dir

        vetor_dir[k] = (x[k] - vetor_dir[k]) / A[k][k]
    return vetor_dir
```

### Resultado esperado

A célula abaixo testa seus resultados contra um calculado com o numpy.

```python
np.random.seed(42)
A_teste = np.random.uniform(10, 50, size=(21, 21))
x_teste = np.random.uniform(5, 20, size=21)
y_teste = A_teste.dot(x_teste)
assert np.allclose(elim_gauss(A_teste, y_teste), x_teste)
```

```python

```

```python

```

## Tarefa

Faça uma função `ajuste_reta` que recebe um vetor de valores de x e um de dados observados e retorna a solução de minimos quadrados $\bar{p}$ (vetor com os valores de $a$ e $b$ estimados).

Aplique essa função nos dados observados simulados acima. Cheque se a solução bate com o valor esperado (você pode fazer isso usando um `assert`).

Faça um gráfico dos dados observados (pontos pretos) pelos dados preditos pela solução que você obteve agora (linha vermelha). O gráfico deve conter uma legenda. A legenda para os dados preditos deve ser da forma "y = 234x + 244" (trocando os números pelos valores que você estimou).

**Dica**:r

* Quantos elementos deve ter o vetor retornado?
* Para inserir números em uma string (texto): `"y = {}".format(123.1)"` $\to$ `"y = 123.1"`
* Para formatar os números que você quer inserir numa string: `"y = {:.4f}".format(123.242524536362446353436335)` $\to$  `"y = 123.2425"`

```python
#Para definir a função ajuste_reta

def ajuste_reta(x, y):

    return elim_gauss(eqnormais_sistema(jacobiana(x)), eqnormais_lado_direito(jacobiana(x),y))

```

```python
r = ajuste_reta(x, dados_obs)
print(r)

#y = a*x + b
```

    [9.7422960022585752, 52.577381832766335]

```python
plt.figure()
#plotando x e y em pontos pretos.
plt.plot(x, dados_obs, "ok", label='Dados observados')
plt.plot(x, y, "-r", label='y = 9.74x + 52.58')
#para plotar a legenda:
legend = plt.legend(loc='upper left', shadow=True)

plt.xlabel("x")
plt.ylabel("y")
```

### Resultado esperado

Os valores estimados para $\bar{p}$ devem ser aproximadamente:

    [9.742296, 52.57738183]

O gráfico deve ser parecido com o abaixo:

## Tarefa Bônus

Podemos utilizar o  método dos mínimos quadrados para ajustar qualquer equação que seja linear com relação as parâmetros ($a$ e $b$ no caso da reta). Isso quer dizer que podemos ajustar uma parábola:

$$
d_i = ax_i^2 + bx + c
$$

Dessa vez, os parâmetros que queremos estimar são $a$, $b$ e $c$. Note que agora temos 3 parâmetros, não 2. Por isso, a Jacobiana terá 3 colunas ao invés de 2.

Faça ao menos as seguintes funções:

* `jacobiana_parabola`: calcula e retorna a matriz Jacobiana para o caso da parábola. Deve receber como argumento somente o vetor de coordenadas x.
* `ajuste_parabola`: calcula a solução de mínimos quadrados para o caso de uma parábola. Deve receber como argumento o vetor de coordenadas x e o vetor de dados. Deve retornar o vetor de parâmetros $\bar{p}$ estimado (contem os valores de $a$, $b$ e $c$)

Teste suas funções com os dados gerados abaixo. Note que estamos usando o mesmo vetor x. Gere gráficos dos dados fabricados e também dos dados preditos pela estimativa (como os que foram feitos acima).

O que acontece se você tentar ajustar uma reta aos dados da parábola? E se tentar ajustar uma parábola aos dados da reta?

**Dicas**:

* Você precisa criar outras funções para montar o sistema de equações normais e calcular a solução do sistema?

```python
a_par, b_par, c_par = 2, 20, 200
dados_parabola = a_par*x**2 + b_par*x + c_par + erro
```

```python

```

```python

```

```python

```

```python

```

### Resultado esperado

Os gráficos que você deve gerar deverão ser parecidos com os abaixo:


# Bayesian Logistic Regression

Contact: mihaelacr@google.com

Aim of this colab:
Logistic regression with a twist! We will use variational inference to learn a distribution over the learned parameters.

Througout this work we will be focusing on a binary classification task, on
a small dataset (the UCD breast cancer dataset).

## Logistic regression

Learn the weights which best classify the $(x, y)$ pairs in the dataset.
The weights ($w$) and biases $b$, form the parameters which are learned via the following maximization problem:

\begin{equation}
\mathbb{E}_{p^*(x, y)} \log p(y|x, \theta) = \\
 \mathbb{E}_{p^*(x, y)} \left[y \log \sigma(w x + b) + (1 - y) \log(1- \sigma(w x + b)\right]
\end{equation}

Here $\sigma$ denotes the sigmoid function.

## Bayesian logistic regression - computing the evidence lower bound

Learn a distribution over the weights which best classify the $(x, y)$ pairs in the dataset.

We now want to learn the distributional parameters of $q(w)$ in order to maximize:

\begin{equation}
\mathbb{E}_{p^*(x, y)} \log p_w(y| x) = \\
\mathbb{E}_{p^*(x, y)} \log \int p(y|x, w) p(w) \delta w = \\
\mathbb{E}_{p^*(x, y)} \log \int p(y|x, w) p(w) \frac{q(w)}{q(w)} \delta w \ge \\
\mathbb{E}_{p^*(x, y)} \mathbb{E}_{q(w)} \log \left[p(y| x, w) \frac{p(w)}{q(w)} \right]= \\
\mathbb{E}_{q(w)} \mathbb{E}_{p^*(x, y)} \log p(y|x, w)  -  KL(q(w)||p(w)) = \\
 \mathbb{E}_{q(w)}\mathbb{E}_{p^*(x, y)} \left[y \log \sigma(w x + b) + (1 - y) \log(1- \sigma(w x + b)\right] -  KL(q(w)||p(w))
\end{equation}

In Bayesian Logistic Regression, we thus learn a distribution over parameters
$w$ which can explain the data, while staying close to a chosen prior $p(w)$.

Throughout this lab, we will use Gaussian Distributions for $p(w)$ and $q(w)$.

For more details, see
[this paper](https://pdfs.semanticscholar.org/e407/ea7fda6d152d2186f4b5e27aa04ec2d32dcd.pdf)
or this tutorial
[https://www.ece.rice.edu/~vc3/elec633/logistic.pdf].

### Gradient estimation

In Bayesian Logistic Regression, we aim to learn the parameters of the distribution $q(w)$. We will call these parameters $\theta$. In the case of a
Gaussian distribution, these will be the mean and covariance matrix of a
multivariate distribution. Since we will use stochastic gradient descent to learn $\theta$, we need to be able to compute the gradients with respect to
$\theta$ of our objective. Since our objective contains an expectation with respect to $q(w)$, this can be challenging. To illustrate this, from now on we will denote $q(w)$ as $q_{\theta}(w)$.

Specifically, we are interested in:
\begin{equation}
  \nabla_{\theta} \left[\mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y|x, w)  -  KL(q_{\theta}(w)||p(w))\right]
\end{equation}

Since in we will be using Gaussian distributions for $q_{\theta}(w)$ and $p(w)$
$KL(q_{\theta}(w)||p(w))$ can be computed in closed form and we can rely on
TensorFlow's automatic differentiation to compute
$\nabla_{\theta} KL(q_{\theta}(w)||p(w))$.

Let's now turn attention to computing $\nabla_{\theta} \mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y| x, w)$. Unlike the KL, the integral
$\mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y|x, w)$
is not tractable, and thus cannot be computed in closed form. Thus, we are interested in other approaches to compute the gradient.

### Reinforce gradient estimation

A first approach to compute an estimate of the gradient is to use the
REINFORCE gradient estimator:

\begin{equation}
  \nabla_{\theta} \mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y| x, w)  = \\
  \nabla_{\theta} \int q_{\theta}(w) \mathbb{E}_{p^*(x, y)} \log p(y| x, w) \delta w = \\
  \int \nabla_{\theta} \left[q_{\theta}(w) \mathbb{E}_{p^*(x, y)} \log p(y| x, w) \right] \delta w = \\
 \int \nabla_{\theta} \left[q_{\theta}(w)\right] \mathbb{E}_{p^*(x, y)} \log p(y| x, w) \delta w = \\
 \int q_{\theta}(w) \nabla_{\theta} \left[\log q_{\theta}(w)\right] \mathbb{E}_{p^*(x, y)} \log p(y| x, w)  \delta w = \\
 \mathbb{E}_{q_{\theta}(w)} \left[ \nabla_{\theta} \log q_{\theta}(w) \mathbb{E}_{p^*(x, y)} \log p(y| x, w) \right]
\end{equation}

We can use samples from $q_{\theta}(w)$ to compute the Monte Carlo estimate of
the last integral, to get an unbiased estimator of the true gradient. The more
samples we use to estimate the integral, the more accurate the gradient estimate
will be.

### Pathwise gradient estimation (reparametrization)

In the REINFORCE estimator, we did not use any knowledge of the variational
distribution $q_{\theta}(w)$.
For a Gaussian distribution, we can use the reparametrization trick to obtain
an unbiased gradient estimator of  $\nabla_{\theta} \mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y| x, w)$.

To do so, we will use the fact that
\begin{equation}
    z \sim N(\mu, \sigma), z = \mu + \epsilon \sigma, {\text {with }} \epsilon \sim N(0, 1)
\end{equation}

Namely:
\begin{equation}
  \nabla_{\theta} \mathbb{E}_{q_{\theta}(w)} \mathbb{E}_{p^*(x, y)} \log p(y| x, w)  = \\
  \nabla_{\theta} \mathbb{E}_{p(\epsilon)} \mathbb{E}_{p^*(x, y)} \log p(y| x, \mu + \epsilon \sigma)  = \\
  \mathbb{E}_{p(\epsilon)} \nabla_{\theta} \mathbb{E}_{p^*(x, y)} \log p(y| x, \mu + \epsilon \sigma)
\end{equation}

Note that we were able to move the gradient inside the integral since
$p(\epsilon)$ does not depend on $\theta$.
To estimate the last integral, we can use a Monte Carlo estimate using samples
of $p(\epsilon)$.

## Your tasks

*   Define the Gaussian posterior distribution.
*   Fill in the code for the ELBO
*   Fill in the reinforce function to create the surrogate loss
*   Fill in the loss function for reparametrization
*   Visualize the effects of batch size, learning rates and number of posterior samples on the learned model, as well as gradient variance.

```
import enum
import numpy as np

import math
from sklearn import datasets
from sklearn import linear_model
import collections

from matplotlib import pyplot as plt
import tensorflow as tf
import tensorflow_probability as tfp
import seaborn as sns

tfd = tfp.distributions

```

```
sns.set(rc={"lines.linewidth": 2.8}, font_scale=2)
sns.set_style("whitegrid")
```

## Get the dataset and visualize it

```
def get_data(normalize = True):
    data = datasets.load_breast_cancer()

    print(data.target_names)
    print(data.feature_names)

    features = np.array(data.data, dtype=np.float32)
    targets = np.array(data.target, dtype=np.float32)

    if normalize:
        # Note: the data dimensions have very different scales.
        # We normalize by the mean and scale of the entire dataset.
        features = features - features.mean(axis=0)
        features = features / features.std(axis=0)

    # Add a dimension of 1 (bias).
    features = np.concatenate((features, np.ones((features.shape[0], 1))), axis=1)
    features = np.array(features, dtype=np.float32)

    return features, targets, data.feature_names
```

### Dataset

This datasets has meaningful features: we are trying to classify based on these features whether someone has breast cancer or not. We can look at the
features we are using to classify the data below.

```
features, targets, feature_names = get_data()
```

    ['malignant' 'benign']
    ['mean radius' 'mean texture' 'mean perimeter' 'mean area'
     'mean smoothness' 'mean compactness' 'mean concavity'
     'mean concave points' 'mean symmetry' 'mean fractal dimension'
     'radius error' 'texture error' 'perimeter error' 'area error'
     'smoothness error' 'compactness error' 'concavity error'
     'concave points error' 'symmetry error' 'fractal dimension error'
     'worst radius' 'worst texture' 'worst perimeter' 'worst area'
     'worst smoothness' 'worst compactness' 'worst concavity'
     'worst concave points' 'worst symmetry' 'worst fractal dimension']

### Trivial baseline

How well can we do with a constant classifier?

If we look at the average label, we can see how many 1s are in the data, so we know that our classifier has to get more than that accuracy

```
np.mean(targets)
```

    0.6274165

```
features.shape
```

    (569, 31)

```
dataset_size = features.shape[0]
data_dims = features.shape[-1]
```

## Logistic regression baseline - how well can we do on this dataset?

Let's look at a simple, non Bayesian classification approach to get an idea of how well we can do on the dataset.

```
lr = linear_model.LogisticRegression()
```

```
lr.fit(features, targets)
lr.score(features, targets)
```

    /usr/local/lib/python2.7/dist-packages/sklearn/linear_model/logistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
      FutureWarning)

    0.9876977152899824

## Utilities

```
def get_shape_list(tensor):
  return tensor.shape.as_list()
```

```
def get_sklearn_data_as_tensors(
      features, targets, batch_size, dataset_name='breast_cancer'):
  """Read sklearn datasets as tf.Tensors.

  Args:
    batch_size: Integer or None. If None, the entire dataset is used.
    dataset_name: A string, the name of the dataset.

  Returns:
    A tuple of size two containing two tensors of rank 2 `[B, F]`, the data
    features and targets.
  """
  dataset = tf.data.Dataset.from_tensor_slices((features, targets))
  if batch_size:
    # Shuffle, repeat, and batch the examples.
    batched_dataset = dataset.shuffle(1000).repeat().batch(batch_size)
  else:
    batch_size = features.shape[0]
    batched_dataset = dataset.repeat().batch(batch_size)

  iterator = batched_dataset.make_one_shot_iterator()
  batch_features, batch_targets = iterator.get_next()

  data_dim = features.shape[1]
  batch_features.set_shape([batch_size, data_dim])
  batch_targets.set_shape([batch_size])

  return batch_features, batch_targets
```

## Bayesian Logistic Regression - define the prior and the posterior

We will learn a distribution over parameters, so we will first define prior distribution, and then a posterior distribution over parameters which we
will learn.

Both of these distibutions will be Gaussian.

## Task: implement the posterior distribution

You have to implement two functions

1.   multi_normal, which takes as arguments the mean and the log scale of the normal distribution, and returns a tensorflow distribution object. Have a look at the TensorFlow distibutions package.
2.   diagonal_gaussian_posterior which returns a tuple of two elements, one being the posterior distribution (a call to multi_normal) and the learned variables. Your task here is to define the variables we will learn. The argument `data_dims` tells you the size of the learned parameters, and thus the size of the variables of the distribution).

```
def multi_normal(loc, log_scale):
  return tfp.distributions.MultivariateNormalDiag(loc, tf.math.exp(log_scale))

def diagonal_gaussian_posterior(data_dims):
  mean = tf.Variable(tf.zeros([data_dims], dtype=tf.float32), name="mean")

  diag = tf.math.log(tf.ones([data_dims], dtype=tf.float32)) # diag log-covariance
  log_scale = tf.Variable(diag , name="log_scale")

  learned_vars = [mean, log_scale]
  return multi_normal(loc=mean, log_scale=log_scale), learned_vars
```

## Bayesian Logistic Regression - the model

### Hypers

```
BATCH_SIZE = 500
NUM_POSTERIOR_SAMPLES = 100
```

```
prior = multi_normal(loc=tf.zeros(data_dims), log_scale=tf.zeros(data_dims))
posterior, learned_vars  = diagonal_gaussian_posterior(data_dims)
```

```
def _predictions(logits):
  return tf.to_float(logits >= 0)

def _accuracy(targets, predictions):
  # `predictions` have rank 2: batch_size, num_posterior samples.
  # We expand dims the targets to compute the accuracy per sample.
  targets = tf.expand_dims(targets, axis=1)
  return tf.reduce_mean(tf.to_float(tf.equal(targets, predictions)))

def linear_model(data, targets, posterior_samples):
  num_posterior_samples = tf.shape(posterior_samples)[0]
  logits = tf.matmul(data, posterior_samples, transpose_b=True)
  # Make targets [B, 1] to use broadcasting.
  targets = tf.expand_dims(targets, axis=1)
  targets = targets * tf.ones([1, num_posterior_samples])

  log_probs = - tf.nn.sigmoid_cross_entropy_with_logits(
      labels=targets, logits=logits)
  return log_probs, logits
```

## Task: Run the model and return the components of the elbo

```
def run_model(posterior, num_samples):
  # Sample from the posterior to obtain a set of parameters used for
  # prediction. Use `num_samples` for the number of samples.
  posterior_samples = posterior.sample(num_samples)

  # Compute the log probs of the data under all the models we have sampled.
  # These tensors are [num_samples, B]. Use the features and targets tensors.
  log_probs, logits = linear_model(features, targets, posterior_samples)

  # Compute the KL between the posterior and the prior.
  # Note: since we use Gaussian distributions, the closed form of the
  # KL can be computed. Remember that distributions are objects in TensorFlow!
  kl = posterior.kl_divergence(prior)

  # Sum over data. Normalize by dataset size to ensure that the gradients
  # are not very different in magnitude when we change batch size.
  param_log_probs = tf.reduce_mean(log_probs, axis=0) * dataset_size
  param_log_probs.shape.assert_is_compatible_with([num_samples])

  return param_log_probs, kl, posterior_samples, logits
```

```
param_log_probs, kl, posterior_samples, logits = run_model(
      posterior, NUM_POSTERIOR_SAMPLES)
predictions = _predictions(logits)
accuracy = _accuracy(targets=targets, predictions=predictions)
accuracy = tf.reduce_mean(accuracy)

```

    WARNING: Logging before flag parsing goes to stderr.
    W0706 15:10:01.309685 140030529763200 deprecation.py:323] From /usr/local/lib/python2.7/dist-packages/tensorflow/python/ops/nn_impl.py:180: where (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
    Instructions for updating:
    Use tf.where in 2.0, which has the same broadcast rule as np.where
    W0706 15:10:01.346157 140030529763200 deprecation.py:323] From <ipython-input-15-e4da80cc1f88>:2: to_float (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
    Instructions for updating:
    Use `tf.cast` instead.

## Task: compute the elbo from the log probs and the KL

`per_sample_elbo` should have shape [num_samples], since we obtain a model prediction for each of the sampled parameters.

```
per_sample_elbo = param_log_probs - kl
elbo = tf.reduce_mean(per_sample_elbo)
```

## Training loop

## Training the model via reparametrization

```
# Stochastic loss depends on the output of a sampling operation  (posterior samples)
# but tensorflow implements reparametrization by default.
per_sample_reparametrization_loss = - per_sample_elbo
reparametrization_loss = tf.reduce_mean(per_sample_reparametrization_loss)
```

```
optimizer = tf.train.GradientDescentOptimizer(0.0001)
reparam_min_op = optimizer.minimize(reparametrization_loss)
```

```
NUM_ITERATIONS = 1000
```

```
sess = tf.Session()

# Initialize all variables
sess.run(tf.initialize_all_variables())

reparam_accuracies = []
reparam_kls = []
reparam_elbos = []

for i in xrange(NUM_ITERATIONS):
  sess.run(reparam_min_op)

  if i % 10 == 0:
    reparam_acc, reparam_kl, reparam_elbo = sess.run([accuracy, kl, elbo])
    reparam_accuracies += [reparam_acc]
    reparam_kls += [reparam_kl]
    reparam_elbos += [reparam_elbo]

    print('Iteration {}. Elbo {}. KL {}'.format(
        i, reparam_elbo, reparam_kl))

reparam_learned_mean, reparam_learned_log_scale = sess.run(learned_vars)
```

    W0706 15:10:01.922456 140030529763200 deprecation.py:323] From /usr/local/lib/python2.7/dist-packages/tensorflow/python/util/tf_should_use.py:193: initialize_all_variables (from tensorflow.python.ops.variables) is deprecated and will be removed after 2017-03-02.
    Instructions for updating:
    Use `tf.global_variables_initializer` instead.

    Iteration 0. Elbo -1251.31628418. KL 0.00287126004696
    Iteration 10. Elbo -697.800720215. KL 0.307448148727
    Iteration 20. Elbo -537.026550293. KL 0.765623211861
    Iteration 30. Elbo -435.274505615. KL 1.27441883087
    Iteration 40. Elbo -351.531005859. KL 1.71919393539
    Iteration 50. Elbo -303.941864014. KL 2.16450643539
    Iteration 60. Elbo -260.008758545. KL 2.56153178215
    Iteration 70. Elbo -263.777099609. KL 2.92100739479
    Iteration 80. Elbo -234.770721436. KL 3.26699590683
    Iteration 90. Elbo -239.377182007. KL 3.59330368042
    Iteration 100. Elbo -224.065063477. KL 3.88889718056
    Iteration 110. Elbo -187.876937866. KL 4.17269849777
    Iteration 120. Elbo -188.153579712. KL 4.45011806488
    Iteration 130. Elbo -175.901901245. KL 4.71661520004
    Iteration 140. Elbo -155.265014648. KL 4.97964668274
    Iteration 150. Elbo -164.109680176. KL 5.22092580795
    Iteration 160. Elbo -153.392990112. KL 5.46521854401
    Iteration 170. Elbo -166.389511108. KL 5.70708227158
    Iteration 180. Elbo -141.348587036. KL 5.93668699265
    Iteration 190. Elbo -144.340988159. KL 6.14716243744
    Iteration 200. Elbo -150.441848755. KL 6.34233903885
    Iteration 210. Elbo -125.198425293. KL 6.53200817108
    Iteration 220. Elbo -144.606735229. KL 6.72982215881
    Iteration 230. Elbo -124.996162415. KL 6.90616846085
    Iteration 240. Elbo -123.129951477. KL 7.0823802948
    Iteration 250. Elbo -116.140907288. KL 7.2649936676
    Iteration 260. Elbo -122.178184509. KL 7.44574928284
    Iteration 270. Elbo -127.089424133. KL 7.61699581146
    Iteration 280. Elbo -126.283309937. KL 7.78921937943
    Iteration 290. Elbo -117.368141174. KL 7.95003604889
    Iteration 300. Elbo -117.763244629. KL 8.10859680176
    Iteration 310. Elbo -115.204460144. KL 8.26020336151
    Iteration 320. Elbo -113.344589233. KL 8.4087677002
    Iteration 330. Elbo -119.965705872. KL 8.56992721558
    Iteration 340. Elbo -113.99962616. KL 8.71299934387
    Iteration 350. Elbo -105.818740845. KL 8.86545181274
    Iteration 360. Elbo -100.249084473. KL 9.0091381073
    Iteration 370. Elbo -110.714012146. KL 9.15460586548
    Iteration 380. Elbo -103.108894348. KL 9.28317070007
    Iteration 390. Elbo -105.591644287. KL 9.42404651642
    Iteration 400. Elbo -111.089073181. KL 9.55483055115
    Iteration 410. Elbo -104.87991333. KL 9.67768478394
    Iteration 420. Elbo -110.888923645. KL 9.80382919312
    Iteration 430. Elbo -103.24887085. KL 9.931016922
    Iteration 440. Elbo -109.799942017. KL 10.0544033051
    Iteration 450. Elbo -98.0338363647. KL 10.1714935303
    Iteration 460. Elbo -100.708076477. KL 10.2848911285
    Iteration 470. Elbo -101.14617157. KL 10.3957271576
    Iteration 480. Elbo -93.3948516846. KL 10.5126533508
    Iteration 490. Elbo -107.306007385. KL 10.6308822632
    Iteration 500. Elbo -100.181022644. KL 10.7451038361
    Iteration 510. Elbo -95.855255127. KL 10.8539943695
    Iteration 520. Elbo -100.132675171. KL 10.9568748474
    Iteration 530. Elbo -100.38646698. KL 11.0542888641
    Iteration 540. Elbo -94.6835021973. KL 11.1537256241
    Iteration 550. Elbo -94.338722229. KL 11.2570905685
    Iteration 560. Elbo -90.4855880737. KL 11.357252121
    Iteration 570. Elbo -98.0821456909. KL 11.4538021088
    Iteration 580. Elbo -93.8483581543. KL 11.5528078079
    Iteration 590. Elbo -94.2413787842. KL 11.650138855
    Iteration 600. Elbo -84.8176193237. KL 11.7399148941
    Iteration 610. Elbo -88.862487793. KL 11.8348445892
    Iteration 620. Elbo -90.7114181519. KL 11.9179830551
    Iteration 630. Elbo -89.9356384277. KL 12.0149831772
    Iteration 640. Elbo -91.7482910156. KL 12.1029577255
    Iteration 650. Elbo -89.2460861206. KL 12.1858530045
    Iteration 660. Elbo -91.8519744873. KL 12.2712144852
    Iteration 670. Elbo -91.9219665527. KL 12.3608808517
    Iteration 680. Elbo -90.779586792. KL 12.4472103119
    Iteration 690. Elbo -87.8686599731. KL 12.5342903137
    Iteration 700. Elbo -88.755279541. KL 12.6139793396
    Iteration 710. Elbo -89.0103912354. KL 12.6986293793
    Iteration 720. Elbo -91.2760543823. KL 12.7775554657
    Iteration 730. Elbo -84.5751495361. KL 12.8627347946
    Iteration 740. Elbo -88.3510131836. KL 12.946138382
    Iteration 750. Elbo -81.0643081665. KL 13.0226335526
    Iteration 760. Elbo -87.4532012939. KL 13.1025733948
    Iteration 770. Elbo -85.3113555908. KL 13.1825962067
    Iteration 780. Elbo -91.157623291. KL 13.2549219131
    Iteration 790. Elbo -82.2567977905. KL 13.3327140808
    Iteration 800. Elbo -84.6036605835. KL 13.4073734283
    Iteration 810. Elbo -88.7576293945. KL 13.4813165665
    Iteration 820. Elbo -86.5616226196. KL 13.5525226593
    Iteration 830. Elbo -84.7662887573. KL 13.6196088791
    Iteration 840. Elbo -86.3528594971. KL 13.6932125092
    Iteration 850. Elbo -81.4763946533. KL 13.7570486069
    Iteration 860. Elbo -83.6050796509. KL 13.8267087936
    Iteration 870. Elbo -80.9121170044. KL 13.892824173
    Iteration 880. Elbo -84.220993042. KL 13.9599637985
    Iteration 890. Elbo -81.2252044678. KL 14.0247545242
    Iteration 900. Elbo -84.1262969971. KL 14.0935535431
    Iteration 910. Elbo -82.5146560669. KL 14.1550140381
    Iteration 920. Elbo -85.5831832886. KL 14.2202100754
    Iteration 930. Elbo -84.2529678345. KL 14.2814331055
    Iteration 940. Elbo -84.1190109253. KL 14.3443193436
    Iteration 950. Elbo -82.031288147. KL 14.4068174362
    Iteration 960. Elbo -82.321395874. KL 14.4710903168
    Iteration 970. Elbo -80.7244949341. KL 14.5387954712
    Iteration 980. Elbo -79.9401016235. KL 14.5994224548
    Iteration 990. Elbo -82.4692764282. KL 14.6628885269

```
fig, axes = plt.subplots(1, 3, figsize=(3*8,5))

axes[0].plot(reparam_elbos, label='ELBO')
axes[0].set_title('Time', fontsize=15)
axes[0].set_ylim((-500, -50))
axes[0].legend()

axes[1].plot(reparam_kls, label='KL')
axes[1].set_title('Time', fontsize=15)
axes[1].legend()

axes[2].plot(reparam_accuracies, label='Accuracy')
axes[2].set_title('Time', fontsize=15)
axes[2].legend()
```

## Training the model via reinforce

## Task: define the surrogate reinforce loss for the log prob term.

Note: we do not need to use reinforce the KL term, since we can compute the KL analytically for the two Gaussians, and can use standard backprop.

For the reinforce implementation, instead of changing the gradients, we will change the loss such that TensorFlow's automatic differentiation does the right thing. This is called a surrogate loss.

The gradient we want to obtain is the following (since we want to minimize the loss, we add a minus):
\begin{equation}
- \mathbb{E}_{q_{\theta}(w)} \left[ \nabla_{\theta} \log q_{\theta}(w) \mathbb{E}_{p^*(x, y)} \log p(y| x, w) \right]
\end{equation}

so we need to construct a loss which has this gradient, by ensuring that the gradients will flow through the $\log q_{\theta}(w)$ term, but not the rest.

```
# Since TensorFlow does automatic differentation, we have to use
# tf.stop_gradient to ensure the gradient only flows to
# the log_{q_\theta}(w) term.
per_sample_reinforce_loss =  -posterior.log_prob(tf.stop_gradient(posterior_samples)) * tf.stop_gradient(param_log_probs)   # Shape [num_samples, batch size].
# Reduce now over the number of samples.
reinforce_loss = tf.reduce_mean(per_sample_reinforce_loss) + kl
```

```
optimizer = tf.train.GradientDescentOptimizer(0.0001)
reinforce_min_op = optimizer.minimize(reinforce_loss)
```

```
NUM_ITERATIONS = 1000
```

```
reinforce_accuracies = []
reinforce_kls = []
reinforce_elbos = []

sess = tf.Session()

# Initialize all variables
sess.run(tf.initialize_all_variables())

for i in xrange(NUM_ITERATIONS):
  sess.run(reinforce_min_op)

  if i % 10 == 0:
    reinforce_acc, reinforce_kl, reinforce_elbo = sess.run([accuracy, kl, elbo])
    reinforce_accuracies += [reinforce_acc]
    reinforce_kls += [reinforce_kl]
    reinforce_elbos += [reinforce_elbo]

    print('Iteration {}. Elbo {}. KL {}'.format(
        i, reinforce_elbo, reinforce_kl))

reinforce_learned_mean, reinforce_learned_log_scale = sess.run(learned_vars)
```

    Iteration 0. Elbo -1116.80126953. KL 0.0217651166022
    Iteration 10. Elbo -687.183959961. KL 0.510267496109
    Iteration 20. Elbo -453.471405029. KL 1.03551530838
    Iteration 30. Elbo -410.559204102. KL 1.53315734863
    Iteration 40. Elbo -344.857574463. KL 2.00079274178
    Iteration 50. Elbo -295.77444458. KL 2.42567944527
    Iteration 60. Elbo -294.467803955. KL 2.85116958618
    Iteration 70. Elbo -245.117263794. KL 3.27838110924
    Iteration 80. Elbo -228.88848877. KL 3.60616755486
    Iteration 90. Elbo -222.709915161. KL 3.93956565857
    Iteration 100. Elbo -204.184204102. KL 4.27431440353
    Iteration 110. Elbo -186.381469727. KL 4.60125017166
    Iteration 120. Elbo -201.87689209. KL 4.86276388168
    Iteration 130. Elbo -173.708618164. KL 5.13902950287
    Iteration 140. Elbo -167.708618164. KL 5.33069610596
    Iteration 150. Elbo -168.621566772. KL 5.58919334412
    Iteration 160. Elbo -161.376098633. KL 5.81932401657
    Iteration 170. Elbo -158.452926636. KL 6.00672531128
    Iteration 180. Elbo -143.865905762. KL 6.19362735748
    Iteration 190. Elbo -140.297363281. KL 6.40803050995
    Iteration 200. Elbo -129.455200195. KL 6.6296453476
    Iteration 210. Elbo -138.195205688. KL 6.76893568039
    Iteration 220. Elbo -126.772705078. KL 6.96852207184
    Iteration 230. Elbo -127.587432861. KL 7.14093971252
    Iteration 240. Elbo -125.308326721. KL 7.34381103516
    Iteration 250. Elbo -125.432365417. KL 7.52608203888
    Iteration 260. Elbo -139.445022583. KL 7.72913169861
    Iteration 270. Elbo -113.273925781. KL 7.90194129944
    Iteration 280. Elbo -119.543899536. KL 8.02332878113
    Iteration 290. Elbo -111.953933716. KL 8.19211483002
    Iteration 300. Elbo -119.218818665. KL 8.35975170135
    Iteration 310. Elbo -119.145683289. KL 8.48949813843
    Iteration 320. Elbo -110.967712402. KL 8.67914581299
    Iteration 330. Elbo -109.579559326. KL 8.84544754028
    Iteration 340. Elbo -108.504173279. KL 8.99921131134
    Iteration 350. Elbo -102.252586365. KL 9.10935211182
    Iteration 360. Elbo -112.689689636. KL 9.25692558289
    Iteration 370. Elbo -107.4817276. KL 9.39167022705
    Iteration 380. Elbo -104.112724304. KL 9.48276615143
    Iteration 390. Elbo -111.464347839. KL 9.63346290588
    Iteration 400. Elbo -98.569732666. KL 9.75494194031
    Iteration 410. Elbo -107.651115417. KL 9.88669967651
    Iteration 420. Elbo -98.7607498169. KL 9.99015045166
    Iteration 430. Elbo -102.658576965. KL 10.0823078156
    Iteration 440. Elbo -101.766960144. KL 10.2478313446
    Iteration 450. Elbo -99.1033401489. KL 10.324262619
    Iteration 460. Elbo -97.9621887207. KL 10.4312391281
    Iteration 470. Elbo -97.7506942749. KL 10.5612602234
    Iteration 480. Elbo -96.5518188477. KL 10.7611818314
    Iteration 490. Elbo -101.28276062. KL 10.861623764
    Iteration 500. Elbo -95.2830963135. KL 10.9162893295
    Iteration 510. Elbo -94.7788162231. KL 10.9994258881
    Iteration 520. Elbo -97.3159790039. KL 11.1313438416
    Iteration 530. Elbo -97.5160675049. KL 11.2363739014
    Iteration 540. Elbo -95.607635498. KL 11.3185443878
    Iteration 550. Elbo -94.6198348999. KL 11.3863220215
    Iteration 560. Elbo -87.7221221924. KL 11.4867782593
    Iteration 570. Elbo -96.006942749. KL 11.6144142151
    Iteration 580. Elbo -104.44165802. KL 11.72914505
    Iteration 590. Elbo -92.0488891602. KL 11.847073555
    Iteration 600. Elbo -90.1584243774. KL 11.9548988342
    Iteration 610. Elbo -95.3857116699. KL 12.0338802338
    Iteration 620. Elbo -96.1010437012. KL 12.1311693192
    Iteration 630. Elbo -87.9766921997. KL 12.214723587
    Iteration 640. Elbo -90.6804580688. KL 12.3170566559
    Iteration 650. Elbo -86.4716186523. KL 12.4050207138
    Iteration 660. Elbo -92.253036499. KL 12.4865493774
    Iteration 670. Elbo -92.6006469727. KL 12.537147522
    Iteration 680. Elbo -84.3231277466. KL 12.6281881332
    Iteration 690. Elbo -86.8777084351. KL 12.700094223
    Iteration 700. Elbo -88.5244216919. KL 12.7614059448
    Iteration 710. Elbo -93.7727432251. KL 12.8245038986
    Iteration 720. Elbo -88.4225006104. KL 12.9040660858
    Iteration 730. Elbo -86.4209289551. KL 13.003074646
    Iteration 740. Elbo -89.3245773315. KL 13.066991806
    Iteration 750. Elbo -83.9625396729. KL 13.2090320587
    Iteration 760. Elbo -85.9223556519. KL 13.2696228027
    Iteration 770. Elbo -85.957244873. KL 13.3328294754
    Iteration 780. Elbo -87.6296005249. KL 13.3856296539
    Iteration 790. Elbo -89.4624404907. KL 13.4574918747
    Iteration 800. Elbo -87.6330337524. KL 13.5334854126
    Iteration 810. Elbo -84.2621459961. KL 13.5975008011
    Iteration 820. Elbo -80.3770599365. KL 13.6888542175
    Iteration 830. Elbo -84.3121948242. KL 13.7708616257
    Iteration 840. Elbo -84.4102020264. KL 13.8029575348
    Iteration 850. Elbo -84.5846176147. KL 13.8728990555
    Iteration 860. Elbo -86.505241394. KL 13.9493656158
    Iteration 870. Elbo -85.7620773315. KL 13.9937839508
    Iteration 880. Elbo -83.2697372437. KL 14.0379056931
    Iteration 890. Elbo -85.0736846924. KL 14.103433609
    Iteration 900. Elbo -84.2102050781. KL 14.1537284851
    Iteration 910. Elbo -81.9311141968. KL 14.1667337418
    Iteration 920. Elbo -85.272605896. KL 14.2589683533
    Iteration 930. Elbo -81.8618469238. KL 14.3228845596
    Iteration 940. Elbo -84.7690124512. KL 14.3466062546
    Iteration 950. Elbo -75.5114974976. KL 14.3993320465
    Iteration 960. Elbo -84.6055755615. KL 14.4412851334
    Iteration 970. Elbo -78.50365448. KL 14.5460262299
    Iteration 980. Elbo -82.2919311523. KL 14.6307439804
    Iteration 990. Elbo -81.2669372559. KL 14.7096118927

```
fig, axes = plt.subplots(1, 3, figsize=(3*8,5))

axes[0].plot(reinforce_elbos, label='ELBO')
axes[0].set_title('Time', fontsize=15)
axes[0].set_ylim((-500, -50))
axes[0].legend()

axes[1].plot(reinforce_kls, label='KL')
axes[1].set_title('Time', fontsize=15)
axes[1].legend()

axes[2].plot(reinforce_accuracies, label='Accuracy')
axes[2].set_title('Time', fontsize=15)
axes[2].legend()
```

## Task: Analysis

* How sensitive are the different estimators to the number of posterior samples used? Try 1, 10, 100.
* How sensitive are the different estimators to the learning rate used? Try 1e-4, 1e-3 and 1e-2 for both estimators.
* How sensitive are the different estimators to batch size? Try batch size 10 for both estimators.

# Visualize learned parameters

* Let's visualize the learned parameters, as well as check the different uncertanties around different parameters
* Can we now see which parameters are most important in the classification problem?

```
learned_mean = reparam_learned_mean
learned_scale = np.exp(reparam_learned_log_scale)
```

```
PARAM_INDEX = 10
```

```
def gaussian(x, mu, sig):
    return np.exp(-np.power(x - mu, 2.) / (2 * np.power(sig, 2.)))
```

```
mean = learned_mean[PARAM_INDEX]
scale = learned_scale[PARAM_INDEX]
x_values = np.linspace(-3, 3, num=1000)
y_values = [gaussian(x, mean, scale) for x in x_values]

plt.figure(figsize=(7,5))
plt.plot(x_values, y_values)
plt.vlines(
      learned_mean[PARAM_INDEX],
      ymin=0,
      ymax=gaussian(mean, mean, scale),
      colors='g',
      alpha=0.8)

plt.grid('off')
x_std_values = np.linspace(mean - scale, mean + scale, num=1000)
y_std_values = [gaussian(x, mean, scale) for x in x_std_values]
plt.fill_between(x_std_values, y_std_values, 0, alpha=0.3, color='b')

plt.title('Parameter name= {}'.format(feature_names[PARAM_INDEX]))
```

## Visualize gradient variance - which estimator has lower variance?

* Let's assess the gradient variance per of each estimator, by looking at how much gradients vary for each estimators.

```
def per_batch_grads(ys, xs):
  """Computes the gradients of ys[i] wrt xs."""
  grads = []

  for y in tf.unstack(ys, axis=0):
    grads.append(tf.squeeze(tf.gradients(y, xs)[0]))
  return tf.stack(grads, axis=0)
```

```
def compute_grad_var(grads, param_index):
  return np.mean(grads[:, param_index]), np.std(grads[:, param_index])**2
```

```
per_batch_reinforce_grads = per_batch_grads(per_sample_reinforce_loss, learned_vars[0])
per_batch_reinforce_grads.shape
```

    TensorShape([Dimension(100), Dimension(31)])

```
per_batch_reparam_grads = per_batch_grads(per_sample_reparametrization_loss, learned_vars[0])
per_batch_reparam_grads.shape
```

    TensorShape([Dimension(100), Dimension(31)])

```
reinforce_grads = []

sess = tf.Session()

# Initialize all variables
sess.run(tf.initialize_all_variables())

for i in xrange(NUM_ITERATIONS):
  sess.run(reinforce_min_op)

  if i % 10 == 0:
    reinforce_grads.append(sess.run(per_batch_reinforce_grads))
```

```
reparam_grads = []

sess = tf.Session()

# Initialize all variables
sess.run(tf.initialize_all_variables())

for i in xrange(NUM_ITERATIONS):
  sess.run(reparam_min_op)

  if i % 10 == 0:
    reparam_grads.append(sess.run(per_batch_reparam_grads))
```

```
PARAM_INDEX = 19
```

```
reinforce_grad_vars = []
for timestep_grad in reinforce_grads:
  _, reinforce_grad_var = compute_grad_var(timestep_grad, PARAM_INDEX)
  reinforce_grad_vars.append(reinforce_grad_var)
```

```
reparam_grad_vars = []
for timestep_grad in reparam_grads:
  _, reparam_grad_var = compute_grad_var(timestep_grad, PARAM_INDEX)
  reparam_grad_vars.append(reparam_grad_var)
```

```
plt.figure(figsize=(7,5))
plt.plot(reinforce_grad_vars, label='reinforce')
plt.plot(reparam_grad_vars, label='reparametrization')
plt.yscale('log')
plt.xlabel('It')
plt.ylabel('Gradient variance')
plt.legend()
```

### Task: Analysis

* What do you notice about gradient variance as training progresses?
* What do you notice about the variance of the different estimators?
* How do batch size and number of posterior samples affect the variance of the different estimators?
* How does gradient variance affect convergence?
* Which gradient estimator would you use in practice for this problem?

## More

You can find similar experiments with more analysis and more gradient estimators in [this paper](https://arxiv.org/pdf/1906.10652.pdf). See Section 8.3.


```
%matplotlib inline
```

```
from pylab import *
```

```
x = linspace(0, 5, 10)
y = x ** 2
```

```
figure()
plot(x, y, 'r')
xlabel('x')
ylabel('y')
title('title')
show()
```

$$c = \sqrt(a^2 + b^2)$$

```
from __future__ import division
import sympy as sym
from IPython.display import display
from sympy import *
from sympy.interactive import printing

printing.init_printing(use_latex='mathjax')

x, y, z = symbols("x y z")
k, m, n = symbols("k m n", integer=True)
f, g, h = map(Function, 'fgh')
```