Manual Neural Network

In this notebook we will manually build out a neural network that mimics the TensorFlow API. This will greatly help your understanding when working with the real TensorFlow!


Quick Note on Super() and OOP


In [1]:
class SimpleClass():
    def __init__(self, str_input):
        print("SIMPLE" + str_input)

In [2]:
class ExtendedClass(SimpleClass):
    def __init__(self):
        print('EXTENDED')

The child class will use its own initialization method, if not specified otherwise.


In [3]:
s = ExtendedClass()


EXTENDED

If we want to use initialization from the parent class, we can do that using:

super().__init__()

In [4]:
class ExtendedClass(SimpleClass):
    def __init__(self):
        super().__init__(" My String")
        print('EXTENDED')

In [5]:
s = ExtendedClass()


SIMPLE My String
EXTENDED

Operation


In [6]:
class Operation():
    """
    An Operation is a node in a "Graph". TensorFlow will also use this concept of a Graph.
    
    This Operation class will be inherited by other classes that actually compute the specific
    operation, such as adding or matrix multiplication.
    """
    def __init__(self, input_nodes = []):
        """
        Intialize an Operation
        """
        # The list of input nodes
        self.input_nodes = input_nodes 
        
        # Initialize list of nodes consuming this node's output
        self.output_nodes = [] 
        
        # For every node in the input, we append this operation (self) to the list of
        # the consumers of the input nodes
        for node in input_nodes:
            node.output_nodes.append(self)
        
        # There will be a global default graph (TensorFlow works this way)
        # We will then append this particular operation
        # Append this operation to the list of operations in the currently active default graph
        _default_graph.operations.append(self)
  
    def compute(self):
        """ 
        This is a placeholder function. It will be overwritten by the actual specific operation
        that inherits from this class.
        """
        pass

Example Operations

Addition


In [7]:
class add(Operation):
    def __init__(self, x, y):
        super().__init__([x, y])

    def compute(self, x_var, y_var):
        self.inputs = [x_var, y_var]
        return x_var + y_var

Multiplication


In [8]:
class multiply(Operation):
    def __init__(self, a, b):
        super().__init__([a, b])
    
    def compute(self, a_var, b_var):
        self.inputs = [a_var, b_var]
        return a_var * b_var

Matrix Multiplication


In [9]:
class matmul(Operation):
    def __init__(self, a, b):
        super().__init__([a, b])
    
    def compute(self, a_mat, b_mat):
        self.inputs = [a_mat, b_mat]
        return a_mat.dot(b_mat)

Placeholders


In [10]:
class Placeholder():
    """
    A placeholder is a node that needs to be provided a value for computing the output in the Graph.
    In case of supervised learning, X (input) and Y (output) will require placeholders.
    """
    def __init__(self):
        self.output_nodes = []
        _default_graph.placeholders.append(self)

Variables


In [11]:
class Variable():
    """
    This variable is a changeable parameter of the Graph.
    For a simple neural networks, it will be weights and biases.
    """
    
    def __init__(self, initial_value = None):
        self.value = initial_value
        self.output_nodes = []
        
        _default_graph.variables.append(self)

Graph


In [12]:
class Graph():  
    def __init__(self):
        self.operations = []
        self.placeholders = []
        self.variables = []
        
    def set_as_default(self):
        """
        Sets this Graph instance as the Global Default Graph
        """
        global _default_graph
        _default_graph = self

A Basic Graph

$$ z = Ax + b $$

With A=10 and b=1

$$ z = 10x + 1 $$

Just need a placeholder for x and then once x is filled in we can solve it!


In [13]:
g = Graph()

In [14]:
g.set_as_default()

In [15]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[]
Placeholders:
[]
Variables:
[]

In [16]:
A = Variable(10)

In [17]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[]
Placeholders:
[]
Variables:
[<__main__.Variable object at 0x00000178E3B24048>]

In [18]:
b = Variable(1)

In [19]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[]
Placeholders:
[]
Variables:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Variable object at 0x00000178E3B241D0>]

In [20]:
# Will be filled out later
x = Placeholder()

In [21]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[]
Placeholders:
[<__main__.Placeholder object at 0x00000178E3B242E8>]
Variables:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Variable object at 0x00000178E3B241D0>]

In [22]:
y = multiply(A,x)

In [23]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[<__main__.multiply object at 0x00000178E3B24320>]
Placeholders:
[<__main__.Placeholder object at 0x00000178E3B242E8>]
Variables:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Variable object at 0x00000178E3B241D0>]

In [24]:
z = add(y, b)

In [25]:
print("Operations:")
print(g.operations)
print("Placeholders:")
print(g.placeholders)
print("Variables:")
print(g.variables)


Operations:
[<__main__.multiply object at 0x00000178E3B24320>, <__main__.add object at 0x00000178E3B24390>]
Placeholders:
[<__main__.Placeholder object at 0x00000178E3B242E8>]
Variables:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Variable object at 0x00000178E3B241D0>]

