Neural Networks
In this last chapter, we will take a look at the mathematical tools used to model and understand biological neural networks.
We're going to see how to use Python to do operations on matrices and then we will take a look at some of the fundamental concepts in Graph Theory which will be the starting point for network modeling.
What we're going to do in this Notebook, is that we're going to see how to use SymPy and NumPy to implement some matrix properties and operations, and after that we're going to use another Python Package "NetworkX" which is used to study and implement graphs and networks.
The necessary libraries and functions are :
import numpy as np
import sympy as sp
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from itertools import combinations
from collections import Counter
Matrix addition and substraction
And we want to find $A+B$ or $A-B$
# Creating the arrays
A = np.array([2,5,7,4,8,3,9,1,4])
B = np.array([1,5,3,3,6,1,7,5,9])
# Reshaping them to a 3x3 array
for mat in (A,B) :
mat.shape = (3,3) #
print("A + B \n" , A + B, "\nA - B \n", A - B) # Matrix addition and substraction
A = sp.Matrix(3, 3,[2, 5, 7, 4, 8, 3, 9, 1, 4]) # First argument is number of row and second is number of columns
B = sp.Matrix(3, 3, [1, 5, 3, 3, 6, 1, 7, 5, 9])
And the addition/substration are as simple as :
A + B
A - B
Matrix multiplication or dot product
With NumPy, this can be done using the function np.dot() :
A = np.array([[2,3,4]])
B = np.array([4,1,5])
B.shape = (3,1)
print('A.B \n', np.dot(A,B))
And with SymPy, the multiplication symbol will do the trick :
A = sp.Matrix(1,3, [2,3,4]) # 1x3
B = sp.Matrix(3,1, [4,1,5]) # 3x1
A * B
In SymPy :
A = sp.Matrix(2,2,[2,8,5,-6])
B = sp.Matrix(2,2,[7,-4,3,12])
A*B
B*A
And with NumPy :
A = np.array([2,8,5,-6])
B = np.array([7,-4,3,12])
for mat in (A,B) :
mat.shape = (2,2)
print('AB \n', np.dot(A,B), '\n BA \n', np.dot(B,A))
Solving equations with matrices
Let's imagine that we have the following equation :
Solving it at hand would look like this : $$\Rightarrow \begin{bmatrix} 2x+4 & 2y+6 \\ 6 & 0 \end{bmatrix} = \begin{bmatrix} 3 & y \\ 6 & z \end{bmatrix}$$ $$\Rightarrow \begin{cases} z = 0 \\ y = -6 \\ x = \frac{-1}{2} \end{cases}$$
x, y, z = sp.symbols('x,y,z') # Initialize our variable symbols
lhs = 2 * sp.Matrix([[x+2, y+3],[3,0]]) # Create the matrix on the left hand side
lhs
rhs = sp.transpose(sp.Matrix([[3,6],[y,z]])) # Create the matrix on the right hand side
rhs
sp.solve(sp.Eq(lhs,rhs)) # And solve the equality between the two
Identity matrix
$$ A = \begin{bmatrix} 2 & 5 & 7 \\ 4 & 8 & 3 \\ 9 & 1 & 4 \\\end{bmatrix} \ et \ I = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\\end{bmatrix}$$Let's verify that $AI = A$
With NumPy :
A = np.array([2,5,7,4,8,3,9,1,4])
A.shape = (3,3)
I = np.eye(3) # Identity matrix 3x3
print('A.I \n', np.dot(A,I))
And with SymPy :
A = sp.Matrix(3,3,[2,5,7,4,8,3,9,1,4])
I = sp.eye(3)
A * I
sp.Eq(A*I, A) # Verifying that A.I is A
Matrix transposition
With the following matrices, let's verify an important property of matrices and their transpose : $(AB)^\top = B^\top A^\top$ $$ A = \begin{bmatrix} 3 & 2 & 3 \\ 2 & 2 & 1 \\ 2 & 3 & 1 \\\end{bmatrix} \ et \ B = \begin{bmatrix} 2 & -2 \\ -3 & 4 \\ 1 & 1\\\end{bmatrix}$$
SymPy :
A = sp.Matrix(3,3,[3,2,3,2,2,1,2,3,1])
B = sp.Matrix(3,2,[2,-2,-3,4,1,1])
(A*B).T # We can use sp.transpose(A*B) too
B.T * A.T
(A*B).T == B.T * A.T # Let's verify the property
NumPy :
A = np.array([3,2,3,2,2,1,2,3,1])
A.shape = (3,3)
B = np.array([2,-2,-3,4,1,1])
B.shape = (3,2)
print('(AB)T \n', np.dot(A,B).T, '\n BT.AT \n', np.dot(B.T,A.T)) # .T method will give the array transpose
np.array_equal(np.dot(A,B).T, np.dot(B.T,A.T)) # Verifying the equality
Matrix Determinant
With SymPy :
A = sp.Matrix(2,2,[2,3,4,-2])
A.det()
And with NumPy :
A = np.array([[2,3],[4,-2]])
np.linalg.det(A)
The determinant is :
$$det = +2 \begin{pmatrix} -3 & 2 \\ 5 & 1 \end{pmatrix} - (-2)\begin{pmatrix} 1 & 2 \\ -2 & 1 \end{pmatrix} +4\begin{pmatrix} 1 & -3 \\ -2 & 5 \end{pmatrix}$$$$\Rightarrow det = +2(-13) +2(5) +4(-1) = -20$$Easily with SymPy :
I = sp.Matrix(3,3,[2,-2,4,1,-3,2,-2,5,1])
I.det()
And with NumPy :
I = np.array([2,-2,4,1,-3,2,-2,5,1])
I.shape = (3,3)
np.linalg.det(I)
Matrix inverse
the matrix inverse has a property which is that $AA^{−1} = A^{−1}A = I$.
We can verify it with SymPy :
A = sp.Matrix(3,3,[1,1,1,1,-1,-1,1,1,-1])
A_1 = A.inv() # Calculating the inverse
A_1
A*A_1 == A_1*A == sp.eye(3) # Verifying the property
And with NumPy, it goes as follows :
A = np.array([1,1,1,1,-1,-1,1,1,-1])
A.shape = (3,3)
A_1 = np.linalg.inv(A)
print('A^-1 \n', A_1)
if np.array_equal(np.dot(A,A_1), np.dot(A_1,A)) and np.array_equal(np.dot(A,A_1), np.eye(3)) :
print("The property has been verified")
Solving linear systems of equations
Let's take the following system of equations :
We can solve them in the following manner with NumPy :
a = np.array([[1,1,1],[1,-1,-1],[1,1,-1]])
b = np.array([[8],[-4],[6]])
sol = np.linalg.solve(a,b)
print('The solution is the vector column [x,y,z] \n',sol)
np.array_equal(np.dot(a,sol),b) # Verifying the solution is true
Eigenvalues and Eigenvectors
B = np.array([2,16,8,4,14,8,-8,-32,-18])
B.shape = (3,3) # A 3x3 matrix
eig_vals, eig_vects = np.linalg.eig(B)
print("The Eigenvalues are \n", eig_vals, "\nAnd the Eigenvectors are \n", eig_vects)
Graph creation and structure description
def graph(nodes, edges, type="simple"):
"""This function creates a graph and sends back the degree distribution as well
as the sparse matrix. graph can be simple or oriented by using the parameter type.
nodes : a list of graph vertices.
edges : a list of tuples, each tuple contains two vertices to represent an edge.
"""
if type == "simple":
g = nx.Graph()
elif type == "oriented":
g = nx.DiGraph()
g.add_nodes_from(nodes) # Add nodes from the list of nodes
g.add_edges_from(edges) # Add edges
a = nx.adjacency_matrix(g) # The corresponding adjacency matrix
degree, freq = zip( \
*Counter(sorted([deg for node, deg in g.degree()])).items()) # Sorting the degrees and counting them
fig, ax = plt.subplots(1, 3, figsize=(15, 5), dpi=150)
plt.subplot(131) # Drawing the graph
nx.draw(g, with_labels=True)
plt.title("Graph")
plt.subplot(132) # The degree distribution
plt.bar(degree, freq, width=0.2)
plt.xlabel("Degrees")
plt.ylabel("Frequency")
plt.xticks(nodes)
plt.title("Degree distribution of the graph")
plt.subplot(133) # The sparse matrix
plt.spy(a, marker='x')
plt.xticks([i for i in range(len(nodes))], nodes)
plt.yticks([i for i in range(len(nodes))], nodes)
plt.title("Sparse Matrix")
return g, a # Graph and adjacency matrix
vertices = [n for n in range(1, 5)]
g1, a1 = graph(vertices, list(combinations(
vertices, 2))) # combinations function creates the list of edges needed
From the degree distribution diagram, we can see that our graph has 4 nodes of degree 3.
g2, a2 = graph(vertices, [(1,2),(2,1),(2,3),(2,1),(4,1),(4,2),(4,3)], type = "oriented")
Centrality measures
Now let's write a function that can represent those types of indicators for a certain graph.
def centrality(graph, centr_type, title):
"""This function creates a represntation for a given centrality type and for a given graph.
graph : networkx graph object
centr_type : type of centrality on a given graph, for example : nx.degree_centrality(graph)
title : string for the title of the representation.
"""
plt.figure(dpi=150)
pos = nx.spring_layout(graph)
nodes = nx.draw_networkx_nodes(
graph,
pos,
node_size=250,
cmap=plt.cm.plasma,
node_color=list(centr_type.values()
), # color nodes according to their centrality values
nodelist=list(centr_type.keys()))
nodes.set_norm(mcolors.SymLogNorm(linthresh=0.01, linscale=1, base=10))
labels = nx.draw_networkx_labels(graph, pos)
edges = nx.draw_networkx_edges(graph, pos)
plt.title(title)
plt.colorbar(nodes)
plt.axis('off')
Now let's initialize a random graph so we can try our function on.
ver = [n for n in range(1, 15)] # nodes from 1 to 14
edges = [(l1,l2) for l1,l2 in map(lambda n : (n,8), [n for n in range(11,15)])] + \
[(l1,l2) for l1,l2 in map(lambda n : (n,6), [n for n in range(1,6)])] + \
[(7,6), (8,7),(9,7), (10,7)] # list of edges
g3, a3 = graph(ver, edges)
Degree centrality
Let's check the degree centrality for our 14 vertices.
for node in g3.nodes():
print(node, nx.degree_centrality(g3)[node])
centrality(g3, nx.degree_centrality(g3), 'Degree Centrality')
Eigenvector centrality
for node in g3.nodes():
print(node, nx.eigenvector_centrality(g3)[node])
centrality(g3, nx.eigenvector_centrality(g3), 'Eigenvector centrality')
Closeness centrality
for node in g3.nodes():
print(node, nx.closeness_centrality(g3)[node])
centrality(g3, nx.closeness_centrality(g3), 'Closeness centrality')
Betweennes centrality
for node in g3.nodes():
print(node, nx.betweenness_centrality(g3)[node])
centrality(g3, nx.betweenness_centrality(g3),'Betweenness centrality')