```python
# %load ../../preconfig.py
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(color_codes=True)
plt.rcParams['axes.grid'] = False

#import numpy as np
#import pandas as pd

#import sklearn

#import itertools

import logging
logger = logging.getLogger()
```

25 All-Pairs Shortest Paths
===========================

### 25.1 Shortest paths and matrix multiplication
This section presents a dynamic-programming algorithm:

1. Characterize the structure of an optimal solution.
   all subpaths of a shortest path are shortest paths.

2. Recursively define the value of an optimal solution.
   let $l_{i, j}^{(m)}$ be the minimum weight of any path from vertex $i$ to vertex $j$ that contains at most $m$ edges. Thus,
   \begin{equation}
       l_{i, j}^{(0)} = \begin{cases}
           0 & \quad \text{if } i = j \\
           \infty & \quad \text{otherwise}
       \end{cases}
   \end{equation}

   we recursively define:
   \begin{equation}
       l_{i, j}^{(m)} = \displaystyle \min_{1 \leq k \leq n} \{ l_{i, k}^{(m-1)} + w_{k, j} \}
   \end{equation}

3. Compute the value of an optimal solution in a bottom-up fashion.
   We now compupte a series of matrices $L^{(1)}, L^{(2)}, \dotsc, L^{(n-1)}$, where $L^{(1)} = W$.

```python
plt.imshow(plt.imread('./res/ext_short_path.png'))
```

If we replace $\infty$ by 0 in `EXTEND-SHORTEST-PATHS`, it can be converted to matrix muplication: $L^{(n-1)} = W^{n-1}$.

##### Improving the running time
We can compute $L^{(n-1)}$ by divide-and-conquer methods: $O(n) \to $O(\lg n)$.

Total running time: $O(n^3 n) \to O(n^3 \lg n)$

```python
# Exercises
```

### 25.2 The Floyd-Warshall algorithm
use a different dynamic-programming formulation to solve. $O(V^3)$.

#### The structure of a shortest path
**intermediate** vertex of a simple path $p = <v_1, v_2, \dotsc, v_n>$ is any vertex of $p$ other than $v_1$ or $v_n$.

Consider all paths from $i$ to $j$ whose intermediate vertices are all drawn from $\{1, 2, \dotsc, k\}$, and let $p$ be a minimum-weight path from among them.

+ If $k$ is not an intermediate vertex of path $p$, then all intermediate vertices of path $p$ are in the set $\{1, 2, \dotsc, k-1\}$.

+ If $k$ is an intermediate vertex of path $p$, then we decompose $p$ into $i \overset{p_1}{\to} k \overset{p_2}{\to} j$, so all intermediate vertices of $p_1$ are in the set $\{1, 2, \dotsc, k-1\}$, same with $p_2$.

```python
plt.imshow(plt.imread('./res/fig25_3.png'))
```

#### A recursive solution to the all-pairs shortest-paths problem
Let $d_{i j}^{(k)}$ be the weight of a shortest path from vertex $i$ to vertex $j$ for which all intermediate vertices are in the set $\{1, 2, \dotsc, k\}$.

\begin{equation}
    d_{i j}^{(k)} = \begin{cases}
        w_{i j} & \quad \text{if } k = 0 \\
        \min(d_{i j}^{(k-1)}, d_{i k}^{(k-1)} + d_{k j}^{(k-1)}) & \quad \text{if } k \geq 1
    \end{cases}
\end{equation}

#### Computing the shortest-path weights bottom up

```python
plt.imshow(plt.imread('./res/floyd_warshall.png'))
```

#### Constructing a shortest path

#### Transitive closure of a directed graph
to determine whether $G$ contains a path from $i$ to $j$ for all vetex pairs $i, j \in V$.

This method subsitutes the logical OR and logical AND for the arithmetic operations $\min$ and $+$ in the Floyd_Warshall algorithm.

```python
#Exercises
```

