# Convex Optimization for Machine Learning & Computer Vision
## Week 6 Programming Exercise - Multinomial Logistic Regression
| Name  | Matriculation Number  | E-mail  |
| :------- | :----------------------- | :--------- |
| Akbar, Uzair   | 03697290 | [uzair.akbar@tum.de](mailto:uzair.akbar@tum.de) |

```python
import numpy as np
import matplotlib.pyplot as plt
from urllib import request
import os
import gzip
import shutil
```

```python
# Toy data related

_TOY_DATA_CLUSTER_CENTERS = ((2, -4), (-3, 3), (3, 1), (-5, -2), (-4, -5))
_TOY_DATA_COLORS = ('g', 'r', 'b', 'm', 'y')

def get_toy_data(classes=5,
                 samples_per_class=500,
                 feature_dim=2,
                 noise_dim=100,
                 feature_sigma=1.0,
                 noise_sigma=3.0,
                 cluster_centers=_TOY_DATA_CLUSTER_CENTERS,
                 test_ratio=0.1,
                 visualize=True):
    num_samples = samples_per_class * classes
    total_dim = feature_dim + noise_dim + 1
    data = np.zeros((num_samples, total_dim))
    for j in range(classes):
        # generate useful features
        data[j*samples_per_class:(j+1)*samples_per_class, 0:feature_dim] = \
            np.array(cluster_centers[j]).reshape(1, feature_dim) + \
            np.random.randn(samples_per_class, feature_dim) * feature_sigma
        # generate random features
        data[j*samples_per_class:(j+1)*samples_per_class,
             feature_dim:total_dim-1] = \
            np.random.randn(samples_per_class, noise_dim) * noise_sigma
        # generate ground-truth labels
        data[j*samples_per_class:(j+1)*samples_per_class, total_dim-1] = \
            np.ones(samples_per_class) * j
    # shuffle data
    data = np.random.permutation(data)
    # Split data into training and test set
    sp_idx = num_samples - int(test_ratio * num_samples)
    train_data = data[:sp_idx, :]
    test_data = data[sp_idx:, :]
    # visualize if asked to
    if visualize:
        plt.figure('train_data')
        train_colors = [_TOY_DATA_COLORS[int(c)] for c in train_data[:, -1]]
        plt.scatter(train_data[:, 0], train_data[:, 1], c=train_colors)
        plt.figure('test_data')
        test_colors = [_TOY_DATA_COLORS[int(c)] for c in test_data[:, -1]]
        plt.scatter(test_data[:, 0], test_data[:, 1], c=test_colors)
        plt.show()
    return train_data, test_data, classes
```

```python
# MNIST data related

MNIST_CLASSES = 10
_MNIST_URL_BASE = 'http://yann.lecun.com/exdb/mnist/'
_MNIST_FILES = {'train_image': 'train-images-idx3-ubyte',
                'train_label': 'train-labels-idx1-ubyte',
                'test_image': 't10k-images-idx3-ubyte',
                'test_label': 't10k-labels-idx1-ubyte'}

def download_unzip_gz(from_url, save_as):
    print('Downloading from {}'.format(from_url))
    with request.urlopen(from_url) as src, open(save_as, 'wb') as dst:
        shutil.copyfileobj(gzip.GzipFile(fileobj=src), dst)
    print('Saved as {}'.format(save_as))

def load_MNIST_images(from_file):
    with open(from_file, 'rb') as content:
        return np.frombuffer(content.read(), np.uint8, offset=16
                             ).reshape(-1, 28*28)

def load_MNIST_labels(from_file):
    with open(from_file, 'rb') as content:
        return np.frombuffer(content.read(), np.uint8, offset=8).reshape(-1, 1)

def visualize_MNIST_data(data, rows=6, cols=10):
    out = np.zeros((28 * rows, 28 * cols))
    for r in range(rows):
        for c in range(cols):
            out[r*28:(r+1)*28, c*28:(c+1)*28] = \
                data[r+rows*c, :-1].reshape(28, 28)
    plt.figure('MNIST training image examples')
    plt.imshow(out / 255., cmap='gray')
    plt.show()

def get_MNIST_data(save_dir='.', visualize=True):
    # download MNIST data if not present already
    for filename in _MNIST_FILES.values():
        filepath = os.path.join(save_dir, filename)
        if os.path.exists(filepath):
            print('Found {}'.format(filepath))
        else:
            download_unzip_gz(_MNIST_URL_BASE+filename+'.gz', filepath)
    # load MNIST data
    train_image = load_MNIST_images(
            os.path.join(save_dir, _MNIST_FILES['train_image']))
    train_label = load_MNIST_labels(
            os.path.join(save_dir, _MNIST_FILES['train_label']))
    train_data = np.concatenate((train_image, train_label),
                                axis=1).astype(np.float64)
    test_image = load_MNIST_images(
            os.path.join(save_dir, _MNIST_FILES['test_image']))
    test_label = load_MNIST_labels(
            os.path.join(save_dir, _MNIST_FILES['test_label']))
    test_data = np.concatenate((test_image, test_label),
                               axis=1).astype(np.float64)
    # visualize examples if required
    visualize_MNIST_data(train_data)
    return train_data, test_data, MNIST_CLASSES
```

```python
# optimization related

def softmax(z, dim):
    ez = np.exp(z - np.max(z, dim, keepdims=True))
    return ez / np.sum(ez, dim, keepdims=True)

def log_softmax(z, dim):
    dz = z - np.max(z, dim, keepdims=True)
    return dz - np.log(np.sum(np.exp(dz), dim, keepdims=True))

def classifier(X, W, b):
    return X @ W + b

def score(X, W, b):
    return softmax(classifier(X, W, b), 1)

def average_loss(X, y, W, b):
    y = y.astype(np.long)
    return np.mean(-log_softmax(classifier(X, W, b), 1)[np.arange(len(y)), y])

# the differentiable objective
def objective_diff(X, y, W, b, lambda1, lambda2):
    return average_loss(X, y, W, b) + 0.5 * lambda2 * np.sum(np.square(b)) + \
        0.5 * lambda1 * np.sum(np.square(W))

# the non-differentiable objective
def objective_nondiff(X, y, W, b, lambda1, lambda2):
    return average_loss(X, y, W, b) + 0.5 * lambda2 * np.sum(np.square(b)) + \
        0.5 * lambda1 * np.sum(np.sqrt(np.sum(np.square(W), 1)))
```

