https://github.com/maartz/nn
A Feed Forward Neural Network in Erlang
https://github.com/maartz/nn
ai erlang feedforward-neural-network
Last synced: about 1 month ago
JSON representation
A Feed Forward Neural Network in Erlang
- Host: GitHub
- URL: https://github.com/maartz/nn
- Owner: Maartz
- Created: 2025-01-13T16:44:58.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2025-01-22T16:16:05.000Z (over 1 year ago)
- Last Synced: 2025-01-22T17:25:55.560Z (over 1 year ago)
- Topics: ai, erlang, feedforward-neural-network
- Language: Erlang
- Homepage:
- Size: 19.5 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Feed-Forward Neural Network in Erlang/OTP
[](https://www.erlang.org/)
[](doc/readme.html)
[](LICENSE)
> A concurrent, message-passing based neural network implementation using Erlang's actor model
## Overview
This project implements a **Feed-Forward Neural Network (FFNN)** using Erlang/OTP's actor model with a **perturbation-based learning algorithm**. Each component of the neural network (neurons, sensors, actuators, cortex, and scapes) is implemented as a separate concurrent process, allowing for **parallel execution** and **message-passing based communication**.
### Key Features
- 🧠**Actor-based architecture** - Each neuron, sensor, and actuator runs as an independent process
- âš¡ **Concurrent execution** - Natural parallelism through Erlang's process model
- 🔄 **Perturbation learning** - Evolutionary-style weight optimization without backpropagation
- 📊 **Built-in benchmarking** - Statistical analysis across multiple training runs
- 💾 **Persistent genotypes** - Save and load trained networks via Mnesia/ETS
- 📚 **Complete documentation** - ExDoc-generated API documentation with examples
## Architecture
### System Components
The system consists of several key modules:
1. `exoself.erl` - Orchestrates the network creation, lifecycle, and training loop
2. `genotype.erl` - Generates and persists network structure and weights
3. `morphology.erl` - Defines problem-specific sensor/actuator configurations
4. `scape.erl` - Implements environment simulations (e.g., XOR problem)
5. `sensor.erl` - Handles input generation from scapes
6. `neuron.erl` - Implements neuron behavior with weight management
7. `actuator.erl` - Manages output processing and fitness collection
8. `cortex.erl` - Orchestrates synchronous network operation cycles
9. `trainer.erl` - Manages training sessions with fitness tracking
10. `benchmarker.erl` - Runs statistical benchmarks across multiple training runs
11. `platform.erl` - Gen_server for managing shared scapes and modules
12. `records.hrl` - Defines data structures
### Process Hierarchy
```mermaid
graph TD
P[Platform] -.->|Manages| PSC[Public Scapes]
P -.->|Mnesia| DB[(Database)]
B[Benchmarker] -->|Spawns| T[Trainer]
T -->|Spawns| E[ExoSelf]
E -->|Creates| SC[Private Scapes]
E -->|Creates| C[Cortex]
E -->|Creates| S[Sensors]
E -->|Creates| N[Neurons]
E -->|Creates| A[Actuators]
C -->|Sync| S
S -->|Percepts| SC
S -.->|Can use| PSC
S -->|Forward| N
N -->|Forward| A
A -->|Actions| SC
A -.->|Can use| PSC
SC -->|Fitness| A
PSC -.->|Fitness| A
A -->|Sync| C
C -->|Results| E
E -->|Best Fitness| T
T -->|Statistics| B
```
### Message Flow (One Evaluation Cycle)
```mermaid
sequenceDiagram
participant E as ExoSelf
participant C as Cortex
participant S as Sensor
participant SC as Scape
participant N as Neuron
participant A as Actuator
E->>C: Send PIDs & IDs
C->>S: {sync}
S->>SC: {sense}
SC->>S: {percept, Input}
S->>N: {forward, Input}
N->>N: Compute: tanh(dot(Input, Weights))
N->>A: {forward, Output}
A->>SC: {action, Output}
SC->>A: {Fitness, HaltFlag}
A->>C: {sync, Fitness, HaltFlag}
C->>E: {evaluation_completed, Fitness, Cycles, Time}
```
### Key Data Structures
#### Core Neural Network Records
```erlang
-record(sensor, {id, cortex_id, name, scape, vector_length, fanout_ids}).
-record(actuator, {id, cortex_id, name, scape, vector_length, fanin_ids}).
-record(neuron, {id, cortex_id, activation_function, input_ids, output_ids}).
-record(cortex, {id, sensor_ids, actuator_ids, neuron_ids}).
```
**Note:** Sensors and actuators include a `scape` field which specifies the environment (e.g., `{private, xor_sim}` for a private XOR simulator).
#### Evolutionary Algorithm Records (Future Support)
```erlang
-record(agent, {id, generation, population_id, specie_id, cortex_id, fingerprint,
constraint, evolution_history=[], fitness, innovation_factor=0, pattern=[]}).
-record(specie, {id, population_id, fingerprint, constraint, agent_ids=[], dead_pool=[],
champion_ids=[], fitness, innovation_factor=0}).
-record(population, {id, platform_id, specie_ids=[], morphologies=[], innovation_factor}).
-record(constraint, {morphology=xor_mimic, neural_afs=[tanh, cos, gauss, abs]}).
```
These records support evolutionary/genetic algorithm capabilities:
- **agent**: Individual neural network in a population with evolutionary history
- **specie**: Group of similar agents sharing a fingerprint
- **population**: Collection of species being evolved
- **constraint**: Evolutionary constraints (morphology type, available activation functions)
## Neural Network Mathematics
### Dot Product
The dot product is used in neurons to compute the weighted sum of inputs:
```erlang
dot([I | Input], [W | Weights], Acc) ->
dot(Input, Weights, I * W + Acc);
dot([], [], Acc) ->
Acc.
```
This operation:
- Multiplies each input by its corresponding weight
- Sums all products
- Determines neuron activation strength
### Activation Function (tanh)
The network uses hyperbolic tangent (tanh) as its activation function:
```erlang
tanh(Val) ->
math:tanh(Val).
```
Key properties:
- Bounds output between -1 and 1
- Non-linear transformation
- Smooth gradient
- Zero-centered output
Benefits for neural networks:
1. Prevents numerical overflow
2. Allows for negative outputs
3. Strong gradients near zero
4. Smooth activation curves
## Detailed Component Descriptions
### ExoSelf (exoself.erl)
The ExoSelf process is the top-level orchestrator responsible for:
1. **Genotype Loading**: Reads network configuration from ETS table files
2. **Process Spawning**: Creates all cerebral units (cortex, sensors, neurons, actuators) and scapes
3. **Connection Mapping**: Establishes IdsNPIds ETS table mapping element IDs to PIDs
4. **Process Linking**: Sends initialization messages with connection information
5. **Training Loop**: Implements perturbation-based learning:
- Perturbs random subset of neuron weights (probability = 1/sqrt(total_neurons))
- Backs up weights when fitness improves
- Restores weights when fitness degrades
- Terminates after MAX_ATTEMPTS (50) consecutive failures
6. **Genotype Persistence**: Saves trained weights back to genotype file
7. **Result Reporting**: Communicates final fitness/statistics to trainer process
### Cortex (cortex.erl)
The Cortex orchestrates the synchronous operation of the neural network:
- **Cycle Management**: Triggers sensors to begin each evaluation cycle
- **Synchronization**: Collects sync messages from all actuators before starting next cycle
- **Fitness Accumulation**: Aggregates fitness scores from actuators
- **Evaluation Completion**: Detects end of evaluation (via EndFlag) and reports to ExoSelf
- **State Transitions**: Switches between active (running) and inactive (waiting for reactivation) states
- **Timing**: Tracks cycle count and execution time for performance metrics
### Scapes (scape.erl)
Scapes implement the problem environments:
- **XOR Simulator** (`xor_sim/1`): Provides XOR training data
- Cycles through 4 XOR cases: `[{[-1,-1],[-1]}, {[1,-1],[1]}, {[-1,1],[1]}, {[1,1],[-1]}]`
- Calculates Mean Squared Error (MSE) between network output and target
- Returns fitness as `1/(MSE + 0.00001)` after completing all 4 cases
- **Protocol**: Responds to `{sense}` messages with percepts, receives `{action, Output}` messages
- **Scope**: Can be private (spawned per network) or public (shared across networks)
### Sensors (sensor.erl)
Sensors generate or retrieve input data:
- **Scape Communication**: Sends `{sense}` message and receives `{percept, Vector}` response
- **Data Forwarding**: Broadcasts sensory vector to all connected neurons (fanout)
- **Sensor Types**:
- `xor_GetInput/2`: Retrieves input from XOR scape
- `rng/1`: Generates random numbers (for testing)
- **Synchronization**: Triggered by cortex `{sync}` messages
### Neurons (neuron.erl)
Neurons perform the core neural computation:
- **Weighted Sum**: Computes dot product of inputs and weights
- **Activation**: Applies tanh activation function
- **Weight Management**:
- `weight_backup`: Stores current weights in process dictionary
- `weight_restore`: Reverts to backed up weights
- `weight_perturb`: Randomly perturbs weights (probability = 1/sqrt(total_weights))
- **Saturation**: Limits weight values to ±2π range
- **Delta Multiplier**: Uses 2Ï€ for perturbation magnitude
### Actuators (actuator.erl)
Actuators collect network outputs and interact with scapes:
- **Output Collection**: Gathers outputs from all connected neurons (fanin)
- **Scape Interaction**: Sends `{action, Output}` to scape, receives `{Fitness, HaltFlag}`
- **Fitness Reporting**: Forwards fitness and halt flag to cortex
- **Actuator Types**:
- `xor_SendOutput/2`: Sends output to XOR scape and gets fitness
- `pts/2`: Prints result to screen (for debugging)
### Platform (platform.erl)
The Platform module is a gen_server that manages shared infrastructure:
- **Scape Management**: Hosts public scapes that can be shared across multiple networks
- **Module Supervision**: Starts and stops supervised modules
- **Mnesia Integration**: Initializes and manages Mnesia database for evolutionary algorithms
- **Database Schema**: Creates tables for populations, species, agents, and neural components
- **Utility Functions**:
- `platform:sync()`: Recompiles all modules using `make:all([load])`
- `platform:create()`: Creates Mnesia schema and tables
- `platform:reset()`: Deletes and recreates Mnesia schema
- `platform:start()`: Starts the platform gen_server
- `platform:stop()`: Gracefully stops the platform
**Note**: Currently configured with empty module and scape lists. Designed for future evolutionary algorithm support with persistent storage in Mnesia.
## Documentation
**Full API documentation** is available via ExDoc. To generate and view:
```bash
# Generate documentation
rebar3 ex_doc
# Open in browser (macOS)
open doc/readme.html
# Or navigate to doc/readme.html in your browser
```
The documentation includes:
- **Module documentation** with detailed descriptions
- **Function specifications** with types and examples
- **Type definitions** for all records and custom types
- **Interactive search** and navigation
- **Mermaid diagrams** for architecture visualization
## Getting Started
### Prerequisites
- **Erlang/OTP 26+** - Required for running the neural network
- **Rebar3** - Build tool and dependency manager
### Installation
```bash
# Clone the repository
git clone https://github.com/yourusername/NN.git
cd NN
# Compile the project
rebar3 compile
# Generate documentation (optional)
rebar3 ex_doc
```
### Quick Start
```bash
# Start an Erlang shell with the compiled project
rebar3 shell
```
```erlang
% Create a neural network for XOR problem with 3 hidden neurons
1> genotype:construct(my_network, xor_mimic, [3]).
% Train the network (runs until convergence or max attempts)
2> exoself:map(my_network).
% The trained network is automatically saved to 'my_network' file
```
## Usage
### Creating a Network
Create a network genotype (blueprint) with custom topology:
```erlang
% Basic: One hidden layer with 3 neurons
genotype:construct(my_network, xor_mimic, [3]).
% Advanced: Two hidden layers with 5 and 3 neurons
genotype:construct(deep_network, xor_mimic, [5, 3]).
```
**Parameters:**
- `my_network` - Filename for saving the genotype
- `xor_mimic` - Morphology (problem domain configuration)
- `[3]` or `[5, 3]` - Hidden layer sizes (number of neurons per layer)
### Training a Network
#### Simple Training (Single Run)
```erlang
% Create and train in one go
exoself:map(my_network).
```
**What happens:**
1. Loads genotype from file
2. Spawns all neural processes (cortex, sensors, neurons, actuators, scapes)
3. Runs perturbation-based training (up to 50 attempts)
4. Saves improved weights back to genotype file
5. Prints final fitness and statistics
#### Advanced Training with Trainer
```erlang
% Basic training (5 attempts, infinite eval limit, infinite fitness target)
trainer:go(xor_mimic, [2]).
% Training with custom parameters
MaxAttempts = 10,
EvalLimit = 1000,
FitnessTarget = 0.9,
trainer:go(xor_mimic, [2], MaxAttempts, EvalLimit, FitnessTarget).
```
The trainer will:
- Create a new genotype for each training run
- Run until MaxAttempts, EvalLimit, or FitnessTarget is reached
- Save the best genotype to a file (e.g., `best_12345`)
- Print the final results
### Running Benchmarks
```erlang
% Run 100 training sessions and collect statistics
benchmarker:go(xor_mimic, [2]).
% Custom number of runs
benchmarker:go(xor_mimic, [2], 50).
% Full control over all parameters
benchmarker:go(xor_mimic, [2], MaxAttempts, EvalLimit, FitnessTarget, TotRuns).
```
Benchmark output includes:
- Fitness: Max, Min, Avg, Std
- Evaluations: Max, Min, Avg, Std
- Cycles: Max, Min, Avg, Std
- Time: Max, Min, Avg, Std
## Development Commands
### Building and Testing
```bash
# Compile the project
rebar3 compile
# Start interactive shell with compiled modules
rebar3 shell
# Clean build artifacts
rebar3 clean
# Generate documentation
rebar3 ex_doc
# Open documentation in browser (macOS)
open doc/readme.html
```
### Alternative: Manual Compilation (without rebar3)
If you prefer to work directly in the Erlang shell:
```erlang
% Start Erlang shell
erl
% Compile all modules
1> make:all([load]).
% Or compile individual modules
2> c(genotype).
3> c(neuron).
% Recompile changed modules (via platform)
4> platform:sync().
```
## Network Flow
### 1. Initialization Phase
- ExoSelf loads genotype from ETS file
- Spawns scape processes for environment simulation
- Spawns cortex, sensors, neurons, and actuators
- Creates IdsNPIds mapping table (ID ↔ PID)
- Links all processes by sending initialization messages with connection info
### 2. Evaluation Cycle
- **Cortex** sends `{sync}` to all sensors
- **Sensors** request percepts from scapes, forward to neurons
- **Neurons** compute weighted sum + tanh activation, forward to next layer
- **Actuators** collect outputs, send to scape, receive fitness
- **Actuators** send `{sync, Fitness, HaltFlag}` to cortex
- **Cortex** accumulates fitness, checks for evaluation completion
### 3. Learning Phase (Perturbation-Based)
- **ExoSelf** receives evaluation results from cortex
- If fitness improved:
- Send `{weight_backup}` to all neurons → saves current weights
- Reset attempt counter
- If fitness degraded:
- Send `{weight_restore}` to perturbed neurons → revert to backup
- Increment attempt counter
- Select random neuron subset (probability = 1/sqrt(N))
- Send `{weight_perturb}` to selected neurons
- Send `{reactivate}` to cortex to start next evaluation
### 4. Termination
- After MAX_ATTEMPTS (50) consecutive failures, training ends
- ExoSelf collects final weights from neurons
- Updates genotype and saves to file
- Sends results to trainer process (if registered)
- All processes receive `{terminate}` messages and shut down
## Implementation Details
### Concurrency Model
- Each component (cortex, sensor, neuron, actuator, scape) runs as a separate Erlang process
- Communication exclusively via asynchronous message passing
- ExoSelf manages the phenotype lifecycle without OTP supervision trees
- Processes use receive loops to handle messages
- IdsNPIds ETS table provides O(1) ID-to-PID and PID-to-ID lookups
### Data Persistence (Genotype/Phenotype Separation)
- **Genotype**: Static network blueprint stored in ETS table files
- Contains structure (connectivity), initial weights, morphology
- Created by `genotype:construct/3`
- Updated with trained weights after successful training
- Loaded via `genotype:load_from_file/1`
- **Phenotype**: Running network of concurrent processes
- Spawned from genotype by `exoself:map/1`
- Lives only during training/evaluation
- Neurons maintain current weights and backup weights in memory
- Terminated after training; weights persisted back to genotype
### Process Spawning Pattern
All processes follow a common spawning pattern:
```erlang
gen(ExoSelf_PId, Node) ->
spawn(Node, ?MODULE, prep, [ExoSelf_PId]).
prep(ExoSelf_PId) ->
receive
{ExoSelf_PId, InitData} ->
loop(InitData)
end.
```
This allows distributed deployment and clean initialization.
### Weight Perturbation Algorithm
Neurons use a random perturbation strategy:
- Perturbation probability per weight: `MP = 1/sqrt(TotalWeights)`
- Perturbation magnitude: `(rand:uniform() - 0.5) * 2Ï€`
- Weights saturated to range `[-2Ï€, 2Ï€]`
- Same perturbation applied to bias weights
### Morphology System
Morphologies define problem-specific interfaces:
- `morphology:Morphology(sensors)` returns sensor specifications
- `morphology:Morphology(actuators)` returns actuator specifications
- Each sensor/actuator specifies its scape (environment)
- Currently implemented: `xor_mimic` for XOR problem
### Scape Protocol
Scapes must implement:
- `{ScapePId, sense}` → respond with `{ScapePId, percept, Vector}`
- `{ActuatorPId, action, Output}` → respond with `{ScapePId, Fitness, HaltFlag}`
- `{ExoSelfPId, terminate}` → cleanup and terminate
HaltFlag = 1 signals evaluation complete; 0 means continue.
## Resources
For learning more about Erlang/OTP and neural networks:
- [Learn You Some Erlang](https://learnyousomeerlang.com/) - Excellent Erlang tutorial
- [Erlang Documentation](https://www.erlang.org/docs) - Official documentation
- [Making reliable distributed systems in the presence of software errors](https://erlang.org/download/armstrong_thesis_2003.pdf) - Joe Armstrong's thesis on Erlang
## Example Sessions
### Basic Network Creation and Training
```erlang
Eshell V14.1.1
% Compile all modules
1> make:all([load]).
% Create a genotype for XOR problem with 2 hidden neurons
2> genotype:construct(test_nn_genotype, xor_mimic, [2]).
ok
% Print the genotype structure
3> genotype:print(test_nn_genotype).
{cortex,cortex,[{sensor,{-0.5,1.234}}],[{actuator,{-0.5,5.678}}],
[{neuron,{1,1}},{neuron,{1,2}},{neuron,{2,1}}]}
{sensor,{-0.5,1.234},cortex,xor_GetInput,{private,xor_sim},2,
[{neuron,{1,1}},{neuron,{1,2}}]}
...
% Train the network
4> exoself:map(test_nn_genotype).
ExoSelf: Starting prep for test_nn_genotype
Cortex: Starting with 1 sensors, 3 neurons, 1 actuators
Actuator {actuator,{-0.5,5.678}}: Got fitness 12.5, endflag 1
Cortex:<0.95.0> finished training. Genotype has been backed up.
Fitness:156.78
TotEvaluations:8
TotCycles:32
TimeAcc:45678
<0.89.0>
```
### Running a Benchmark
```erlang
% Run 10 training sessions to collect statistics
5> benchmarker:go(xor_mimic, [2], 10).
<0.120.0>
Starting benchmark run 10 of 10
Run complete: Fitness=145.67 Evals=12
...
Benchmark results for: xor_mimic
Fitness::
Max:245.89
Min:98.45
Avg:156.23
Std:34.56
Evals::
Max:25
Min:5
Avg:12.3
Std:5.2
...
```
### Using the Trainer
```erlang
% Train with custom limits
6> trainer:go(xor_mimic, [2], 10, 100, 200.0).
<0.130.0>
% Trainer will create unique genotype files like:
% - experimental_123456 (working copy)
% - best_123456 (best solution found)
```
## Key Concepts
### Genotype vs Phenotype
- **Genotype** = Blueprint (ETS table file with structure and weights)
- **Phenotype** = Running network (living processes doing computation)
- Training happens in phenotype, results saved to genotype
### Perturbation-Based Learning
Instead of backpropagation, this system uses evolutionary-style learning:
1. Randomly tweak some weights
2. If performance improves → keep changes
3. If performance degrades → revert changes
4. Repeat until convergence or max attempts
### Message-Passing Architecture
- No shared memory between components
- All communication via Erlang messages
- Natural parallelism and fault isolation
- Supports distributed deployment across nodes
### Morphologies
Define the "body" of the neural network:
- What sensors does it have? (inputs)
- What actuators does it have? (outputs)
- What scape (environment) does it interact with?
## Extending the System
### Adding a New Morphology
1. Define sensors and actuators in `morphology.erl`:
```erlang
my_problem(sensors) ->
[#sensor{id={sensor,helpers:generate_id()},
name=my_GetInput,
scape={private,my_sim},
vector_length=4}];
my_problem(actuators) ->
[#actuator{id={actuator,helpers:generate_id()},
name=my_SendOutput,
scape={private,my_sim},
vector_length=2}].
```
2. Implement sensor/actuator functions in their respective modules
3. Implement scape in `scape.erl`:
```erlang
my_sim(ExoSelf_PId) ->
% Initialize environment
my_sim(ExoSelf_PId, InitialState).
my_sim(ExoSelf_PId, State) ->
receive
{From, sense} ->
From ! {self(), percept, InputVector},
my_sim(ExoSelf_PId, State);
{From, action, Output} ->
Fitness = evaluate(Output, State),
HaltFlag = check_done(State),
From ! {self(), Fitness, HaltFlag},
my_sim(ExoSelf_PId, update_state(State));
{ExoSelf_PId, terminate} ->
ok
end.
```