143 lines
3.0 KiB
Ruby
Executable File
143 lines
3.0 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
require 'pp'
|
|
|
|
# a single neuron
|
|
class Node
|
|
attr_accessor :activation, :input_nodes
|
|
attr_reader :id
|
|
|
|
def initialize(id)
|
|
@id = id
|
|
@input_nodes = Array.new
|
|
@activation = 0.0
|
|
end
|
|
|
|
def update
|
|
# activate unless this is an input node
|
|
@activation = activate(sum_input) unless /i[0-9]+/.match(@id)
|
|
end
|
|
|
|
def <=>(other)
|
|
@id <=> other.id
|
|
end
|
|
|
|
private
|
|
|
|
def sum_input
|
|
@input_nodes.inject(0.0) { |sum,node| sum + node.activation }
|
|
end
|
|
|
|
def activate(x)
|
|
Math.tanh(x)
|
|
end
|
|
end
|
|
|
|
# a basic ANN (assumes all weights are 1.0)
|
|
class Network
|
|
attr_accessor :nodes
|
|
|
|
def initialize(n_in, n_hidden, n_out)
|
|
@nodes = Hash.new
|
|
n_in.times { |i| @nodes["i#{i}"] = Node.new("i#{i}") }
|
|
n_hidden.times { |i| @nodes["h#{i}"] = Node.new("h#{i}") }
|
|
n_out.times { |i| @nodes["o#{i}"] = Node.new("o#{i}") }
|
|
end
|
|
|
|
def connect(from_id, to_id)
|
|
@nodes[to_id].input_nodes << @nodes[from_id]
|
|
@order = nil # mark activation order as being dirty
|
|
end
|
|
|
|
def update
|
|
# calculate activation order if it's 'nil'
|
|
@order ||= activation_order
|
|
|
|
# simply update each node in order
|
|
@order.each do |layer|
|
|
puts "Layer"
|
|
layer.each { |node| puts "updating node #{node.id}"; node.update }
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def activation_order
|
|
# do the recursive search, starting with the output layer
|
|
# then, add the input nodes to the front
|
|
[input_nodes] + a_o_bfs(output_nodes)
|
|
end
|
|
|
|
def find_nodes(re)
|
|
# collect all of the output node ids, this is the end layer of the ANN
|
|
node_ids = @nodes.keys.select { |k,v| re.match(k) }
|
|
|
|
# use id's to get array of nodes, then sort
|
|
node_ids.collect { |id| @nodes[id] }.sort
|
|
end
|
|
|
|
def input_nodes
|
|
find_nodes(/i[0-9]+/)
|
|
end
|
|
|
|
def hidden_nodes
|
|
find_nodes(/h[0-9]+/)
|
|
end
|
|
|
|
def output_nodes
|
|
find_nodes(/o[0-9]+/)
|
|
end
|
|
|
|
def a_o_bfs(current_layer, seen=nil, order=nil)
|
|
# setup some state for this recursion
|
|
seen ||= Hash.new
|
|
order ||= Array.new
|
|
|
|
# mark current nodes as seen
|
|
current_layer.each { |n| seen[n] = true }
|
|
|
|
# prepend current layer to final order
|
|
order = [current_layer.sort] + order
|
|
|
|
# get next layer
|
|
next_layer = current_layer.collect { |n| n.input_nodes }
|
|
next_layer = next_layer.flatten.select { |n| not seen[n] and not /i[0-9]+/.match(n.id) }
|
|
|
|
if not next_layer.empty?
|
|
# recurse into next layer if it exists
|
|
a_o_bfs(next_layer, seen, order)
|
|
else
|
|
# finished
|
|
order
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
# create a network
|
|
n = Network.new(3,4,3)
|
|
n.connect('i0','h0')
|
|
n.connect('h0','o0')
|
|
n.connect('i1','h2')
|
|
n.connect('i2','h1')
|
|
n.connect('h1','h0')
|
|
n.connect('h2','h0')
|
|
n.connect('h2','h3')
|
|
n.connect('h3','o2')
|
|
|
|
# run some data through the network
|
|
10.times do
|
|
# give network some data
|
|
n.nodes['i0'].activation = (2.0 * Kernel.rand) - 1.0
|
|
n.nodes['i1'].activation = (2.0 * Kernel.rand) - 1.0
|
|
|
|
# update network
|
|
n.update
|
|
|
|
# read outputs
|
|
puts n.nodes['o0'].activation
|
|
puts
|
|
end
|
|
|