### 25.3 Johnson's algorithm for sparse graphs
+ if all edge weight $w$ in a graph $G = (V, E)$ are nonnegative, find shortest paths by running Dijkstra's algorithm once from each vertext;

+ if $G$ has negative-weight edges but no negative weight cycles, we use *reweighting* to convert it to a graph of nonegative edges.

```python
#Exercises
```


```python
import sympy as sp
import tensorflow as tf
import tensorflow.python.keras.backend as K
import tensorflow_probability as tfp

import MLGeometry as mlg
from MLGeometry import bihomoNN as bnn

import math
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

np.random.seed(42)
tf.random.set_seed(42)
```

    Hello from BihomoNN! Arbitrary dimension!

```python
# Matplotlib options
%matplotlib inline
#%matplotlib notebook
plt.style.use('dark_background')
plt.rcParams['figure.dpi'] = 200
plt.rcParams['figure.figsize'] = [4, 2]
plt.rcParams['text.usetex'] = True
```

## Modules

```python
# Plot training get_loss_hist
def plot_train_hist(model, val=False, y_scale='linear'):
    x1 = np.arange(1, len(model.get_loss_hist())+1)
    plt.scatter(x1, model.get_loss_hist(), marker='.', label='Training loss' )
    #if val:
    #    plt.scatter(x1, model.get_loss_hist['val_loss'], marker='.', label='Validation loss')
    plt.xlabel('Training epochs')
    plt.ylabel('Loss')
    plt.yscale(y_scale)
    plt.legend()
    print("Final loss: ", model.get_loss_hist()[-1])
    return plt.show()
```

```python
def cal_total_loss(dataset, kahler_pot, loss_function, c1):
    total_loss = tf.constant(0, dtype=tf.float32)
    total_mass = tf.constant(0, dtype=tf.float32)

    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset):
        curv_tr = curvature_trace(points, kahler_pot, hermitian_met, mass)
        mass_sum = tf.reduce_sum(mass)
        total_loss += loss_func(c1, curv_tr, mass)  * mass_sum
        total_mass += mass_sum
    total_loss = total_loss / total_mass

    return total_loss.numpy()

```

Networks for $\mathcal{O}_X(1)$, $\mathcal{O}_X(2)$, $\mathcal{O}_X(4)$ respectively.

\begin{itemize}
\item "dim" is the number of projective coordinates of points on the CY hypersurface.
\item "vec" is a vector giving the number of neurons for layers 1 through d-1
\item "rank" is the rank of the vector bundle -- so far should just be 1
\end{itemize}

```python
class Hermitian_metric_O1(tf.keras.Model):
    def __init__(self, dim, rank, vec):
        super(Hermitian_metric_O1, self).__init__()
        self.dim = dim
        self.rank = rank
        self.vec = vec
        self.loss_hist = []
        print("Bundle rank", self.rank)
        self.bihomogeneous = bnn.Bihomogeneous(self.dim)
        self.layer1 = bnn.Dense(self.dim**2, self.rank)

    def append_loss(self, loss):
        self.loss_hist.append([loss])

    def get_loss_hist(self):
        return self.loss_hist

    def call(self, inputs):
        x = self.bihomogeneous(inputs)
        x = tf.math.log(x)
        return x

class Hermitian_metric_O2(tf.keras.Model):
    def __init__(self, dim, rank, vec):
        super(Hermitian_metric_O2, self).__init__()
        self.dim = dim
        self.rank = rank
        self.vec = vec
        self.loss_hist = []
        print("Bundle rank", self.rank)
        self.bihomogeneous = bnn.Bihomogeneous(self.dim)
        self.layer1 = bnn.Dense(self.dim**2, self.vec[0], activation=tf.square)
        self.layer2 = bnn.Dense(self.vec[0], self.rank)

    def append_loss(self, loss):
        self.loss_hist.append([loss])

    def get_loss_hist(self):
        return self.loss_hist

    def call(self, inputs):
        x = self.bihomogeneous(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = tf.math.log(x)
        return x

class Hermitian_metric_O4(tf.keras.Model):
    def __init__(self, dim, rank, vec):
        super(Hermitian_metric_O4, self).__init__()
        self.dim = dim
        self.rank = rank
        self.vec = vec
        self.loss_hist = []
        print("Bundle rank", self.rank)
        self.bihomogeneous = bnn.Bihomogeneous(self.dim)
        self.layer1 = bnn.Dense(dim**2, self.vec[0], activation=tf.square)
        self.layer2 = bnn.Dense(self.vec[0], self.vec[1], activation=tf.square)
        self.layer3 = bnn.Dense(self.vec[1], self.rank)

    def append_loss(self, loss):
        self.loss_hist.append([loss])

    def get_loss_hist(self):
        return self.loss_hist

    def call(self, inputs):
        x = self.bihomogeneous(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = tf.math.log(x)
        return x
```

