#!/usr/bin/python
# -*- coding: cp1252 -*-
from math import e
from random import random

def ListReverse( List ):
    ListRev = 1*List
    ListRev.reverse()
    return ListRev

class Link:
    def __init__( self, Index, WeightRange ):
        self.Weight = WeightRange*(2*random()-1)
        self.WeightChange = 0.0
        self.Index = Index

class Neuron:
    def __init__( self, Index, WeightRange ):
        self.BiasWeight = WeightRange*(2*random()-1)
        self.BiasChange, self.Value, self.Delta = 0.0, 0.0, 0.0
        self.Index = Index

    def Activation( self ):
        return (1.0 / (1.0 + e**(-self.Value)))

    def ActDerivation( self, FlatSpotVar ):
        return self.Value * (1.0 - self.Value) + FlatSpotVar

class Layer:
    def __init__( self, NeuronCount, Index, WeightRange ):
        self.Neurons = [Neuron(I, WeightRange) for I in range(NeuronCount)]
        self.NeuronCount = NeuronCount
        self.Index = Index

class Net:
    def __init__( self, LayerList, LearningRate = 0.1, Momentum = 0.1, \
                  FlatSpotVar = 0.01, WeightRange = 0.5 ):
        # Variablen an Netz-Instanz übergeben
        self.LearningRate = LearningRate
        self.Momentum = Momentum
        self.FlatSpotVar = FlatSpotVar
        self.WeightRange = WeightRange
        self.GlobalError = 10.0**10
        # Schichten initialisieren:
        self.Layers = [Layer(NC, I, WeightRange) for (NC, I) \
                       in zip(LayerList, range(len(LayerList)))]
        self.LayerCount = len(self.Layers)-1
        # Verbindungen initialisieren:
        for L in self.Layers[:-1]:
            for N in L.Neurons:
                N.Links = [Link(I, self.WeightRange) for I \
                           in range(self.Layers[L.Index + 1].NeuronCount)]
                N.LinkCount = len(N.Links)

    def Forward( self, InList ):
        '''
        Berechnet Aktivierungswerte aller Neuronen für ein Muster.
        '''
        if (len(InList) != self.Layers[0].NeuronCount):
            raise Exception('Num. Eingabedaten != Num. Eingabeneuronen')
        
        for L in self.Layers:
            for N in L.Neurons:
                if (L.Index == 0):
                    # Werte für Eingabeschicht direkt aus Eingabe
                    N.Value = InList[N.Index]
                else:
                    # Werte für versteckte/Ausgabeschicht aus vorh. Schicht
                    N.Value = 0.0
                    for PreN in self.Layers[L.Index - 1].Neurons:
                        N.Value += PreN.Value * PreN.Links[N.Index].Weight
                    N.Value += N.BiasWeight
                    N.Value = N.Activation()

        # Werte der Ausgabeschicht als Liste zurückgeben                
        return [N.Value for N in self.Layers[self.LayerCount].Neurons]

    def BackProp( self, InList, OutList ):
        '''
        Berechnet Fehler für ein Ein-/Ausgabemuster, passt Gewichte an.
        '''
        if (len(OutList) != self.Layers[self.LayerCount].NeuronCount):
            raise Exception('Num. Ausgabedaten != Num. Ausgabeneuronen')
       
        # Aktivierungswerte berechnen
        self.Forward(InList)
        
        # Fehlersignal in Ausgabeschicht
        for N in self.Layers[self.LayerCount].Neurons:
            N.Delta = (OutList[N.Index] - N.Value) \
                      * N.ActDerivation(self.FlatSpotVar)
            
        # Gewichtetes Fehlersignal in versteckten Schichten
        # und in Verknüpfungen der Eingabeschicht
        for L in ListReverse(self.Layers)[1:]:
            for N in L.Neurons:
                N.Delta = 0.0
                for C in N.Links:
                    N.Delta += C.Weight * \
                            self.Layers[L.Index + 1].Neurons[C.Index].Delta
                N.Delta = N.ActDerivation(self.FlatSpotVar) * N.Delta
                
        # Gewichte und Schwellenwerte anpassen
        for L in ListReverse(self.Layers):
            for N in L.Neurons:
                if L.Index > 0:
                    # Nur versteckte/Ausgabeschicht hat Schwellenwerte
                    N.BiasWeight += self.LearningRate * N.Delta \
                                    + self.Momentum * N.BiasChange
                    N.BiasChange = self.LearningRate * N.Delta
                if L.Index < self.LayerCount:
                    # Nur versteckte/Eingabeschicht hat ausgehende Links
                    for C in N.Links:
                        WeightChangeTemp = self.LearningRate * N.Value * \
                            self.Layers[L.Index+1].Neurons[C.Index].Delta
                        C.Weight += WeightChangeTemp \
                                    + self.Momentum * C.WeightChange
                        C.WeightChange = WeightChangeTemp
                        
    def Train( self, InList, OutList, Num ):
        '''
        Trainiert das neuronale Netz (Num) Epochen, gibt Fehler zurück.
        '''
        for t in range(Num):
            map(self.BackProp, InList, OutList)
        self.GlobalError = 0.0
        for (InItem, OutItem) in zip(InList, OutList):
            for (I, O) in zip(self.Forward(InItem),OutItem):
                self.GlobalError += abs(O-I)
        self.GlobalError = self.GlobalError/(len(OutList)*len(OutList[0]))
        return self.GlobalError
