Bardo.PopulationManager.Genotype (Bardo v0.1.0)
View SourceThe Genotype module encapsulates the genetic representation of neural networks.
Overview
In neuroevolution, a genotype serves as the genetic blueprint from which a neural network (phenotype) is constructed. The Genotype module provides functions for creating, manipulating, and evolving these blueprints.
Key Concepts
Topology and Weight Evolving Artificial Neural Networks (TWEANNs)
Unlike traditional neural networks with fixed architectures, TWEANNs can evolve their entire structure during the evolutionary process:
- New neurons can be added
- New connections can be formed
- Existing connections can be modified or removed
- Neural parameters (weights, biases, activation functions) can change
This allows the evolutionary process to discover optimal network structures without the need for manual architecture design or hyperparameter tuning.
Incremental Complexity
The evolutionary process typically begins with minimal seed genotypes and gradually increases complexity as needed to solve the problem:
- Start with the simplest possible network (often just input-output connections)
- Allow mutation operators to add complexity as evolution progresses
- Let natural selection favor the most efficient solutions
Activation Function Diversity
Bardo supports a variety of activation functions beyond the standard sigmoid/tanh:
- Sine, absolute value, sign function
- Gaussian, step functions
- Linear and rectified linear
- Custom user-defined functions
Different activation functions can be used in different parts of the network or constrained to specific subpopulations to explore diverse solution spaces.
Constraints
Evolutionary constraints can be applied to guide the evolutionary process:
- Morphology constraints: Define the available sensors and actuators
- Activation function constraints: Limit which functions can be used
- Topological constraints: Restrict certain kinds of connections
Implementation
The genotype representation uses a structured encoding that tracks:
- Neuron properties (layer, type, activation function, etc.)
- Connection topology (which neurons connect to which)
- Connection weights and other parameters
- Historical markers to aid in crossover operations
Summary
Functions
Adds a connection between two neurons.
Adds a neuron to the genotype.
The clone_agent accepts AgentId and generates a CloneAgentId. It then calls clone_agent which accepts AgentId, and CloneAgentId, and then clones the agent, giving the clone CloneAgentId. The function first creates an ETS table to which it writes the ids of all the elements of the genotype, and their corresponding clone ids. Once all ids and clone ids have been generated, the function then begins to clone the actual elements.
The population mgr should have all the information with regards to the morphologies and specie constraint under which the agent's genotype should be created. Thus construct_agent/3 is run with the SpecieId to which this NN based system will belong, the AgentId that this NN based intelligent agent will have, and the SpecCon (specie constraint) that will define the list of activation functions and other parameters from which the seed agent can choose its parameters. First the generation is set to 0, since the agent is just created, then the construct_cortex/3 is ran, which creates the NN and returns its CxId. Once the NN is created and the the cortex's id is returned, we can fill out the information needed by the agent record, and write it to the database.
Each neuron record is composed by the construct_neuron function. The construct_neuron creates the Input list from the tuples [{Id, Weights}...] using the vector lengths specified in the InputSpecs list. The create_input_idps function uses create_neural_weights_p to generate a tuple list with random weights in the range of -0.5 to 0.5, and plasticity parameters dependent on the PF function. The activation function that the neuron uses is chosen randomly from the neural_afs list within the constraint record passed to the construct_neuron function. construct_neuron uses calculate_roids to extract the list of recursive connection ids from the OutputIds passed to it. Once the neuron record is filled in, it is saved to the database.
Each neuron record is composed by the construct_neuron function. The construct_neuron creates the Input list from the tuples [{Id, Weights}...] using the vector lengths specified in the InputSpecs list. The create_input_idps function uses create_neural_weights_p to generate a tuple list with random weights in the range of -0.5 to 0.5, and plasticity parameters dependent on the PF function. The activation function that the neuron uses is chosen randomly from the neural_afs list within the constraint record passed to the construct_neuron function. construct_neuron uses calculate_roids to extract the list of recursive connection ids from the OutputIds passed to it. Once the neuron record is filled in, it is saved to the database.
The delete_agent accepts the id of an agent, and then deletes that agent's genotype. This function assumes that the id of the agent will be removed from the specie's agent_ids list, and any other clean up procedures, by the calling function.
Gets the IDs of neurons in a specific layer.
The link_neuron function links the neuron to another element. For example, to another neuron.
Creates a new empty genotype.
Prints out the complete genotype of an agent.
The unique_id creates a unique Id, the Id is a floating point value. NOT cryptographically strong.
The update_fingerprint calculates the fingerprint of the agent, where the fingerprint is just a tuple of the various general features of the NN based system, a list of features that play some role in distinguishing its genotype's general properties from those of other NN systems. The fingerprint here is composed of the generalized pattern (pattern minus the unique ids), generalized evolutionary history (evolutionary history minus the unique ids of the elements), a generalized sensor set, and a generalized actuator set.
Functions
Adds a connection between two neurons.
Parameters
genotype
- The genotype to add the connection tofrom_id
- The ID of the source neuronto_id
- The ID of the target neuronweight
- The weight of the connection
Examples
iex> genotype = Bardo.PopulationManager.Genotype.new()
iex> genotype = Bardo.PopulationManager.Genotype.add_neuron(genotype, :input, %{id: "input"})
iex> genotype = Bardo.PopulationManager.Genotype.add_neuron(genotype, :output, %{id: "output"})
iex> genotype = Bardo.PopulationManager.Genotype.add_connection(genotype, "input", "output", 0.5)
Adds a neuron to the genotype.
Parameters
genotype
- The genotype to add the neuron tolayer
- The layer of the neuron (:input, :hidden, :output, or :bias)params
- Optional parameters for the neuron
Examples
iex> genotype = Bardo.PopulationManager.Genotype.new()
iex> genotype = Bardo.PopulationManager.Genotype.add_neuron(genotype, :input)
%{neurons: %{"neuron_1" => %{layer: :input, activation_function: :sigmoid}}, ...}
@spec clone_agent(Bardo.Models.agent_id()) :: {:agent, float()}
The clone_agent accepts AgentId and generates a CloneAgentId. It then calls clone_agent which accepts AgentId, and CloneAgentId, and then clones the agent, giving the clone CloneAgentId. The function first creates an ETS table to which it writes the ids of all the elements of the genotype, and their corresponding clone ids. Once all ids and clone ids have been generated, the function then begins to clone the actual elements.
The population mgr should have all the information with regards to the morphologies and specie constraint under which the agent's genotype should be created. Thus construct_agent/3 is run with the SpecieId to which this NN based system will belong, the AgentId that this NN based intelligent agent will have, and the SpecCon (specie constraint) that will define the list of activation functions and other parameters from which the seed agent can choose its parameters. First the generation is set to 0, since the agent is just created, then the construct_cortex/3 is ran, which creates the NN and returns its CxId. Once the NN is created and the the cortex's id is returned, we can fill out the information needed by the agent record, and write it to the database.
@spec construct_neuron( {:cortex, {:origin, float()}}, non_neg_integer(), Bardo.Models.constraint(), {:neuron, {float(), float()}}, [{Bardo.Models.neuron_ids(), float()}], [{:actuator | :neuron, {float(), float()}}] ) :: :ok
Each neuron record is composed by the construct_neuron function. The construct_neuron creates the Input list from the tuples [{Id, Weights}...] using the vector lengths specified in the InputSpecs list. The create_input_idps function uses create_neural_weights_p to generate a tuple list with random weights in the range of -0.5 to 0.5, and plasticity parameters dependent on the PF function. The activation function that the neuron uses is chosen randomly from the neural_afs list within the constraint record passed to the construct_neuron function. construct_neuron uses calculate_roids to extract the list of recursive connection ids from the OutputIds passed to it. Once the neuron record is filled in, it is saved to the database.
@spec create_neural_weights_p(atom(), non_neg_integer(), [float()]) :: [ {float(), [float()] | []} ]
Each neuron record is composed by the construct_neuron function. The construct_neuron creates the Input list from the tuples [{Id, Weights}...] using the vector lengths specified in the InputSpecs list. The create_input_idps function uses create_neural_weights_p to generate a tuple list with random weights in the range of -0.5 to 0.5, and plasticity parameters dependent on the PF function. The activation function that the neuron uses is chosen randomly from the neural_afs list within the constraint record passed to the construct_neuron function. construct_neuron uses calculate_roids to extract the list of recursive connection ids from the OutputIds passed to it. Once the neuron record is filled in, it is saved to the database.
@spec delete_agent(Bardo.Models.agent_id()) :: :ok
The delete_agent accepts the id of an agent, and then deletes that agent's genotype. This function assumes that the id of the agent will be removed from the specie's agent_ids list, and any other clean up procedures, by the calling function.
Gets the IDs of neurons in a specific layer.
Parameters
genotype
- The genotype to get neurons fromlayer
- The layer to get neurons from
Examples
iex> genotype = Bardo.PopulationManager.Genotype.new()
iex> genotype = Bardo.PopulationManager.Genotype.add_neuron(genotype, :input, %{id: "input1"})
iex> genotype = Bardo.PopulationManager.Genotype.add_neuron(genotype, :input, %{id: "input2"})
iex> Bardo.PopulationManager.Genotype.get_layer_neuron_ids(genotype, :input)
["input1", "input2"]
@spec link_neuron( integer(), [Bardo.Models.sensor_id() | Bardo.Models.neuron_id()], Bardo.Models.neuron_id(), [Bardo.Models.actuator_id() | Bardo.Models.neuron_id()] ) :: [:ok]
The link_neuron function links the neuron to another element. For example, to another neuron.
Creates a new empty genotype.
Returns a map with empty neurons and connections.
Examples
iex> genotype = Bardo.PopulationManager.Genotype.new()
%{neurons: %{}, connections: %{}}
Prints out the complete genotype of an agent.
@spec unique_id() :: float()
The unique_id creates a unique Id, the Id is a floating point value. NOT cryptographically strong.
@spec update_fingerprint(Bardo.Models.agent_id()) :: :ok
The update_fingerprint calculates the fingerprint of the agent, where the fingerprint is just a tuple of the various general features of the NN based system, a list of features that play some role in distinguishing its genotype's general properties from those of other NN systems. The fingerprint here is composed of the generalized pattern (pattern minus the unique ids), generalized evolutionary history (evolutionary history minus the unique ids of the elements), a generalized sensor set, and a generalized actuator set.