## Curvature calculation

\begin{itemize}
\item Issue with normalization needs to be fixed here, in weighted_loss
\item Other issue is taking inverse of K\"ahler metric or curvature tensor when calculating $g^{i\bar{j}} F_{i\bar{j}}$
\end{itemize}

```python
def weighted_MAPE(y_true, y_pred, mass):
    weights = mass / K.sum(mass)
    return K.sum(K.abs(y_true - y_pred) / tf.cast(y_true, tf.float32) * weights)

def weighted_loss(y_true, y_pred, mass):
    weights = mass /K.sum(mass)
    #print(4"loss1:", K.sum( K.abs(y_pred - y_true) * weights))
    #print("loss2:", K.sum( K.abs(y_pred - y_true) * weights) / K.sum(weights))
    #return K.abs( K.sum( (y_pred - y_true) * weights) ) / K.sum(weights)
    return K.sum( K.abs(y_pred - y_true) * weights) / K.sum(weights)

@tf.function
def curvature_trace(points, hermitian_met, kahler_pot, herm_metric, mass):
    kahler_metric = mlg.complex_math.complex_hessian(tf.math.real(kahler_pot(points)), points)
    #print("kahler_metric: ", kahler_metric)
    #inv_kahler_metric = tf.linalg.inv(kahler_metric)
    #print("inverse kahler_metric: ", inv_kahler_metric)
    curvature = mlg.complex_math.complex_hessian(tf.math.real(herm_metric(points)), points)
    #print("curvature", curvature)
    #inv_curvature = tf.linalg.inv(curvature)
    curvature_trace = tf.linalg.trace(tf.matmul(kahler_metric, curvature) )
    # print("contraction", tf.matmul(kahler_metric, curvature) )
    # print("curvature_trace: ", curvature_trace)
    return curvature_trace

```

## Generate points and load Kahler potential network

```python
# Define K3 surface
z0, z1, z2, z3 = sp.symbols('z0, z1, z2, z3')
Z = [z0, z1, z2, z3]
f = z0**4 + z1**4 + z2**4 + z3**4 + 0.5*z0*z1*z2*z3
#f = z0**4 + z1**4 + z2**4 + z3**4 # Switch to Fermat surface
f
```

$\displaystyle z_{0}^{4} + 0.5 z_{0} z_{1} z_{2} z_{3} + z_{1}^{4} + z_{2}^{4} + z_{3}^{4}$

```python
# Create training and testing set
n_pairs = 10000
HS_train = mlg.hypersurface.Hypersurface(Z, f, n_pairs)
HS_test = mlg.hypersurface.Hypersurface(Z, f, n_pairs)
train_set = mlg.tf_dataset.generate_dataset(HS_train)
test_set = mlg.tf_dataset.generate_dataset(HS_test)
train_set = train_set.shuffle(HS_train.n_points).batch(1000)
test_set = test_set.shuffle(HS_test.n_points).batch(1000)
```

```python
# Load CY metric/Kahler potential
kahler_pot = tf.keras.models.load_model("K3_2_kahler_pot")
# kahler_pot = tf.keras.models.load_model("K3_fermat_kahler_pot") # Switch to fermat curve
```

    WARNING:tensorflow:No training configuration found in save file, so the model was *not* compiled. Compile it manually.