Session


In [26]:
import numpy as np

Traversing Operation Nodes

More details about tree post order traversal: https://en.wikipedia.org/wiki/Tree_traversal#Post-order_(LRN)


In [27]:
def traverse_postorder(operation):
    """ 
    PostOrder Traversal of Nodes. 
    Basically makes sure computations are done in the correct order (Ax first , then Ax + b). 
    """
    
    nodes_postorder = []
    def recurse(node):
        if isinstance(node, Operation):
            for input_node in node.input_nodes:
                recurse(input_node)
        nodes_postorder.append(node)

    recurse(operation)
    return nodes_postorder

In [28]:
class Session:
    def run(self, operation, feed_dict = {}):
        """ 
          operation: The operation to compute
          feed_dict: Dictionary mapping placeholders to input values (the data)  
        """
        
        # Puts nodes in correct order
        nodes_postorder = traverse_postorder(operation)
        print("Post Order:")
        print(nodes_postorder)
        
        for node in nodes_postorder:
            if type(node) == Placeholder:
                node.output = feed_dict[node]
                
            elif type(node) == Variable:
                node.output = node.value
                
            else: # Operation
                node.inputs = [input_node.output for input_node in node.input_nodes]
                node.output = node.compute(*node.inputs)
                
            # Convert lists to numpy arrays
            if type(node.output) == list:
                node.output = np.array(node.output)
        
        # Return the requested node value
        return operation.output

In [29]:
sess = Session()

In [30]:
result = sess.run(operation = z, 
                  feed_dict = {x : 10})


Post Order:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Placeholder object at 0x00000178E3B242E8>, <__main__.multiply object at 0x00000178E3B24320>, <__main__.Variable object at 0x00000178E3B241D0>, <__main__.add object at 0x00000178E3B24390>]

The result should look like: Variable (A), Placeholder (x), Multiple operation (Ax), Variable (b), Add (Ax + b)


In [31]:
result


Out[31]:
101

In [32]:
10 * 10 + 1


Out[32]:
101

In [33]:
# Running just y = Ax
# The post order should be only up to 
result = sess.run(operation = y, 
                  feed_dict = {x : 10})


Post Order:
[<__main__.Variable object at 0x00000178E3B24048>, <__main__.Placeholder object at 0x00000178E3B242E8>, <__main__.multiply object at 0x00000178E3B24320>]

In [34]:
result


Out[34]:
100

Looks like we did it!


In [35]:
g = Graph()

g.set_as_default()

A = Variable([[10, 20], [30, 40]])
b = Variable([1, 1])

x = Placeholder()

y = matmul(A,x)

z = add(y,b)

In [36]:
sess = Session()

In [37]:
result = sess.run(operation = z,
                  feed_dict = {x : 10})


Post Order:
[<__main__.Variable object at 0x00000178EAE61D68>, <__main__.Placeholder object at 0x00000178EAE61E10>, <__main__.matmul object at 0x00000178EAE61EB8>, <__main__.Variable object at 0x00000178E3B24518>, <__main__.add object at 0x00000178EAE61B38>]

In [38]:
result


Out[38]:
array([[101, 201],
       [301, 401]])

Activation Function


In [39]:
import matplotlib.pyplot as plt
%matplotlib inline

In [40]:
# Defining sigmoid function
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

In [41]:
sample_z = np.linspace(-10, 10, 100)
sample_a = sigmoid(sample_z)

In [42]:
plt.figure(figsize = (8, 8))
plt.title("Sigmoid")
plt.plot(sample_z, sample_a)


Out[42]:
[<matplotlib.lines.Line2D at 0x178eda0c3c8>]

Sigmoid as an Operation


In [43]:
class Sigmoid(Operation):
    def __init__(self, z):
        # a is the input node
        super().__init__([z])

    def compute(self, z_val):
        return 1 / (1 + np.exp(-z_val))

Classification Example


In [44]:
from sklearn.datasets import make_blobs

In [45]:
# Creating 50 samples divided into 2 blobs with 2 features
data = make_blobs(n_samples = 50,
                  n_features = 2,
                  centers = 2,
                  random_state = 75)

In [46]:
data


