# Certification of Robustness using Zonotopes with DeepZ

In this notebook we will demonstrate the usage of certification using zonotopes within ART. With deterministic certification methods such as DeepZ we can have a guarantee if a datapoint could have its class changed under a given bound. This method was originally proposed in: https://papers.nips.cc/paper/2018/file/f2f446980d8e971ef3da97af089481c3-Paper.pdf

The zonotopes abstraction used here is defined by:
\begin{equation}
    \hat{x} = \eta_0 + \sum_{i=1}^{i=N} \eta_i \epsilon_i
\end{equation}

where $\eta_0$ is the central vector, $\epsilon_i$ are noise symbols, $\eta_i$ are coefficients representing deviations around $\eta_0$.

We can illustrate a 2D toy example of this below in which the initial datapoint has two features, with a central vector of [0.25, 0.25] and these features both have noise terms of [0.25, 0.25]. We push this zonotope through the neural network and show it's intermediate shapes:

We can see that the zonotope changes shape as it is passed through the neural network. When passing though a ReLU it gains another term (going from 2 sets of parallel lines to 3). We can then check if the final zonotope crosses any desicion boundaries and say if a point is certified.

Let's see how to use this method in ART!

```python
import torch
import torch.optim as optim
import numpy as np

from torch import nn
from sklearn.utils import shuffle

from art.estimators.certification import deep_z
from art.utils import load_mnist, preprocess, to_categorical

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

```

```python
# We make an example pytorch classifier

class MNISTModel(nn.Module):
    def __init__(self):
        super(MNISTModel, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1,
                               out_channels=32,
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               dilation=(1, 1),
                               padding=(0, 0))
        self.conv2 = nn.Conv2d(in_channels=32,
                               out_channels=32,
                               kernel_size=(4, 4),
                               stride=(2, 2),
                               dilation=(1, 1),
                               padding=(0, 0))
        self.fc1 = nn.Linear(in_features=800,
                             out_features=10)
        self.relu = nn.ReLU()

    def forward(self, x):
        if isinstance(x, np.ndarray):
            x = torch.from_numpy(x).float().to(device)
        x = self.relu(self.conv1(x))
        x = self.relu(self.conv2(x))
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        return x
```

```python
model = MNISTModel()
opt = optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
(x_train, y_train), (x_test, y_test), min_, max_ = load_mnist()

x_test = np.squeeze(x_test)
x_test = np.expand_dims(x_test, axis=1)
y_test = np.argmax(y_test, axis=1)

x_train = np.squeeze(x_train)
x_train = np.expand_dims(x_train, axis=1)
y_train = np.argmax(y_train, axis=1)
```

```python
# train the model normally

def standard_train(model, opt, criterion, x, y, bsize=32, epochs=5):
    num_of_batches = int(len(x) / bsize)
    for epoch in range(epochs):
        x, y = shuffle(x, y)
        loss_list = []
        for bnum in range(num_of_batches):
            x_batch = np.copy(x[bnum * bsize:(bnum + 1) * bsize])
            y_batch = np.copy(y[bnum * bsize:(bnum + 1) * bsize])

            x_batch = torch.from_numpy(x_batch).float().to(device)
            y_batch = torch.from_numpy(y_batch).type(torch.LongTensor).to(device)

            # zero the parameter gradients
            opt.zero_grad()
            outputs = model(x_batch)
            loss = criterion(outputs, y_batch)
            loss_list.append(loss.data)
            loss.backward()
            opt.step()
        print('End of epoch {} loss {}'.format(epoch, np.mean(loss_list)))
    return model

model = standard_train(model=model,
                       opt=opt,
                       criterion=criterion,
                       x=x_train,
                       y=y_train)

```

    End of epoch 0 loss 0.5815373063087463
    End of epoch 1 loss 0.2648811340332031
    End of epoch 2 loss 0.18593080341815948
    End of epoch 3 loss 0.1360677033662796
    End of epoch 4 loss 0.10795646160840988

```python
# lets now get the predicions for the MNIST test set and see how well our model is doing.
with torch.no_grad():
    test_preds = model(torch.from_numpy(x_test).float().to(device))

test_preds = np.argmax(test_preds.cpu().detach().numpy(), axis=1)
print('Test acc: ', np.mean(test_preds == y_test) * 100)
```

    Test acc:  97.46000000000001

```python
# But how robust are these predictions?
# We can now examine this neural network's certified robustness.
# We pass it into PytorchDeepZ. We will get a print out showing which
# neural network layers have been registered. There will also be a
# warning to tell us that PytorchDeepZ currently infers a reshape when
# a neural network goes from using convolutional to dense layers.
# This will cover the majority of use cases, however, if not then the
# certification layers in art.estimators.certification.deepz.deep_z.py
# can be used to directly build a certified model structure.

zonotope_model = deep_z.PytorchDeepZ(model=model,
                                     clip_values=(0, 1),
                                     loss=nn.CrossEntropyLoss(),
                                     input_shape=(1, 28, 28),
                                     nb_classes=10)
```

    registered <class 'torch.nn.modules.conv.Conv2d'>
    registered <class 'torch.nn.modules.activation.ReLU'>
    registered <class 'torch.nn.modules.conv.Conv2d'>
    registered <class 'torch.nn.modules.activation.ReLU'>
    registered <class 'torch.nn.modules.linear.Linear'>
    Inferred reshape on op num 4

    /home/giulio/Documents/Projects/AI2_for_ART/adversarial-robustness-toolbox/art/estimators/certification/deep_z/pytorch.py:90: UserWarning:
    This estimator does not support networks which have dense layers before convolutional. We currently infer a reshape when a neural network goes from convolutional layers to dense layers. If your use case does not fall into this pattern then consider directly building a certifier network with the custom layers found in art.estimators.certification.deepz.deep_z.py

      "\nThis estimator does not support networks which have dense layers before convolutional. "

```python
# Lets now see how robust our model is!
# First we need to define what bound we need to check.
# Here let's check for L infinity robustness with small bound of 0.05

bound = 0.05
num_certified = 0
num_correct = 0

# lets now loop over the data to check its certified robustness:
# we need to consider a single sample at a time as due to memory and compute footprints batching is not supported.
# In this demo we will look at the first 50 samples of the MNIST test data.
original_x = np.copy(x_test)
for i, (sample, pred, label) in enumerate(zip(x_test[:50], test_preds[:50], y_test[:50])):

    # we make the matrix representing the allowable perturbations.
    # we have 28*28 features and each one can be manipulated independently requiring a different row.
    # hence a 784*784 matrix.
    eps_bound = np.eye(784) * bound

    # we then need to adjust the raw data with the eps bounds to take into account
    # the allowable range of 0 - 1 for pixel data.
    # We provide a simple function to do this preprocessing for image data.
    # However if your use case is not supported then a custom pre-processor function will need to be written.
    sample, eps_bound = zonotope_model.pre_process(cent=sample,
                                                   eps=eps_bound)
    sample = np.expand_dims(sample, axis=0)

    # We pass the data sample and the eps bound to the certifier along with the prediction that was made
    # for the datapoint.
    # A boolean is returned signifying if it can have its class changed under the given bound.
    is_certified = zonotope_model.certify(cent=sample,
                                          eps=eps_bound,
                                          prediction=pred)

    if pred == label:
        num_correct +=1
        if is_certified:
            num_certified +=1

    print('Classified Correct {}/{} and also certified {}/{}'.format(num_correct, i+1, num_certified, i+1))

```

    Classified Correct 1/1 and also certified 1/1
    Classified Correct 2/2 and also certified 2/2
    Classified Correct 3/3 and also certified 2/3
    Classified Correct 4/4 and also certified 3/4
    Classified Correct 5/5 and also certified 4/5
    Classified Correct 6/6 and also certified 4/6
    Classified Correct 7/7 and also certified 4/7
    Classified Correct 8/8 and also certified 4/8
    Classified Correct 9/9 and also certified 4/9
    Classified Correct 10/10 and also certified 4/10
    Classified Correct 11/11 and also certified 5/11
    Classified Correct 12/12 and also certified 6/12
    Classified Correct 13/13 and also certified 7/13
    Classified Correct 14/14 and also certified 8/14
    Classified Correct 15/15 and also certified 9/15
    Classified Correct 16/16 and also certified 10/16
    Classified Correct 17/17 and also certified 11/17
    Classified Correct 18/18 and also certified 12/18
    Classified Correct 19/19 and also certified 12/19
    Classified Correct 20/20 and also certified 13/20
    Classified Correct 21/21 and also certified 14/21
    Classified Correct 22/22 and also certified 14/22
    Classified Correct 23/23 and also certified 15/23
    Classified Correct 24/24 and also certified 16/24
    Classified Correct 25/25 and also certified 16/25
    Classified Correct 26/26 and also certified 17/26
    Classified Correct 27/27 and also certified 18/27
    Classified Correct 28/28 and also certified 19/28
    Classified Correct 29/29 and also certified 20/29
    Classified Correct 30/30 and also certified 20/30
    Classified Correct 31/31 and also certified 21/31
    Classified Correct 32/32 and also certified 21/32
    Classified Correct 33/33 and also certified 22/33
    Classified Correct 34/34 and also certified 22/34
    Classified Correct 35/35 and also certified 23/35
    Classified Correct 36/36 and also certified 24/36
    Classified Correct 37/37 and also certified 25/37
    Classified Correct 38/38 and also certified 25/38
    Classified Correct 39/39 and also certified 26/39
    Classified Correct 40/40 and also certified 26/40
    Classified Correct 41/41 and also certified 26/41
    Classified Correct 42/42 and also certified 26/42
    Classified Correct 43/43 and also certified 27/43
    Classified Correct 44/44 and also certified 27/44
    Classified Correct 45/45 and also certified 28/45
    Classified Correct 46/46 and also certified 28/46
    Classified Correct 47/47 and also certified 28/47
    Classified Correct 48/48 and also certified 29/48
    Classified Correct 49/49 and also certified 30/49
    Classified Correct 50/50 and also certified 31/50