## O(1)

This has stopped working due to no gradients occuring - $\mathcal{O}_X(2)$ is the first that works

```python
# Define and train model for hermitian metric
hermitian_met = Hermitian_metric_O1(4, 1, [])
print("Hermitian metric G network defined")
c1 = tf.constant(1., shape=(1000), dtype=tf.complex64)
optimizer = tf.keras.optimizers.Adam()
loss_func = weighted_MAPE
loss_hist = []
max_epochs = 100
epoch = 0
#print("Beginning training")
while epoch < max_epochs:
    epoch = epoch + 1
    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set):
        with tf.GradientTape() as tape:
            # det_omega = nf.volume_form(points, Omega_Omegabar, mass, restriction, kahler_pot)
            curv_tr = curvature_trace(points, kahler_pot, hermitian_met, mass)
            #print("calculate curv_tr: ", curv_tr)
            #print("c1: ", c1)
            loss = loss_func(curv_tr, c1, mass)
            grads = tape.gradient(loss, hermitian_met.trainable_weights)
        optimizer.apply_gradients(zip(grads, hermitian_met.trainable_weights))
    #if epoch % 50 == 0:
    #    print("epoch %d: loss = %.7f" % (epoch, loss))
    loss_hist.append(loss)
    hermitian_met.append_loss(loss)
    print("epoch %d: loss = %.7f" % (epoch, loss))
```

```python
plot_train_hist(hermitian_met, y_scale='log')
```

## O(2) -- 2 layer network

