diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d2ea62..9b312db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,10 +23,15 @@ jobs: env: GO111MODULE: on - - name: Run coverage + - name: Run Tests run: go test -coverprofile=coverage.txt -covermode=atomic -timeout 40m -v ./... env: GO111MODULE: on - name: Upload coverage to Codecov - run: bash <(curl -s https://codecov.io/bash) + uses: codecov/codecov-action@v4 + with: + files: ./coverage.txt + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/neat/network/fast_network.go b/neat/network/fast_network.go index b67b331..9413c3b 100644 --- a/neat/network/fast_network.go +++ b/neat/network/fast_network.go @@ -10,13 +10,13 @@ import ( // FastNetworkLink The connection descriptor for fast network type FastNetworkLink struct { // The index of source neuron - SourceIndex int + SourceIndex int `json:"source_index"` // The index of target neuron - TargetIndex int + TargetIndex int `json:"target_index"` // The weight of this link - Weight float64 + Weight float64 `json:"weight"` // The signal relayed by this link - Signal float64 + Signal float64 `json:"signal"` } // FastControlNode The module relay (control node) descriptor for fast network @@ -132,8 +132,6 @@ func NewFastModularNetworkSolver(biasNeuronCount, inputNeuronCount, outputNeuron return &fmm } -// ForwardSteps Propagates activation wave through all network nodes provided number of steps in forward direction. -// Returns true if activation wave passed from all inputs to the outputs. func (s *FastModularNetworkSolver) ForwardSteps(steps int) (res bool, err error) { for i := 0; i < steps; i++ { if res, err = s.forwardStep(0); err != nil { @@ -143,9 +141,6 @@ func (s *FastModularNetworkSolver) ForwardSteps(steps int) (res bool, err error) return res, nil } -// RecursiveSteps Propagates activation wave through all network nodes provided number of steps by recursion from output nodes -// Returns true if activation wave passed from all inputs to the outputs. This method is preferred method -// of network activation when number of forward steps can not be easy calculated and no network modules are set. func (s *FastModularNetworkSolver) RecursiveSteps() (res bool, err error) { if len(s.modules) > 0 { return false, errors.New("recursive activation can not be used for network with defined modules") @@ -233,9 +228,6 @@ func (s *FastModularNetworkSolver) recursiveActivateNode(currentNode int) (res b return res, err } -// Relax Attempts to relax network given amount of steps until giving up. The network considered relaxed when absolute -// value of the change at any given point is less than maxAllowedSignalDelta during activation waves propagation. -// If maxAllowedSignalDelta value is less than or equal to 0, the method will return true without checking for relaxation. func (s *FastModularNetworkSolver) Relax(maxSteps int, maxAllowedSignalDelta float64) (relaxed bool, err error) { for i := 0; i < maxSteps; i++ { if relaxed, err = s.forwardStep(maxAllowedSignalDelta); err != nil { @@ -307,16 +299,14 @@ func (s *FastModularNetworkSolver) forwardStep(maxAllowedSignalDelta float64) (i return isRelaxed, err } -// Flush Flushes network state by removing all current activations. Returns true if network flushed successfully or -// false in case of error. func (s *FastModularNetworkSolver) Flush() (bool, error) { for i := s.biasNeuronCount; i < s.totalNeuronCount; i++ { s.neuronSignals[i] = 0.0 + s.neuronSignalsBeingProcessed[i] = 0.0 } return true, nil } -// LoadSensors Set sensors values to the input nodes of the network func (s *FastModularNetworkSolver) LoadSensors(inputs []float64) error { if len(inputs) == s.inputNeuronCount { // only inputs should be provided @@ -329,7 +319,6 @@ func (s *FastModularNetworkSolver) LoadSensors(inputs []float64) error { return nil } -// ReadOutputs Read output values from the output nodes of the network func (s *FastModularNetworkSolver) ReadOutputs() []float64 { // decouple and return outs := make([]float64, s.outputNeuronCount) @@ -337,12 +326,10 @@ func (s *FastModularNetworkSolver) ReadOutputs() []float64 { return outs } -// NodeCount Returns the total number of neural units in the network func (s *FastModularNetworkSolver) NodeCount() int { return s.totalNeuronCount + len(s.modules) } -// LinkCount Returns the total number of links between nodes in the network func (s *FastModularNetworkSolver) LinkCount() int { // count all connections numLinks := len(s.connections) diff --git a/neat/network/fast_network_model_io.go b/neat/network/fast_network_model_io.go new file mode 100644 index 0000000..9eec108 --- /dev/null +++ b/neat/network/fast_network_model_io.go @@ -0,0 +1,114 @@ +package network + +import ( + "encoding/json" + "github.com/yaricom/goNEAT/v4/neat/math" + "io" +) + +// WriteModel is to write this FastModularNetworkSolver as a model to be used later. +func (s *FastModularNetworkSolver) WriteModel(w io.Writer) error { + dataHolder := newFastModularNetworkSolverData(s) + enc := json.NewEncoder(w) + return enc.Encode(dataHolder) +} + +// ReadFMNSModel allows loading model encoding FastModularNetworkSolver. +func ReadFMNSModel(reader io.Reader) (*FastModularNetworkSolver, error) { + var data fastModularNetworkSolverData + dec := json.NewDecoder(reader) + if err := dec.Decode(&data); err != nil { + return nil, err + } + activationFunctions := make([]math.NodeActivationType, len(data.ActivationFunctions)) + for i, f := range data.ActivationFunctions { + activationFunctions[i] = f.NodeActivation + } + var modules []*FastControlNode + if len(data.Modules) > 0 { + modules = make([]*FastControlNode, len(data.Modules)) + for i, m := range data.Modules { + modules[i] = &FastControlNode{ + ActivationType: m.ActivationType.NodeActivation, + InputIndexes: m.InputIndexes, + OutputIndexes: m.OutputIndexes, + } + } + } + fmns := NewFastModularNetworkSolver( + data.BiasNeuronCount, data.InputNeuronCount, data.OutputNeuronCount, + data.TotalNeuronCount, activationFunctions, + data.Connections, data.BiasList, modules, + ) + fmns.Name = data.Name + fmns.Id = data.Id + return fmns, nil +} + +type NodeActivator struct { + NodeActivation math.NodeActivationType +} + +type fastControlNodeData struct { + ActivationType NodeActivator `json:"activation_type"` + InputIndexes []int `json:"input_indexes"` + OutputIndexes []int `json:"output_indexes"` +} + +type fastModularNetworkSolverData struct { + Id int `json:"id"` + Name string `json:"name"` + InputNeuronCount int `json:"input_neuron_count"` + SensorNeuronCount int `json:"sensor_neuron_count"` + OutputNeuronCount int `json:"output_neuron_count"` + BiasNeuronCount int `json:"bias_neuron_count"` + TotalNeuronCount int `json:"total_neuron_count"` + ActivationFunctions []NodeActivator `json:"activation_functions"` + BiasList []float64 `json:"bias_list"` + Connections []*FastNetworkLink `json:"connections"` + Modules []fastControlNodeData `json:"modules,omitempty"` +} + +func newFastModularNetworkSolverData(n *FastModularNetworkSolver) *fastModularNetworkSolverData { + data := &fastModularNetworkSolverData{ + Id: n.Id, + Name: n.Name, + InputNeuronCount: n.inputNeuronCount, + SensorNeuronCount: n.sensorNeuronCount, + OutputNeuronCount: n.outputNeuronCount, + BiasNeuronCount: n.biasNeuronCount, + TotalNeuronCount: n.totalNeuronCount, + ActivationFunctions: make([]NodeActivator, len(n.activationFunctions)), + BiasList: n.biasList, + Connections: n.connections, + Modules: make([]fastControlNodeData, 0), + } + for i, v := range n.activationFunctions { + data.ActivationFunctions[i] = NodeActivator{ + NodeActivation: v, + } + } + if n.modules != nil { + for _, v := range n.modules { + data.Modules = append(data.Modules, fastControlNodeData{ + ActivationType: NodeActivator{NodeActivation: v.ActivationType}, + InputIndexes: v.InputIndexes, + OutputIndexes: v.OutputIndexes, + }) + } + } + return data +} + +func (n *NodeActivator) MarshalText() ([]byte, error) { + if activationName, err := math.NodeActivators.ActivationNameFromType(n.NodeActivation); err != nil { + return nil, err + } else { + return []byte(activationName), nil + } +} + +func (n *NodeActivator) UnmarshalText(text []byte) (err error) { + n.NodeActivation, err = math.NodeActivators.ActivationTypeFromName(string(text)) + return err +} diff --git a/neat/network/fast_network_model_io_test.go b/neat/network/fast_network_model_io_test.go new file mode 100644 index 0000000..6e98d28 --- /dev/null +++ b/neat/network/fast_network_model_io_test.go @@ -0,0 +1,163 @@ +package network + +import ( + "bytes" + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +const jsonFMNStr = `{"id":123456,"name":"test network","input_neuron_count":2,"sensor_neuron_count":3,"output_neuron_count":2,"bias_neuron_count":1,"total_neuron_count":8,"activation_functions":["SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation"],"bias_list":[0,0,0,0,0,0,1,0],"connections":[{"source_index":1,"target_index":5,"weight":15,"signal":0},{"source_index":2,"target_index":5,"weight":10,"signal":0},{"source_index":2,"target_index":6,"weight":5,"signal":0},{"source_index":6,"target_index":7,"weight":17,"signal":0},{"source_index":5,"target_index":3,"weight":7,"signal":0},{"source_index":7,"target_index":3,"weight":4.5,"signal":0},{"source_index":7,"target_index":4,"weight":13,"signal":0}]}` +const jsonFNMStrModule = `{"id":123456,"name":"test network","input_neuron_count":2,"sensor_neuron_count":3,"output_neuron_count":2,"bias_neuron_count":1,"total_neuron_count":8,"activation_functions":["SigmoidSteepenedActivation","SigmoidSteepenedActivation","SigmoidSteepenedActivation","LinearActivation","LinearActivation","LinearActivation","LinearActivation","NullActivation"],"bias_list":[0,0,0,0,0,10,1,0],"connections":[{"source_index":1,"target_index":5,"weight":15,"signal":0},{"source_index":2,"target_index":6,"weight":5,"signal":0},{"source_index":7,"target_index":3,"weight":4.5,"signal":0},{"source_index":7,"target_index":4,"weight":13,"signal":0}],"modules":[{"activation_type":"MultiplyModuleActivation","input_indexes":[5,6],"output_indexes":[7]}]}` + +const networkName = "test network" +const networkId = 123456 + +func TestFastModularNetworkSolver_WriteModel_NoModule(t *testing.T) { + net := buildNamedNetwork(networkName, networkId) + + fmm, err := net.FastNetworkSolver() + require.NoError(t, err, "failed to create fast network solver") + + outBuf := bytes.NewBufferString("") + err = fmm.(*FastModularNetworkSolver).WriteModel(outBuf) + require.NoError(t, err, "failed to write model") + + println(outBuf.String()) + + var expected interface{} + err = json.Unmarshal([]byte(jsonFMNStr), &expected) + require.NoError(t, err, "failed to unmarshal expected json") + var actual interface{} + err = json.Unmarshal(outBuf.Bytes(), &actual) + require.NoError(t, err, "failed to unmarshal actual json") + + assert.EqualValues(t, expected, actual, "model JSON does not match expected JSON") +} + +func TestFastModularNetworkSolver_WriteModel_WithModule(t *testing.T) { + net := buildNamedModularNetwork(networkName, networkId) + + fmm, err := net.FastNetworkSolver() + require.NoError(t, err, "failed to create fast network solver") + + outBuf := bytes.NewBufferString("") + err = fmm.(*FastModularNetworkSolver).WriteModel(outBuf) + require.NoError(t, err, "failed to write model") + + println(outBuf.String()) + + var expected interface{} + err = json.Unmarshal([]byte(jsonFNMStrModule), &expected) + require.NoError(t, err, "failed to unmarshal expected json") + var actual interface{} + err = json.Unmarshal(outBuf.Bytes(), &actual) + require.NoError(t, err, "failed to unmarshal actual json") + + assert.EqualValues(t, expected, actual, "model JSON does not match expected JSON") +} + +func TestReadFMNSModel_NoModule(t *testing.T) { + buf := bytes.NewBufferString(jsonFMNStr) + + fmm, err := ReadFMNSModel(buf) + assert.NoError(t, err, "failed to read model") + assert.NotNil(t, fmm, "failed to deserialize model") + + assert.Equal(t, fmm.Name, networkName, "wrong network name") + assert.Equal(t, fmm.Id, networkId, "wrong network id") + + data := []float64{1.5, 2.0} // bias inherent + err = fmm.LoadSensors(data) + require.NoError(t, err, "failed to load sensors") + + // test that it operates as expected + // + net := buildNetwork() + depth, err := net.MaxActivationDepth() + require.NoError(t, err, "failed to calculate max depth") + + t.Logf("depth: %d\n", depth) + logNetworkActivationPath(net, t) + + data = append(data, 1.0) // BIAS is third object + err = net.LoadSensors(data) + require.NoError(t, err, "failed to load sensors") + res, err := net.ForwardSteps(depth) + require.NoError(t, err, "error when trying to activate objective network") + require.True(t, res, "failed to activate objective network") + + // do forward steps through the solver and test results + // + res, err = fmm.Relax(depth, .1) + require.NoError(t, err, "error while do forward steps") + require.True(t, res, "forward steps returned false") + + // check results by comparing activations of objective network and fast network solver + // + outputs := fmm.ReadOutputs() + for i, out := range outputs { + assert.Equal(t, net.Outputs[i].Activation, out, "wrong activation at: %d", i) + } +} + +func TestReadFMNSModel_ModularNetwork(t *testing.T) { + buf := bytes.NewBufferString(jsonFNMStrModule) + + fmm, err := ReadFMNSModel(buf) + assert.NoError(t, err, "failed to read model") + assert.NotNil(t, fmm, "failed to deserialize model") + + assert.Equal(t, fmm.Name, networkName, "wrong network name") + assert.Equal(t, fmm.Id, networkId, "wrong network id") + + data := []float64{1.0, 2.0} // bias inherent + err = fmm.LoadSensors(data) + require.NoError(t, err, "failed to load sensors") + + // test that it operates as expected + // + net := buildModularNetwork() + depth, err := net.MaxActivationDepth() + require.NoError(t, err, "failed to calculate max depth") + + t.Logf("depth: %d\n", depth) + logNetworkActivationPath(net, t) + + // activate objective network + // + data = append(data, 1.0) // BIAS is third object + err = net.LoadSensors(data) + require.NoError(t, err, "failed to load sensors") + res, err := net.ForwardSteps(depth) + require.NoError(t, err, "error when trying to activate objective network") + require.True(t, res, "failed to activate objective network") + + // do forward steps through the solver and test results + // + res, err = fmm.Relax(depth, 1) + require.NoError(t, err, "error while do forward steps") + require.True(t, res, "forward steps returned false") + + // check results by comparing activations of objective network and fast network solver + // + outputs := fmm.ReadOutputs() + for i, out := range outputs { + assert.Equal(t, net.Outputs[i].Activation, out, "wrong activation at: %d", i) + } + +} +func buildNamedNetwork(name string, id int) *Network { + net := buildNetwork() + net.Name = name + net.Id = id + return net +} + +func buildNamedModularNetwork(name string, id int) *Network { + net := buildModularNetwork() + net.Name = name + net.Id = id + return net +} diff --git a/neat/network/network.go b/neat/network/network.go index 9c9b0c9..00dce6d 100644 --- a/neat/network/network.go +++ b/neat/network/network.go @@ -73,6 +73,8 @@ func (n *Network) FastNetworkSolver() (Solver, error) { inList = append(inList, ne) case HiddenNeuron: hiddenList = append(hiddenList, ne) + default: + // skip } } inputNeuronCount := len(inList) @@ -135,8 +137,11 @@ func (n *Network) FastNetworkSolver() (Solver, error) { modules[i] = &FastControlNode{InputIndexes: inputs, OutputIndexes: outputs, ActivationType: cn.ActivationType} } - return NewFastModularNetworkSolver(biasNeuronCount, inputNeuronCount, outputNeuronCount, totalNeuronCount, - activations, connections, biases, modules), nil + solver := NewFastModularNetworkSolver(biasNeuronCount, inputNeuronCount, outputNeuronCount, totalNeuronCount, + activations, connections, biases, modules) + solver.Id = n.Id + solver.Name = n.Name + return solver, nil } func processList(startIndex int, nList []*NNode, activations []math.NodeActivationType, neuronLookup map[int]int) int { @@ -451,18 +456,18 @@ func (n *Network) MaxActivationDepthWithCap(maxDepthCap int) (int, error) { return 1, nil // just one layer depth } - max := 0 // The max depth + maxDepth := 0 // The max depth for _, node := range n.Outputs { currDepth, err := node.Depth(0, maxDepthCap) if err != nil { return currDepth, err } - if currDepth > max { - max = currDepth + if currDepth > maxDepth { + maxDepth = currDepth } } - return max, nil + return maxDepth, nil } // AllNodes Returns all network nodes including MIMO control nodes: base nodes + control nodes @@ -488,7 +493,7 @@ func (n *Network) maxActivationDepthModular(w io.Writer) (int, error) { // negative cycle detected - fallback to FloydWarshall allPaths, _ = path.FloydWarshall(n) } - max := 0 // The max depth + maxDepth := 0 // The max depth for _, in := range n.inputs { for _, out := range n.Outputs { if paths, _ := allPaths.AllBetween(in.ID(), out.ID()); paths != nil { @@ -500,8 +505,8 @@ func (n *Network) maxActivationDepthModular(w io.Writer) (int, error) { // iterate over returned paths and find the one with maximal length for _, p := range paths { l := len(p) - 1 // to exclude input node - if l > max { - max = l + if l > maxDepth { + maxDepth = l } } } @@ -513,5 +518,5 @@ func (n *Network) maxActivationDepthModular(w io.Writer) (int, error) { } } - return max, nil + return maxDepth, nil } diff --git a/neat/network/solver.go b/neat/network/solver.go index 132542d..4d1b05b 100644 --- a/neat/network/solver.go +++ b/neat/network/solver.go @@ -4,6 +4,7 @@ package network type Solver interface { // ForwardSteps Propagates activation wave through all network nodes provided number of steps in forward direction. // Normally the number of steps should be equal to the activation depth of the network. + // See also Relax for conditional activation waves propagation. // Returns true if activation wave passed from all inputs to the output nodes. ForwardSteps(steps int) (bool, error) @@ -11,8 +12,9 @@ type Solver interface { // Returns true if activation wave passed from all inputs to the output nodes. RecursiveSteps() (bool, error) - // Relax Attempts to relax network given amount of steps until giving up. The network considered relaxed when absolute - // value of the change at any given point is less than maxAllowedSignalDelta during activation waves propagation. + // Relax Attempts to relax network (propagate activation waves) given amount of steps until giving up. + // The network considered relaxed when absolute value of the change at any given point is less + // than maxAllowedSignalDelta during activation waves propagation. // If maxAllowedSignalDelta value is less than or equal to 0, the method will return true without checking for relaxation. Relax(maxSteps int, maxAllowedSignalDelta float64) (bool, error)