```python
# we can then compare this to the empirical PGD performance

from art.estimators.classification import PyTorchClassifier
from art.attacks.evasion.projected_gradient_descent.projected_gradient_descent import ProjectedGradientDescent

classifier = PyTorchClassifier(
    model=model,
    clip_values=(0.0, 1.0),
    loss=criterion,
    optimizer=opt,
    input_shape=(1, 28, 28),
    nb_classes=10,
)

attack = ProjectedGradientDescent(classifier, eps=0.05, eps_step=0.01, verbose=False)
x_train_adv = attack.generate(x_test[:50].astype('float32'))
y_adv_pred = classifier.predict(torch.from_numpy(x_train_adv).float().to(device))
y_adv_pred = np.argmax(y_adv_pred, axis=1)
print('Test acc: ', np.mean(y_adv_pred == y_test[:50]) * 100)
```

    Test acc:  92.0

we can see that the empirical test accuracy is much higher than the certifiable performance. This is because with certifiable techniques we will be providing a lower bound on the performance: there may well be datapoints that the certifier says are unsafe, but in fact cannot have their class changed.


IAGA Summer School 2019

K Index Calculation

```python
# Import notebook dependencies

import os
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

sys.path.append('..')
from src import mag_lib as mag

# Set data paths
ESK_2003_PATH = os.path.abspath('../data/external/ESK_2003/')
K_INDS_PATH = os.path.abspath('../data/external/k_inds/')
```

# 1. Calculating K-indices for a single observatory

The K-index is a global geomagnetic activity index devised by Julius Bartels in 1938 to give a simple measure of the degree of geomagnetic disturbance during each 3-hour (UT) interval. It uses data from ground-based magnetometers to assign a number in the range 0-9 to interval, with K=0 indicating very little geomagnetic activity and K=9 representing an extreme geomagnetic storm. The K-index was introduced at the time of photographic recording, when magnetograms recorded variations in the horizontal geomagnetic field elements declination (D) and horizontal intensity (H), and in the vertical intensity (Z). To derive a K-index an observer would fit, by eye, a quiet-time curve to the records of D and H and measure the range (maximum-minimum) of the deviation of the recording from the curve. The K-index was then assigned according to a conversion table, with the greatest range in D and H 'winning'. The north component (X) may be used instead of H, and the east component (Y) instead of D (X and Y will be used in the examples below). The vertical component Z is not used because it is liable to contamination by local induced currents.

The conversion from range in nanoteslas to index is quasi-logarithmic. The conversion table varies with latitude in an attempt to normalise the K-index distribution for observatories at different latitudes. The table for Eskdalemuir is shown below.

| K | 0 | 1 |2|3|4|5|6|7|8|9|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Lower bound (nT) | 0 | 8 |15|30|60|105|180|300|500|750|

This means that, for instance, K=2 if the measured range is in the interval \[15, 30\] nT.

There was a VERY long debate in IAGA Division V about algorithms that could adequately reproduce the K-indices that an experienced observer would assign. The algorithms and code approved by IAGA are available at the International Service for Geomagnetic Indices: http://isgi.unistra.fr/softwares.php. (Also http://isgi.unistra.fr/what_are_kindices.php)

### Example

In the following cells, we **illustrate** the process. A simple approach is to assume the so-called regular daily variation $S_R$ is made up of 24h, 12h and 8h signals (and, possibly, higher harmonics). A Fourier analysis can be used to investigate this. The functions in the cell below calculate Fourier coefficients from a data sample, and then synthesise a Fourier series using the coefficients.

Some days this simple approach seems to work well, on others it's obviously wrong. Think about another approach you might take.

We will then attempt to calculate K-indices for the day selected by computing the Fourier series up the the number of harmonics selected by subtracting the synthetic harmonic signal from the data, then calculating 3-hr ranges and converting these into the corresponding K-index. The functions to do are also in the following cell.

```python
def fourier(v, nhar):
    npts    = len(v)
    f       = 2.0/npts
    t       = np.linspace(0, npts, npts, endpoint=False)*2*np.pi/npts
    vmn     = np.mean(v)
    v       = v - vmn
    cofs    = [0]*(nhar+1)
    cofs[0] = (vmn,0)
    for i in range(1,nhar+1):
        c, s    = np.cos(i*t), np.sin(i*t)
        cofs[i] = (np.dot(v,c)*f, np.dot(v,s)*f)
    return (cofs)

def fourier_synth(cofs, npts):
    nt  = len(cofs)
    syn = np.zeros(npts)
    t  = np.linspace(0, npts, npts, endpoint=False)*2*np.pi/npts
    for n in range(1, nt):
        for j in range(npts):
            syn[j] += cofs[n][0]*np.cos(n*t[j]) + cofs[n][1]*np.sin(n*t[j])
    return (syn)

def K_plot(d, synd, day, E):
    fig, ax = plt.subplots(2, 1, figsize=(15, 10))
    t = np.linspace(0, 1440, 1440, endpoint= False)/60
    ax[0].plot(t, d)
    ax[0].plot(t, synd, color='red')
    ax[0].set_xlabel('UT (h)', fontsize=14)
    ax[0].set_ylabel(f'{E} (nT)', fontsize=14)
    ax[0].xaxis.set_ticks(np.arange(0, 27, 3))
    ax[0].set_title(f'K-index estimation: {OBSY} {day}, ({E}-range: {np.ptp(d):.1f}nT)', fontsize=20)
    ax[1].plot(t, d-synd)
    ax[1].set_xlabel('UT (h)', fontsize=14)
    ax[1].set_ylabel(f'{E} residuals (nT)', fontsize=14)
    ax[1].xaxis.set_ticks(np.arange(0, 27, 3))
    fig.tight_layout()

def K_calc(d, synd, Kb):
    tmp = np.ptp((d-synd).reshape(8,180), axis=1)
    return(list(np.digitize(tmp, bins=list(Kb.values()), right=False)-1))

def K_load(obscode, year, k_inds_path=K_INDS_PATH):
    kfile  = year + '.' + obscode
    filepath = os.path.join(k_inds_path, obscode, kfile)
    df = pd.read_csv(filepath, skiprows=0, header=None, delim_whitespace=True,
                     parse_dates=[[2,1,0]], index_col=0)
    df = df.drop(3, axis=1)
    df.index.name='Date'
    return(df)
```

First, load in (X, Y, Z) one-minute data from Eskdalemuir for 2003 into a pandas dataframe.

```python
OBSY = 'ESK'
year ='2003'
npts = 1440 # Number of minutes in a day
df = mag.load_year('esk', 2003, ESK_2003_PATH)
df.columns = [col.strip(OBSY) for col in df.columns]
df = df.drop(['DOY', 'F'], axis=1)
ann_means = df[[f'{i}' for i in 'XYZ']].resample('1Y').mean()
df.head()
```

### >> USER INPUT HERE: Choose a day (2003-mm-dd) and the number of harmonics to calculate
#### Return here to investigate different days (note that only the file for 2003 is included in the data directory for this example).

```python
day  = '2003-01-01'
nhar = 3
```

First convert the D variations to units of nT using the annual mean value of H. Then calculate the daily means of D and H.

```python
xmn  = df[day].X.mean()
ymn  = df[day].Y.mean()
xmn, ymn
```

Now analyse the X data.

```python
x     = df[day].X.values - xmn
xcofs = fourier(x, nhar)
synx  = fourier_synth(xcofs, npts)
K_plot(x, synx, day, 'X')
```

Repeat for Y.

```python
y     = df[day].Y.values - ymn
ycofs = fourier(y, nhar)
syny  = fourier_synth(ycofs, npts)
K_plot(y, syny, day, 'Y')
```

