"""
network.py
~~~~~~~~~

A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network.  Gradients are calculated
using backpropagation.  The network uses a hyperbolic tangent function
as the activation function. 
"""


import random
import numpy as np

class Network(object):

    def __init__(self, sizes, obs_scale, rew_scale, eta):
      """The list ``sizes`` contains the number of neurons in the
      respective layers of the network.  The scalar 'obs_scale' scales the 
      observation down to [-1,1] and the 'rew_scale' scales down the 
      reward to a percent of the maximum possible reward from
      the environment."""
      self.obs_scale = obs_scale
      self.rew_scale = rew_scale
      self.num_layers = len(sizes)
      self.sizes = sizes
      self.eta = eta
      self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
      self.weights = [np.random.randn(y, x) 
                        for x, y in zip(sizes[:-1], sizes[1:])]

    def feedforward(self, a):
      """Return the output of the network if ``a`` is input."""
      for b, w in zip(self.biases, self.weights):
        a = tanh(np.dot(w, a)+b)
      return a

    def update(self, mini_batch):
      """Update the network's weights and biases by applying
      gradient descent using backpropagation to a single mini batch.
      The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
      is the learning rate."""
      nabla_b = [np.zeros(b.shape) for b in self.biases]
      nabla_w = [np.zeros(w.shape) for w in self.weights]
      for x, y in mini_batch:
        delta_nabla_b, delta_nabla_w = self.backprop(x, y)
        nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
        nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
      self.weights = [w-(self.eta/len(mini_batch))*nw
                          for w, nw in zip(self.weights, nabla_w)]
      self.biases = [b-(self.eta/len(mini_batch))*nb
                         for b, nb in zip(self.biases, nabla_b)]

    def backprop(self, x, y):
      """Return a tuple ``(nabla_b, nabla_w)`` representing the
      gradient for the cost function C_x.  ``nabla_b`` and
      ``nabla_w`` are layer-by-layer lists of numpy arrays, similar
      to ``self.biases`` and ``self.weights``."""
      nabla_b = [np.zeros(b.shape) for b in self.biases]
      nabla_w = [np.zeros(w.shape) for w in self.weights]
      # feedforward
      activation = x
      activations = [x] # list to store all the activations, layer by layer
      zs = [] # list to store all the z vectors, layer by layer
      for b, w in zip(self.biases, self.weights):
        z = np.dot(w, activation)+b
        zs.append(z)
        activation = tanh(z)
        activations.append(activation)
      # backward pass
      delta = self.cost_derivative(activations[-1], y) * \
        tanh_prime(zs[-1])
      nabla_b[-1] = delta
      nabla_w[-1] = np.dot(delta, activations[-2].transpose())

      for l in xrange(2, self.num_layers):
        z = zs[-l]
        sp = tanh_prime(z)
        delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
        nabla_b[-l] = delta
        nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
      return (nabla_b, nabla_w)

    def cost_derivative(self, output_activations, y):
      """Return the vector of partial derivatives \partial C_x /
      \partial a for the output activations."""
      return (output_activations-y)

#### Miscellaneous functions
def tanh(z):
    """The hyperbolic tangent function."""
    return 2.0/(1.0+np.exp(-2*z)) - 1.0

def tanh_prime(z):
    """Derivative of the hyperbolic tangent function."""
    return 1.0 - tanh(z)*tanh(z)
