# Back-Propagation Neural Networks
#
# Based on Neil Schemenauer's bpnn.py

import math
import random

# calculate a random number where:  a <= rand < b
def rand(a, b):
    return (b-a)*random.random() + a

# Make a matrix (we could use NumPy to speed this up)
def makeMatrix(I, J, fill=0.0):
    m = []
    for i in range(I):
        m.append([fill]*J)
    return m

# our sigmoid function, tanh is a little nicer than the standard 1/(1+e^-x)
def sigmoid(x):
    return math.tanh(x)

# derivative of our sigmoid function (note that this uses y, not x)
def dsigmoid(y):
    return 1.0-y*y

class NN:
    def __init__(self, ni, nh, no):
        # number of input, hidden, and output nodes
        self.ni = ni + 1 # +1 for bias node
        self.nh = nh
        self.no = no

        # activations for nodes
        self.ai = [1.0]*self.ni
        self.ah = [1.0]*self.nh
        self.ao = [1.0]*self.no

        # create weights
        self.wi = makeMatrix(self.ni, self.nh)
        self.wo = makeMatrix(self.nh, self.no)
        # set them to random vaules
        for i in range(self.ni):
            for j in range(self.nh):
                self.wi[i][j] = rand(-2.0, 2.0)
        for j in range(self.nh):
            for k in range(self.no):
                self.wo[j][k] = rand(-2.0, 2.0)
        print "Matrix wi:"
        print self.wi
	print "Matrix wo:"
        print self.wo
        print "=========================="
        # last change in weights for momentum
	print "ni : %d"%(self.ni)
	print "nh : %d"%(self.nh)
	print "no : %d"%(self.no)
        self.ci = makeMatrix(self.ni, self.nh)
        self.co = makeMatrix(self.nh, self.no)
        
        print "Matrix ci: "
	print self.ci
	print "Matrix co: "
	print self.co
	print "=========================="
        self.epochs=0

    def update(self, inputs):
        if len(inputs) != self.ni-1:
            raise ValueError, 'wrong number of inputs'

        # input activations
        for i in range(self.ni-1):
            #self.ai[i] = sigmoid(inputs[i])
            self.ai[i] = inputs[i]
        print "input (%i):" % self.ni
        print self.ai
        # hidden activations
        for j in range(self.nh):
            sum = 0.0
            for i in range(self.ni):
                sum = sum + self.ai[i] * self.wi[i][j]
            self.ah[j] = sigmoid(sum)

        # output activations
        for k in range(self.no):
            sum = 0.0
            for j in range(self.nh):
                sum = sum + self.ah[j] * self.wo[j][k]
            self.ao[k] = sigmoid(sum)

    def backPropagate(self, targets, N, M):
        if len(targets) != self.no:
            raise ValueError, 'wrong number of target values'

        # calculate error terms for output
        output_deltas = [0.0] * self.no
        for k in range(self.no):
            error = targets[k]-self.ao[k]
            output_deltas[k] = dsigmoid(self.ao[k]) * error

        # calculate error terms for hidden
        hidden_deltas = [0.0] * self.nh
        for j in range(self.nh):
            error = 0.0
            for k in range(self.no):
                error = error + output_deltas[k]*self.wo[j][k]
            hidden_deltas[j] = dsigmoid(self.ah[j]) * error

        # update output weights
        for j in range(self.nh):
            for k in range(self.no):
                change = output_deltas[k]*self.ah[j]
                self.wo[j][k] = self.wo[j][k] + N*change + M*self.co[j][k]
                self.co[j][k] = change
                #print N*change, M*self.co[j][k]

        # update input weights
        for i in range(self.ni):
            for j in range(self.nh):
                change = hidden_deltas[j]*self.ai[i]
                self.wi[i][j] = self.wi[i][j] + N*change + M*self.ci[i][j]
                self.ci[i][j] = change

    def calcError(self,targets):
        error = 0.0
        for k in range(len(targets)):
            error = error + (targets[k]-self.ao[k])**2
        return math.sqrt(error)


    def testOne(self,input,target):
      self.update(input)
      error=self.calcError(target)
      return error,self.ao[:]

    def testAll(self,patterns):
      error=0.0
      outputs=[]
      for input,target in patterns:
        e,o=self.testOne(input,target)
        error+=e
        outputs.append(o)
      return error/len(patterns),outputs


    def trainOne(self,input,target,learningRate=0.5,momentum=0.1):
      self.update(input)
      self.backPropagate(target,learningRate,momentum)
      error=self.calcError(target)
      return error

    def trainAll(self,patterns,learningRate=0.5,momentum=0.1):
      error = 0.0
      pat2=list(patterns)
      random.shuffle(pat2)
      for input,target in pat2:
        error+=self.trainOne(input,target,learningRate,momentum)
      self.epochs+=1
      return error/len(patterns)