Calculate the K-indices individually for the X and Y data, and also the final K index (the largest of the X and Y indices).

```python
K_lb   = [0, 8, 15, 30, 60, 105, 180, 300, 500, 750]
K_dict = dict(zip([f'{OBSY}_K{i}' for i in range (10)], K_lb))
Kx = K_calc(x, synx, K_dict)
Ky = K_calc(y, syny, K_dict)
Kf =[max(Kx[i], Ky[i]) for i in range(8)]
```

Load the official 2003 observatory values for comparison and display the first few days.

```python
dfk   = K_load(OBSY.lower(), year)
dfk.columns = ['00','03','06','09','12','15','18','21']
K_obs = list(dfk.loc[day])
dfk.head()
```

Create a data frame containing all K values and print them out to compare.

```python
K = pd.DataFrame({'K_x': Kx, 'K_y': Ky, 'K_final': Kf, 'K_obs': K_obs}).transpose()
K.columns = ['0','3','6','9','12','15','18','21']

print("Comparison of our K-indices with the official values")
K
```

Plot histograms of the observed K-indices for each 3-hour period during 2003 in separate subplots

```python
dfk.hist(figsize=(12,12), bins=[i for i in range(11)], sharey=True, align='left', rwidth=0.8)
plt.suptitle('ESK 2003: Distribution of K-indices for each 3-hour interval')
```

plotted side by side

```python
plt.figure(figsize=(7,7))
plt.hist(dfk.values, bins=[i for i in range(11)], align='left')
plt.legend(dfk.columns)
plt.ylabel('Number of 3-hour intervals')
plt.xlabel('K')
```

and stacked to give the distribution for the whole year

```python
plt.figure(figsize=(7,7))
plt.hist(dfk.values, bins=[i for i in range(11)], stacked=True, align='left', rwidth=0.8)
plt.legend(dfk.columns)
plt.ylabel('Number of 3-hour intervals')
plt.xlabel('K')
```

We also compute a daily sum of the K-indices for the 2003 file, and list days with high and low summed values. Note that this summation is not really appropriate because the K-index is quasi-logarithmic, however, this is a common simple measure of quiet and disturbed days. (These might be interesting days for you to look at.)

```python
dfk['Ksum'] = dfk.sum(axis=1)
Ksort = dfk.sort_values('Ksum')
print('Quiet days: \n\n', Ksort.head(10), '\n\n')
print('Disturbed days: \n\n', Ksort.tail(10))
```

# 2. Note on the Fast Fourier Transform

In the examples above we computed Fourier coefficients in the 'traditional' way, so that if $F(t)$ is a Fourier series representation of $f(t)$, then,

$$
\begin{align}
F(t) &= A_o+\sum_{n=1}^N A_n \cos\left(\frac{2\pi nt}{T}\right)+B_n \sin\left(\frac{2\pi nt}{T}\right)
\end{align}
$$

where $T$ is the fundamental period of $F(t)$. The $A_n$ and $B_n$ are estimated by

$$
\begin{align}
A_o&=\frac{1}{T}\int_0^T f(t) dt\\
A_n&=\frac{2}{T}\int_0^T f(t)\cos\left(\frac{2\pi nt}{T}\right) dt\\
B_n&=\frac{2}{T}\int_0^T f(t)\sin\left(\frac{2\pi nt}{T}\right) dt
\end{align}
$$

With $N$ samples of digital data, the integral for $A_n$ may be replaced by the summation

$$
\begin{align}
A_n&=\frac{2}{T}\sum_{j=0}^{N-1} f_j\cos\left(\frac{2\pi nj\Delta t}{T}\right) \Delta t\\
&=\frac{2}{N}\sum_{j=0}^{N-1} f_j\cos\left(\frac{2\pi nj}{N}\right)
\end{align}
$$

where the sampling interval $\Delta t$ is given by $T = N \Delta t$ and $f_j = f(j \Delta t)$. A similar expression applies for the $B_n$, and these are the coefficients returned by the function _fourier_ above.

The fast Fourier transform (FFT) offers a computationally efficient means of finding the Fourier coefficients. The conventions for the FFT and its inverse (IFFT) vary from package to package. In the _scipy.fftpack_ package, the FFT of a sequence $x_n$ of length $N$ is defined as

$$
\begin{align}
y_k&=\sum_{n=0}^{N-1} x_n\exp\left(-\frac{2\pi i\thinspace kn}{N}\right)\\
&=\sum_{n=0}^{N-1} x_n\left(\cos\left(\frac{2\pi \thinspace kn}{N}\right)-i\sin\left(\frac{2\pi \thinspace kn}{N}\right)\right)
\end{align}
$$

with the inverse defined as,

$$
\begin{align}
x_n&=\frac{1}{N}\sum_{k=0}^{N-1} y_k\exp\left(\frac{2\pi i\thinspace kn}{N}\right)\\
\end{align}
$$

