Neat-Ex

Neat-Ex

An Elixir implementation of the NEAT algorithm, as described here.

Name Last Update
config Loading commit data...
lib Loading commit data...
test Loading commit data...
.gitignore Loading commit data...
LICENSE Loading commit data...
README.md Loading commit data...
mix.exs Loading commit data...
mix.lock Loading commit data...
notes.md Loading commit data...
render Loading commit data...

Neat-Ex

This Elixir library provides the means to define, simulate, and serialize Artificial-Neural-Networks (ANNs), as well as the means to develop them through use of the Neuro-Evolution of Augmenting Toplogies algorithm (NEAT), back-propogation, or gradient approximation.

NEAT, unlike back-propogation, develops both topology and neural weights. It trains using a fitness function instead of just training data. The gradient approximation algorithm is like back-propogation, in that it uses gradient optimization, but instead of calculating the exact gradient using the training data, it approximates the gradient using a training function. Training functions instead of training data allow for more flexibility.

Installation

Add as a dependency

Add {:neat_ex, "~> 1.1.1"} to your list of deps in your mix file, then run mix deps.get.

Or clone a copy

To clone and install, do:

git clone https://gitlab.com/onnoowl/Neat-Ex.git
cd Neat-Ex
mix deps.get

Documentation

For details, the latest documentation can be found at http://hexdocs.pm/neat_ex/index.html. For example usage, see the example below.

News

New in Version 1.1.0

This library is expanding to feature more neural training algorithms.

  • Updated to Elixir 1.2 and shifted from Dicts and Sets to Maps and MapSets
  • Added the Backprop module for back-propogation
  • Added the GradAprox module for gradient approximation and optimization of generic parameters
  • Added the GradAprox.NeuralTrainer module for gradient approximation and optimization of neural networks

Backprop vs GradAprox.NeuralTrainer

For training with a dataset, using the Backprop module is preferable. If the problem only allows for a fitness/error function, then GradAprox.NeuralTrainer should be used. This module approximates gradients instead of precisely calculating them by modifying weights slightly and than re-evaluating the fitness function.

See each module's documentation for more details and example usage.

(Potential) Upcoming Features

  • Newton's Method for gradient optimization
  • Back-propogation through time (BPTT) and automated neural network unfolding

Neat Example Usage

Here's a simple example that shows how to setup an evolution that evolves neural networks to act like binary XORs, where -1s are like 0s (and 1s are still 1s). The expected behavior is listed in dataset, and neural networks are assigned a fitness based on how close to the expected behavior they come. After 50 or so generations, or 10 seconds of computation, the networks exhibit the expected behavior.

#################
# Training data #
#################
dataset = [{{-1, -1}, -1}, {{1, -1}, 1}, {{-1, 1}, 1}, {{1, 1}, -1}] #{{in1, in2} output} -> the expected behavior


####################
# Fitness function #
####################
fitness = fn ann ->
  sim = Ann.Simulation.new(ann)
  error = Enum.reduce dataset, 0, fn {{in1, in2}, out}, error ->
    #We'll use 1 and 2 as input neurons, and we'll use 3 as a bias (it will always be given a value of 1.0)
    inputs = %{1 => in1, 2 => in2, 3 => 1.0}
    #Then we'll evaluate a simulation with these given inputs
    evaled_sim = Ann.Simulation.eval(sim, inputs)
    #Then we'll get the output of the 4th neuron, which is our output
    result = Map.fetch!(evaled_sim.data, 4)
    #Then we'll add the distance between the result and the expected output to the accumulated error
    error + abs(result - out)
  end

  #We're supposed to return fitness (where greater values are better) instead of error (where greater values are worse).
  #The maximum our error can be is 8, so we return (8 - error)^2 as our fitness.
  :math.pow(8 - error, 2)

  #The range of output for all neurons is -1 to positive 1. This means the furthest away the output
  #can ever be from the correct answer is 2. Since our dataset is of size 4, this means the maximum
  #error is 4*2 = 8
  #Doing 8 - error gives us a fitness value instead of an error value, because higher values are better.
  #Finally, we square the result, which is good practice.
end


######################
# Training the model #
######################
#To specify the inputs and outputs, we make a seed_ann. This will be how all the ANNs will start.
#Make a new network with inputs [1, 2, 3], and outputs [4], and use Ann.connectAll to connect
#all of the inputs to the outputs for us. This way it will start with some topology.
seed_ann = Ann.new([1, 2, 3], [4]) |> Ann.connectAll

#Make a new neat struct with this seed_ann and our fitness function
neat = Neat.new_single_fitness(seed_ann, fitness)
#Then evolve it until it reaches fitness level 63 (this fitness's function's max fitness is 64).
{ann, fitness} = Neat.evolveUntil(neat, 63).best
IO.puts Ann.json(ann) #display a json representation of the ANN.


###################
# Using the model #
###################
#We can then try out the produced ANN ourselves by making a simulation
simulation = ann
|> Ann.Simulation.new
|> Ann.Simulation.eval(%{1 => 1.0, 2 => 1.0, 3 => 1.0})

IO.inspect simulation.data[4] #in our example, the output is at neuron #4

XOR

mix xor.single

This command runs the sample xor code, evolving a neural network to act as an XOR logic gate. The resulting network can be viewed visually by running the command ./render xor, and then by opening xor.png.

FishSim

mix fishsim [display_every] [minutes_to_run]

This evolves neural networks to act like fish, and to run away from a shark. Fitness is based on how long fish can survive the shark. It will display ascii art demonstrating the simulation, where the @ sign is the shark, and the digits represent the fish, and the concentration at that specific location (higher numbers show a higher relative concentration of fish).

The evolution will only print out every display_every generations (default 1, meaning every generation). Setting it to 5, for example, will evolve for 5 generations between each display (which is far faster). The evolution lasts minutes_to_run minutes (default is 60).

mix fishsim [display_every] [minutes_to_run] [file_to_record_to]

By including a file name, the simulation will record visualization data to the file rather than displaying ascii art. display_every becomes handy for limiting the size of the visualization file. To view the recording after it's made, use Jonathan's project found here, and pass the file as the first argument.

When the process finishes, you can view the best fish using ./render bestFish, and then by opening bestFish.png

Testing (for contributors)

mix test