Skip to content

Commit

Permalink
introducing mutations
Browse files Browse the repository at this point in the history
- mutation algebra
- TODO:
  - store new fitness in Sync only for success steps
  - try ensure mutation tests run after nodes tests
  • Loading branch information
zampino committed Jun 6, 2015
1 parent 481f25e commit 8fb42d0
Show file tree
Hide file tree
Showing 25 changed files with 516 additions and 154 deletions.
3 changes: 2 additions & 1 deletion lib/exnn/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ defmodule EXNN.Config do
store = %{
remote_nodes: mod.nodes,
pattern: mod.initial_pattern,
fitness: mod.fitness || {nil, nil}
fitness: mod.fitness || {nil, nil} #,
# mutations: mod.mutations
}
Agent.start_link(fn -> store end, name: __MODULE__)
end
Expand Down
44 changes: 40 additions & 4 deletions lib/exnn/connectome.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
defmodule EXNN.Connectome do
import EXNN.Utils.Logger

@doc "accepts patterns of the form:
[sensor: N_s, neuron: [l1: N_1, ..., ld: N_d], actuator: N_a]
where N_s, N_i, N_a are natural numbers.
"

require Logger

# TODO: decouple storage from link/patterns
def start_link do
{:ok, pid} = Agent.start_link(fn() -> HashDict.new end,
name: __MODULE__)
Expand All @@ -15,21 +20,52 @@ defmodule EXNN.Connectome do
pattern
|> EXNN.Pattern.build_layers
|> link([], dimensions)
|> store
|> store_all

{:ok, pid}
end

def store(collection) when is_list(collection) do
collection = List.flatten collection
Enum.each collection, &store(&1)
def all do
unkey = fn(dict)->
dict |> Enum.map &(elem(&1, 1))
end
Agent.get __MODULE__, unkey
end

def neurons do
Enum.filter all, &(:neuron == &1.type)
end

def get(id) do
Agent.get __MODULE__, &(Dict.get &1, id)
end

def store(genome) do
Agent.update __MODULE__,
&HashDict.put(&1, genome.id, genome)
end

@doc "accepts anything map or dict like"
def update(id, dict) do
# skim out unwanted keys!
safe_dict = struct(EXNN.Genome, dict)

update_fun = fn(state) ->
genome = HashDict.get(state, id)
# ugly but used to preserve the type
type = genome.type
genome = Map.merge(genome, safe_dict)
HashDict.put state, id, %{genome | type: type}
end

Agent.update(__MODULE__, update_fun)
end

defp store_all(collection) when is_list(collection) do
collection = List.flatten collection
Enum.each collection, &store(&1)
end

# TOPOLOGY AND CONNECTIONS

def link([], acc, _), do: acc
Expand Down
1 change: 1 addition & 0 deletions lib/exnn/genome.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule EXNN.Genome do
defstruct id: nil, type: nil, ins: [], outs: []