The _scipy_ documentation is a little inconsistent here because it explains the order of the $y_n$ as being $y_1,y_2, \dots y_{N/2-1}$ as corresponding to increasing positive frequency and $y_{N/2}, y_{N/2+1}, \dots y_{N-1}$ as ordered by decreasing negative frequency, for $N$ even. (See: https://docs.scipy.org/doc/scipy/reference/tutorial/fftpack.html.)

The interpretation is that if $y_k=a_k+ib_k$ then will have (for $N$ even), $y_{N-k} = a_k-ib_k$ and so

$$
\begin{align}
a_k&=\frac{1}{2}\text{Re}\left(y_k+y_{N-k}\right)\\
b_k&=\frac{1}{2}\text{Im}\left(y_k-y_{N-k}\right)
\end{align}
$$

and so we expect the relationship to the digitised Fourier series coefficients returned by the function _fourier_ defined above to be,

$$
\begin{align}
A_k&=\phantom{-}\frac{1}{N}\text{Re}\left(a_k+a_{N-k}\right)\\
B_k&=-\frac{1}{N}\text{Im}\left(b_k-b_{N-k}\right)
\end{align}
$$

```python
from scipy.fftpack import fft

# Compute the fft of the x data
xfft = fft(x)

# Compare results for the 24-hour component
k    = 1
print('Fourier coefficients: \n', f'A1 = {xcofs[1][0]} \n', f'B1 = {xcofs[1][1]} \n')
print('scipy FFT outputs: \n', f'a1 = {np.real(xfft[k]+xfft[npts-k])/npts} \n', \
      f'b1 = {-np.imag(xfft[k]-xfft[npts-k])/npts} \n')
```

### References

Menvielle, M. et al. (1995) ‘Computer production of K indices: review and comparison of methods’, Geophysical Journal International. Oxford University Press, 123(3), pp. 866–886. doi: 10.1111/j.1365-246X.1995.tb06895.x.


# Hardening Plasticity

## Overview

After the elastic limit of a material is exceeded and the material yields, further increases in load are usually required for plastic flow to continue.  This phenomenon is known as hardening and is broadly classified as either work or strain-hardening.  Hardening theories of plasticity describe the evolution of the strength through time.  This notebook

- presents an introduction to hardening plasticity,
- implements a hardening "$J_2$" plasticity model in Matmodlab for the case of linear isotropic strain hardening,
- verifies the $J_2$ model against analytic solutions.

## See Also

- [User Defined Materials](UserMaterial.ipynb)
- [Linear Elastic Material](LinearElastic.ipynb)
- [Nonhardening Plasticity](NonhardeningJ2Plasticity.ipynb)

## Contents

1. Hardening Plasticity
2. $J_2$ Plasticity
2. Standard Model Implementation

## Hardening Plasticity

### Overview

Similar to nonhardening theories of plasticity, the hardening theory of plasticity presumes the existence of an elastic limit defined by a yield surface

$$
f\left(\pmb{\sigma}, Y\right) = 0
$$

where $f$ is the yield function.  Unlike the nonhardening theory, the yield function depends not only on the mechanical stress $\pmb{\sigma}$ but also the (nonconstant) yield strength $Y$.

The rate of mechanical stress is given by

$$
\dot{\pmb{\sigma}} = \mathbb{C}{:}\dot{\pmb{\epsilon}}^{\rm e}
$$

where $\mathbb{C}$ is the elastic stiffness and $\dot{\pmb{\epsilon}}^{\rm e}$ the rate of elastic strain.  Presuming that the rate of strain is the sum of elastic and plastic parts, the mechanical response is defined by

$$
\dot{\pmb{\sigma}} = \mathbb{C}{:}\left(\dot{\pmb{\epsilon}} - \dot{\pmb{\epsilon}}^{\rm p}\right)
$$

where $\dot{\pmb{\epsilon}}^{\rm p}$ is the rate of plastic strain.  Replacing $\dot{\pmb{\epsilon}}^{\rm p}$ with $\dot{\lambda}\pmb{m}$, $\dot{\lambda}$ being the magnitude of $\dot{\pmb{\epsilon}}^{\rm p}$ and $\pmb{m}$ its direction, the mechanical response of the material is

$$
\dot{\pmb{\sigma}} = \mathbb{C}{:}\left(\dot{\pmb{\epsilon}} - \dot{\lambda}\pmb{m}\right)
$$

The solution to the plasticity problem is reduced to determining $\dot{\lambda}$ such that $f\left(\pmb{\sigma}(t), Y(t)\right)\leq 0 \ \forall t>0$

### Solution Process

Given the current state of stress $\pmb{\sigma}_n$, the solution to the plasticity problem begins with the hypothesis that the entire strain increment is elastic:

$$
\pmb{\sigma}_{\rm new} \stackrel{?}{=} \pmb{\sigma}_{\rm old} + \mathbb{C}{:}\dot{\pmb{\epsilon}}dt = \pmb{\sigma}^{\rm test}
$$

where the descriptor "test" is used to signal the fact that at this point $\pmb{\sigma}^{\rm test}$ is merely a hypothesized solution.  The hypothesis is validated if $\pmb{\sigma}^{\rm test}$ satisfies the yield condition

$$f\left(\pmb{\sigma}^{\rm test}, Y^{\rm test}\right)\leq 0$$

so that $\pmb{\sigma}_{\rm new}=\pmb{\sigma}^{\rm test}$.

If instead the hypothesis is *falsefied*, i.e., the predicted test stress falls outside of the yield surface defined by $f=0$, the plasticity problem,

$$\begin{align}
\pmb{\sigma}_{\rm new} = \pmb{\sigma}_{\rm old} + \mathbb{C}{:}\left(\dot{\pmb{\epsilon}} - \dot{\lambda}\pmb{m}\right)dt &= \pmb{\sigma}^{\rm trial} - \dot{\lambda}\pmb{A}dt\\
f\left(\pmb{\sigma}^{\rm trial} - \dot{\lambda}\pmb{A}dt, Y\right) &= 0
\end{align}$$

where $\pmb{A}=\mathbb{C}{:}\pmb{m}$, is solved.  $\dot{\pmb{\sigma}}^{\rm trial}=\mathbb{C}{:}\dot{\pmb{\epsilon}}$ is distinguished from $\dot{\pmb{\sigma}}^{\rm test}$ in that for stress driven problems $\dot{\pmb{\sigma}}^{\rm trial}$ is not necessarily known because the strain rates $\dot{\epsilon}$ are not known.

The unknown scalar $\dot{\lambda}$ is determined by noting the following observation: if $f\left(\pmb{\sigma}_{\rm old}, Y_{\rm old}\right)=0$ and, after continued loading, $f\left(\pmb{\sigma}_{\rm new}, Y_{\rm new}\right)=0$, the rate of change of $f$ itself must be zero.  Thus, by the chain rule,

$$
\dot{f}{\left(\pmb{\sigma}, Y\right)}
    =\frac{df}{d\pmb{\sigma}}{:}\dot{\pmb{\sigma}}
     +\frac{df}{dY}\dot{Y} = 0
$$

During elastic loading, the yield strength does not change.  Therefore, we presume that the rate of $Y$ is of the form

$$
\dot{Y} = \dot{\lambda}h_{Y}
$$

where $\dot{\lambda}$ is the rate of plastic strain and $h_{Y}$ the modulus of $Y$.  Since $Y$ is tied to the internal state of the material, it is regarded as a "solution-dependent variable" (SDV) and $h_Y$ a SDV modulus.  SDV moduli must be determined from experiment or microphysical considerations.

Substituting the expression for $\dot{Y}$ in to the consistency condition gives

$$
\frac{df}{d\pmb{\sigma}}{:}\left(\mathbb{C}{:}\dot{\epsilon}-\dot{\lambda}\pmb{A}\right) + \frac{df}{dY}\dot{\lambda}h_Y = 0
$$

Letting

$$
\pmb{n} = \frac{df}{d\pmb{\sigma}}\Big/\Big\lVert\frac{df}{d\pmb{\sigma}}\Big\rVert
$$

the preceding equation can be solved $\dot{\lambda}$, giving

$$
\dot{\lambda}
   = \frac{\pmb{n}{:}\mathbb{C}{:}\dot{\epsilon}}{\pmb{n}{:}\pmb{A}+H}
$$

where

$$
H = -\frac{df}{dY}\Big/\Big\lVert\frac{df}{d\pmb{\sigma}}\Big\rVert h_Y
$$

Substituting $\dot{\lambda}$ in to the expression for stress rate gives

$$\begin{align}
\dot{\pmb{\sigma}}
   &= \mathbb{C}{:}\dot{\pmb{\epsilon}} - \frac{\pmb{n}{:}\mathbb{C}{:}\dot{\epsilon}}{\pmb{n}{:}\pmb{A}+H}\pmb{A}\\
   &= \mathbb{C}{:}\dot{\pmb{\epsilon}} - \frac{1}{\pmb{n}{:}\pmb{A}+H}\pmb{Q}\pmb{A}{:}\dot{\pmb{\epsilon}}\\
   &= \mathbb{D}{:}\dot{\pmb{\epsilon}}
\end{align}$$

where

$$
\pmb{Q} = \mathbb{C}{:}\pmb{n}
$$
and
$$
\mathbb{D} = \mathbb{C} - \frac{1}{\pmb{n}{:}\pmb{A}+H}\pmb{Q}\pmb{A}
$$

The stress rate is then integrated through time to determine $\pmb{\sigma}$

### Integration Procedure

Unlike the nonhardening case, the expression for the stress rate

$$
\dot{\pmb{\sigma}}
  = \dot{\pmb{\sigma}}^{\rm trial}
  - \frac{\pmb{n}{:}\dot{\pmb{\sigma}}^{\rm trial}}{\pmb{n}{:}\pmb{A}+H}\pmb{A}
$$

*is not* a projection of the trial stress rate, but it is still true that

$$\pmb{\sigma}_{\rm new} = \pmb{\sigma}^{\rm trial} - \Gamma\pmb{A}$$

$\Gamma$ is determined by satisfying the now *evolving* yield condition.  In other words, $\Gamma$ is the solution to

$$f\left(\pmb{\sigma}^{\rm trial} - \Gamma\pmb{A}, Y(\Gamma)\right)=0$$

The unknown $\Gamma$ is found such that $f\left(\pmb{\sigma}(\Gamma), Y(\Gamma)\right)=0$.  The solution can be found by solving the preceding equation iteratively by applying Newton's method so that

$$
\Gamma^{i+1} = \Gamma^i
- \frac{f\left(\pmb{\sigma}(\Gamma^{n}), Y(\Gamma)\right)}
       {\frac{df}{d\pmb{\sigma}}{:}\frac{d\pmb{\sigma}}{d\Gamma} + \frac{df}{dY}\frac{dY}{d\Gamma}}
= \Gamma^i
+ \frac{g\left(\pmb{\sigma}(\Gamma^{n}), Y(\Gamma)\right)}
       {\pmb{n}{:}\pmb{A}-\frac{df}{dY}\frac{dY}{d\Gamma}}
$$

where $g = f\Big/\lVert df/d\pmb{\sigma}\rVert$.

When $\Gamma^{i+1}-\Gamma^i<\epsilon$, where $\epsilon$ is a small number, the iterations are complete and the updated stress can be determined.

Note that the scalar $\Gamma$ is also equal to $\Gamma=\dot{\lambda}dt$, but since $\dot{\lambda}=0$ for elastic loading, $\Gamma=\dot{\lambda}dt^p$, where $dt^p$ is the plastic part of the time step.  This gives $\Gamma$ the following physical interpretation: it is the magnigute of the total plastic strain increment.

## $J_2$ Plasticity

The equations developed thus far are general in the sense that they apply to any material that can be modeled by hardening plasticity.  The equations are now specialized to the case of isotropic hypoelasticity and $J_2$ plasticity by defining

$$\begin{align}
\dot{\pmb{\sigma}} &= 3K\,\mathrm{iso}\dot{\pmb{\epsilon}}^{\rm e} + 2G\,\mathrm{dev}\dot{\pmb{\epsilon}}^{\rm e} \\
f\left(\pmb{\sigma}\right) &= \sqrt{J_2} - \frac{1}{\sqrt{3}}Y\left(\epsilon^p_{eq}\right)
\end{align}
$$

where $J_2$ is the second invariant of the stress deviator, defined as

$$J_2 = \frac{1}{2}\pmb{s}{:}\pmb{s}, \quad \pmb{s} = \pmb{\sigma} - \frac{1}{3}\mathrm{tr}\left(\pmb{\sigma}\right)\pmb{I}$$

and $Y\left(\epsilon^p_{eq}\right)$ is the plastic strain dependent yield strength in tension.

Additionally, we adopt the assumption of an **associative flow rule** wherein $\pmb{m}=\pmb{n}$.  Accordingly,

$$\begin{align}
\frac{df}{d\pmb{\sigma}}&=\frac{1}{2\sqrt{J_2}}\pmb{s}, &\pmb{n}=\frac{1}{\sqrt{2J_2}}\pmb{s} \\
\pmb{A}&=\frac{2G}{\sqrt{2J_2}}\pmb{s}, &\pmb{Q}=\frac{2G}{\sqrt{2J_2}}\pmb{s}
\end{align}$$

### Linear Hardening

Returning now to the definition of $Y$.  Let

$$Y = Y_0 + Y_1 \epsilon^p_{eq}$$

where $Y_0$ is the initial strength and $Y_1$ is a fitting parameter.  Taking the rate of $Y$ allows determining the SDV modulus $h_Y$:

$$\dot{Y} = Y_1\dot{\epsilon}^p_{eq} = h_Y\dot{\lambda}, \quad h_Y = \sqrt{\frac{2}{3}}Y_1$$

#### Required Parameters

The model as described above requires at minimum 4 parameters: 2 independent elastic moduli and the yield strength parameters $Y_0$ and $Y_1$.

#### Solution Dependent Variables

Evaluation of $Y$ requires storing and tracking the equivalent plastic strain $\epsilon_{eq}^p$

### Power-Law Hardening

A phenomenological power-law model presumes that

$$Y = Y_0 + Y_1 \left(\epsilon^p_{eq}\right)^m$$

where $Y_0$ is the initial strength and $K_1$ is a fitting parameter.  Taking the rate of $Y$ allows determining the SDV modulus $h_Y$:

$$\dot{Y} = h_Y\dot{\lambda}, \quad h_Y = \sqrt{\frac{2}{3}}mY_1\left[\frac{Y-Y_0}{Y_1}\right]^{\frac{m-1}{m}}$$

#### Required Parameters

The model as described above requires at minimum 4 parameters: 2 independent elastic moduli and the yield strength parameters $Y_0$, $Y_1$, and $m$.

#### Solution Dependent Variables

Evaluation of $Y$ requires storing and tracking the equivalent plastic strain $\epsilon_{eq}^p$

### Work Hardening

A phenomenological linear work hardening model presumes that

$$Y = Y_0 + Y_1 W_P, \quad W_P = \int_0^t \pmb{\sigma}{:}\dot{\pmb{\epsilon}}^p dt$$

where $Y_0$ is the initial strength and $K_1$ is a fitting parameter.  Taking the rate of $Y$ allows determining the SDV modulus $h_Y$:

$$\dot{Y} = h_Y\dot{\lambda}, \quad h_Y = mY_1\left[\frac{Y-Y_0}{Y_1}\right]^{\frac{m-1}{m}}\sqrt{\pmb{m}{:}\pmb{m}}$$

#### Required Parameters

The model as described above requires at minimum 4 parameters: 2 independent elastic moduli and the yield strength parameters $Y_0$, $Y_1$, and $m$.

#### Solution Dependent Variables

Evaluation of $Y$ requires storing and tracking the equivalent plastic strain $\epsilon_{eq}^p$

## Model Implementation

The plastic material described above is implemented as `HardeningPlasticMaterial` in `matmodlab2/materials/plastic3.py`.  `HardeningPlasticMaterial` is implemented as a subclass of the `matmodlab2.core.material.Material` class.  `HardeningPlasticMaterial` defines

- `name`: *class attribute*

   Used for referencing the material model in the `MaterialPointSimulator`.

- `eval`: *instance method*

   Updates the material stress, stiffness (optional), and state dependent variables to the end of the time increment.

In the model, in addition to some standard functions imported from `Numpy`, several helper functions are imported from various locations in Matmodlab:

- `matmodlab.core.tensor`

   - `isotropic_part`, `deviatoric_part`: computes the isotropic and deviatoric parts of a second-order symmetric tensor stored as an array of length 6
   - `magnitude`: computes the magnitude $\left(\lVert x_{ij} \rVert=\sqrt{x_{ij}x_{ij}}\right)$ of a second-order symmetric tensor stored as an array of length 6
   - `double_dot`: computes the double dot product of second-order symmetric tensors stored as an array of length 6
   - `VOIGT`: mulitplier for converting tensor strain components to engineering strain components

### Model Source

The source of the file can be viewed by executing the following cell.

```python
%pycat ../matmodlab2/materials/plastic3.py
```

```python
%pylab inline
from bokeh.io import output_notebook
from bokeh.plotting import *
from matmodlab2 import *
from numpy import *
from plotting_helpers import create_figure
output_notebook()
```

    Populating the interactive namespace from numpy and matplotlib
    Setting up the Matmodlab notebook environment

    Loading BokehJS ...

```python
import pandas as pd
pd.set_option('precision', 20)
```

### Verification

Exercising the elastic model through a path of uniaxial stress should result in the slope of axial stress vs. axial strain being equal to the input parameter `E` for the elastic portion.  The maximum stress should be equal to the input parameter `Y`.

**Note:** the input parameters to a standard material are given as a dictionary of `name:value` pairs for each paramter.  Parameters not specified are initialized to a value of zero.

```python
mps1 = MaterialPointSimulator('uplastic-std')
p = {'E': 10e6, 'Nu': .333, 'Y0': 40e3}
mps1.material = HardeningPlasticMaterial(**p)
mps1.run_step('ESS', (.004, 0, 0), frames=50)

i = where((mps1.df['E.XX'] > 0.) & (mps1.df['E.XX'] < .005))
E = mps1.df['S.XX'].iloc[i] / mps1.df['E.XX'].iloc[i]
assert allclose(E.iloc[0], 10e6, atol=1e-3, rtol=1e-3)
assert amax(mps1.df['S.XX']) - 40e3 < 1e-6
#plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
mps1.plot('E.XX', 'S.XX')

print(mps1.df[['F.XX','F.YY','F.ZZ','S.XX']])
```

```python
mps1 = MaterialPointSimulator('uplastic-std')
p = {'K': 15, 'G': 10, 'Y0': 1}
mps1.material = VonMisesMaterial(**p)
mps1.run_step('EEE', (.075, 0, 0), frames=50)
mps1.plot('E.XX', 'S.XX')

print(mps1.df[['F.XX','S.XX','S.YY']])
```

```python
1.020201  0.990711  0.990711
```

#### Linear Hardening

```python
mps1 = MaterialPointSimulator('uplastic-std')
p = {'E': 10e6, 'Nu': .333, 'Y0': 40e3, 'Y1': 2e6}
mps1.material = HardeningPlasticMaterial(**p)
mps1.run_step('ESS', (.02, 0, 0), frames=50)

plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
plot.line(mps1.df['E.XX'], mps1.df['S.XX'])
show(plot);
```

#### Nonlinear Hardening

```python
mps1 = MaterialPointSimulator('uplastic-std')
p = {'E': 10e6, 'Nu': .333, 'Y0': 40e3, 'Y1': 2e4, 'm': .4}
mps1.material = HardeningPlasticMaterial(**p)
mps1.run_step('ESS', (.02, 0, 0), frames=50)
plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
plot.line(mps1.df['E.XX'], mps1.df['S.XX'])
show(plot);
```

### Validation

Let us now fit experimental data

```python
from pandas import read_excel
from scipy.optimize import fmin
from scipy.stats import linregress
```

```python
L0 = 2.25
D0 = 0.525
A0 = pi * (D0 / 2.) ** 2
df = read_excel('aldat.xls', skiprows=9)

# subtract initial displacement and compute stress/strain
df['Crosshead displacement'] -= df['Crosshead displacement'].iloc[0]
df['Stress'] = df['Load'] / A0
df['Strain'] = log((L0 + df['Crosshead displacement']) / L0)
df['dStress'] = ediff1d(df['Stress'], to_begin=0)
df['dStrain'] = ediff1d(df['Strain'], to_begin=0)
```

#### Determine elastic response

```python
plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
plot.line(df['Strain'], df['Stress'])
show(plot);
```

```python
df = df[df['Strain'] >= 1.5e-3]
df_e = df[(df['Strain'] < .017) & (df['Strain'] > .004)].copy()
df_e['Strain'] -= df_e['Strain'].iloc[0]
plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
plot.line(df_e['Strain'], df_e['Stress'])
show(plot)
```

```python
E = polyfit(df_e['Strain'], df_e['Stress'], 1)[0]
p = {'E': E}
plot = figure()
plot.circle(df['Strain'], df['Stress'])
ee = linspace(0, .015)
plot.line(ee, E * ee, color='red')
show(plot)
```

```python
df['dEE'] = df['dStress'] / E
df['dEP'] = df['dStrain'] - df['dEE']
df['EE'] = cumsum(df['dEE'])
df['EP'] = cumsum(df['dEP'])
df['EPEQ'] = sqrt(2./3.) * df['EP']
dwp = df['dStress'] * df['dEP']
df['WP'] = cumsum(dwp)
i = df['Stress'].argmax()
df_p = df[(df['Strain'] >= .02) & (df['Strain'] < df['Strain'][i])]
```

```python
#print df.loc(i)
#df_p = df[(df['Strain'] >= .03) & (df['Strain'] < .15)]

M, N = 350, 350
p1 = figure(x_axis_label='EPEQ', y_axis_label='Y',
            width=M, plot_height=N,
            title='Linear Hardening')
p1.circle(df_p['EPEQ'], df_p['Stress'])

p2 = figure(x_axis_label='Log[EPEQ]', y_axis_label='Log[Y]',
            width=M, plot_height=N,
            title='Power Law Hardening')
p2.circle(log(df_p['EPEQ']), log(df_p['Stress']))

p3 = figure(x_axis_label='EP', y_axis_label='Y',
            width=M, plot_height=N,
            title='Work Hardening')
p3.circle(df_p['WP'], df_p['Stress'])

p4 = figure(x_axis_label='E', y_axis_label='EE, EP',
            width=M, plot_height=N)
p4.circle(df_p['EE'], df_p['Strain'], legend='EE')
p4.circle(df_p['EP'], df_p['Strain'], color='red', legend='EP')

gp = gridplot([[p1, p2], [p3, p4]])
show(gp)
```

### Power law model fit

```python
p['Y0'] = 38e3
x = polyfit(log(df_p['EPEQ']), log(df_p['Stress']-p['Y0']) , 1)
p['m'] = x[0]
p['Y1'] = exp(x[1])
p, x
```

    ({'E': 2718471.6546587194,
      'Y0': 38000.0,
      'Y1': 17591.472469736145,
      'm': 0.43268943216746447},
     array([ 0.43268943,  9.77516954]))

```python
mps = MaterialPointSimulator('uplastic-std')
p['Nu'] = .333
mps.material = HardeningPlasticMaterial(**p)
mps.run_step('ESS', (.08, 0, 0), frames=100)

plot = create_figure(bokeh=True, x_axis_label='Strain', y_axis_label='Stress')
plot.line(mps.df['E.XX'], mps.df['S.XX'], color='red', legend='Model')
plot.circle(df['Strain'], df['Stress'], legend='Experiment')
show(plot)
```


Installation Instructions

Run in tf35.

If you run in python 27 you will see artifacts like vertical lines at the end of Latek equations

The contribution of this work is

to put into 1 place the derivation of backpropagation for linear and logistic neurons with 0 and 1 hidden layer

add torch verificaiton of backprop which is not documented on internet.

Intro to Backprop

Backpropagation in supervised neural nets is used to set the weights given a set of training data. The most common use
case for NN are in using feedforward networks with hidden layers to model multiple functions. Hidden layers can have a
width or number of neurons and depth.

Backpropagation is the chain rule applied to the above graph.
$y=g(f(x))$

$\frac{dy}{dx} = \frac{dg}{dx} = \frac{dg}{df} \; \frac{df}{dx}$

Linear Weight Backpropagation

Note: the definition of a neuron requires a set of weights and an activation function. To keep things
    as simple as possible there is no activation
    function in this example. This is backpropagation applied to a linear multiplication

The linear operation $w_ix_i+b$ converts the b in $z=\sum\limits_{i=n}^Nw_ix_i+b$ into a weight term by adding a 1 into the
input of the neuron. This converts b into $w_0$

The notation for a linear neuron can have
subscripts starting from 1 or 0 and may/maynot explicitly
mention the existence of a b term: $z=\sum\limits_{i=1}^N w_ix_i + b = \sum\limits_{i=0}^N w_ix_i$

To find the weights of the linear neuron given a set of training/test data we apply the delta rule(Hinton Coursera)
which is a 2 step process of 1) defining a square error loss and 2) taking a derivative and using the delta rule to
determine an iterative procedure as a substitute for an exact analytical solution. The delta rule shows how much
to change the weights as we apply each training sample. We simplify this and call the operation without an
activation function a linear operation.