```python
# Define and train model for hermitian metric
hermitian_met = Hermitian_metric_O2(4, 1, [100])
print("Hermitian metric G network defined")
c1 = tf.constant(2., shape=(1000), dtype=tf.complex64)
optimizer = tf.keras.optimizers.Adam()
loss_func = weighted_loss
loss_hist = []
max_epochs = 100
epoch = 0
#print("Beginning training")
while epoch < max_epochs:
    epoch = epoch + 1
    # print("epoch: ", epoch)
    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set):
        # print("mass: ", mass)
        with tf.GradientTape() as tape:
            curv_tr = curvature_trace(points, kahler_pot, hermitian_met, mass)
            loss = loss_func(c1, curv_tr, mass)
            grads = tape.gradient(loss, hermitian_met.trainable_weights)
        optimizer.apply_gradients(zip(grads, hermitian_met.trainable_weights))
    #if epoch % 50 == 0:
    #    print("epoch %d: loss = %.7f" % (epoch, loss))
    loss_hist.append(loss)
    hermitian_met.append_loss(loss)
    print("epoch %d: loss = %.7f" % (epoch, loss))
```

    Bundle rank 1
    Hermitian metric G network defined

    /home/rehandeen/miniconda3/envs/ml/lib/python3.7/site-packages/tensorflow/python/framework/indexed_slices.py:437: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor("gradients/boolean_mask/GatherV2_grad/Reshape_1:0", shape=(None,), dtype=int64), values=Tensor("gradients/boolean_mask/GatherV2_grad/Reshape:0", shape=(None, None), dtype=float32), dense_shape=Tensor("gradients/boolean_mask/GatherV2_grad/Cast:0", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.
      "shape. This may consume a large amount of memory." % value)

    epoch 1: loss = 1.8530517
    epoch 2: loss = 1.7761823
    epoch 3: loss = 1.9177244
    epoch 4: loss = 2.1445618
    epoch 5: loss = 1.9240010
    epoch 6: loss = 1.8159497
    epoch 7: loss = 1.8918860
    epoch 8: loss = 1.8607010
    epoch 9: loss = 1.8071690
    epoch 10: loss = 1.8703868
    epoch 11: loss = 1.9045316
    epoch 12: loss = 1.8840197
    epoch 13: loss = 1.9351382
    epoch 14: loss = 1.8825184
    epoch 15: loss = 1.8489228
    epoch 16: loss = 1.8160678
    epoch 17: loss = 1.8275738
    epoch 18: loss = 1.9263090
    epoch 19: loss = 2.1647797
    epoch 20: loss = 1.9420241
    epoch 21: loss = 1.8876498
    epoch 22: loss = 1.7746891
    epoch 23: loss = 1.8622558
    epoch 24: loss = 69.0447388
    epoch 25: loss = 1.8927996
    epoch 26: loss = 67.6524811
    epoch 27: loss = 2.2623255
    epoch 28: loss = 2.7635796
    epoch 29: loss = 1.8788116
    epoch 30: loss = 2.7704821
    epoch 31: loss = 1.8360044
    epoch 32: loss = 1.8098383
    epoch 33: loss = 1.7904291
    epoch 34: loss = 1.9492515
    epoch 35: loss = 2.8494475
    epoch 36: loss = 1.9189346
    epoch 37: loss = 2.8535781
    epoch 38: loss = 1.9496382
    epoch 39: loss = 2.3075874
    epoch 40: loss = 1.9206651
    epoch 41: loss = 1.8794242
    epoch 42: loss = 1.9078844
    epoch 43: loss = 1.8220702
    epoch 44: loss = 1.7780540
    epoch 45: loss = 1.8123091
    epoch 46: loss = 1.8770700
    epoch 47: loss = 1.7924942
    epoch 48: loss = 1.8670156
    epoch 49: loss = 1.8687153
    epoch 50: loss = 1.8470352
    epoch 51: loss = 1.8655076
    epoch 52: loss = 1.9507931
    epoch 53: loss = 1.8912206
    epoch 54: loss = 1.9319706
    epoch 55: loss = 1.8656404
    epoch 56: loss = 2.1391022
    epoch 57: loss = 1.8235198
    epoch 58: loss = 1.8545661
    epoch 59: loss = 2.0132174
    epoch 60: loss = 1.8122526
    epoch 61: loss = 1.8237226
    epoch 62: loss = 1.8498476
    epoch 63: loss = 2.4456494
    epoch 64: loss = 1.7778357
    epoch 65: loss = 1.9233171
    epoch 66: loss = 1.9169573
    epoch 67: loss = 4.0750332
    epoch 68: loss = 1.8947203
    epoch 69: loss = 1.8400772
    epoch 70: loss = 1.7951586
    epoch 71: loss = 2.0553885
    epoch 72: loss = 1.8736607
    epoch 73: loss = 4.1241732
    epoch 74: loss = 2.2956414
    epoch 75: loss = 69.7000351
    epoch 76: loss = 1.7375624
    epoch 77: loss = 1.9428915
    epoch 78: loss = 2.1031342
    epoch 79: loss = 1.9618220
    epoch 80: loss = 1.8304392
    epoch 81: loss = 1.9818938
    epoch 82: loss = 2.0585945
    epoch 83: loss = 1.8535718
    epoch 84: loss = 1.8984205
    epoch 85: loss = 69.2592010
    epoch 86: loss = 1.8892763
    epoch 87: loss = 1.9996934
    epoch 88: loss = 72.7105942
    epoch 89: loss = 1.8821390
    epoch 90: loss = 1.8867180
    epoch 91: loss = 1.9367231
    epoch 92: loss = 1.8597287
    epoch 93: loss = 2.2476821
    epoch 94: loss = 1.8916152
    epoch 95: loss = 1.8896673
    epoch 96: loss = 1.8957443
    epoch 97: loss = 1.8553892
    epoch 98: loss = 70.5552216
    epoch 99: loss = 1.8582850
    epoch 100: loss = 1.9341935

```python
plot_train_hist(hermitian_met, y_scale='linear')
```

Validation/test data

```python
cal_total_loss(test_set, hermitian_met, kahler_pot, weighted_loss, c1)
```

    2.2119193

## O(4) -- 3 layer network

Seems to train reasonably well compared to the others,

