Current section
Files
Jump to
Current section
Files
src/neuroevolution_genetic.erl
%% @doc Genetic operators for neuroevolution.
%%
%% This module provides crossover and mutation operators for evolving
%% neural network weights. These are the core genetic algorithms used
%% by the neuroevolution server to create offspring from parent individuals.
%%
%% == Crossover ==
%%
%% Crossover combines weights from two parent networks to create offspring.
%% Currently implements uniform crossover where each weight has 50% chance
%% of coming from either parent.
%%
%% == Mutation ==
%%
%% Mutation perturbs weights by adding small random values. The mutation
%% rate controls what fraction of weights are modified, and mutation
%% strength controls the magnitude of changes.
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-module(neuroevolution_genetic).
-include("neuroevolution.hrl").
%% API
-export([
crossover_uniform/2,
mutate_weights/3,
create_offspring/4
]).
%%% ============================================================================
%%% API Functions
%%% ============================================================================
%% @doc Uniform crossover of two weight lists.
%%
%% For each weight position, randomly selects from either parent with
%% equal probability (50/50).
%%
%% Both weight lists must be the same length.
%%
%% Example:
%% ```
%% Parent1 = [1.0, 2.0, 3.0, 4.0],
%% Parent2 = [5.0, 6.0, 7.0, 8.0],
%% %% Might produce: [1.0, 6.0, 3.0, 8.0]
%% Child = neuroevolution_genetic:crossover_uniform(Parent1, Parent2).
%% '''
-spec crossover_uniform(Weights1, Weights2) -> ChildWeights when
Weights1 :: [float()],
Weights2 :: [float()],
ChildWeights :: [float()].
crossover_uniform(Weights1, Weights2) ->
lists:zipwith(
fun(W1, W2) ->
case rand:uniform() < 0.5 of
true -> W1;
false -> W2
end
end,
Weights1,
Weights2
).
%% @doc Mutate weights with given rate and strength.
%%
%% Each weight has MutationRate probability of being perturbed.
%% When mutated, a random value in [-Strength, +Strength] is added.
%%
%% Example:
%% ```
%% Weights = [1.0, 2.0, 3.0],
%% Rate = 0.1, %% 10% of weights mutated
%% Strength = 0.3, %% Changes up to +/- 0.3
%% Mutated = neuroevolution_genetic:mutate_weights(Weights, Rate, Strength).
%% '''
-spec mutate_weights(Weights, MutationRate, MutationStrength) -> MutatedWeights when
Weights :: [float()],
MutationRate :: float(),
MutationStrength :: float(),
MutatedWeights :: [float()].
mutate_weights(Weights, MutationRate, MutationStrength) ->
lists:map(
fun(W) ->
case rand:uniform() < MutationRate of
true ->
%% Perturb: add random value in [-Strength, +Strength]
Delta = (rand:uniform() - 0.5) * 2 * MutationStrength,
W + Delta;
false ->
W
end
end,
Weights
).
%% @doc Create offspring from two parent individuals.
%%
%% Combines crossover and mutation in a single operation:
%% 1. Extract weights from both parent networks
%% 2. Perform uniform crossover
%% 3. Apply mutation
%% 4. Create new network with child weights
%%
%% Returns a new individual record with lineage tracking.
-spec create_offspring(Parent1, Parent2, Config, Generation) -> Offspring when
Parent1 :: individual(),
Parent2 :: individual(),
Config :: neuro_config(),
Generation :: generation(),
Offspring :: individual().
create_offspring(Parent1, Parent2, Config, Generation) ->
%% Extract weights from parent networks
Weights1 = network_evaluator:get_weights(Parent1#individual.network),
Weights2 = network_evaluator:get_weights(Parent2#individual.network),
%% Crossover
ChildWeights = crossover_uniform(Weights1, Weights2),
%% Mutation
MutatedWeights = mutate_weights(
ChildWeights,
Config#neuro_config.mutation_rate,
Config#neuro_config.mutation_strength
),
%% Create new network with child weights
{InputSize, HiddenLayers, OutputSize} = Config#neuro_config.network_topology,
Network = network_evaluator:create_feedforward(InputSize, HiddenLayers, OutputSize),
ChildNetwork = network_evaluator:set_weights(Network, MutatedWeights),
%% Create offspring individual with lineage
ChildId = make_ref(),
#individual{
id = ChildId,
network = ChildNetwork,
parent1_id = Parent1#individual.id,
parent2_id = Parent2#individual.id,
generation_born = Generation,
is_offspring = true
}.