define the square error loss

Note: we do this derivation for squared error loss but many times we use a different loss function.
Different loss functions would require modification in this step.
$E = \frac{1}{2}\sum\limits_{n in training}(t_n-y_n)^2$

verbatim Hinton Courser lecture3.pdf

or the total error over the entire training set of size N is:
$E_{total} = \frac{1}{2}\sum\limits_{i=0}^N (t_i -y_i)^2$

We can also define the error for a single training sample:
$E_i = \frac{1}{2} (t_i-y_i)^2$

Take the derivative of the total sum error loss over the entire training set

We can take the derivative of either the total loss or each step. It does not matter as long
as we are consistent with the notation and clear which option we take.

Chain rule (copied verbatim from Hinton Coursera lecture3.pdf)

$\frac{\partial E}{\partial w_i}=\sum\limits_{n} \frac{\partial E}{\partial y^n} \cdot \frac{\partial y^n}{\partial w_i}$

We can calculate the 2 above parts separately

$\frac{\partial E}{\partial y_n}=\frac{1}{2}(2)(t_n-y_n)(-1) = -(t_n-y_n)$

The above is Hinton notation. Many use the opposite with the training samples $t_n$ reversed from the
training set labels $y_n$.
Will not matter can adjust when defining delta rule

$\frac{\partial E}{\partial y_n} = -(t_n-y_n)$