def collect(type, ids) do
ids |> Enum.map &build(type, &1)
Expand Down
5 changes: 3 additions & 2 deletions lib/exnn/neuron.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ defmodule EXNN.Neuron do
input.
"""
use EXNN.NodeServer
alias EXNN.Utils.Math

defstruct id: nil, ins: [], outs: [], bias: 0,
activation: &EXNN.Math.id/1, acc: [], trigger: [], metadata: []
activation: &Math.id/1, acc: [], trigger: [], metadata: []

def initialize(genome) do
Dict.merge(genome, trigger: Dict.keys(genome.ins), acc: [])
Expand All @@ -37,7 +38,7 @@ defmodule EXNN.Neuron do
def impulse(neuron) do
{activation_input, acc} = List.foldl(neuron.ins,
{0, neuron.acc},
&EXNN.Math.labelled_scalar_product/2)
&Math.labelled_scalar_product/2)

neuron = %{neuron | acc: acc}
_impulse = neuron.activation.(activation_input + neuron.bias)
Expand Down
24 changes: 17 additions & 7 deletions lib/exnn/node_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ defmodule EXNN.NodeServer do
# public api

# TODO: maybe import some API module in the quote below
@doc "process and forward message"
def forward(id, message, metadata) do
GenServer.call(id, {:forward, message, metadata})
end

@doc "injects partially modified genome into node."
@doc "injects partial genome into node."
def patch(id, partial) do
GenServer.call(id, {:patch, partial})
end

@doc "dumps current genome to Connectome"
def dump(id) do
GenServer.call(id, :dump)
end

defmacro __using__(options) do
quote do
use GenServer
import EXNN.Utils.Logger

def start_link(genome) do
GenServer.start_link(__MODULE__, genome, name: genome.id)
Expand All @@ -27,17 +34,20 @@ defmodule EXNN.NodeServer do

# server callbacks
@doc "NodeServer basic protocol action is to react to
a :forward event.
message is a keyword [origin: value]
"
a :forward event.
message is a keyword [origin: value]"
def handle_call({:forward, message, metadata}, _from, connectable) do
{:reply, :ok, EXNN.Connection.signal(connectable, message, metadata)}
end

def handle_call({:patch, fun}, _from, node) do
state = Map.merge(node, fun.(node))
destruct = Map.from_struct state
{:reply, destruct, state}
end

def handle_call({:patch, partial}, _from, node) do
{:reply, :ok, Map.merge(node, partial)}
def handle_call(:dump, _from, node) do
{:reply, node, node}
end

defoverridable [initialize: 1]
Expand Down
4 changes: 3 additions & 1 deletion lib/exnn/nodes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ defmodule EXNN.Nodes do
{name, refs} = HashDict.pop(state.refs, ref)
IO.puts "/////// node: #{name} went DOWN with reason: #{inspect(_reason)} ////////////////////"
names = HashDict.delete(state.names, name)
# GenEvent.sync_notify(state.events, {:exit, name, pid})
# TODO: reload the node from current connectome
# genome = EXNN.Connectome.at name
# {refs, names} = start_node(genome, refs, names)
{:noreply, %{state | names: names, refs: refs}}
end

Expand Down
5 changes: 2 additions & 3 deletions lib/exnn/nodes/loader.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
defmodule EXNN.Nodes.Loader do

def start_link do
# implement state dump for restarts
result = Agent.get(EXNN.Connectome, &(&1))
result = EXNN.Connectome.all
|> Enum.each(&register/1)
{result, self}
end

def register({id, genome}) do
def register(genome) do
EXNN.Nodes.register(genome)
end

Expand Down
1 change: 0 additions & 1 deletion lib/exnn/pattern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ defmodule EXNN.Pattern do
end

def map([], acc) do
IO.puts "layers: #{inspect(acc)}"
acc
end

Expand Down
3 changes: 1 addition & 2 deletions lib/exnn/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule EXNN.Supervisor do
Supervisor.start_link(__MODULE__, mod)
end


# TODO: group workers in parent-supervisors

def init(mod) do
Expand All @@ -17,7 +16,7 @@ defmodule EXNN.Supervisor do
|> child(EXNN.NodeSupervisor, :supervisor, [])
|> child(EXNN.Nodes, [])
|> child(EXNN.Nodes.Loader, [])
|> child(EXNN.Trainer, :supervisor, [])
|> child(EXNN.Trainer.Supervisor, :supervisor, [])
|> supervise(strategy: :one_for_one)
end

Expand Down
18 changes: 3 additions & 15 deletions lib/exnn/trainer.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
defmodule EXNN.Trainer do
use Supervisor
use EXNN.Utils.Supervisor

def start_link do
Supervisor.start_link(__MODULE__, :ok)
def start do
# here the trainer FSM enters training mode
EXNN.Trainer.Sync.sync
end

def init(:ok) do
IO.puts "starting trainer supervisor"
[]
# |> child(EXNN.Fitness.Supervisor, :supervisor, [])
|> child(EXNN.Fitness.Starter, [])
|> child(EXNN.Trainer.Sync, [])
|> supervise(strategy: :one_for_all)
end

end
52 changes: 52 additions & 0 deletions lib/exnn/trainer/mutations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule EXNN.Trainer.Mutations do
use GenServer

alias EXNN.Trainer.Mutations.Set
alias EXNN.Trainer.Mutations.Agent

import EXNN.Utils.Logger

def start_link do
GenServer.start_link __MODULE__,
:ok,
name: __MODULE__
end

def init :ok do
{:ok, %{
neurons: EXNN.Connectome.neurons,
history: []
}}
end

# client api

def step do
GenServer.call __MODULE__, :step
end

def revert do
GenServer.call __MODULE__, :revert
end

# server callbacks

def handle_call :step, _from, state do
log "step", [], :debug
mutation_set = Set.generate(state.neurons)
{:ok, neurons} = Agent.apply mutation_set

{:reply, :ok, %{state |
neurons: neurons,
history: [mutation_set | state.history]}
}
end

def handle_call :revert, _from, state do
log "revert", [], :debug
[mutation_set | rest] = state.history
inverse_set = Set.invert mutation_set
{:ok, neurons} = Agent.apply inverse_set
{:reply, :ok, %{state | neurons: neurons, history: rest}}
end
end
33 changes: 33 additions & 0 deletions lib/exnn/trainer/mutations/agent.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule EXNN.Trainer.Mutations.Agent do
@moduledoc false
import EXNN.Utils.Logger
alias EXNN.Trainer.Mutations.Set.Mutation

def apply mutation_set do
mutation_set
|> Enum.map(&spawn_async_task/1)
|> EXNN.Utils.Task.wait_all

{:ok, EXNN.Connectome.neurons}
end

def spawn_async_task(mutation) do
Task.async __MODULE__, :apply_mutation, [mutation]
end

def apply_mutation %Mutation{
type: :alter_weights,
id: id,
changes: changes} do

patch_fn = fn(genome)->
new_weights = changes |> Enum.reduce genome.ins, fn({key, old, new}, weights)->
Keyword.put weights, key, new
end
%{ins: new_weights}
end

res = EXNN.NodeServer.patch(id, patch_fn)
EXNN.Connectome.update(id, res)
end
end
64 changes: 64 additions & 0 deletions lib/exnn/trainer/mutations/set.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule EXNN.Trainer.Mutations.Set do
@moduledoc false
import EXNN.Utils.Logger

alias EXNN.Utils.Math
alias EXNN.Utils.Random
alias EXNN.Trainer.Mutations.Set.Mutation
# alias EXNN.Config

@mutation_types [
:alter_weights,
# :add_link,
# :delete_link,
# :add_node,
# :remove_node
]

def generate(neurons) do
length = Enum.count neurons
candidates = Random.sample neurons, Math.inv_sqrt(length)
Enum.map candidates, &generate_for(&1)
end

def generate_for(genome) do
# type = Random.sample @mutation_types
Mutation.new genome, type: :alter_weights
end

def invert(set), do: invert(set, [])

def invert([], done), do: done

def invert([first|rest], done) do
invert(rest, [Mutation.inverse(first) | done])
end

defmodule Mutation do
defstruct type: nil, id: nil, changes: []

def new(genome, type: type) do
struct(__MODULE__, [type: type, id: genome.id])
|> build_changes(genome)
end

def build_changes %Mutation{type: :alter_weights}=mutation, genome do
weights = genome.ins
keys = Keyword.keys(weights)
length = Enum.count keys
space = Math.inv_sqrt(length)
sampled = keys |> Random.sample(space)
sampled |> Enum.reduce mutation, fn(key, acc)->
old = weights[key]
new = Random.coefficient(old)
%{acc | changes: [{key, old, new} | acc.changes]}
end
end

def inverse(%Mutation{type: :alter_weights, changes: changes}=mutation) do
inverse_changes = Enum.map changes,
fn({key, old, new}) -> {key, new, old} end
%{mutation | changes: inverse_changes}
end
end
end
Loading

0 comments on commit 8fb42d0

Please sign in to comment.