```python
# Define and train model for hermitian metric
hermitian_met = Hermitian_metric_O4(4, 1, [10,100])
print("Hermitian metric G network defined")
c1 = tf.constant(4., shape=(1000), dtype=tf.complex64)
optimizer = tf.keras.optimizers.Adam()
loss_func = weighted_loss
max_epochs = 100
epoch = 0
while epoch < max_epochs:
    epoch = epoch + 1
    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set):
        with tf.GradientTape() as tape:
            curv_tr = curvature_trace(points, kahler_pot, hermitian_met, mass)
            #print("calculate curv_tr: ", curv_tr)
            #print("c1: ", c1)
            loss = loss_func(c1, curv_tr, mass)
            # print("calculated loss: ", loss)
            grads = tape.gradient(loss, hermitian_met.trainable_weights)
        optimizer.apply_gradients(zip(grads, hermitian_met.trainable_weights))
    #if epoch % 50 == 0:
    #    print("epoch %d: loss = %.7f" % (epoch, loss))
    hermitian_met.append_loss(loss)
    print("epoch %d: loss = %.7f" % (epoch, loss))
```

    Bundle rank 1
    Hermitian metric G network defined
    epoch 1: loss = 39.1416168
    epoch 2: loss = 4.8356481
    epoch 3: loss = 5.1060371
    epoch 4: loss = 4.9163494
    epoch 5: loss = 8.4873629
    epoch 6: loss = 5.5122018
    epoch 7: loss = 5.5622468
    epoch 8: loss = 5.2413821
    epoch 9: loss = 7.6594210
    epoch 10: loss = 5.4671988
    epoch 11: loss = 15.8508539
    epoch 12: loss = 4.7944870
    epoch 13: loss = 77.4971542
    epoch 14: loss = 5.1881475
    epoch 15: loss = 7.3809376
    epoch 16: loss = 4.9800577
    epoch 17: loss = 4.7370062
    epoch 18: loss = 16.2137451
    epoch 19: loss = 51.6156998
    epoch 20: loss = 4.9800963
    epoch 21: loss = 4.9426069
    epoch 22: loss = 7.3301363
    epoch 23: loss = 4.9958892
    epoch 24: loss = 7.2246909
    epoch 25: loss = 6.4574966
    epoch 26: loss = 4.6439958
    epoch 27: loss = 4.7956567
    epoch 28: loss = 66.7174606
    epoch 29: loss = 5.6579127
    epoch 30: loss = 4.8684931
    epoch 31: loss = 5.1801181
    epoch 32: loss = 7.0541368
    epoch 33: loss = 5.0552402
    epoch 34: loss = 6.4329433
    epoch 35: loss = 5.2751637
    epoch 36: loss = 4.8662567
    epoch 37: loss = 4.9226565
    epoch 38: loss = 4.9565253
    epoch 39: loss = 4.7795029
    epoch 40: loss = 4.9895248
    epoch 41: loss = 5.8227243
    epoch 42: loss = 62.4973907
    epoch 43: loss = 4.9713850
    epoch 44: loss = 4.9309902
    epoch 45: loss = 12.2955914
    epoch 46: loss = 4.5828123
    epoch 47: loss = 4.6924667
    epoch 48: loss = 4.9613571
    epoch 49: loss = 5.0706782
    epoch 50: loss = 55.7571831
    epoch 51: loss = 5.0076923
    epoch 52: loss = 4.9956741
    epoch 53: loss = 6.3211546
    epoch 54: loss = 4.9254479
    epoch 55: loss = 5.0457163
    epoch 56: loss = 4.8293262
    epoch 57: loss = 18.7749176
    epoch 58: loss = 4.7855334
    epoch 59: loss = 4.9768386
    epoch 60: loss = 5.4051833
    epoch 61: loss = 5.0015378
    epoch 62: loss = 5.3300638
    epoch 63: loss = 54.4691811
    epoch 64: loss = 6.0305481
    epoch 65: loss = 5.0184321
    epoch 66: loss = 5.4298310
    epoch 67: loss = 5.0740871
    epoch 68: loss = 5.3365836
    epoch 69: loss = 5.0727849
    epoch 70: loss = 11.3423214
    epoch 71: loss = 5.0674057
    epoch 72: loss = 5.4737391
    epoch 73: loss = 16.9494286
    epoch 74: loss = 5.2799644
    epoch 75: loss = 17.7276020
    epoch 76: loss = 4.8561254
    epoch 77: loss = 5.4398880
    epoch 78: loss = 16.7964439
    epoch 79: loss = 5.5640235
    epoch 80: loss = 4.9933209
    epoch 81: loss = 5.1368594
    epoch 82: loss = 5.0643578
    epoch 83: loss = 4.9390888
    epoch 84: loss = 4.9271803
    epoch 85: loss = 16.8124161
    epoch 86: loss = 54.8416443
    epoch 87: loss = 5.1930008
    epoch 88: loss = 4.7853456
    epoch 89: loss = 4.8331809
    epoch 90: loss = 6.5880585
    epoch 91: loss = 5.0633583
    epoch 92: loss = 5.3941231
    epoch 93: loss = 4.9493608
    epoch 94: loss = 4.8264008
    epoch 95: loss = 5.3359580
    epoch 96: loss = 5.9302816
    epoch 97: loss = 5.1752796
    epoch 98: loss = 7.1600146
    epoch 99: loss = 5.0132895
    epoch 100: loss = 5.2278247