$\frac{\partial y}{\partial w_i} = x_i$

$\frac{\partial E}{\partial w_i}= -x_i(t_n-y_n)$

The delta rule takes the above and multiplies a learning rate, epsilon to the expression:

$\delta w_i = w_i + \epsilon x_i(t_n-y_n)$

The above example was shown without an activation function. To be a neuron we need
a binary threshold activation function or something which mimics a binary threshold function like a
logistic. The Udactiy hw assignment adds a linear activation function $y=f(x)$ to do regression. Technically
this is not a neuron because a neuron is modeled after the brain neuron which
requires a binary threshold operator. A linear function $y=f(x)$ is not a threshold operator.
Adding a linear activation function for a single weight and input look like this:

This is not the Udacity hw1 neuron. This is a simplified example with multiple inputs and input weights,
1 hidden node, oue hidden node output and one output node y. This reduces the summations to
only the ones at the input node to keep things simple.

This adds an additional set of weights $w_h$

$E = \frac{1}{2}(t_n-y_n)^2$

$y=f(x)$

This is crappy Notation practice from Udacity. They should leave the y=f(x) equation out as it is confusing and
obvious. It does not need to be written down because it is confusing why it is a constructive statement
and is not needed. It is not part of the equations used in the chain rule.

$y=x$

$x=h(z) \cdot w_{h}$

$z=\sum\limits_{i=0}^N w_{i} x_{i}$

$\frac{\partial E}{\partial w_h} = \frac{\partial E}{\partial y} \cdot \frac{\partial y}{\partial x} \cdot \frac{\partial x}{\partial w_h}=-(t_n-y_n)(1)h(z) $

$\frac{\partial E}{\partial w_i} = \frac{\partial E}{\partial y} \cdot \frac{\partial y}{\partial x} \cdot \frac{\partial x}{\partial h} \cdot \frac{\partial h}{\partial w_i}$

$\frac{\partial y}{\partial x } = 1 $

$\frac{\partial x}{\partial h} = w_h $

$\frac{\partial h}{\partial w_i}= x_i$

$\frac{\partial E}{\partial w_{i}} = -(t_n-y_n)(1) \cdot w_h \cdot x_i$

Feedforward Network with 1 hidden layer and linear activation function

Adding multiple neurons together adds a summation operation in the path between the hidden layers to
the activation function. This expands on the above example by adding a new set of summation operators.

To generalize for the case where there are n inputs, m hidden nodes and p outputs, we add summations to include
the general case for a fully connected hidden layer with 2 linear neurons.

$h(z) = \sum\limits_{m=0}^M w_{im} x_{im}$

$y = x$

$x = \sum\limits_{n=0}^N w_{on} h_{on}$

$E = \frac{1}{2}(t_n-y_n)^2$

$\frac{\partial E}{\partial w_{oh}} = \frac{\partial E}{\partial y} \frac{\partial y}{\partial x} \frac{\partial x}{\partial w_{on}}$

$\frac{\partial E}{\partial y} = -\sum\limits_{m=0}^M(t_m-y_m) $

$\frac{\partial y}{\partial x} =  1$

$\frac{\partial x}{\partial w_{on}} =\sum\limits_{n=0}^N h_{on} $

$\frac{\partial E}{\partial w_{im}} = \frac{\partial E}{\partial y} \frac{\partial y}{\partial x} \frac{\partial x}{\partial h} \frac{\partial h}{\partial w_{im}}$

$\frac{dx}{dh}=w_{on}$

$\frac{\partial h}{\partial w_{im}} = x_{im}$