Out[46]:
(array([[  7.3402781 ,   9.36149154],
        [  9.13332743,   8.74906102],
        [  1.99243535,  -8.85885722],
        [  7.38443759,   7.72520389],
        [  7.97613887,   8.80878209],
        [  7.76974352,   9.50899462],
        [  8.3186688 ,  10.1026025 ],
        [  8.79588546,   7.28046702],
        [  9.81270381,   9.46968531],
        [  1.57961049,  -8.17089971],
        [  0.06441546,  -9.04982817],
        [  7.2075117 ,   7.04533624],
        [  9.10704928,   9.0272212 ],
        [  1.82921897,  -9.86956281],
        [  7.85036314,   7.986659  ],
        [  3.04605603,  -7.50486114],
        [  1.85582689,  -6.74473432],
        [  2.88603902,  -8.85261704],
        [ -1.20046211,  -9.55928542],
        [  2.00890845,  -9.78471782],
        [  7.68945113,   9.01706723],
        [  6.42356167,   8.33356412],
        [  8.15467319,   7.87489634],
        [  1.92000795,  -7.50953708],
        [  1.90073973,  -7.24386675],
        [  7.7605855 ,   7.05124418],
        [  6.90561582,   9.23493842],
        [  0.65582768,  -9.5920878 ],
        [  1.41804346,  -8.10517372],
        [  9.65371965,   9.35409538],
        [  1.23053506,  -7.98873571],
        [  1.96322881,  -9.50169117],
        [  6.11644251,   9.26709393],
        [  7.70630321,  10.78862346],
        [  0.79580385,  -9.00301023],
        [  3.13114921,  -8.6849493 ],
        [  1.3970852 ,  -7.25918415],
        [  7.27808709,   7.15201886],
        [  1.06965742,  -8.1648251 ],
        [  6.37298915,   9.77705761],
        [  7.24898455,   8.85834104],
        [  2.09335725,  -7.66278316],
        [  1.05865542,  -8.43841416],
        [  6.43807502,   7.85483418],
        [  6.94948313,   8.75248232],
        [ -0.07326715, -11.69999644],
        [  0.61463602,  -9.51908883],
        [  1.31977821,  -7.2710667 ],
        [  2.72532584,  -7.51956557],
        [  8.20949206,  11.90419283]]),
 array([1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1,
        1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1,
        1, 0, 0, 0, 0, 1]))

In [47]:
features = data[0]
plt.scatter(features[:,0],features[:,1])


Out[47]:
<matplotlib.collections.PathCollection at 0x178ee8a27f0>

In [48]:
labels = data[1]
plt.scatter(x = features[:,0], 
            y = features[:,1], 
            c = labels,
            cmap = 'coolwarm')


Out[48]:
<matplotlib.collections.PathCollection at 0x178ee950e80>

In [49]:
# DRAW A LINE THAT SEPERATES CLASSES
x = np.linspace(0, 11 ,10)
y = -x + 5
plt.scatter(features[:,0],
            features[:,1],
            c = labels,
            cmap = 'coolwarm')
plt.plot(x,y)


Out[49]:
[<matplotlib.lines.Line2D at 0x178ee97b6d8>]

Defining the Perceptron

$$ y = mx + b $$$$ y = -x + 5 $$$$ f1 = mf2 + b , m = 1$$$$ f1 = -f2 + 5 $$$$ f1 + f2 - 5 = 0 $$

Convert to a Matrix Representation of Features

$$ w^Tx + b = 0 $$$$ \Big(1, 1\Big)f - 5 = 0 $$

Then if the result is > 0 its label 1, if it is less than 0, it is label=0

Example Point

Let's say we have the point f1=2 , f2=2 otherwise stated as (8,10). Then we have:

$$ \begin{pmatrix} 1 , 1 \end{pmatrix} \begin{pmatrix} 8 \\ 10 \end{pmatrix} + 5 = $$

In [50]:
z = np.array([1, 1]).dot(np.array([[8], [10]])) - 5
print(z)


[13]

In [51]:
a = 1 / (1 + np.exp(-z))
print(a)


[0.99999774]

Or if we have (4,-10)


In [52]:
z = np.array([1,1]).dot(np.array([[2],[-10]])) - 5
print(z)


[-13]

In [53]:
a = 1 / (1 + np.exp(-z))
print(a)


[2.2603243e-06]

Using an Example Session Graph


In [54]:
g = Graph()

In [55]:
g.set_as_default()

In [56]:
x = Placeholder()

In [57]:
w = Variable([1,1])

In [58]:
b = Variable(-5)

In [59]:
z = add(matmul(w,x),b)

In [60]:
a = Sigmoid(z)

In [61]:
sess = Session()

In [62]:
sess.run(operation = a,
         feed_dict = {x : [8, 10]})


Post Order:
[<__main__.Variable object at 0x00000178EE98CAC8>, <__main__.Placeholder object at 0x00000178EE98C8D0>, <__main__.matmul object at 0x00000178EE98C208>, <__main__.Variable object at 0x00000178EE98CB70>, <__main__.add object at 0x00000178EEA07160>, <__main__.Sigmoid object at 0x00000178EE98CC88>]
Out[62]:
0.999997739675702

In [63]:
sess.run(operation = a,
         feed_dict = {x : [2, -10]})


Post Order:
[<__main__.Variable object at 0x00000178EE98CAC8>, <__main__.Placeholder object at 0x00000178EE98C8D0>, <__main__.matmul object at 0x00000178EE98C208>, <__main__.Variable object at 0x00000178EE98CB70>, <__main__.add object at 0x00000178EEA07160>, <__main__.Sigmoid object at 0x00000178EE98CC88>]
Out[63]:
2.2603242979035746e-06

Great Job!