Current section
Files
Jump to
Current section
Files
src/evolve/network_factory.erl
%% @doc Network factory wrapper for faber_tweann's network_evaluator.
%%
%% This module implements the network factory interface expected by evolution
%% strategies, delegating to the real network_evaluator from faber_tweann.
%%
%% The factory interface provides:
%% create_feedforward/1 to create a new feedforward network,
%% mutate/2 to mutate a network's weights,
%% crossover/2 to create offspring from two parent networks.
%%
%% This abstraction enables dependency injection for testing
%% (use mock_network_factory in tests), clean separation between
%% evolution logic and network implementation, and future support
%% for different network types (RNNs, LSTMs, etc.).
%%
%% @author R.G. Lefever
%% @copyright 2024-2026 R.G. Lefever
%% @end
-module(network_factory).
-export([
create_feedforward/1,
create_compiled_feedforward/1,
compile/1,
mutate/2,
mutate_cfc/2,
crossover/2
]).
%% @doc Create a new feedforward neural network.
%%
%% Delegates to network_evaluator:create_feedforward/3 from faber_tweann.
%% Topology is {InputSize, HiddenLayers, OutputSize} where HiddenLayers
%% is a list of layer sizes.
%% @end
-spec create_feedforward(Topology) -> network_evaluator:network() when
Topology :: {pos_integer(), [pos_integer()], pos_integer()}.
create_feedforward({InputSize, HiddenLayers, OutputSize}) ->
network_evaluator:create_feedforward(InputSize, HiddenLayers, OutputSize).
%% @doc Create a NIF-compiled feedforward network for fast evaluation.
%%
%% Combines network creation and NIF compilation in one step.
%% Uses the native path when configured (speedup unmeasured; see ROADMAP.md).
%% @end
-spec create_compiled_feedforward(Topology) -> Result when
Topology :: {pos_integer(), [pos_integer()], pos_integer()},
Result :: {ok, nif_network:compiled_network()} | {error, term()}.
create_compiled_feedforward({InputSize, HiddenLayers, OutputSize}) ->
nif_network:compile_feedforward(InputSize, HiddenLayers, OutputSize).
%% @doc Compile an existing network for NIF-accelerated evaluation.
%%
%% Takes a network from create_feedforward/1 and compiles it for
%% fast repeated evaluation via NIF.
%% @end
-spec compile(Network) -> Result when
Network :: network_evaluator:network(),
Result :: {ok, nif_network:compiled_network()} | {error, term()}.
compile(Network) ->
nif_network:compile(Network).
%% @doc Mutate a network's weights.
%%
%% Creates a copy of the network with mutated weights. The mutation applies
%% gaussian noise to each weight with the given strength (standard deviation).
%% @end
-spec mutate(Network, MutationStrength) -> network_evaluator:network() when
Network :: network_evaluator:network(),
MutationStrength :: float().
mutate(Network, MutationStrength) ->
%% Get current weights
Weights = network_evaluator:get_weights(Network),
%% Apply gaussian mutation to each weight
MutatedWeights = [W + (rand:normal() * MutationStrength) || W <- Weights],
%% Create new network with mutated weights
network_evaluator:set_weights(Network, MutatedWeights).
%% @doc Crossover two networks to produce offspring.
%%
%% Performs uniform crossover: each weight in the offspring is randomly
%% selected from either parent with equal probability.
%% @end
-spec crossover(Parent1, Parent2) -> network_evaluator:network() when
Parent1 :: network_evaluator:network(),
Parent2 :: network_evaluator:network().
crossover(Parent1, Parent2) ->
%% Get weights from both parents
Weights1 = network_evaluator:get_weights(Parent1),
Weights2 = network_evaluator:get_weights(Parent2),
%% Uniform crossover: randomly select each weight from either parent
ChildWeights = lists:zipwith(
fun(W1, W2) ->
case rand:uniform() < 0.5 of
true -> W1;
false -> W2
end
end,
Weights1,
Weights2
),
%% Create child network with crossed weights
network_evaluator:set_weights(Parent1, ChildWeights).
%% @doc Mutate a CfC network's weights and neuron parameters.
%%
%% Extends standard weight mutation with CfC-specific parameter mutations:
%% tau values perturbed with gaussian noise clamped to [0.01, 10.0],
%% state bounds perturbed with gaussian noise clamped to [0.1, 5.0],
%% and a 5% chance to toggle neuron type between standard and cfc.
%%
%% For standard networks (no neuron_meta), behaves identically to mutate/2.
%% @end
-spec mutate_cfc(Network, MutationStrength) -> network_evaluator:network() when
Network :: network_evaluator:network(),
MutationStrength :: float().
mutate_cfc(Network, MutationStrength) ->
%% 1. Mutate weights (same as standard)
Weights = network_evaluator:get_weights(Network),
MutatedWeights = [W + (rand:normal() * MutationStrength) || W <- Weights],
Network1 = network_evaluator:set_weights(Network, MutatedWeights),
%% 2. Mutate CfC neuron parameters
case network_evaluator:get_neuron_meta(Network1) of
undefined ->
Network1;
Meta ->
MutatedMeta = mutate_neuron_meta(Meta, MutationStrength),
network_evaluator:set_neuron_meta(Network1, MutatedMeta)
end.
%% @private Mutate neuron metadata for all layers.
mutate_neuron_meta(LayerMetas, Strength) ->
[mutate_layer_meta(LayerM, Strength) || LayerM <- LayerMetas].
%% @private Mutate neuron metadata for a single layer.
mutate_layer_meta(NeuronMetas, Strength) ->
[mutate_single_neuron_meta(M, Strength) || M <- NeuronMetas].
%% @private Mutate a single neuron's CfC parameters.
mutate_single_neuron_meta(#{neuron_type := Type, tau := Tau,
state_bound := Bound} = Meta, Strength) ->
%% Mutate tau: gaussian perturbation, clamp to [0.01, 10.0]
NewTau = clamp(Tau + rand:normal() * Strength * 0.5, 0.01, 10.0),
%% Mutate state bound: gaussian perturbation, clamp to [0.1, 5.0]
NewBound = clamp(Bound + rand:normal() * Strength * 0.3, 0.1, 5.0),
%% Neuron type toggle: ~5% chance
NewType = case rand:uniform() < 0.05 of
true ->
case Type of
standard -> cfc;
cfc -> standard
end;
false ->
Type
end,
Meta#{neuron_type := NewType, tau := NewTau, state_bound := NewBound}.
%% @private Clamp value to range.
clamp(Val, Min, _Max) when Val < Min -> Min;
clamp(Val, _Min, Max) when Val > Max -> Max;
clamp(Val, _Min, _Max) -> Val.