$\frac{\partial E}{\partial w_{im}} = -(t_n-y_n) \cdot 1 \cdot w_{on} \cdot x_{im}$

Some links similar to the udacity hw1:
    http://machinelearningmastery.com/implement-backpropagation-algorithm-scratch-python/

This process of using the chain rule to calculate the $\delta w_i$ generalizes to more layers.
There are 2 steps used to calculate the weights by observing onlyt the output of a NN.
1) Use the chain rule/backpropagation for each layer.
2) Iterating to a approximate solution using some form of SGD.

Udacity Numerical Example of Backprop

Problem w/the Udacity Material above

Gradient Descent isn't used in NN, SGD is used. There are several problems with SGD which should be characterized.
1) the use of regularization and the effect on weight distribution

weight noise -- keeps weights centered in multiple +-(int)

L1 regularization -- keeps the weights near 0

L2 regularization -- prevents large weights

learning rate decay-- prevents oscillations at end of training. smaller steps at end.

random sampling of input space for batches

Tensorflow verification

```python
import numpy as np
import tensorflow as tf

input_data=[[0.1],[0.3]]

x = tf.placeholder(tf.float32,shape=[2,1],name="x")

w=[[0.4,-0.2]]
w2=0.1
z = tf.matmul(w,x,name="z")
value = tf.reduce_mean(z)
layer1 = tf.sigmoid(value,name="layer1")
funny = tf.sigmoid(1.0,name="funny")

sess = tf.InteractiveSession()
sess.run(tf.initialize_all_variables())
sess.run(z,feed_dict={x: input_data})
sess.run(value,feed_dict={x: input_data})
#sess.run(funny)

layer2 = tf.sigmoid(tf.multiply(layer1, w2))
sess.run(layer2,feed_dict={x: input_data})

```

    WARNING:tensorflow:From <ipython-input-52-67fcd33d7ecf>:16: 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.

    0.51237243

```python
#add visualization and gradients to above.
#1) define an error metric, use *, which is (y-yexpected) and a y placeholder with output value
#2) define

input_data=[[0.1],[0.3]]
output_data = [1]

x = tf.placeholder(tf.float32,shape=[2,1],name="x")
y = tf.placeholder(tf.float32,shape=[1],name="y")

#w=[[0.4,-0.2]]
#w2=0.1
w = tf.Variable(tf.zeros([1, 2]))
w2 = tf.Variable(tf.zeros([1]))

z = tf.matmul(w,x,name="z")
value = tf.reduce_mean(z)
layer1 = tf.sigmoid(value,name="layer1")
layer2 = tf.sigmoid(tf.multiply(layer1, w2))
mse = -.5*tf.reduce_mean((y-layer2)*(y-layer2))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(mse)
sess = tf.InteractiveSession()
sess.run(tf.initialize_all_variables())
#sess.run(z,feed_dict={x: input_data})
#sess.run(value,feed_dict={x: input_data})

#sess.run(layer2,feed_dict={x: input_data})
for i in range(10):
    sess.run(train_step, feed_dict={x: input_data, y: output_data})

#how you print out both? Not just the last one?
sess.run(w)

#sess.run(w2)

```

    WARNING:tensorflow:From <ipython-input-69-2ad5bb073151>:24: 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.

    array([[ 0.0023491 ,  0.00704729]], dtype=float32)

```python

```

Logistic function

A logistic neuron requires a set of weights and an Activation function at the output of y. Without
an activation function this is not a neuron. The AF is not in the figure hence this is not a neuron
and we first do back propagation with the logistic function first. Backpropagation refers to the
method of determining the weights for a given training sample using gradient descent to compute the
least loss or error with a set of weights per training sample.

From the Hinton Notation the output of the logistic is y: $\sigma(z) = \frac{1}{1+exp(-z)}$ and
the input to the logistic is z. For the diagram above we use $y = \frac{1}{1+exp(-z)}$ The input to the logistic is a summation of 2 terms, the bias b and
the product of the input with the weight.
$z = \sum\limits_{i=1}^{N}x_iw_i+b_i$

From the Ng Notation the output of the logistic neuron is y and the bias terms are
combined with the weight terms in $\theta$ : $y = \frac{1}{1+exp(-\theta_i \cdot x_i)}$

We will use the Hinton notation and start the index term from 0 to avoid writing a separate bias term.

For the a single logistic function with 1 weight, multiple inputs and 1 output:
We want to calculate the derivative of the output y as a function of the weights $w_i.$ We use the derivative when
set to 0 to determine the amount to change the state of the weights as we iterate using the training sets towards a
converged value.

$\frac{\partial y}{\partial w}=\frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial w}$

The derivative of the logistic WRT z.
$\frac{dy}{dz} = y(1-y)$

$\frac{dz}{dw}=x_i$

$\frac{\partial y}{\partial w}=x_i \cdot y(1-y)$

To derive the delta rule, add a multiply by the learning rate and add it to the existing weights

$\delta w=w_i + \epsilon \cdot x_i \cdot y(1-y)$

To mirror the linear derivation we add an activation function to the output. In this case we add a logistic
activation function as seen below.

Add a hidden layer h(z) where $h(z) = \frac{1}{1+exp(-z)}$ There are 2 logistic functions, the other one is at
the output of the activation function, $y(h(z)) = \frac{1}{1+exp(-h(z)}$

$z = b + \sum\limits_{i=1}^N x_iw_i = \sum\limits_{i=0}^N x_iw_i$

Most use the notation start the index from 0 and do not bother to write out the b term. This is consistent with
tensor notation used in statistical physics.
There are 2 weights we need the update rule for, $w_h$ and $w_i$
$\frac{\partial E}{\partial w_h}$

$\frac{\partial E}{\partial w_i}$

Assign a temporary variable to the output of the hidden layer which is also teh input to the activation function, variable $u$

$u = b + \sum\limits_{i=1}^M h_iw_h = \sum\limits_{i=0}^M x_iw_h$
where M is the number of hidden nodes.

Using the chain rule, we expand the above to:

$\frac{\partial E}{\partial w_h}=\frac{\partial y_i}{\partial u_i} \frac{\partial u_i}{\partial w_h}$

$\frac{\partial u}{\partial w_h}= \sum\limits_{i=0}^M h_i$

$\frac{\partial E}{\partial y_n} = -(t_n-y_n)$ where n is over the training set data.

$\frac{\partial E}{\partial w_h}= -(t_n-y_n) \cdot h_i $

$\frac{\partial E}{\partial w_i}= \frac{\partial E}{\partial y}
\frac{\partial y}{\partial u} \frac{\partial u}{\partial h}
\frac{\partial h}{\partial z} \frac{\partial z}{\partial w_i}$

$\frac{\partial E}{\partial y}=-(t_n-y_n)$

$\frac{\partial y}{\partial u}= y(1-y)$

$\frac{\partial u}{\partial h}=w_h$

Add a temporary variable z, $z=\sum\limits_{i=0}^I w_i x_i$

$h(z) = \frac{1}{1+exp(-z)}$ and

$\frac{\partial h}{\partial z} = h(1-h)$

$\frac{\partial z}{\partial w_i}=x_i$

$weight update=\epsilon \cdot -(t_n-y_n)\cdot x_i \cdot y_i(1-y_i) \cdot h_i(1-h_i) \cdot w_h $

Multiclass classification Cross Entropy and Softmax

Source: https://www.cs.toronto.edu/~urtasun/courses/CSC411/10_nn1.pdf
Neither Hinton Coursera or Udacity DL MOOC treats this material that follows in detail.
Logistic loss not good with squared error loss because:
1) small gradients near edges, will never converge. The S shape of the logistic shows near 1 the slope flattens out
dramatically. The gradients are very small when close to 1.
2) the gradients zig zag between + and - values.

Insteand of squared loss use Cross Entropy loss or multinomial logistic loss and instead of using a logistic
as the activation function use a softmax as the activation function.

The softmax function is for multiple classes vs. logistic which is used for 2 classes. The softmax function is
defined as $softmax(X_i) = \frac{exp(X_i)}{\sum \limits_{i=1}^N X_i}$ The softmax returns a probability
distribution over N classes and the total probabilities sum to 1.

(verify)For a binary classification we can see from logistic regression binary CE works better than squared error:
For binary classification the CE loss with a logistic activation is:
$ E = binary CE = -\frac{1}{n} \sum\limits_{i=0}^N ylog(a) + (1-y)log(1-a) $
where

$a = \sigma(z)$

$\sigma(z) = \frac{1}{(1+exp(-z))}$

and

$z = \sum\limits_{i=0}^N w_i x_i + b$

$\frac{\partial CE}{\partial w_i} = \frac{-1}{n} \sum\limits_{i=1}^{N} \frac{\partial(yln(a))}{\partial a} + \frac{\partial(1-y)ln(1-a)}{\partial a} $

