API
Simple Artificial Neural Networks
Simple Artificial Neural Networks (SANN) is a naive Python implementation of an artificial neural network (ANN) that's useful for educational purposes and clarifying the concepts of feed-forward neural networks, backpropagation, neuro-evolution of weights and biases, and genetic algorithms. SANN is not intended for production use or performance-critical applications. Rather, use it for educational, playful or small-scale projects. 😉
See: https://ntoll.org/article/ai-curtain/ for a comprehensive and informal exploration of the concepts behind this code.
Copyright (c) 2025 Nicholas H.Tollervey (ntoll@ntoll.org).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
backpropagate(ann, inputs, expected_outputs, learning_rate=0.1)
Perform backpropagation to adjust the weights of the ann based on the
expected_outputs.
This function calculates the error for each node in the output layer,
propagates that error back through the network, and adjusts the weights
accordingly. The learning_rate determines how much the weights are
adjusted during each update.
It returns the updated ANN with adjusted weights.
Source code in docs/sann.py
clean_network(ann)
Remove the outputs stored in nodes to clean up the ann, so only the
weights and biases remain.
Source code in docs/sann.py
create_network(structure)
Return a dict representing a simple artificial neural network (ANN).
The structure argument should be a list containing the number of nodes in
each layer of a fully connected feed-forward neural network.
The resulting dictionary will contain a list of layers, where each layer is a list of nodes. Each node is represented as a dictionary containing its incoming weights from the previous layer and a bias value. The weights and bias are randomly initialised to a value between -1 and 1.
The first layer is ignored since it is the input layer and has no weights nor bias associated with it. There must be at least two layers (an input layer and an output layer) for the ANN to be valid.
Other arbitrary properties are added to the returned dictionary, such as
a fitness score, which can be used for training or evolution of the ANN,
and a structure that defines the topology of the ANN (i.e. the number of
nodes in each layer).
Source code in docs/sann.py
crossover(mum, dad)
Perform crossover between two parent ANNs (mum and dad) to create two
child ANNs. The children inherit weights and biases from both parents
through the following process:
- Two split points are chosen randomly. A split point is always at the boundary between two nodes in a layer.
- The first child inherits weights and biases from the
mumup to the first split point, then from thedaduntil the second split point, and finally from themumagain. - The second child inherits weights and biases from the
dadup to the first split point, then from themumuntil the second split point, and finally from thedadagain. - Nodes are treated as a continuous sequence across layers, so the split points can cross layer boundaries.
- The children are returned as a tuple of two new ANN structures.
Source code in docs/sann.py
evolve(layers, population_size, fitness_function, halt_function, generate_function=simple_generate, fittest_proportion=0.5, mutation_chance=0.01, mutation_amount=0.1, reverse=True, log=lambda x: None)
Evolve a population of ANNs using a genetic algorithm.
The layers define the topology of the ANNs as a list of layer sizes (as
per the create_ann function in this module). The population_size is an
integer defining the number of ANNs in each generation.
The fitness_function takes an individual ANN to evaluate and the current
population (of siblings), and returns a fitness score that is annotated
as the network's ann["fitness"] value. The halt_function takes the
current population and generation count to determine if the genetic
algorithm should stop.
The generate_function should take a list of the current population
sorted by fitness, along with the optional fittest_proportion that
determines the proportion of the fittest individuals to retain. The
mutation_chance, and mutation_amount parameters are used to control
the mutation process. The generate_function returns a new unsorted
population for the next generation.
The reverse flag indicates if the fittest ANN has the highest (True)
or lowest (False) fitness score. Finally, the log function can be used
to log each generation during the course of evolution. It defaults to a
no-op function that does nothing.
When the genetic algorithm halts, it returns the final population ordered by fitness.
Source code in docs/sann.py
mutate(ann, mutation_chance=0.01, mutation_amount=0.1)
Mutate the ann by randomly adjusting weights and biases. Return the
mutated ANN.
The mutation_chance determines the likelihood of each weight or bias
being mutated. A higher mutation_chance means more frequent changes.
The randomly selected weight or bias has its value changed by a small
random amount within the -/+ mutation_amount range.
Source code in docs/sann.py
roulette_wheel_selection(population)
Select a neural network from the population, with the fittest networks
having a higher chance of being selected.
A random number between 0 and the total fitness score of all the ANNs in a population is chosen (a point within a slice of a roulette wheel). The code iterates through the ANNs adding up the fitness scores. When the subtotal is greater than the randomly chosen point it returns the ANN at that point "on the wheel".
Source code in docs/sann.py
run_network(ann, inputs)
Perform a forward pass through the ann using the given inputs.
The inputs are a list of values that are fed into the first layer of the ANN. The output of each layer is calculated and passed to the next layer until the final output is produced and returned as a list of values.
Source code in docs/sann.py
sigmoid(activation, threshold=0.0, shape=1.0)
Calculate the output value of a sigmoid based node.
Take the activation value, a threshold value, and a shape parameter,
and return the output value found somewhere on an s-shaped sigmoid curve.
Source code in docs/sann.py
simple_generate(old_population, fittest_proportion=0.5, mutation_chance=0.01, mutation_amount=0.1)
Generate a new population of ANNs by performing crossover and mutation
on the old_population.
The new population is created by selecting the fittest ANNs from the
old_population. The fittest proportion is defined by the
fittest_proportion argument, where 0.5 means half of the
old_population is used as parents.
The new population is filled with children created from pairs of parents
selected using roulette wheel selection. Each pair of parents undergoes
crossover to produce two children, which are then mutated. The new
population is returned, which should be the same size as the
old_population.
The mutation_chance and mutation_amount parameters control the
mutation process for the children and are passed to the mutate
function.
Source code in docs/sann.py
sum_inputs(inputs)
Calculate the activation value from a list of pairs of x input values
and w weights. This is essentially just the dot product.
train(ann, training_data, epochs=1000, learning_rate=0.1, log=lambda x: None)
Supervised training of the ann using the provided training_data.
The training_data is a list of tuples where each tuple contains inputs and
the expected output. The ANN is trained for a specified number of epochs,
adjusting the weights by the learning_rate, and based on the error
between actual and expected outputs.
The log function can be used to log progress during training. It defaults
to a no-op function that does nothing.