It is [straighhtforward to show](https://math.stackexchange.com/questions/1428344/what-is-the-derivation-of-the-derivative-of-softmax-regression-or-multinomial-l) that the gradient of the log-softmax loss can be re-written in vector form as the following.

$
\begin{align}
\nabla_{\mathbf{W}}\,\mathcal{l}(\mathbf{W}, \mathbf{b}, \mathbf{x}, \mathbf{y}) & = & -\frac{1}{N}\mathbf{x}^T\Big( \mathbf{y} - \sigma(\mathbf{W}^T\mathbf{x} + \mathbf{b})\Big)\\
\nabla_{\mathbf{b}}\,\mathcal{l}(\mathbf{W}, \mathbf{b}, \mathbf{x}, \mathbf{y}) & = & -\frac{1}{N} \Big( \mathbf{y} - \sigma(\mathbf{W}^T\mathbf{x} + \mathbf{b})\Big).
\end{align}
$

Where $\mathbf{y} \in \{0, 1\}^K$ is a one-hot vector with $y_k = 1$ for ground-truth class $k$ and $\sigma(.)$ is the soft-max function.

```python
# gradient related

# one hot representation
def one_hot(t, c):
    return np.eye(c)[t.astype(np.int)]

# TODO: gradients of the average logsoftmax loss w.r.t. W
def grad_W_avgloss(X, score, y):
    return np.matmul(X.T, (score - one_hot(y, score.shape[1]))) / X.shape[0]

# TODO: gradients of the average logsoftmax loss w.r.t. b
# You can use e.g. scipy.optimize.minimize to compare your result for the
# differentiable objective objective_diff.
def grad_b_avgloss(score, y):
    return np.mean(score - one_hot(y, score.shape[1]), axis=0)

# TODO: gradients of the square regularization term for W in objective_diff
def grad_W_sqreg(W, lambda1):
    return lambda1 * W

# TODO: gradients of the square regularization term for b in both objectives
def grad_b_sqreg(b, lambda2):
    return lambda2 * b
```

For $J(\mathbf{X})=\frac{\lambda_1}{2}\left\Vert{\mathbf{X}}\right\Vert_{1, 2}$, the definition of the proximal operator gives

$
\begin{align*}
\because\; & \text{prox}_{\tau J}\big(\mathbf{X}\big) & :=\; & \arg \min_\mathbf{Y} J(\mathbf{Y}) + \frac{1}{2\tau}\left\Vert \mathbf{Y} - \mathbf{X} \right\Vert^2_2 & & \\
\Rightarrow & & =\; & \arg \min_\mathbf{Y} \frac{\lambda_1}{2}\left\Vert{\mathbf{Y}}\right\Vert_{1, 2} + \frac{1}{2\tau}\left\Vert \mathbf{Y} - \mathbf{X} \right\Vert^2_2 & & \\
\Rightarrow & & =\; & \arg \min_\mathbf{Y} \frac{\lambda_1}{2}\Big(\left\Vert{\mathbf{Y}}\right\Vert_{1, 2} + \frac{1}{2}\frac{2}{\tau\lambda_1}\left\Vert \mathbf{Y} - \mathbf{X} \right\Vert^2_2\Big) & & \\
\Rightarrow & & =\; & \arg \min_\mathbf{Y}\left\Vert{\mathbf{Y}}\right\Vert_{1, 2} + \frac{1}{2\tau'}\left\Vert \mathbf{Y} - \mathbf{X} \right\Vert^2_2\;&,\;\;\;\; & \tau'=\frac{\tau\lambda_1}{2}\\
\therefore\; & \text{prox}_{\tau J}\big(\mathbf{X}\big) & =\; & \text{prox}_{\tau' \left\Vert . \right\Vert_{1, 2}}\big(\mathbf{X}\big)\;&,\;\;\;\; & \tau'=\frac{\tau\lambda_1}{2}
\end{align*}
$

As we already know from [theoretical exercise 6.4](https://github.com/uzairakbar/convex-optimization/blob/master/exercises/week6/solutionSubmission6.pdf), the proximal operator for the $l_{1, 2}$-norm is given as:

$
{prox}_{\tau' \left\Vert . \right\Vert_{1, 2}}\big(\mathbf{X}\big) = \Big\{ \mathbf{Y} \in \mathbf{R}^{m\times n} \;\Big|\; \mathbf{Y}_i=\begin{cases}
\mathbf{0}\; & , & \;\left\Vert \mathbf{X}_i \right\Vert_2 \leq \tau'\\
\mathbf{X}_i - \tau'\frac{\mathbf{X}_i}{\left\Vert \mathbf{X}_i \right\Vert_2}\; & , & \;\left\Vert \mathbf{X}_i \right\Vert_2 \gt \tau'
\end{cases}\Big\}
$

```python
np.random.seed(43)  # manual random seed for reproducible results
use_mnist = True  # REMARK: set to true once your code works on toy data

# get data
if use_mnist:
    train_data, test_data, classes = get_MNIST_data(save_dir='.',
                                                    visualize=True)
else:
    train_data, test_data, classes = get_toy_data(visualize=True)
X, X_test = train_data[:, :-1], test_data[:, :-1]
feature_dim = X.shape[1]
y, y_test = train_data[:, -1], test_data[:, -1]

# Normalize input data
X, X_test = X / np.max(X), X_test / np.max(X_test)

# initialize parameters to be trained
W = np.random.rand(feature_dim, classes)
b = np.zeros((1, classes))

# TODO(optional): Do derivative check (for debugging only).
# You can use scipy.optimize.grad_check on objective_diff to verify the 4
# gradient functions you have implemented.
pass

# define optimization parameters
lambda1, lambda2 = 0.01, 0.02
max_iter = 6000
if use_mnist:
    tau = 0.7
else:
    tau = 1 / (np.mean(np.sum(np.square(X), axis=1)) + lambda1)
threshold = 1e-12

# define lists that should be filled up in the following updates
obj_val = []
train_error = []
test_error = []
# perform proximal gradient iterations
for i in range(max_iter):
    # TODO: implement proximal gradient here...
    ########################################################################
    # calculate the train and test score
    score_train, score_test = score(X, W, b), score(X_test, W, b)

    # calculate the train and test prediction
    prediction_train, prediction_test = (np.argmax(score_train, axis=1),
                                         np.argmax(score_test, axis=1))

    # calculate objective value, test error and training error
    obj_val.append(objective_nondiff(X, y, W, b, lambda1, lambda2))
    train_error.append(np.mean((prediction_train != y
                                ).astype(np.float))*100.)
    test_error.append(np.mean((prediction_test != y_test
                                ).astype(np.float))*100.)

    # do gradient descent iteration on the differentiable objective
    W = W - tau * grad_W_avgloss(X, score_train, y)
    b = b - tau * (grad_b_avgloss(score_train, y) + grad_b_sqreg(b, lambda2))

    # apply proximal operator of 1,2-norm to W
    row_l2_norm = np.sqrt(np.sum(np.square(W), axis=1))
    tau_ = tau*lambda1/2
    W[row_l2_norm <= tau_, :] = 0
    W[row_l2_norm > tau_, :] = W[row_l2_norm > tau_, :] - \
            tau_ * W[row_l2_norm > tau_, :] / row_l2_norm[row_l2_norm > tau_, np.newaxis]

    # display current results every 500 iterations
    if i%500 == 0:
        print('iteration={}\t |\t obj={:.2f}\t |\t train={:.2f}\t |\t test={:.2f}'.format(
                i, obj_val[-1], train_error[-1], test_error[-1]))
    ########################################################################
    # evalualte stopping criterion
    if i > 0 and obj_val[i-1] - obj_val[i] < threshold:
        break

# Plot objective value, train / test loss
fig = plt.figure('Results')
fig.set_size_inches(15, 5)
ax = fig.add_subplot(1, 3, 1)
ax.plot(range(len(obj_val)), obj_val, 'r')
ax.set_title("Objective Value")
ax.grid()
ax = fig.add_subplot(1, 3, 2)
ax.plot(range(len(train_error)), train_error, 'g')
ax.set_title("Train Error")
ax.grid()
ax = fig.add_subplot(1, 3, 3)
ax.plot(range(len(test_error)), test_error, 'b')
ax.set_title("Test Error")
ax.grid()
plt.show()
# plot classifier for toy data case
if not use_mnist:
    s = 0.1
    x1Grid, x2Grid = np.meshgrid(np.arange(-4, 4, s), np.arange(-4, 4, s))
    xGrid = np.stack((x1Grid.flatten(), x2Grid.flatten()), axis=1)
    scores_train = classifier(xGrid, W[0:2, :], b)
    plt.figure('classifier')
    test_colors = [_TOY_DATA_COLORS[int(c)] for c in test_data[:, -1]]
    plt.scatter(test_data[:, 0], test_data[:, 1], c=test_colors)
    for c in range(classes):
        color = _TOY_DATA_COLORS[c]
        plt.contour(x1Grid, x2Grid,
                    scores_train[:, c].reshape(x1Grid.shape),
                    levels=1, colors=color)
    plt.show()
```

The useless features are assigned low weights in $\mathbf{W}^\ast$, which are the background pixels that occupy most of the area in the image (therefore, the closer a pixel is to the edge of the image, the higher the likelihood it belongs to the background and subsequently the edge pixels/features will be assigned low weights).

```python
# visualization of feature importance
plt.imshow(np.abs(W.T), cmap='hot', interpolation='nearest', aspect='auto')
plt.xlabel("feature index")
plt.ylabel("class")
plt.title("Feature Importance - Absolute Feature Weights")
plt.show()
```

Therefore, $l_{1, 2}$ norm performs better because it promotes sparsity, and useless features are ignored in classification. In contrast, $l_2$ norm promotes dense weights, which encourages the model to use irrelevant features despite no predictive power in terms of labels.

***


# K-Means Clustering

# 1) Use the "Breast Cancer Wisconsin (Diagnostic) Data Set" from Kaggle to try and cluster types of cancer cells.

Here's the original dataset for your reference:

<https://www.kaggle.com/uciml/breast-cancer-wisconsin-data>

## This is a supervised learning dataset

(Because it has **labels** - The "diagnosis" column.)

```
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA # You don't necessarily have to use this
from sklearn.cluster import KMeans # You don't necessarily have to use this
from sklearn.preprocessing import StandardScaler # You don't necessarily have to use this

df = pd.read_csv("https://raw.githubusercontent.com/ryanleeallred/datasets/master/Cancer_Cells.csv")
print(df.shape)
df.head()
```

    (569, 33)

      id
      diagnosis
      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      fractal_dimension_mean
      radius_se
      texture_se
      perimeter_se
      area_se
      smoothness_se
      compactness_se
      concavity_se
      concave points_se
      symmetry_se
      fractal_dimension_se
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst
      Unnamed: 32

      0
      842302
      M
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      0.07871
      1.0950
      0.9053
      8.589
      153.40
      0.006399
      0.04904
      0.05373
      0.01587
      0.03003
      0.006193
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890
      NaN

      1
      842517
      M
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      0.05667
      0.5435
      0.7339
      3.398
      74.08
      0.005225
      0.01308
      0.01860
      0.01340
      0.01389
      0.003532
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902
      NaN

      2
      84300903
      M
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      0.05999
      0.7456
      0.7869
      4.585
      94.03
      0.006150
      0.04006
      0.03832
      0.02058
      0.02250
      0.004571
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758
      NaN

      3
      84348301
      M
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      0.09744
      0.4956
      1.1560
      3.445
      27.23
      0.009110
      0.07458
      0.05661
      0.01867
      0.05963
      0.009208
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300
      NaN

      4
      84358402
      M
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      0.05883
      0.7572
      0.7813
      5.438
      94.44
      0.011490
      0.02461
      0.05688
      0.01885
      0.01756
      0.005115
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678
      NaN

## Now it's an unsupervised learning dataset

(Because we've removed the diagnosis label) - Use this version.

```
# df = df.drop('diagnosis', axis=1)
# df = df.drop('id', axis=1)
# df = df.drop('Unnamed: 32', axis=True)
y = df['diagnosis']
x = df.drop(['diagnosis', 'id', 'Unnamed: 32'], axis=True)
x.head()
```

      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      fractal_dimension_mean
      radius_se
      texture_se
      perimeter_se
      area_se
      smoothness_se
      compactness_se
      concavity_se
      concave points_se
      symmetry_se
      fractal_dimension_se
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst

      0
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      0.07871
      1.0950
      0.9053
      8.589
      153.40
      0.006399
      0.04904
      0.05373
      0.01587
      0.03003
      0.006193
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890

      1
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      0.05667
      0.5435
      0.7339
      3.398
      74.08
      0.005225
      0.01308
      0.01860
      0.01340
      0.01389
      0.003532
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902

      2
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      0.05999
      0.7456
      0.7869
      4.585
      94.03
      0.006150
      0.04006
      0.03832
      0.02058
      0.02250
      0.004571
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758

      3
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      0.09744
      0.4956
      1.1560
      3.445
      27.23
      0.009110
      0.07458
      0.05661
      0.01867
      0.05963
      0.009208
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300

      4
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      0.05883
      0.7572
      0.7813
      5.438
      94.44
      0.011490
      0.02461
      0.05688
      0.01885
      0.01756
      0.005115
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678

```
print(x.shape)

```

    (569, 30)

```
plt.scatter(x['perimeter_se'], x['perimeter_mean']);
```

```
from sklearn.cluster import KMeans

kmeans = KMeans(2)

kmeans.fit(x)
print(kmeans.labels_)
```

    [1 1 1 0 1 0 1 0 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 1 1 0 1 1 1 1 0 1 1 1 1 0
     0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0
     0 1 0 1 1 0 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
     0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0
     0 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0
     0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 1 1 0 0
     0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 1 0 1 0 0 0 0 1 0 0 0 0 0 1 0 1 1 1 0 1 0 1
     0 1 1 1 0 1 1 0 0 0 0 0 0 1 0 1 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0
     0 0 1 0 1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1
     0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0
     0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0
     1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1
     0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 0 0 0 0 1 1 1 0 1 0]

```
x['cluster_labels'] = kmeans.labels_

x.head()
```

      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      fractal_dimension_mean
      radius_se
      texture_se
      perimeter_se
      area_se
      smoothness_se
      compactness_se
      concavity_se
      concave points_se
      symmetry_se
      fractal_dimension_se
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst
      cluster_labels

      0
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      0.07871
      1.0950
      0.9053
      8.589
      153.40
      0.006399
      0.04904
      0.05373
      0.01587
      0.03003
      0.006193
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890
      1

      1
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      0.05667
      0.5435
      0.7339
      3.398
      74.08
      0.005225
      0.01308
      0.01860
      0.01340
      0.01389
      0.003532
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902
      1

      2
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      0.05999
      0.7456
      0.7869
      4.585
      94.03
      0.006150
      0.04006
      0.03832
      0.02058
      0.02250
      0.004571
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758
      1

      3
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      0.09744
      0.4956
      1.1560
      3.445
      27.23
      0.009110
      0.07458
      0.05661
      0.01867
      0.05963
      0.009208
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300
      0

      4
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      0.05883
      0.7572
      0.7813
      5.438
      94.44
      0.011490
      0.02461
      0.05688
      0.01885
      0.01756
      0.005115
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678
      1

```
y[:10]
```

    0    M
    1    M
    2    M
    3    M
    4    M
    5    M
    6    M
    7    M
    8    M
    9    M
    Name: diagnosis, dtype: object

```
x['diagnosis'] = df['diagnosis']
```

```
pd.crosstab(x['diagnosis'], x['cluster_labels'])
```

      cluster_labels
      0
      1

      diagnosis

      B
      356
      1

      M
      82
      130

## Let's do it!

- You might want to do some data exploration to see if you can find specific columns that will help you find distinct clusters of cells
- You might want to use the elbow method to decide on the number of clusters to use.

```
# Deciding on the columns that could potentially provide distinct clusters of cells

# print(df.isnull().any())
df.describe()
# texture_worst
# parameter_se
# parameter_mean
```

      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      fractal_dimension_mean
      radius_se
      texture_se
      perimeter_se
      area_se
      smoothness_se
      compactness_se
      concavity_se
      concave points_se
      symmetry_se
      fractal_dimension_se
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst

      count
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000
      569.000000

      mean
      14.127292
      19.289649
      91.969033
      654.889104
      0.096360
      0.104341
      0.088799
      0.048919
      0.181162
      0.062798
      0.405172
      1.216853
      2.866059
      40.337079
      0.007041
      0.025478
      0.031894
      0.011796
      0.020542
      0.003795
      16.269190
      25.677223
      107.261213
      880.583128
      0.132369
      0.254265
      0.272188
      0.114606
      0.290076
      0.083946

      std
      3.524049
      4.301036
      24.298981
      351.914129
      0.014064
      0.052813
      0.079720
      0.038803
      0.027414
      0.007060
      0.277313
      0.551648
      2.021855
      45.491006
      0.003003
      0.017908
      0.030186
      0.006170
      0.008266
      0.002646
      4.833242
      6.146258
      33.602542
      569.356993
      0.022832
      0.157336
      0.208624
      0.065732
      0.061867
      0.018061

      min
      6.981000
      9.710000
      43.790000
      143.500000
      0.052630
      0.019380
      0.000000
      0.000000
      0.106000
      0.049960
      0.111500
      0.360200
      0.757000
      6.802000
      0.001713
      0.002252
      0.000000
      0.000000
      0.007882
      0.000895
      7.930000
      12.020000
      50.410000
      185.200000
      0.071170
      0.027290
      0.000000
      0.000000
      0.156500
      0.055040

      25%
      11.700000
      16.170000
      75.170000
      420.300000
      0.086370
      0.064920
      0.029560
      0.020310
      0.161900
      0.057700
      0.232400
      0.833900
      1.606000
      17.850000
      0.005169
      0.013080
      0.015090
      0.007638
      0.015160
      0.002248
      13.010000
      21.080000
      84.110000
      515.300000
      0.116600
      0.147200
      0.114500
      0.064930
      0.250400
      0.071460

      50%
      13.370000
      18.840000
      86.240000
      551.100000
      0.095870
      0.092630
      0.061540
      0.033500
      0.179200
      0.061540
      0.324200
      1.108000
      2.287000
      24.530000
      0.006380
      0.020450
      0.025890
      0.010930
      0.018730
      0.003187
      14.970000
      25.410000
      97.660000
      686.500000
      0.131300
      0.211900
      0.226700
      0.099930
      0.282200
      0.080040

      75%
      15.780000
      21.800000
      104.100000
      782.700000
      0.105300
      0.130400
      0.130700
      0.074000
      0.195700
      0.066120
      0.478900
      1.474000
      3.357000
      45.190000
      0.008146
      0.032450
      0.042050
      0.014710
      0.023480
      0.004558
      18.790000
      29.720000
      125.400000
      1084.000000
      0.146000
      0.339100
      0.382900
      0.161400
      0.317900
      0.092080

      max
      28.110000
      39.280000
      188.500000
      2501.000000
      0.163400
      0.345400
      0.426800
      0.201200
      0.304000
      0.097440
      2.873000
      4.885000
      21.980000
      542.200000
      0.031130
      0.135400
      0.396000
      0.052790
      0.078950
      0.029840
      36.040000
      49.540000
      251.200000
      4254.000000
      0.222600
      1.058000
      1.252000
      0.291000
      0.663800
      0.207500

```
# First lets use the elbow method to decide on the number of clusters

sum_of_squared_distances = []
K = range(1, 20)
for k in K:
    km = KMeans(n_clusters=k) #cluster using scykit.learn
    km = km.fit(df)
    sum_of_squared_distances.append(km.inertia_)
```

```
plt.plot(K, sum_of_squared_distances, 'bx-')
plt.xlabel('k')
plt.ylabel('Sum_of_squared_distances')
plt.title('Elbow Method For Optimal k')
plt.show()

# Seems we could use 3 clusters
```

```
# # Perform K-Means Clustering on the Dataset

# from sklearn.cluster import KMeans

# kmeans = KMeans(n_clusters=22)
# kmeans.fit(df)
# labels = kmeans.labels_

# print(labels)
```

    [1 1 1 0 1 0 1 0 0 0 1 1 1 0 0 0 1 1 2 0 0 0 0 2 1 1 0 1 1 1 1 0 1 1 1 1 0
     0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0
     0 1 0 1 1 0 0 0 2 1 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0
     0 0 0 0 0 0 0 1 1 0 1 2 0 0 0 0 1 0 1 0 1 1 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0
     0 0 0 0 0 0 0 0 1 1 0 0 0 1 2 0 2 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 2 1 1 0 0
     0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 2 0 0 0 0 1 0 0 1 0 2 1 0 0 0 0 1 2 0 0
     0 1 0 0 0 0 0 0 1 0 0 1 0 0 2 1 0 1 0 0 0 0 1 0 0 0 0 0 1 0 1 1 1 0 1 0 1
     0 1 1 1 0 1 2 0 0 0 0 0 0 2 0 1 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0
     0 0 1 0 1 0 2 0 0 0 1 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 2 2
     1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0
     0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0
     1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 2 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 0 0 1 0 1 0 1 1 0 0 0 0 0 1 1 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 1 1
     0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
     0 0 0 0 0 0 0 0 1 1 1 1 1 0]

```
new_series = pd.Series(labels)
df['clusters'] = new_series.values

df.head()
```

      radius_mean
      texture_mean
      perimeter_mean
      area_mean
      smoothness_mean
      compactness_mean
      concavity_mean
      concave points_mean
      symmetry_mean
      fractal_dimension_mean
      radius_se
      texture_se
      perimeter_se
      area_se
      smoothness_se
      compactness_se
      concavity_se
      concave points_se
      symmetry_se
      fractal_dimension_se
      radius_worst
      texture_worst
      perimeter_worst
      area_worst
      smoothness_worst
      compactness_worst
      concavity_worst
      concave points_worst
      symmetry_worst
      fractal_dimension_worst
      clusters

      0
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      0.07871
      1.0950
      0.9053
      8.589
      153.40
      0.006399
      0.04904
      0.05373
      0.01587
      0.03003
      0.006193
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890
      1

      1
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      0.05667
      0.5435
      0.7339
      3.398
      74.08
      0.005225
      0.01308
      0.01860
      0.01340
      0.01389
      0.003532
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902
      1

      2
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      0.05999
      0.7456
      0.7869
      4.585
      94.03
      0.006150
      0.04006
      0.03832
      0.02058
      0.02250
      0.004571
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758
      1

      3
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      0.09744
      0.4956
      1.1560
      3.445
      27.23
      0.009110
      0.07458
      0.05661
      0.01867
      0.05963
      0.009208
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300
      0

      4
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      0.05883
      0.7572
      0.7813
      5.438
      94.44
      0.011490
      0.02461
      0.05688
      0.01885
      0.01756
      0.005115
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678
      1

## Check you work:

This is something that in a truly unsupervised learning situation **WOULD NOT BE POSSIBLE**. But for educational purposes go back and grab the true diagnosis column (label) from the original dataset. Take your cluster labels and compare them to the original diagnosis column. You can make scatterplots for each to see how they compare or you can calculate a percent accuracy score like:
\begin{align}
\frac{\text{Num Correct Labels}}{\text{Num Total Observations}}
\end{align}

```
# Your Code Here
```

# 2) Perform PCA on your dataset first and *then* use k-means clustering.

- You need to standardize your data before PCA.
- First try clustering just on PC1 and PC2 so that you can make a scatterplot of your clustering.
- Then use use a scree plot to decide how many principal components to include in your clustering, and use however many principal components you need in order to retain 90% of the variation of the original dataset

```
##### Your Code Here #####
```

## Check your work:

- Compare your PC1, PC2 clustering scatterplot to the clustering scatterplots you made on the raw data
- Calculate accuracy scores for both the PC1,PC2 Principal component clustering and the 90% of explained variance clustering.

How do your accuracy scores -when preprocessing the data with PCA- compare to the accuracy when simply clustering on the raw data?

# Stretch Goals:

- Study for the Sprint Challenge
- Work on your Data Storytelling Project

```

```


# Introduction

The quantum Fourier transform and quantum phase estimation provide the foundation for many quantum algorithms, including the quantum matrix inversion, which is extensively used in quantum machine learning. It is therefore worthwhile developing a good understanding of these building blocks before moving on to more complex algorithms.

We must emphasize that starting with this notebook, the algorithms presented are coherent quantum protocols. By that, we mean that the input and output of an algorithm is a quantum state that we do not have classical information about. The protocol itself might use measurements: in this sense, they are not fully coherent, since we gain some, but incomplete classical information about the quantum system. We might also perform post-selection, which means that a gate is controlled by the classical output of a measurement. In some cases, we entirely discard a calculation based on a measurement output.

Why does it matter that we begin and end up with quantum states? Can't we just use state preparation starting from classical data and then perform tomography on the final state? We could do that, but state preparation and tomography are resource-intensive, and they are likely to destroy any quantum advantage.

An additional problem is that the quantum Fourier transformation and other quantum algorithms similar in complexity require a very large number of gates on a large number of high-quality qubits. This is why the practical relevance of these algorithms is not immediate, but since they are the core of many quantum machine learning protocols, it is essential that we take a look at them.

```python
import numpy as np
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import execute
from qiskit import BasicAer
π = np.pi
```

    /home/pwittek/.anaconda3/envs/qiskit/lib/python3.7/site-packages/marshmallow/schema.py:364: ChangedInMarshmallow3Warning: strict=False is not recommended. In marshmallow 3.0, schemas will always be strict. See https://marshmallow.readthedocs.io/en/latest/upgrading.html#schemas-are-always-strict
      ChangedInMarshmallow3Warning

# Quantum Fourier Transform

The quantum Fourier transform is a quantum algorithm for the discrete Fourier transform over the amplitudes of a wavefunction. The exposition here follows the introduction in [[1](#1)]. A similar approach can be found in the [Qiskit tutorials](https://github.com/Qiskit/qiskit-tutorials/blob/master/community/algorithms/shor_algorithm.ipynb).

The classical discrete Fourier transform acts on a vector $\vec{x}=\begin{bmatrix}x_0\\ \vdots\\ x_{N-1}\end{bmatrix}$ and maps it to the vector $\vec{y}=\begin{bmatrix}y_0\\ \vdots\\ y_{N-1}\end{bmatrix}$, where $y_k = \frac{1}{\sqrt{N}}\sum_{j=0}^{N-1}x_je^{ \boldsymbol{2\pi i} \frac{jk}{N}}$.

The quantum Fourier transform acts on an amplitude-encoded variant of this vector, the quantum state $|x\rangle=\sum_{i=0}^{N-1} x_i |i \rangle$ and maps it to the quantum state $|y\rangle=\sum_{k=0}^{N-1} y_k |k \rangle$, where
$y_k = \frac{1}{\sqrt{N}}\sum_{j=0}^{N-1}x_je^{\boldsymbol{2\pi i} \frac{jk}{N}}$. Since the transformed state is also in the superposition of the computational basis, in essence, only the amplitudes are transformed.

To derive a circuit for a power of two $N=2^n$, consider the transform acts on the state $| x \rangle = | x_1...x_n \rangle$ where $x_1$ is the most significant bit, unlike the usual convention followed in the course. We will rewrite a number $y$ in the fractional binary notation  as $j = 0.j_1...j_n = \sum_{k=1}^n j_k/2^k$. For example $0.8125_d = 0.1101_b = \sum_{k=1}^4 j_k/2^k = 1/2 + 1/4 + 0/8 + 1/16$, where the subscripts $d$ stands for decimal and $b$ for binary. The action of the unitary $U$ describing the transform can be expanded as
\begin{aligned}
U |x \rangle = U |x_1 x_2 \cdots x_n \rangle& = \frac{1}{\sqrt{N}} \sum_{k=0}^{2^n-1} e^{\boldsymbol{2\pi i} xk / 2^n} |k \rangle \\
& = \frac{1}{\sqrt{N}} \sum_{k_1=0}^{1}\ldots\sum_{k_n=0}^{1} e^{\boldsymbol{2\pi i} x\left(\sum_{l=1}^n k_l2^{-l}\right) } \vert k_1 ... k_n \rangle \\
& = \frac{1}{\sqrt{N}} \sum_{k_1=0}^{1}\ldots\sum_{k_n=0}^{1} \bigotimes_{l=1}^n e^{\boldsymbol{2\pi i} x k_l2^{-l}} | k_1 ... k_n \rangle \\
& = \frac{1}{\sqrt{N}} \bigotimes_{l=1}^n \sum_{k_l=0}^{1}  e^{\boldsymbol{2\pi i} x k_l2^{-l}} | k_1 ... k_n \rangle \\
& = \frac{1}{\sqrt{N}} \bigotimes_{l=1}^n  \left(|0\rangle + e^{\boldsymbol{2\pi i} x 2^{-l} } |1\rangle \right) \\
& = \frac{1}{\sqrt{N}} \left(|0\rangle + e^{\boldsymbol{2\pi i} 0.x_n} |1\rangle\right) \otimes...\otimes  \left(\vert0\rangle + e^{\boldsymbol{2\pi i} 0.x_1.x_2...x_{n-1}.x_n} |1\rangle\right)
\end{aligned}

This form of the QFT is useful for deriving a circuit, since only the last qubit depends on the the
values of all the other input qubits. The remaining qubits depend less and less on the input qubits. The simple structure also allows to decompose the unitary as Hadamard gates and rotations. On three qubits, we can define the circuit as follows:

```python
q = QuantumRegister(3, 'q')
c = ClassicalRegister(1, 'c')
qft = QuantumCircuit(q, c)
qft.h(q[0])
qft.cu1(π/2, q[1], q[0])
qft.h(q[1])
qft.cu1(π/4, q[2], q[0])
qft.cu1(π/2, q[2], q[1])
qft.h(q[2]);
```

We can plot it to make the qubit dependencies more apparent:

```python
from qiskit.tools.visualization import circuit_drawer
circuit_drawer(qft)
```

The conditional rotations dominate the complexity, which scales as $O(N^2)$.

# Quantum phase estimation

The quantum phase estimation algorithm estimates the phase of a unitary operator $U$, that is, it estimates $\theta$ in  $U|\psi \rangle =e^{\boldsymbol{2\pi i} \theta }|\psi \rangle$. Here $|\psi\rangle$ is an eigenvector and $e^{\boldsymbol{2\pi i}\theta}$ is the corresponding eigenvalue. Since $U$ is unitary, all of its eigenvalues have an absolute value of 1.

The $|\psi\rangle$ is in one set of qubit registers. An additional set of $n$ qubits form an ancilla register. A critical element is the ability to perform the controlled unitary $C-U^{2^k}$ -- it is usually assumed that it is provided to the phase estimation protocol.

First, the uniform superposition is prepared in the ancilla register via the application of Hadamard gates $H$. These qubits will act as controls for the unitary operators at different time steps. Our goal is to create a superposition of $U$ as the unitary is applied for different durations. Since the eigenvalues are always situated on the complex unit circle, these differently evolved components in the superposition help reveal the eigenstructure. Given that the ancilla register we have a superposition of all possible time steps between $0$ and $2^{n-1}$, we will end up with a superposition of all possible evolutions to encode binary representations of the eigenvalues. At the end of this procedure, we have the state
\begin{aligned}
& \frac {1}{2^{\frac {n}{2}}} (|0\rangle+{e^{\boldsymbol{2\pi i} \theta 2^{n-1}}}|1\rangle ) \otimes \cdots (|0\rangle+{e^{\boldsymbol{2\pi i} \theta 2^{1}}}|1\rangle ) \otimes (|0\rangle+{e^{\boldsymbol{2\pi i} \theta 2^{0}}}|1\rangle ) = \\
&\frac {1}{2^{\frac {n}{2}}}\sum _{k=0}^{2^{n}-1}e^{\boldsymbol{2\pi i} \theta k}|k\rangle
\end{aligned}
in the ancilla. To write the ancilla in this form, we exploit that the controlled unitary operations when applied, introduce a global phase, and it is this global phase that we see in the ancilla. This phenomenon is also known as the phase kickback.

As a final step, we apply an inverse Fourier transform on the ancilla. Measuring out in the computational basis, we get the phase in the ancilla register:

\begin{align}
\frac {1}{2^{\frac {n}{2}}}\sum _{k=0}^{2^{n}-1}e^{\boldsymbol{2\pi i} \theta k}|k\rangle \otimes | \psi \rangle \xrightarrow{\mathcal{QFT}_n^{-1}} | 2^n \theta \rangle \otimes | \psi \rangle
\end{align}

The circuit for phase estimation is the following:

As an example, let us take a $2\times 2$ unitary matrix that is defined via the Hamiltonian $\frac{1}{2}\begin{bmatrix}3 & 1 \\1 & 3\end{bmatrix}$, which has eigenvectors of powers of two. Starting with $|\psi\rangle=|0\rangle$ in the main register, we prepare the superposition in the ancilla:

```python
qpe = QuantumCircuit(q, c)
qpe.h(q[0])
qpe.h(q[1]);
```

Next we perform the controlled unitary operations:

```python
# Controlled-U0
qpe.cu3(-π / 2, -π / 2, π / 2, q[1], q[2])
qpe.cu1(3 * π / 4, q[1], q[2])
qpe.cx(q[1], q[2])
qpe.cu1(3 * π / 4, q[1], q[2])
qpe.cx(q[1], q[2])
# Controlled-U1
qpe.cx(q[0], q[2]);
```

We apply quantum inverse Fourier transformation to write the phase to the ancilla register:

```python
qpe.swap(q[0], q[1])
qpe.h(q[1])
qpe.cu1(-π / 2, q[0], q[1])
qpe.h(q[0]);
```

Finally, we can plot the circuit:

```python
circuit_drawer(qpe)
```

# References
[1] M. Nielsen, I. Chuang. (2000). *Quantum Computation and Quantum Information*. Cambridge University Press.


# Algorithm for deriving moment equations
$$
\def\n{\mathbf{n}}
\def\x{\mathbf{x}}
\def\N{\mathbb{\mathbb{N}}}
\def\X{\mathbb{X}}
\def\NX{\mathbb{\N_0^\X}}
\def\C{\mathcal{C}}
\def\Jc{\mathcal{J}_c}
\def\DM{\Delta M_{c,j}}
\newcommand\diff{\mathop{}\!\mathrm{d}}
\def\Xc{\mathbf{X}_c}
\newcommand{\muset}[1]{\dot{\{}#1\dot{\}}}
$$

For a compartment population $\n \in \NX$ evolving stochastically according to stoichiometric equations from transition classes $\C$, we want to find an expression for
$$
\frac{\diff}{\diff t}\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>
$$
in terms of expectations of population moments $M^\alpha, M^{\beta}, \ldots$

### (1)
From the definition of the compartment dynamics, we have
$$
\diff M^\gamma = \sum_{c \in \C} \sum_{j \in \Jc} \DM^\gamma \diff R_{c,j}
$$
We apply Ito's rule to derive
$$
\diff f(M^\gamma, M^{\gamma'}, \ldots) = \sum_{c \in \C} \sum_{j \in \Jc}
    \left(
        f(M^\gamma + \DM^\gamma, M^{\gamma'} + \DM^{\gamma'}, \ldots)
        - f(M^\gamma, M^{\gamma'}, \ldots)
    \right) \diff R_{c,j}
$$

Assume, that $f(M^\gamma, M^{\gamma'}, \ldots)$ is a polynomial in $M^{\gamma^i}$ with $\gamma^i \in \N_0^D$.

Then $\diff f(M^\gamma, M^{\gamma'}, \ldots)$ is a polynomial in $M^{\gamma^k}, \DM^{\gamma^l}$ with $\gamma^k, \gamma^l \in \N_0^D$, that is,
$$
\diff f(M^\gamma, M^{\gamma'}, \ldots) = \sum_{c \in \C} \sum_{j \in \Jc}
    \sum_{q=1}^{n_q} Q_q(M^{\gamma^k}, \DM^{\gamma^l})
    \diff R_{c,j}
$$
where $Q_q(M^{\gamma^k}, \DM^{\gamma^l})$ are monomials in $M^{\gamma^k}, \DM^{\gamma^l}$.

### (2)
Let's write $Q_q(M^{\gamma^k}, \DM^{\gamma^l})$ as
$$
Q_q(M^{\gamma^k}, \DM^{\gamma^l}) = k_q \cdot \Pi M^{\gamma^k} \cdot \Pi M^{\gamma^k}
$$
where $k_q$ is a constant,
  $\Pi M^{\gamma^k}$ is a product of powers of $M^{\gamma^k}$, and
  $\Pi \DM^{\gamma^l}$ is a product of powers of $\DM^{\gamma^l}$.

Analogous to the derivation in SI Appendix S.3, we arrive at the expected moment dynamics
$$
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t} =
    \sum_{c \in \C} \sum_{q=1}^{n_q} \left<
        \sum_{j \in \Jc} k_q \cdot \Pi M^{\gamma^k} \cdot \Pi \DM^{\gamma^k} \cdot h_{c,j}(\n)
    \right>
$$

### (3)
Analogous to SI Appendix S.4, the contribution of class $c$, monomial $q$ to the expected dynamics of $f(M^\gamma, M^{\gamma'}, \ldots)$ is
$$
\begin{align}
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t}
    &= \left<
        {\large\sum_{j \in \Jc}} k_q \cdot \Pi M^{\gamma^k} \cdot \Pi \DM^{\gamma^l} \cdot h_{c,j}(\n)
    \right>
    \\
    &= \left<
        {\large\sum_{\Xc}} w(\n; \Xc) \cdot k_c \cdot k_q \cdot \Pi M^{\gamma^k} \cdot g_c(\Xc) \cdot
        \left<
            \Pi \DM^{\gamma^l} \;\big|\; \Xc
        \right>
    \right>
\end{align}
$$

### (4)
Let's consider the expression $A = \sum_{\Xc} w(\n; \Xc) \cdot l(\n; \Xc)$ for the following cases of reactant compartments:
$\Xc = \emptyset$,
$\Xc = \muset{\x}$, and
$\Xc = \muset{\x, \x'}$.

(1) $\Xc = \emptyset$:

Then $w(\n; \Xc) = 1$, and
$$
    A = l(\n)
$$

(2) $\Xc = \muset{\x}$:

Then $w(\n; \Xc) = \n(\x)$, and
$$
    A = \sum_{\x \in \X} \n(\x) \cdot l(\n; \muset{\x})
$$

(3) $\Xc = \muset{\x, \x'}$:

Then
$$
    w(\n; \Xc) = \frac{\n(\x)\cdot(\n(\x')-\delta_{\x,\x'})}
    {1+\delta_{\x,\x'}},
$$
and
$$
\begin{align}
    A &= \sum_{\x \in \X} \sum_{\x' \in \X}
            \frac{1}{2-\delta_{\x,\x'}}
            \cdot w(\n; \Xc) \cdot l(\n; \muset{\x, \x'}) \\
      &= \sum_{\x \in \X} \sum_{\x' \in \X}
            \frac{\n(\x)\cdot(\n(\x')-\delta_{\x,\x'})}{2}
            \cdot l(\n; \muset{\x, \x'}) \\
      &= \sum_{\x \in \X} \sum_{\x' \in \X}
            \n(\x)\cdot\n(\x') \cdot \frac{1}{2}l(\n; \muset{\x, \x'})
         \: - \:
         \sum_{\x \in \X}
            \n(\x) \cdot \frac{1}{2}l(\n; \muset{\x, \x})
\end{align}
$$

### (5)
Now let
$$
l(\n; \Xc) = k_c \cdot k_q \cdot \Pi(M^{\gamma^k}) \cdot g_c(\Xc) \cdot
        \left<
            \Pi \DM^{\gamma^l} \;\big|\; \Xc
        \right>
$$

Plugging in the concrete $\gamma^l$ and expanding, $l(\n; \Xc)$ is a polynomial in $\Xc$.

Monomials are of the form $k \x^\alpha$ or $k \x^\alpha \x'^\beta$ with $\alpha, \beta \in \N_0^D$.
(Note that occurences of $\Pi M^{\gamma^k}$ are part of the constants $k$.)

Consider again the different cases of reactant compartments $\Xc$:

(1) $\Xc = \emptyset$:
$$
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t}
    = \left<l(\n)\right>
$$

(2) $\Xc = \muset{\x}$:
$$
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t}
    = \left<R(l(\n; \muset{\x})\right>
$$
where $R$ replaces all $k \x^\alpha$ by $k M^\alpha$.

(3) $\Xc = \muset{\x, \x'}$:
$$
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t}
    = \frac{1}{2}\left<R'(l(\n; \muset{\x, \x'})\right>
    \: - \:
    \frac{1}{2}\left<R(l(\n; \muset{\x, \x})\right>
$$
where $R'$ replaces all $k \x^\alpha \x'^\beta$ by $k M^\alpha M^\beta$,
and again $R$ replaces all $k \x^\alpha$ by $k M^\alpha$.

### (6)
Finally, sum over contributions from all $c$, $q$ for the total
$$
\frac{\diff\left< f(M^\gamma, M^{\gamma'}, \ldots) \right>}{\diff t}
$$


```python
'''This file gives all of the functions used to define states
and implement quantum operations.'''

import cmath, random, numpy
import functools
import  matplotlib.pyplot as plt
import sys
import os
import math
from qutip import*

from sympy import*
#from sympsi import*
from scipy import optimize

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import time
import math
from qutip import *
from qutip.ipynbtools import plot_animation
import numpy as np
import matplotlib.pyplot as plt
import qutip
%matplotlib inline
import matplotlib.pylab as plt
import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from IPython.display import display, Math, Latex
import cmath
from mpl_toolkits.axes_grid1 import AxesGrid
from scipy.special import factorial as fac

xvec = np.arange(-80.,80.)*5./80
yvec = np.arange(-50.,50)*5/40
X,Y = np.meshgrid(xvec, xvec)  ##Some plotting params
X1,Y1 = np.meshgrid(yvec,yvec)
N_dim = 35 ##Dimenstion of the Hilbert spac

'''Annihilation operators.  Each "a" is the same, but we define different ones so
as to make defining different operations on different modes less confusing.'''
a1 = destroy(N_dim) ##This is for single-photon field
a2 = destroy(N_dim) ##for coherent field
a3 = destroy(N_dim) ##for vacuum field

'''Define a function for n choose k'''
def n_choose_k(n,k):
    return fac(n)/(fac(n-k)*fac(k))

'''Applies the displacement operator to a density matrix'''
def D(state,alpha):
    Rho_new=displace(N_dim,alpha)*state*displace(N_dim,alpha).dag()
    return Rho_new

'''Define a rotation in phase space, or phase shifter operation'''
def Phase(theta):
    b=-1j*theta*a1.dag()*a1;
    return b.expm()

'''Squeezing operation, inputs a density matrix and outputs the
squeezed density matrix for squeezing parameter r'''
def Sq(state,r):
    Rho_new=squeeze(N_dim,r)*state*squeeze(N_dim,r).dag();
    return Rho_new

'''The function below creates a beamsplitter operation that acts on
two modes. The value for k determines what number Fock state could be
filtered out of the first state based on a single photon input for the
second BS port, followed by single photon detection.'''
def BS_operator_filtering(a1, a2, k):
    theta_k = np.arctan(1/np.sqrt(k))
    T = np.sin(theta_k)*np.sin(theta_k)
    R = np.cos(theta_k)*np.cos(theta_k)
    print('I am filtering', k, 'and:', theta_k*180/math.pi)
    print('BS T is : ', T, 'and : ', R)
    b = theta_k*(tensor(a1,a2.dag()) - tensor(a1.dag(),a2))
    return b.expm()

'''Define Schrodinger cat states'''
def cat_plus(alpha):
    cat = (1/(np.sqrt(2)*np.sqrt(1+np.e**(-alpha*alpha.conj()))))*(coherent(N_dim,-alpha)+(coherent(N_dim,alpha)))
    return cat
def cat_minus(alpha):
    cat = (1/(np.sqrt(2)*np.sqrt(1-np.e**(-alpha*alpha.conj()))))*(-coherent(N_dim,-alpha)+(coherent(N_dim,alpha)))
    return cat

'''Define superpositions of squeezed vacuum (SSV), which are
superpositions where the vacuum has been squeezed and then
displaced along the antisqueezed axis of the squeezing, in a
way where the number of elements in the sum gives the degree
of symmetry of the state. Degree 2 SSV states are equivalent
to cat states, but with a different squeezing parameter'''
def SSV_plus(r,alpha):
    state = ket2dm((displace(N_dim,alpha)+displace(N_dim,-alpha))
                   *squeeze(N_dim,r)*fock(N_dim,0))
    norm_state = state/state.tr()
    return norm_state
def SSV_minus(r,alpha):
    state = ket2dm((displace(N_dim,alpha)-displace(N_dim,-alpha))
                   *squeeze(N_dim,r)*fock(N_dim,0))
    norm_state = state/state.tr()
    return norm_state

'''General SSV state, where r is the squeezing parameter, amp is the
displacement amplitude, and M is the degree of symmetry.'''
def SSV_gen(r,amp,M):
    state=0;
    for k in range(M):
        state+=displace(N_dim,amp*np.e**(2j*k*np.pi/M))*squeeze(N_dim,r*np.e**(4j*k*np.pi/M))*fock(N_dim,0)
    norm_state=ket2dm(state.unit())
    return norm_state

'''Degree 3 symmetry SSV state'''
def sq_tri_cat(r,amp):
    state=ket2dm((displace(N_dim,amp)*squeeze(N_dim,r)*fock(N_dim,0)
                  +displace(N_dim,amp*np.e**(2j*np.pi/3))
                  *squeeze(N_dim,r*np.e**(4j*np.pi/3))*fock(N_dim,0)
                  +displace(N_dim,amp*np.e**(4j*np.pi/3))
                  *squeeze(N_dim,r*np.e**(2j*np.pi/3))*fock(N_dim,0)))
    norm_state=state/state.tr()
    return norm_state

'''Degree 4 symmetry SSV state'''
def sq_quad_cat(r,amp):
    state=ket2dm((displace(N_dim,amp)*squeeze(N_dim,r)*fock(N_dim,0)
                  +displace(N_dim,amp*np.e**(2j*np.pi/4))
                  *squeeze(N_dim,r*np.e**(4j*np.pi/4))*fock(N_dim,0)
                  +displace(N_dim,amp*np.e**(4j*np.pi/4))
                  *squeeze(N_dim,r*np.e**(8j*np.pi/4))*fock(N_dim,0)
                  +displace(N_dim,amp*np.e**(6j*np.pi/4))
                  *squeeze(N_dim,r*np.e**(12j*np.pi/4))*fock(N_dim,0)))
    norm_state=state/state.tr()
    return norm_state

'''Utilize positive-operator value measurments (POVM) of the detector
to define a PNR detector with efficiency eta'''
def pnr_resolution_detector(eta, click, n_truc):
    pi_n = 0;
    l = np.arange(click,n_truc)
    for i in l:
        pi_n +=  n_choose_k(i,click)*math.pow((1-eta),(i-click))*math.pow(eta,click)*fock(N_dim,i)*fock(N_dim,i).dag()
        #print("The final Povm element is: ", pi_0)
    return Qobj(pi_n)

'''Performs photon catalysis with Fock state input.  Both inputs
are density matrices, and the returned output mode is a normalized
density matrix after the PNR detection'''
def Fock_Filter_povm(in_state,in_fock,refl,num_det,eta,n_truc):
    Projector = tensor(pnr_resolution_detector(eta, num_det, n_truc),qeye(N_dim));
    Initial_state=tensor(in_state,ket2dm(fock(N_dim,in_fock)));
    theta_k=np.arccos(np.sqrt(refl));

    BS1= ((theta_k)*(tensor(a1,a2.dag()) - tensor(a1.dag(),a2))).expm()

    Rho=BS1*Initial_state*BS1.dag();

    Rho_filtered = ((Rho*Projector).ptrace(1))/((Rho*Projector).tr())
    '''The operation .ptrace(m) takes the partial trace over every mode
    EXCEPT m, where the numbering startes at 0.  So .ptrace(1) means
    you keep mode 1, which is actually the 2nd mode'''
    print('BS has reflectivity',refl,' and I am detecting the |',num,
          '> state,where my detector has efficiency', eta)
    return Rho_filtered

'''Performs photon catalysis with Fock state input and calculates
the probability of success.'''
def Fock_Filter_prob(in_state,in_fock,refl,num_det,eta,n_truc):
    Projector = tensor(pnr_resolution_detector(eta, num_det, n_truc),qeye(N_dim));
    Initial_state=tensor(in_state,ket2dm(fock(N_dim,in_fock)));
    theta_k=np.arccos(np.sqrt(refl));

    BS1= ((theta_k)*(tensor(a1,a2.dag()) - tensor(a1.dag(),a2))).expm()

    Rho=BS1*Initial_state*BS1.dag();
    P=(Rho*Projector).tr()
    print('The probability of a sucessful detection is:',P)
    Rho_filtered = ((Rho*Projector).ptrace(1))/((Rho*Projector).tr())
    #Rho_filtered=Rho*Projector
    '''The operation .ptrace(m) takes the partial trace over every mode
    EXCEPT m, where the numbering startes at 0.  So .ptrace(1) means you
    keep mode 1, which is actually the 2nd mode'''
    print('BS has reflectivity',refl,' and I am detecting the |',num,
          '> state, where my detector has efficiency', eta)
    return Rho_filtered

'''Generic photon catalysis where the two input states are allowed to be
arbitrary. Takes in two density matrices and returns a density matrix
along with success probability.'''
def catalysis(in1,in2,refl,num_det,eta,n_truc):
    Projector = tensor(pnr_resolution_detector(eta, num_det, n_truc),qeye(N_dim));
    Initial_state=tensor(in1,in2);
    theta_k=np.arccos(np.sqrt(refl));

    BS1= ((theta_k)*(tensor(a1,a2.dag()) - tensor(a1.dag(),a2))).expm()

    Rho=BS1*Initial_state*BS1.dag();

    P=(Rho*Projector).tr()
    print('The probability of a sucessful detection is:',P)

    Rho_filtered = ((Rho*Projector).ptrace(1))/((Rho*Projector).tr())
    '''The operation .ptrace(m) takes the partial trace over every mode
    EXCEPT m, where the numbering startes at 0.  So .ptrace(1) means you
    keep mode 1, which is actually the 2nd mode'''
    print('BS has reflectivity',refl,' and I am detecting the |',num,
          '> state, where my detector has efficiency', eta)
    return Rho_filtered

'''Defines the fidelity between two arbitrary quantum states'''
def fid(state1,state2):
    F=np.absolute((state1.sqrtm()*state2*state1.sqrtm()).sqrtm().tr())
    return F

```

```python
def my_hex_GKP(mu, d, delta, cutoff, nmax=400):
    r"""Hexagonal GKP code state.
    The Hex GKP state is defined by
    .. math::
        |mu> = \sum_{n_1,n_2=-\infty}^\infty e^{-i(q+\sqrt{3}p)/2}
            \sqrt{4\pi/\sqrt{3}d}(dn_1+\mu) e^{iq\sqrt{4\pi/\sqrt{3}d}n_2}|0>
    where d is the dimension of a code space, \mu=0,1,...,d-1, |0> is the
    vacuum state, and the states are modulated by a Gaussian envelope in the
    case of finite energy:
    ..math:: e^{-\Delta ^2 n}|\mu>
    Args:
        d (int): the dimension of the code space.
        mu (int): mu=0,1,...,d-1.
        delta (float): width of the modulating Gaussian envelope.
        cutoff (int): the Fock basis truncation of the returned density matrix.
        nmax (int): the Hex GKP state |mu> is calculated by performing the
            sum using n1,n1=-nmax,...,nmax.
    Returns:
        Density Matrix: a size [cutoff] complex Qobj.
    """
    n1 = np.arange(-nmax, nmax+1)[:, None]
    n2 = np.arange(-nmax, nmax+1)[None, :]

    n1sq = n1**2
    n2sq = n2**2

    sqrt3 = np.sqrt(3)

    #arg1 = -1j*np.pi*n2*(d*n1+mu)/d
    arg1 = -1j*np.pi*n2*(d*n1+mu)/d
    arg2 = -np.pi*(d**2*n1sq+n2sq-d*n1*(n2-2*mu)-n2*mu+mu**2)/(sqrt3*d)
    arg2 *= 1-np.exp(-2*delta**2)

    #amplitude = (np.exp(arg1)*np.exp(arg2)).flatten()[:, None]
    amplitude = (np.exp(arg1)).flatten()[:, None]

    alpha = np.sqrt(np.pi/(2*sqrt3*d)) * (sqrt3*(d*n1+mu) - 1j*(d*n1-2*n2+mu))
    #alpha *= np.exp(-delta**2)

    alpha = alpha.flatten()[:, None]
    n = np.arange(cutoff)[None, :]
    coherent = np.exp(-0.5*np.abs(alpha)**2)*alpha**n/np.sqrt(fac(n))

    #hex_state = np.sum(amplitude*coherent*np.exp(-n*delta**2), axis=0)
    hex_state = ket2dm(Qobj(np.sum(amplitude*coherent*np.exp(-n*delta**2), axis=0)))
    final=hex_state/hex_state.tr()
    return final

def sqrGKP(mu, d, delta, cutoff, nmax=400):
    """Square grid GKP state
    The GKP state is defined by
    .. math::
        |mu> = \sum_{n_1,n_2=-\infty}^\infty e^{-i(p)
            \sqrt{2\pi/d}(dn_1+\mu)} e^{iq\sqrt{2\pi/d}n_2}|0>
    where d is the dimension of a code space, \mu=0,1,...,d-1, |0> is the
    vacuum state, and the states are modulated by a Gaussian envelope in the
    case of finite energy:
    ..math:: e^{-\Delta ^2 n}|\mu>
    Args:
        d (int): the dimension of the code space.
        mu (int): mu=0,1,...,d-1.
        delta (float): width of the modulating Gaussian envelope.
        cutoff (int): the Fock basis truncation of the returned density matrix.
        nmax (int): the GKP state |mu> is calculated by performing the
            sum using n1,n1=-nmax,...,nmax.
    Returns:
        Density Matrix: a size [cutoff] complex Qobj.
    """
    n1 = np.arange(-nmax, nmax+1)[:, None]
    n2 = np.arange(-nmax, nmax+1)[None, :]

    n1sq = n1**2
    n2sq = n2**2

    sqrt3 = np.sqrt(3)

    #arg1 = -1j*np.pi*n2*(d*n1+mu)/d
    arg1 = 1j*np.pi*n2*(d*n1+mu)/d

    amplitude = (np.exp(arg1)).flatten()[:, None]

    alpha = np.sqrt(np.pi/(d)) * ((d*n1+mu - 1j*n2))

    alpha = alpha.flatten()[:, None]
    n = np.arange(cutoff)[None, :]
    coherent = np.exp(-0.5*np.abs(alpha)**2)*alpha**n/np.sqrt(fac(n))

    #hex_state = np.sum(amplitude*coherent*np.exp(-n*delta**2), axis=0)
    sqr_state = ket2dm(Qobj(np.sum(amplitude*coherent*np.exp(-n*delta**2), axis=0)))
    final=sqr_state/sqr_state.tr()
    return final
```

```python

```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, C.D. Cooper, G.F. Forsyth.

# Riding the wave

We've reached the third lesson of the module _Riding the wave: Convection problems_, where we explore the numerical solution of conservation laws. We learned in the [first lesson](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_01_conservationLaw.ipynb) how to think about conservation laws, starting with the most intuitive case: that of conservation of mass. But there are many physical quantities that can be conserved: energy, for example. Or cars on a road.

Developing a conservation law for traffic in a one-lane roadway is fun, because we can relate to the insights it gives. We've all experienced traffic slowing down to a crawl when the density of cars gets very high, and stepping on the accelerator pedal with glee when there are no cars on the road!

In the previous two lessons, we developed a traffic-flow model and explored [different choices of numerical scheme](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_02_convectionSchemes.ipynb).  Not everything worked as you expected, and by now you might be realizing how some restrictions come about from the numerical methods themselves, while others still are imposed by the models we use. This lesson will develop a better traffic model, and also show you some impressive SymPy kung-fu.

## Traffic flow, revisited

### A better flux model

Like you saw in the first lesson, cars obey a typical conservation law,

$$
\begin{equation}
\frac{\partial \rho}{\partial t} + \frac{\partial F}{\partial x} = 0
\end{equation}
$$

where $F$ is the flux, $F=\rho u$—flux equals density times velocity. From our experience on the road, we know that the traffic speed is a function of traffic density, and we proposed as a first approximation a linear relation between the two. Thus,

$$
\begin{equation}
F(\rho) = \rho \, u_{max}\left(1 - \frac{\rho}{\rho_{max}} \right)
\end{equation}
$$

This flux model meets the two requirements, based on a qualitative view of traffic flow, that:
1. $u \rightarrow u_{max}$ and $F\rightarrow 0$ when $\rho \rightarrow 0$.
2. $u \rightarrow 0$ as $\rho \rightarrow \rho_{max}$

However, it leads to some unrealistic or at least improbable results.  For example, note that if the traffic speed is a linear function of density, the flux function will be quadratic (see Figure 1). In this case, the maximum flux will occur when $\rho^{\star} = \rho_{max}/2$, corresponding to a traffic speed $u_{max}/2$.

#### Figure 1. Traffic speed (left) and flux (right) vs. density.

A good question to ask here is:  should the maximum flux on a given stretch of road have a strict dependence on the maximum speed that the roadway allows, be it by physical restrictions or posted speed limits? In other words, do we really expect the maximum flux to increase if we allow arbitrarily high speeds?

Probably not.  But there *should* be some ideal traffic speed, $u^{\star}$, corresponding to an ideal traffic density, $\rho^{\star}$, resulting in the maximum traffic flux:

$$
\begin{equation}
F_{\rm max} = \rho^{\star}u^{\star}
\end{equation}
$$

Let us improve the initial flux model by taking this observation into account. One thing that we can try is to introduce a flux model that is cubic in $\rho$ instead of quadratic:

$$
\begin{equation}
F(\rho) = u_{\rm max}\rho (1 - A\rho - B \rho^2)
\end{equation}
$$

This new model still meets the first criterion listed above: $F\rightarrow 0$ when $\rho \rightarrow 0$. Moreover, we impose the following conditions:

* When $\rho = \rho_{\rm max}$ traffic flux goes to zero:

$$
\begin{equation}
F(\rho_{\rm max}) = 0 = u_{\rm max}\, \rho_{\rm max}(1 - A \rho_{\rm max} - B \rho_{\rm max}^2)
\end{equation}
$$

* Based on eq. (3), maximum flux occurs when $\rho = \rho^{\star}$ and $F'(\rho^{\star}) = 0$:

$$
\begin{equation}
F'(\rho^{\star}) = 0  = u_{\rm max}(1 - 2A\rho^{\star} - 3B(\rho^{\star})^2)
\end{equation}
$$

* $u^{\star}$ is obtained when $\rho = \rho^{\star}$:

$$
\begin{equation}
u^{\star} = u_{\rm max}(1 - A \rho^{\star} - B(\rho^{\star})^2)
\end{equation}
$$

We have three equations and four unknowns $A,B,\rho^{\star}, u^{\star}$. However, in practice, the ideal traffic speed could be obtained for a given road by observations. Similarly to $u_{max}$ and $\rho_{max}$ it will therefore be taken as a parameter.

### Solving the new flux equation

Equations $(5)$, $(6)$, and $(7)$ are not incredibly difficult to solve with pen and paper. Instead of following that route, we can use [SymPy](http://sympy.org/en/index.html), the symbolic mathematics library of Python.  You used SymPy already in the [Burgers' equation lesson](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_04_1DBurgers.ipynb) of Module 2, _"Space & Time"_, and you will learn some new functionalities here.

We begin by importing SymPy, initializing $\LaTeX$ printing and defining a set of symbolic variables that we'll use in the calculations. Remember: variables are not defined automatically, you have to tell SymPy that a name will correspond to a symbolic variable by using the keyword `symbols`. This behavior is different from many other symbolic math systems that implicitly construct symbols for you, so you may be perplexed if you've used these other sytems before. The reason for this behavior in SymPy is that the system is fully built on Python, a general-purpose language that needs you to define all objects before using them.

```python
import sympy
sympy.init_printing()

(u_max, u_star, rho_max,
 rho_star, A, B) = sympy.symbols('u_max u_star rho_max rho_star A B')
```

Notice that we used the special character `_` (the under-dash) to create a symbol with a subscript. Here, for example, we've created the symbols $u_{max}$ and $u_{star}$, representing $u_{\rm max}$ and $u^{\star}$ ($u$-star) in the equations, and assigned them to the variable names `u_max` and `u_star`. SymPy also allows you to create symbols with a superscript by means of the special character `^`. Be careful, though: SymPy is built on Python, so you denote an exponent in an expression by `**` (this may also be different from other symbolic math systems that you have used in the past).

Next, use `sympy.Eq()` to define the three equations—corresponding to Equations $(5)$, $(6)$ and $(7)$, above—in terms of symbolic variables. The function `sympy.Eq()` creates an equality between two SymPy objects, passed as arguments separated by a comma. We need to remember that the equal sign in Python is used for variable assignment, and for that reason it cannot be used to build a symbolic equation with SymPy. That is why we need `sympy.Eq()` to create symbolic equalities. But the equal sign _is_ used to _assign_ an equation to a name: here, `eq1`, `eq2`, `eq3` are names for the symbolic equalities we create with `sympy.Eq()`.

```python
eq1 = sympy.Eq(0, u_max * rho_max * (1 - A * rho_max - B * rho_max**2))
eq2 = sympy.Eq(0, u_max * (1 - 2 * A * rho_star - 3 * B * rho_star**2))
eq3 = sympy.Eq(u_star, u_max * (1 - A * rho_star - B * rho_star**2))
```

Check this out: you can display these equations in pretty typeset mathematics just by executing their name in a code cell:

```python
eq1
```

```python
eq2
```

```python
eq3
```

As stressed above, we have three equations with three unknowns (assuming we have some observed value of $u_{star}$, corresponding to the ideal traffic speed)—there must be a solution!

To eliminate the term with $B$ in `eq2`, leaving it only in terms of $A$ and $\rho^{\star}$, we might subtract `3*eq3`.  But this will not work in SymPy if you attempt equation subtraction using the equation names. Just like we couldn't use the equal sign (which in Python means assignment) to create a symbolic equality (needing `sympy.Eq` instead), we can't use mathematical operators directly on the SymPy equations, because they are really _equalities_. What does it mean to subtract two _equalities_? It doesn't make sense. Try it ...

```python
eq2 - 3 * eq3
```

See?  SymPy just printed out what you suggested but it did not manipulate algebraically the left and right sides of `eq2` like we were aiming for. What we _can_ do is create a _new_ equation, perform the left-hand side (LHS) and right-hand side (RHS) operations separately and then recombine them into our desired result. For this, it is helpful to know that there are built-in properties of a SymPy equality for the left-hand side and the right-hand side of the equality. Check it out:

```python
eq4 = sympy.Eq(eq2.lhs - 3 * eq3.lhs, eq2.rhs - 3 * eq3.rhs)
eq4
```

That still needs a little work. SymPy offers several methods to help reduce expressions. You can use [`simplify()`](http://docs.sympy.org/latest/modules/simplify/simplify.html?highlight=simplify#sympy.simplify.simplify.simplify) to attempt to make the expression "simpler," but you can imagine that the quality of an expression being simple is not well defined. One may expect the simpler expression to be shorter, maybe; but not always. The SymPy `simplify()` function applies several strategies, heuristically, to give you an equivalent reduced expression. But some expressions are uncooperative and `simplify()` gives up, returning the argument unchanged. Let's see what it can do with our expression `eq4`.

```python
eq4.simplify()
```

That is actually a useful result. We see that `eq4` allows solving for $\rho^{\star}$ in terms of $A$, for example (remember that $u_{max}$ is known: the road's maximum velocity). Then we could try to manipulate `eq1` to solve for $B$ in terms of $A$, as well, so that we can substitute back in `eq2` leaving everything in terms of $A$. We can do all that without the help of `simplify()`, but using it interactively to reason about the long expression helped us to decide on a course of action.

Another SymPy function that can help us examine complicated expressions is [`expand()`](http://docs.sympy.org/latest/modules/core.html?highlight=expand#sympy.core.function.expand). Its purpose is to expand bracketed factors in expressions and group powers of symbols. Let's see what that does here. Notice first that `eq4` hasn't changed; we just printed the result of applying `simplify()` to it.

```python
eq4
```

And now we print the result of applying `expand()` to it.

```python
eq4.expand()
```

That's very similar to our previous result, except without the bracketed factor. Simplifying an expression can be accomplished by expanding or factoring terms, or a combination of the two. Whether to `simplify()` or `expand()` depends on the situation and you just have to experiment.

We now have an idea of what to do with our three equations. We'll get expressions in terms of $A$ from `eq4` and `eq1` and substitute them back into `eq2`. For that, we can use the SymPy functions [`solve()`](http://docs.sympy.org/dev/modules/solvers/solvers.html#sympy.solvers.solvers.solve) and `subs()`, respectively. The arguments to `solve()` are the equality that needs to be solved, and the symbol to solve for.

**Note**: `sympy.solve()` always returns results in a *list*, since you often end up with multiple solutions for a given variable.  These linear equations will only return one solution, so we can skip a step and ask right away for the `[0]`-th element of the list.

```python
rho_sol = sympy.solve(eq4, rho_star)[0]
rho_sol
```

```python
B_sol = sympy.solve(eq1, B)[0]
B_sol
```

The arguments to the SymPy function [`subs()`](http://docs.sympy.org/dev/modules/core.html#sympy.core.basic.Basic.subs) are given as (old, new) pairs, where the "new" expression substitutes the "old" one. So here, in `eq2`, we substitute `rho_sol` in place of `rho_star` and we substitute `B_sol` in place of `B`.

```python
quadA = eq2.subs([(rho_star, rho_sol), (B, B_sol)])
quadA
```

```python
quadA.simplify()
```

It's a little bit ugly, but that is quadratic in $A$, and that means we can solve for the roots of the equation. SymPy's `solve()` function in this case will return a list with the two roots. They are long expressions, so let's print each root separately.

```python
A_sol = sympy.solve(quadA, A)
```

```python
A_sol[0]
```

```python
A_sol[1]
```

### Evaluating the new flux equation

Quadratic equations, of course, have two solutions. Here we have to select the positive root, otherwise our model would have an inconsistency. Are you able to see why?  To determine its value, we need to fill in some actual numbers into the model.

Let's start with the same numerical values that we used in [lesson 1](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_01_conservationLaw.ipynb) for $\rho_{\rm max}$ and $u_{\rm max}$. But we also have to supply a value for $u^{\star}$, a quantity that should be experimentally observed in a given road. We propose $u^{\star} = 0.7\, u_{\rm max}$ for this exercise.  This would correspond to 84 km/h for a highway with a 120-km/h speed limit, for example.

Let's numerically evaluate the solutions for $A$ using the following values:

$$
\begin{align}
\rho_{\rm max} &=10.0 \nonumber\\ u_{\rm max} &=1.0 \nonumber\\ u^{\star} &=0.7 \nonumber
\end{align}
$$

Now evaluate the numeric result for each root of $A$ using the `evalf()` function, where you pass the numeric substitution as an argument. It's very cool.  Let's evaluate both roots and we will pick the positive one.

Let's try the `[0]`-th solution for $A$, first:

```python
A_val_0 = A_sol[0].evalf(subs={u_star: 0.7, u_max: 1.0, rho_max: 10.0})
A_val_0
```

Now let's try the `[1]`-th solution for $A$:

```python
A_val_1 = A_sol[1].evalf(subs={u_star: 0.7, u_max: 1.0, rho_max: 10.0})
A_val_1
```

Because Sympy seems not to return the roots in a deterministic order and  we want to use the positive root, we will automatically pick this one by using the Python built-in `max` function.

```python
A_val = max(A_val_0, A_val_1)
A_val
```

Now, numerically evaluate $B$ in the same way using the positive root `A_val`:

```python
B_val = B_sol.evalf(subs={rho_max: 10.0, A: A_val})
B_val
```

### Turn off $\LaTeX$

$\LaTeX$ output is great when we're looking at algebraic expressions, but it's a little too much for simple numeric output.  Let's turn it off for the rest of the exercise:

```python
sympy.init_printing(use_latex=False)
```

## Green light: take 2

Let's re-examine the green-light problem from [lesson 1](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_01_conservationLaw.ipynb) but using our newly computed traffic-flux equation.  We shouldn't have to change much—in fact, we can simply create a new `flux` function and leave the rest of the code unchanged.

There's one last bit of housekeeping to do so we don't run into trouble -- we used the variables `rho_max` and `u_max` in our SymPy code and we defined them as SymPy `symbols`.  You can check on the status of a variable using `type()`.

```python
print(type(rho_max), type(u_max))
```

    <class 'sympy.core.symbol.Symbol'> <class 'sympy.core.symbol.Symbol'>

If we try to use SymPy variables with NumPy arrays, Python is going to be very unhappy.  We can re-define them as floats and avoid that messy situation.

```python
rho_max = 10.0
u_max = 1.0
```

```python
def flux(rho, u_max, A, B):
    """
    Computes the traffic flux for the better model.

    Parameters
    ----------
    rho : numpy.ndarray
       Traffic density along the road as a 1D array of floats.
    u_max : float
        Maximum speed allowed on the road.
    A : float
        Scaling coefficient for rho.
    B : float
        Scaling coefficient for rho squared.

    Returns
    -------
    F : numpy.ndarray
        The traffic flux along the road as a 1D array of floats.
    """
    F = rho * u_max * (1.0 - A * rho - B * rho**2)
    return F
```

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

```python
def rho_green_light(x, rho_light):
    """
    Computes the "green light" initial condition.
    It consists of a shock with a linear distribution behind it.

    Parameters
    ----------
    x : numpy.ndarray
        Locations on the road as a 1D array of floats.
    rho_light : float
        Car density at the stoplight.

    Returns
    -------
    rho : numpy.ndarray
        The initial car density along the road
        as a 1D array of floats.
    """
    rho = numpy.zeros_like(x)
    mask = numpy.where(x < 2.0)
    rho[mask] = rho_light * x[mask] / 2.0
    return rho
```

```python
# Set parameters.
nx = 81  # number of locations on the road
L = 4.0  # length of the road
dx = L / (nx - 1)  # distance between two consecutive locations
nt = 30  # number of time steps to compute
rho_light = 5.0  # car density at the traffic light.

# Define the locations on the road.
x = numpy.linspace(0.0, L, num=nx)

# Compute the initial traffic density.
rho0 = rho_green_light(x, rho_light)
```

```python
# Plot the initial car density on the road.
fig = pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel(r'$x$')
pyplot.ylabel(r'$\rho$')
pyplot.grid()
line = pyplot.plot(x, rho0,
                   color='C0', linestyle='-', linewidth=2)[0]
pyplot.xlim(0.0, L)
pyplot.ylim(-0.5, 6.0)
pyplot.tight_layout();
```

```python
def ftbs(rho0, nt, dt, dx, bc_value, *args):
    """
    Computes the traffic density on the road
    at a certain time given the initial traffic density.

    Parameters
    ----------
    rho0 : numpy.ndarray
        The initial car density along the road
        as a 1D array of floats.
    nt : integer
        The number of time steps to compute.
    dt : float
        The time-step size to integrate.
    dx : float
        The distance between two consecutive locations.
    bc_value : float
        The constant density at the first station.
    args : list or tuple
        Positional arguments to be passed to the flux function.

    Returns
    -------
    rho_hist : list of numpy.ndarray objects
        The history of the car density along the road.
    """
    rho_hist = [rho0.copy()]
    rho = rho0.copy()
    for n in range(nt):
        # Compute the flux.
        F = flux(rho, *args)
        # Advance in time.
        rho[1:] = rho[1:] - dt / dx * (F[1:] - F[:-1])
        # Set the left boundary condition.
        rho[0] = bc_value
        # Record the time-step solution.
        rho_hist.append(rho.copy())
    return rho_hist
```

```python
# Set time-step size based on CFL limit.
sigma = 1.0
dt = sigma * dx / u_max  # time-step size

# Compute the traffic density at all time steps.
rho_hist = ftbs(rho0, nt, dt, dx, rho0[0], u_max, A_val, B_val)
```

Now that we have computed the history of the car density along the road, let's create an animation to visualize the results.

```python
from matplotlib import animation
from IPython.display import HTML
```

```python
def update_plot(n, rho_hist):
    """
    Update the line y-data of the Matplotlib figure.

    Parameters
    ----------
    n : integer
        The time-step index.
    rho_hist : list of numpy.ndarray objects
        The history of the numerical solution.
    """
    fig.suptitle('Time step {:0>2}'.format(n))
    line.set_ydata(rho_hist[n])
```

```python
# Create an animation of the traffic density.
anim = animation.FuncAnimation(fig, update_plot,
                               frames=nt, fargs=(rho_hist,),
                               interval=100)
# Display the video.
HTML(anim.to_html5_video())
```

That definitely looks different!  Do you think this is more or less accurate than our previous model?

##### Dig Deeper

The new traffic-flux model that we developed here changes the way that traffic patterns evolve. In this lesson, we only experimented with the most basic scheme: forward-time, backward-space.  Try to implement the green-light problem using one of the second-order schemes from [Lesson 2](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_02_convectionSchemes.ipynb).

## References

* Neville D. Fowkes and John J. Mahony, *"An Introduction to Mathematical Modelling,"* Wiley & Sons, 1994. Chapter 14: Traffic Flow.

---

###### The cell below loads the style of the notebook.

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```


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

```python
from tensorflow.contrib.layers import fully_connected
from sklearn.preprocessing import MultiLabelBinarizer
```

```python
from Scripts.CreateTrainingBatches import CreateTrainingBatches
```

```python
with open(os.path.join('Data','data_X_y.p'), 'rb') as handle:
    data_X_y = pickle.load(handle)

with open(os.path.join('Data','training_params.p'), 'rb') as handle:
    training_params = pickle.load(handle)
```

```python
from Scripts.Word2VecUtilities import Word2VecUtilities
```

```python
w2v_model= Word2VecUtilities.load_word2vector_model(os.path.join('Data','w2v_model.h5'))
```

```python
X_train, X_valid, X_test = [data_X_y['X_train'],data_X_y['X_valid'], data_X_y['X_test']]
y_train, y_valid, y_test = [data_X_y['y_train'],data_X_y['y_valid'], data_X_y['y_test']]
```

```python
def return_actual_text(x, rev_vocab_dict):
    actual_text = " ".join([rev_vocab_dict[word_id] for word_id in x  if rev_vocab_dict[word_id]!='my_dummy'])
    return actual_text
```

```python
rev_vocab_dict = training_params['rev_vocab_dict']
```

```python
return_actual_text(X_train[0], rev_vocab_dict), y_train[0]
```

```python
vocab_size = training_params['vocab_size']
mlb = MultiLabelBinarizer()
X_train = mlb.fit_transform(X_train)
X_valid = mlb.transform(X_valid)
```

```python
create_training_batches_object = CreateTrainingBatches(X_train, y_train, X_valid, y_valid)
```

```python
def print_metrics(np_prob, np_y):
    neg_accuracy = np.mean((np_prob<0.5)[(np_y==0)])
    pos_accuracy = np.mean((np_prob>0.5)[(np_y==1)])
    accuracy = np.mean((pos_accuracy, neg_accuracy))
    print('Negative accuracy',neg_accuracy)
    print('Positive accuracy',pos_accuracy)
    print('Accuracy', accuracy)
    return accuracy
```

Given an input $x$, the model should be able to map it to $\hat{y}$
\begin{align}
\hat{y} & = \sigma(w^Tx+b) \\
\end{align}
The paramters of the model are the weights $w$ and the bias $b$

```python
tf.reset_default_graph()
learning_rate = 0.01

doc_vectors = tf.placeholder(dtype=tf.float32,shape=[None, vocab_size], name='doc_vectors')
weights = tf.Variable(tf.random_uniform([doc_vectors.get_shape().as_list()[1], 1], minval=-1.0, maxval=1.0), name='weights')
bias = tf.zeros(1, name='bias')
logits = tf.matmul(doc_vectors, weights) + bias
prob = tf.nn.sigmoid(logits, name='prob')
```

The loss is an estimate the difference between the true label $y$ and the predicted label $\hat{y}$
\begin{align}
\ L(\hat{y},y) & = - [y log(\hat{y}) + (1 - y) log(1 - \hat{y})] \\
\end{align}

```python
y = tf.placeholder(tf.float32, [None, 1], name='y')
losses = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=logits, name='x_entropy')
loss = tf.reduce_mean(losses, name='loss')
```

Update the Parameter to minimze the loss
Here $\alpha$ is the learning rate and $\frac{\partial L}{\partial w}$ is the partial derivative of Loss $L$ with respect to weight $w$
\begin{align}
\ w = w - \alpha \frac{\partial L}{\partial w}\\
\ b = b - \alpha \frac{\partial L}{\partial b}\\
\end{align}

```python
with tf.name_scope('train'):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    training_op = optimizer.minimize(loss, name='train_op')
```

```python
file_writer = tf.summary.FileWriter('tf_logs/logistic_regression', tf.get_default_graph())
```

```python
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.InteractiveSession()
init.run()
```

```python
highest_validation_accuracy = 0.5
for i in range(500):
    x_train_samples, y_train_samples = create_training_batches_object.create_training_data()

    _, np_prob, np_y, np_loss = sess.run([training_op, prob, y, loss],
                                         feed_dict={doc_vectors: x_train_samples, y: y_train_samples})
    if i%100==0:
        print('Epoch', i, 'Loss',np_loss)
        x_valid_samples, y_valid_samples = create_training_batches_object.create_validation_data()

        np_prob, np_y, np_loss = sess.run([prob, y, loss],
                                          feed_dict={doc_vectors: x_valid_samples, y: y_valid_samples})

        validation_accuracy = print_metrics(np_prob, np_y)
        if validation_accuracy > highest_validation_accuracy:
            print('Saved model with highest accuracy')
            saver.save(sess, os.path.join('Models', 'tf_models','model.ckpt'))
            highest_validation_accuracy = validation_accuracy
        print('-----------------------------')
```

```python
tf.reset_default_graph()
learning_rate = 0.01

doc_vectors = tf.placeholder(dtype=tf.float32,shape=[None, vocab_size], name='doc_vectors')
layer_one_output = fully_connected(doc_vectors, 100, activation_fn=tf.nn.relu)
logits = fully_connected(layer_one_output,1, activation_fn=None)
prob = tf.nn.sigmoid(logits, name='prob')
```

```python
y = tf.placeholder(tf.float32, [None, 1], name='y')
losses = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=logits, name='x_entropy')
loss = tf.reduce_mean(losses, name='loss')
```

```python
with tf.name_scope('train'):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    training_op = optimizer.minimize(loss, name='train_op')
```

```python
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.InteractiveSession()
init.run()
```

```python
highest_validation_accuracy = 0.5
for i in range(500):
    x_train_samples, y_train_samples = create_training_batches_object.create_training_data()

    _, np_prob, np_y, np_loss = sess.run([training_op, prob, y, loss],
                                         feed_dict={doc_vectors: x_train_samples, y: y_train_samples})
    if i%100==0:
        print('Epoch', i, 'Loss',np_loss)
        x_valid_samples, y_valid_samples = create_training_batches_object.create_validation_data()

        np_prob, np_y, np_loss = sess.run([prob, y, loss],
                                          feed_dict={doc_vectors: x_valid_samples, y: y_valid_samples})

        validation_accuracy = print_metrics(np_prob, np_y)
        if validation_accuracy > highest_validation_accuracy:
            print('Saved model with highest accuracy')
            saver.save(sess, os.path.join('Models', 'tf_models','model.ckpt'))
            highest_validation_accuracy = validation_accuracy
        print('-----------------------------')
```

```python
X_train, X_valid, X_test = [data_X_y['X_train'],data_X_y['X_valid'], data_X_y['X_test']]
y_train, y_valid, y_test = [data_X_y['y_train'],data_X_y['y_valid'], data_X_y['y_test']]
create_training_batches_object = CreateTrainingBatches(X_train, y_train, X_valid, y_valid)
```

```python
embedding_matrix = training_params['embedding_matrix']

tf.reset_default_graph()

X = tf.placeholder(dtype=tf.int32,shape=[None, 300], name='X')
y = tf.placeholder(tf.float32, [None, 1], name='y')
tf_keep_prob = tf.placeholder(tf.float32, name='tf_keep_prob')
tf_embedding_matrix = tf.Variable(initial_value=embedding_matrix, trainable=False,
                                  dtype=tf.float32, name='tf_embedding_matrix')
X_embeddings = tf.nn.embedding_lookup(tf_embedding_matrix, X, name='X_embeddings')
```

```python
n_neurons = 50
learning_rate = 0.01
```

```python
with tf.variable_scope('RNN', initializer=tf.contrib.layers.xavier_initializer()):
    rnn_cell = tf.contrib.rnn.GRUCell(num_units=n_neurons)
    outputs, states = tf.nn.dynamic_rnn(rnn_cell, X_embeddings, dtype=tf.float32)
    doc_vectors = tf.concat(states, 1, name='conc_outputs')

logits = fully_connected(doc_vectors, 1, activation_fn=None)
prob = tf.nn.sigmoid(logits, name='prob')

x_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=logits, name='x_entropy')
loss = tf.reduce_mean(x_entropy, name='loss')

with tf.name_scope('train'):
    optimizer = tf.train.AdamOptimizer(learning_rate)
    training_op = optimizer.minimize(loss, name='train_op')
```

```python
init = tf.global_variables_initializer()
saver = tf.train.Saver()
sess = tf.InteractiveSession()
init.run()
```

```python
highest_validation_accuracy = 0.5
for i in range(500):
    x_train_samples, y_train_samples = create_training_batches_object.create_training_data()

    _, np_prob, np_y, np_loss = sess.run([training_op, prob, y, loss],
                                              feed_dict={X: x_train_samples,
                                                         y: y_train_samples,
                                                         tf_keep_prob: 0.8})
    if i%100==0:
        print('Epoch', i, 'Loss',np_loss)
        x_valid_samples, y_valid_samples = create_training_batches_object.create_validation_data()

        np_prob, np_y, np_loss = sess.run([prob, y, loss],
                                                  feed_dict={X: x_valid_samples,
                                                             y: y_valid_samples,
                                                             tf_keep_prob: 1})
        validation_accuracy = print_metrics(np_prob, np_y)
        if validation_accuracy > highest_validation_accuracy:
            print('Saved model with highest accuracy')
            saver.save(sess, os.path.join('Models', 'tf_models','model.ckpt'))
            highest_validation_accuracy = validation_accuracy
        print('-----------------------------')
```