The derivative of a log $\frac{d(log_b(x))}{dx} = \frac{1}{xlog(b)}$

$\frac{\partial yln(a)}{\partial s} = y \cdot \frac{1}{aln(e)}=\frac{y}{a} = \frac{y}{\sigma(z)}$

$\frac{\partial((1-y)ln(1-a))}{\partial a}=(1-y) \frac{1}{(1-a)ln(e)} = \frac{1-y}{1-a} = \frac{1-y}{1-\sigma(z)}$

$\frac{\partial CE}{\partial w_i} = \frac{-1}{n} \sum\limits_{n=1}^N (\frac{y}{\sigma(z)} - \frac{1-y}{1-\sigma(z)})\frac{\partial a}{\partial w_{ij}}$

$\frac{\partial a}{\partial w_i} = \frac{\partial \sigma(z)}{\partial z} \cdot \frac{\partial z}{\partial w_i}$

$\frac{\partial z}{\partial w_i} = x_i$

$\frac{\partial a}{\partial z} = \frac{ \partial \sigma(z)}{\partial z} = \sigma(z)(1-\sigma(z)) $

$\frac{\partial CE}{\partial w_i} =\frac{-1}{n} \sum\limits_{n=1}^N (\frac{y}{\sigma(z)} - \frac{1-y}{1-\sigma(z)}) \sigma(z)(1-\sigma(z))x_i$

$\frac{\partial CE}{\partial w_i} =\frac{y(1-\sigma(z))-\sigma(z)(1-y)}{\sigma(z)(1-\sigma(z))} \cdot \sigma(z) (1-\sigma(z))x_i$

$\frac{\partial CE}{\partial w_i} = (y-y\sigma(z) - \sigma(z) +y\sigma(z))x_i$

$\frac{\partial CE}{\partial w_i} = \sum \limits_{i=1}^N (y-\sigma(z))x_i$

where y is the target value and $\sigma(z) is the output ...$

For multiclass the CE becomes
$CE = \sum\limits_{n=1}^{training} \sum\limits_{k=1}^{classes} t_k^{(n)} log(o_k^{(n)})$

$CE$

Softmax regression model/classification and cross entropy

For more than 2 classes we can use a combination of logistic regression models or softmax regression which is the
generalization of logistic regression to multiple classes.

We saw the logistic function used to output probability and
$\{ (x^{(1)}, y^{(1)}), \ldots, (x^{(m)}, y^{(m)}) \}$ from a dataset with input X, class Y per input and with m trained
samples. Using Ng's notation our hypothesis/logistic is:
$
\begin{align}
h_\theta(x) = \frac{1}{1+\exp(-\theta^Tx)},
\end{align}
$
where $\theta$ is our weight matrix. Hinton uses notation $y=\frac{1}{1+exp(-z)}$

For multiple classes logistc loss also called the cross entropy loss function is:

$
\begin{align}
J(\theta) = -\frac{1}{m} \left[ \sum_{i=1}^m y^{(i)} \log h_\theta(x^{(i)}) + (1-y^{(i)}) \log (1-h_\theta(x^{(i)})) \right]
\end{align}
$

which is derived from the law of total probability for multiple classes. Ecah class is reprented by $y_k$ where for 10 classes k would be 1..10

The softmax loss is:

$p(y^{(i)} = j | x^{(i)} ; \theta) = \frac{e^{\theta_j^T x^{(i)}}}{\sum_{l=1}^k e^{ \theta_l^T x^{(i)}} }$

and the derivative of the logistic loss is in Ng notation:

$
\begin{align}
\nabla_{\theta_j} J(\theta) = - \frac{1}{m} \sum_{i=1}^{m}{ \left[ x^{(i)} \left( 1\{ y^{(i)} = j\}  - p(y^{(i)} = j | x^{(i)}; \theta) \right) \right]  }
\end{align}
$

Hinton calls Cross entropy as the right cost function to use for softmax.

Feedforward network with 1 linear hidden layer and Cross Entropy activation function

A cross entropy loss is used when the data is mutually exclusive, the data can only belong to one of n possible
classes. The UFDL tutorial from Ng is the only source which mentions when is it better to use multiple logistic
classifiers vs. one with a CE loss.

NOTE: ADD for CIFAR10

There are 2 sets of weights, the set of weights from the input to the hidden layer and the set of weights from
the hidden layer to the output.

The shape of the input weight mstrix is [number of inputs x number of hidden layers]
The shape of the output hidden layer weight matrix is [number if hidden layers x number of outputs]
If this is a classification problem the number of outputs is equal to the number of classes.

The hidden layer activation function is a logistic type.

The output layer activateion function is a Cross Entropy Activation function.

CE loss: $$

BackPropagation Examples with weight sharing

Vanishing gradient
https://github.com/dmlc/mxnet-notebooks/blob/master/python/tutorials/understanding_vanish_gradient.ipynb

Backpropatation exercise of unrolled RNN from Hinton:

Find $\frac{\partial E}{\partial z_1}$

$\frac{\partial E}{\partial z_1} = \frac{\partial E_0}{\partial z_1} + \frac{\partial E_1}{\partial z_1} + \frac{\partial E_2}{\partial z_1}$
partial E0 wrt Z1 is 0 bc it is behind in time to z1 so it should not contain any dependent terms on z1.

$
\frac{\partial E_1}{\partial z_1}=
\frac{\partial E_1}{\partial y_1}\frac{\partial y_1}{\partial z_1}=
\frac{\partial E_1}{\partial y_1}\frac{\partial y_1}{\partial h_1}\frac{\partial h_1}{\partial z_1}
$

$\frac{\partial E_1}{\partial y_1}=-(t_1-y_1)=-(-0.1-.1)=-0.2$
$\frac{\partial y_1}{\partial h_1}=W_{hy}=0.25$
$\frac{\partial h_1}{\partial z_1}=h_1(1-h_1)=.4(1-0.4)=-.24$

$\frac{\partial E}{\partial z_1}=-.2*.25*-.24=.012$

$
\frac{\partial E_2}{\partial z_1}=
\frac{\partial E_2}{\partial y_2}
\frac{\partial y_2}{\partial h_2}
\frac{\partial h_2}{\partial z_2}
\frac{\partial z_2}{\partial h_1}
\frac{\partial h_1}{\partial z_1}
$

$\frac{\partial E_2}{\partial y_2}=-(t_2-y_2)=-(-0.2-.2)=.4$
$\frac{\partial y_2}{\partial h_2}=W_{hy}=.25$
$\frac{\partial h_2}{\partial z_2} = h_2(1-h_2)=.8*(1-.8)=-.16$
$\frac{\partial z_2}{\partial h_1}=W_{hh}=0.5$
$\frac{\partial h_1}{\partial z_1}=h_1(1-h_1)=.4(1-.4)=-.24$

$\frac{\partial E_2}{\partial z_1}=.4*.25*-.16*.5*-.24=.00192$

$.012+.00192=.01392$

Given

$t_0,t_1,t_2$=[0.1,-0.1,-0.2]
$x_0,x_1,x_2$=[18,9,-8]
$h_0,h_1,h_2$=[.2,.4,.8]
$y_0,y_1,y_2$=[.05,.1,.2]

```python
#use to calcluate -.355 value. Too many mistakes by hand.
def calculate(x0,x1,x2,Wxh,Xhh,Why,hbias,ybias):
    z0 = Wxh*x0+hbias
    h0 = 1/(1+np.exp(-z0))
    y0=Why*h0+ybias
    z1=Wxh*x1+Whh*h0+hbias
    h1=1/(1+np.exp(-z1))
    y1 = Why*h1+ybias
    z2=Wxh*x2+Whh*h1+hbias
    h2=1/(1+np.exp(-z2))
    y2 = Why*h2+ybias
    print(z0,z1,z2,h0,h1,h2,y0,y1,y2)

#calculate(18,9,-8,)
calculate()

```

```python
#vanishing gradient RNN example.
import numpy as np
def cal(x0,x1,x2,x3,Wxh,Whh,Why,hbias,ybias):
    z0 = Wxh*x0+hbias
    h0 = 1/(1+np.exp(-z0))
    y0=Why*h0+ybias
    z1=Wxh*x1+Whh*h0+hbias
    h1=1/(1+np.exp(-z1))
    y1 = Why*h1+ybias
    z2=Wxh*x2+Whh*h1+hbias
    h2=1/(1+np.exp(-z2))
    y2 = Why*h2+ybias
    z3=Wxh*x3+Whh*h2+hbias
    h3=1/(1+np.exp(-z3))
    y3 = Why*h3+ybias
    return (z0,z1,z2,z3,h0,h1,h2,h3,y0,y1,y2,y3)

results = cal(1,0,0,0,1,-2,1,0,0)

prod=1
for x in results:
    prod *=x*prod
prod

#can see this goes to 0 real fast. even before adding the other Whh, Wxh constants
```

    8.0493585136504298e-162