```python
plot_train_hist(hermitian_met, y_scale='log')
```

Validation/test data

```python
cal_total_loss(test_set, hermitian_met, kahler_pot, weighted_loss, c1)
```

    6.729216


```python
%matplotlib notebook
import matplotlib.pyplot as plt

import numpy as np
import pandas as pd

import sympy
from sympy import Matrix, init_printing

from scipy.sparse.linalg import svds,eigs

import sklearn
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics.pairwise import cosine_distances
from sklearn.metrics import pairwise_distances

from time import time

!pip install surprise
import surprise
from surprise import SVD
from surprise import Dataset
from surprise.model_selection import cross_validate

init_printing()
```

```python
data = pd.read_csv('top50.csv',encoding = "ISO-8859-1")
data.index = [data["Track.Name"]]
```

```python

data = data[['Beats.Per.Minute',
       'Energy', 'Danceability', 'Loudness..dB..', 'Liveness', 'Valence.',
       'Length.', 'Acousticness..', 'Speechiness.', 'Popularity']]

```

```python
def index_to_instance(df,index=None):
    if index:
        return XYZ(df)[index][1]
    else:
        return XYZ(df)
def XYZ(df):
    return sorted(list(zip(list(df.index.codes[0].data),list(df.index.levels[0].array))))
def value_to_index_map(array):
    array1 = zip(array,range(len(array)))
    return array1
```

```python
index_to_instance(data,10)
```

    'One Thing Right'

```python
class RecSysContentBased():
    def __init__(self):
        pass
    def fit(self,train):
        self.train_set = train
        df1 = cosine_similarity(train)
        self.similarity = df1
        self.distances = pairwise_distances(train,metric='euclidean')
    def evaluate(self,user):
        d = sorted(value_to_index_map(self.distances[user]))
        return list(index_to_instance(self.train_set,d[i][1]) for i in range(len(d)))
    def predict(self):
        pass
    def test(self,testset):
        pass

```

```python
model = RecSysContentBased()

```

```python
model.fit(data)
```

```python
print("Top 5 Songs closest to {0} are: \n{1}".format(index_to_instance(data,10),pd.Series(model.evaluate(10)[1:6])))
```

    Top 5 Songs closest to One Thing Right are:
    0                                Old Town Road - Remix
    1                                              Happier
    2    fuck, i'm lonely (with Anne-Marie) - from 13 ...
    3                        boyfriend (with Social House)
    4                                             Takeaway
    dtype: object

```python

```