neuronet 6.1.0 β 7.0.230416
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +133 -782
- data/lib/neuronet/connection.rb +65 -0
- data/lib/neuronet/constants.rb +110 -0
- data/lib/neuronet/feed_forward.rb +89 -0
- data/lib/neuronet/gaussian.rb +19 -0
- data/lib/neuronet/layer.rb +111 -0
- data/lib/neuronet/log_normal.rb +21 -0
- data/lib/neuronet/neuron.rb +146 -0
- data/lib/neuronet/scale.rb +50 -0
- data/lib/neuronet/scaled_network.rb +50 -0
- data/lib/neuronet.rb +13 -619
- metadata +109 -18
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module / Connection class
|
4
|
+
module Neuronet
|
5
|
+
# Connections between neurons are there own separate objects. In Neuronet, a
|
6
|
+
# neuron contains it's bias, and a list of it's connections. Each connection
|
7
|
+
# contains it's weight (strength) and connected neuron.
|
8
|
+
class Connection
|
9
|
+
attr_accessor :neuron, :weight
|
10
|
+
|
11
|
+
# Connection#initialize takes a neuron and a weight with a default of 0.0.
|
12
|
+
def initialize(neuron = Neuron.new, weight: 0.0)
|
13
|
+
@neuron = neuron
|
14
|
+
@weight = weight
|
15
|
+
end
|
16
|
+
|
17
|
+
# The connection's mu is the activation of the connected neuron.
|
18
|
+
def mu = @neuron.activation
|
19
|
+
alias activation mu
|
20
|
+
|
21
|
+
# The connection's mju is πΎππ'.
|
22
|
+
def mju = @weight * @neuron.derivative
|
23
|
+
|
24
|
+
# The connection kappa is a component of the neuron's sum kappa:
|
25
|
+
# πΏ := πΎ π'
|
26
|
+
def kappa = @weight * @neuron.lamda
|
27
|
+
|
28
|
+
# The weighted activation of the connected neuron.
|
29
|
+
def weighted_activation = @neuron.activation * @weight
|
30
|
+
|
31
|
+
# Consistent with #update
|
32
|
+
alias partial weighted_activation
|
33
|
+
|
34
|
+
# Connection#update returns the updated activation of a connection, which is
|
35
|
+
# the weighted updated activation of the neuron it's connected to:
|
36
|
+
# weight * neuron.update
|
37
|
+
# This method is the one to use whenever the value of the inputs are changed
|
38
|
+
# (or right after training). Otherwise, both update and value should give
|
39
|
+
# the same result. When back calculation are not needed, use
|
40
|
+
# Connection#weighted_activation instead.
|
41
|
+
def update = @neuron.update * @weight
|
42
|
+
|
43
|
+
# Connection#backpropagate modifies the connection's weight in proportion to
|
44
|
+
# the error given and passes that error to its connected neuron via the
|
45
|
+
# neuron's backpropagate method.
|
46
|
+
def backpropagate(error)
|
47
|
+
@weight += @neuron.activation * Neuronet.noise[error]
|
48
|
+
if @weight.abs > Neuronet.maxw
|
49
|
+
@weight = @weight.positive? ? Neuronet.maxw : -Neuronet.maxw
|
50
|
+
end
|
51
|
+
@neuron.backpropagate(error)
|
52
|
+
self
|
53
|
+
end
|
54
|
+
# On how to reduce the error, the above makes it obvious how to interpret
|
55
|
+
# the equipartition of errors among the connections. Backpropagation is
|
56
|
+
# symmetric to forward propagation of errors. The error variable is the
|
57
|
+
# reduced error, π(see the wiki notes).
|
58
|
+
|
59
|
+
# A connection inspects itself as "weight*label:...".
|
60
|
+
def inspect = "#{Neuronet.format % @weight}*#{@neuron.inspect}"
|
61
|
+
|
62
|
+
# A connection puts itself as "weight*label".
|
63
|
+
def to_s = "#{Neuronet.format % @weight}*#{@neuron}"
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module / Constants
|
4
|
+
module Neuronet
|
5
|
+
# Neuronet allows one to set the format to use for displaying float values,
|
6
|
+
# mostly used in the inspect methods.
|
7
|
+
# [Docs](https://docs.ruby-lang.org/en/master/format_specifications_rdoc.html)
|
8
|
+
FORMAT = '%.13g'
|
9
|
+
|
10
|
+
# An artificial neural network uses a squash function to determine the
|
11
|
+
# activation value of a neuron. The squash function for Neuronet is the
|
12
|
+
# [Sigmoid function](http://en.wikipedia.org/wiki/Sigmoid_function) which sets
|
13
|
+
# the neuron's activation value between 0.0 and 1.0. This activation value is
|
14
|
+
# often thought of on/off or true/false. For classification problems,
|
15
|
+
# activation values near one are considered true while activation values near
|
16
|
+
# 0.0 are considered false. In Neuronet I make a distinction between the
|
17
|
+
# neuron's activation value and it's representation to the problem. This
|
18
|
+
# attribute, activation, need never appear in an implementation of Neuronet,
|
19
|
+
# but it is mapped back to it's unsquashed value every time the implementation
|
20
|
+
# asks for the neuron's value. One should scale the problem with most data
|
21
|
+
# points between -1 and 1, extremes under 2s, and no outbounds above 3s.
|
22
|
+
# Standard deviations from the mean is probably a good way to figure the scale
|
23
|
+
# of the problem.
|
24
|
+
SQUASH = ->(unsquashed) { 1.0 / (1.0 + Math.exp(-unsquashed)) }
|
25
|
+
UNSQUASH = ->(squashed) { Math.log(squashed / (1.0 - squashed)) }
|
26
|
+
DERIVATIVE = ->(squash) { squash * (1.0 - squash) }
|
27
|
+
|
28
|
+
# I'll want to have a neuron roughly mirror another later. Let [v] be the
|
29
|
+
# squash of v. Consider:
|
30
|
+
# v = b + w*[v]
|
31
|
+
# There is no constant b and w that will satisfy the above equation for all v.
|
32
|
+
# But one can satisfy the equation for v in {-1, 0, 1}. Find b and w such
|
33
|
+
# that:
|
34
|
+
# A: 0 = b + w*[0]
|
35
|
+
# B: 1 = b + w*[1]
|
36
|
+
# C: -1 = b + w*[-1]
|
37
|
+
# Use A and B to solve for b and w:
|
38
|
+
# A: 0 = b + w*[0]
|
39
|
+
# b = -w*[0]
|
40
|
+
# B: 1 = b + w*[1]
|
41
|
+
# 1 = -w*[0] + w*[1]
|
42
|
+
# 1 = w*(-[0] + [1])
|
43
|
+
# w = 1/([1] - [0])
|
44
|
+
# b = -[0]/([1] - [0])
|
45
|
+
# Verify A, B, and C:
|
46
|
+
# A: 0 = b + w*[0]
|
47
|
+
# 0 = -[0]/([1] - [0]) + [0]/([1] - [0])
|
48
|
+
# 0 = 0 # OK
|
49
|
+
# B: 1 = b + w*[1]
|
50
|
+
# 1 = -[0]/([1] - [0]) + [1]/([1] - [0])
|
51
|
+
# 1 = ([1] - [0])/([1] - [0])
|
52
|
+
# 1 = 1 # OK
|
53
|
+
# Using the squash function identity, [v] = 1 - [-v]:
|
54
|
+
# C: -1 = b + w*[-1]
|
55
|
+
# -1 = -[0]/([1] - [0]) + [-1]/([1] - [0])
|
56
|
+
# -1 = ([-1] - [0])/([1] - [0])
|
57
|
+
# [0] - [1] = [-1] - [0]
|
58
|
+
# [0] - [1] = 1 - [1] - [0] # Identity substitution.
|
59
|
+
# [0] = 1 - [0] # OK, by identity(0=-0).
|
60
|
+
# Evaluate given that [0] = 0.5:
|
61
|
+
# b = -[0]/([1] - [0])
|
62
|
+
# b = [0]/([0] - [1])
|
63
|
+
# b = 0.5/(0.5 - [1])
|
64
|
+
# w = 1/([1] - [0])
|
65
|
+
# w = 1/([1] - 0.5)
|
66
|
+
# w = -2 * 0.5/(0.5 - [1])
|
67
|
+
# w = -2 * b
|
68
|
+
BZERO = 0.5 / (0.5 - SQUASH[1.0])
|
69
|
+
WONE = -2.0 * BZERO
|
70
|
+
|
71
|
+
# Although the implementation is free to set all parameters for each neuron,
|
72
|
+
# Neuronet by default creates zeroed neurons. Association between inputs and
|
73
|
+
# outputs are trained, and neurons differentiate from each other randomly.
|
74
|
+
# Differentiation among neurons is achieved by noise in the back-propagation
|
75
|
+
# of errors. This noise is provided by rand + rand. I chose rand + rand to
|
76
|
+
# give the noise an average value of one and a bell shape distribution.
|
77
|
+
NOISE = ->(error) { error * (rand + rand) }
|
78
|
+
|
79
|
+
# One may choose not to have noise.
|
80
|
+
NO_NOISE = ->(error) { error }
|
81
|
+
|
82
|
+
# To keep components bounded, Neuronet limits the weights, biases, and values.
|
83
|
+
# Note that on a 64-bit machine SQUASH[37] rounds to 1.0, and
|
84
|
+
# SQUASH[9] is 0.99987...
|
85
|
+
MAXW = 9.0 # Maximum weight
|
86
|
+
MAXB = 18.0 # Maximum bias
|
87
|
+
MAXV = 36.0 # Maximum value
|
88
|
+
|
89
|
+
# Mu learning factor.
|
90
|
+
LEARNING = 1.0
|
91
|
+
|
92
|
+
# The above constants are the defaults for Neuronet. They are set below in
|
93
|
+
# accessable module attributes. The user may change these to suit their
|
94
|
+
# needs.
|
95
|
+
class << self
|
96
|
+
attr_accessor :format, :squash, :unsquash, :derivative, :bzero, :wone,
|
97
|
+
:noise, :maxw, :maxb, :maxv, :learning
|
98
|
+
end
|
99
|
+
self.squash = SQUASH
|
100
|
+
self.unsquash = UNSQUASH
|
101
|
+
self.derivative = DERIVATIVE
|
102
|
+
self.bzero = BZERO
|
103
|
+
self.wone = WONE
|
104
|
+
self.noise = NOISE
|
105
|
+
self.format = FORMAT
|
106
|
+
self.maxw = MAXW
|
107
|
+
self.maxb = MAXB
|
108
|
+
self.maxv = MAXV
|
109
|
+
self.learning = LEARNING
|
110
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module / FeedForward class
|
4
|
+
module Neuronet
|
5
|
+
# A Feed Forward Network
|
6
|
+
class FeedForward < Array
|
7
|
+
# Example:
|
8
|
+
# ff = Neuronet::FeedForward.new([2, 3, 1])
|
9
|
+
def initialize(layers)
|
10
|
+
length = layers.length
|
11
|
+
raise 'Need at least 2 layers' if length < 2
|
12
|
+
|
13
|
+
super(length) { Layer.new(layers[_1]) }
|
14
|
+
1.upto(length - 1) { self[_1].connect(self[_1 - 1]) }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the input layer.
|
18
|
+
def set(input)
|
19
|
+
first.set(input)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def input = first.values
|
24
|
+
|
25
|
+
# Update the network.
|
26
|
+
def update
|
27
|
+
# update up the layers
|
28
|
+
1.upto(length - 1) { self[_1].partial }
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def output = last.values
|
33
|
+
|
34
|
+
# Consider:
|
35
|
+
# m = Neuronet::FeedForward.new(layers)
|
36
|
+
# Want:
|
37
|
+
# output = m * input
|
38
|
+
def *(other)
|
39
|
+
set(other)
|
40
|
+
update
|
41
|
+
last.values
|
42
|
+
end
|
43
|
+
|
44
|
+
# π + π§ π' + π§ π§'π" + π§ π§'π§"π"' + ...
|
45
|
+
# |π§| ~ |πΎ||ππ|
|
46
|
+
# |βπΎ| ~ βπ
|
47
|
+
# |ππ| ~ ΒΌ
|
48
|
+
# |π| ~ 1+β|π'| ~ 1+Β½π
|
49
|
+
def expected_mju!
|
50
|
+
sum = 0.0
|
51
|
+
mju = 1.0
|
52
|
+
reverse[1..].each do |layer|
|
53
|
+
n = layer.length
|
54
|
+
sum += mju * (1.0 + (0.5 * n))
|
55
|
+
mju *= 0.25 * Math.sqrt(layer.length)
|
56
|
+
end
|
57
|
+
@expected_mju = Neuronet.learning * sum
|
58
|
+
end
|
59
|
+
|
60
|
+
def expected_mju
|
61
|
+
@expected_mju || expected_mju!
|
62
|
+
end
|
63
|
+
|
64
|
+
def average_mju
|
65
|
+
last.average_mju
|
66
|
+
end
|
67
|
+
|
68
|
+
def train(target, mju = expected_mju)
|
69
|
+
last.train(target, mju)
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def pair(input, target, mju = expected_mju)
|
74
|
+
set(input).update.train(target, mju)
|
75
|
+
end
|
76
|
+
|
77
|
+
def pairs(pairs, mju = expected_mju)
|
78
|
+
pairs.shuffle.each { |input, target| pair(input, target, mju) }
|
79
|
+
return self unless block_given?
|
80
|
+
|
81
|
+
pairs.shuffle.each { |i, t| pair(i, t, mju) } while yield
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def inspect = map(&:inspect).join("\n")
|
86
|
+
|
87
|
+
def to_s = map(&:to_s).join("\n")
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module
|
4
|
+
module Neuronet
|
5
|
+
# "Normal Distribution"
|
6
|
+
# Gaussian sub-classes Scale and is used exactly the same way. The only
|
7
|
+
# changes are that it calculates the arithmetic mean (average) for center and
|
8
|
+
# the standard deviation for spread.
|
9
|
+
class Gaussian < Scale
|
10
|
+
def set(inputs)
|
11
|
+
@center ||= inputs.sum.to_f / inputs.length
|
12
|
+
unless @spread
|
13
|
+
sum2 = inputs.map { @center - _1 }.sum { _1 * _1 }.to_f
|
14
|
+
@spread = Math.sqrt(sum2 / (inputs.length - 1.0))
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module
|
4
|
+
module Neuronet
|
5
|
+
# Layer is an array of neurons.
|
6
|
+
class Layer < Array
|
7
|
+
# Length is the number of neurons in the layer.
|
8
|
+
def initialize(length)
|
9
|
+
super(length) { Neuron.new }
|
10
|
+
end
|
11
|
+
|
12
|
+
# This is where one enters the "real world" inputs.
|
13
|
+
def set(inputs)
|
14
|
+
0.upto(length - 1) { self[_1].value = inputs[_1] || 0.0 }
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the real world values: [value, ...]
|
19
|
+
def values
|
20
|
+
map(&:value)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Allows one to fully connect layers.
|
24
|
+
def connect(layer = Layer.new(length), weights: [])
|
25
|
+
# creates the neuron matrix...
|
26
|
+
each_with_index do |neuron, i|
|
27
|
+
weight = weights[i] || 0.0
|
28
|
+
layer.each { neuron.connect(_1, weight:) }
|
29
|
+
end
|
30
|
+
# The layer is returned for chaining.
|
31
|
+
layer
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set layer to mirror input:
|
35
|
+
# bias = BZERO.
|
36
|
+
# weight = WONE
|
37
|
+
# Input should be the same size as the layer. One can set sign to -1 to
|
38
|
+
# anti-mirror. One can set sign to other than |1| to scale.
|
39
|
+
def mirror(sign = 1)
|
40
|
+
each_with_index do |neuron, index|
|
41
|
+
neuron.bias = sign * Neuronet.bzero
|
42
|
+
neuron.connections[index].weight = sign * Neuronet.wone
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Doubles up the input mirroring and anti-mirroring it. The layer should be
|
47
|
+
# twice the size of the input.
|
48
|
+
def antithesis
|
49
|
+
sign = 1
|
50
|
+
each_with_index do |n, i|
|
51
|
+
n.connections[i / 2].weight = sign * Neuronet.wone
|
52
|
+
n.bias = sign * Neuronet.bzero
|
53
|
+
sign = -sign
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Sums two corresponding input neurons above each neuron in the layer.
|
58
|
+
# Input should be twice the size of the layer.
|
59
|
+
def synthesis
|
60
|
+
semi = Neuronet.wone / 2
|
61
|
+
each_with_index do |n, i|
|
62
|
+
j = i * 2
|
63
|
+
c = n.connections
|
64
|
+
n.bias = Neuronet.bzero
|
65
|
+
c[j].weight = semi
|
66
|
+
c[j + 1].weight = semi
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set layer to average input.
|
71
|
+
def average(sign = 1)
|
72
|
+
bias = sign * Neuronet.bzero
|
73
|
+
each do |n|
|
74
|
+
n.bias = bias
|
75
|
+
weight = sign * Neuronet.wone / n.connections.length
|
76
|
+
n.connections.each { _1.weight = weight }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# updates layer with current values of the previous layer
|
81
|
+
def partial
|
82
|
+
each(&:partial)
|
83
|
+
end
|
84
|
+
|
85
|
+
def average_mju
|
86
|
+
Neuronet.learning * sum { Neuron.mju(_1) } / length
|
87
|
+
end
|
88
|
+
|
89
|
+
# Takes the real world target for each neuron in this layer and
|
90
|
+
# backpropagates the error to each neuron.
|
91
|
+
def train(target, mju = nil)
|
92
|
+
0.upto(length - 1) do |index|
|
93
|
+
neuron = self[index]
|
94
|
+
error = (target[index] - neuron.value) /
|
95
|
+
(mju || (Neuronet.learning * Neuron.mju(neuron)))
|
96
|
+
neuron.backpropagate(error)
|
97
|
+
end
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
# Layer inspects as "label:value,..."
|
102
|
+
def inspect
|
103
|
+
map(&:inspect).join(',')
|
104
|
+
end
|
105
|
+
|
106
|
+
# Layer puts as "label,..."
|
107
|
+
def to_s
|
108
|
+
map(&:to_s).join(',')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module
|
4
|
+
module Neuronet
|
5
|
+
# "Log-Normal Distribution"
|
6
|
+
# LogNormal sub-classes Gaussian to transform the values to a logarithmic
|
7
|
+
# scale.
|
8
|
+
class LogNormal < Gaussian
|
9
|
+
def set(inputs)
|
10
|
+
super(inputs.map { |value| Math.log(value) })
|
11
|
+
end
|
12
|
+
|
13
|
+
def mapped(inputs)
|
14
|
+
super(inputs.map { |value| Math.log(value) })
|
15
|
+
end
|
16
|
+
|
17
|
+
def unmapped(outputs)
|
18
|
+
super(outputs).map { |value| Math.exp(value) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module / Neuron class
|
4
|
+
module Neuronet
|
5
|
+
# A Neuron is capable of creating connections to other neurons. The
|
6
|
+
# connections attribute is a list of the neuron's connections to other
|
7
|
+
# neurons. A neuron's bias is it's kicker (or deduction) to it's activation
|
8
|
+
# value, a sum of its connections values.
|
9
|
+
class Neuron
|
10
|
+
# For bookkeeping, each Neuron is given a label, starting with 'a' by
|
11
|
+
# default.
|
12
|
+
class << self; attr_accessor :label; end
|
13
|
+
Neuron.label = 'a'
|
14
|
+
|
15
|
+
attr_reader :label, :activation, :connections
|
16
|
+
attr_accessor :bias
|
17
|
+
|
18
|
+
# The neuron's mu is the sum of the connections' mu(activation), plus one
|
19
|
+
# for the bias:
|
20
|
+
# π := 1+βπ'
|
21
|
+
def mu
|
22
|
+
return 0.0 if @connections.empty?
|
23
|
+
|
24
|
+
1 + @connections.sum(&:mu)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Reference the library's wiki:
|
28
|
+
# πβ ~ π(πβ + π§ββ±πα΅’ + π§ββ±π§α΅’Κ²πβ±Ό + π§ββ±π§α΅’Κ²π§β±Όα΅πβ + ...)
|
29
|
+
# π§ββ±πα΅’ is:
|
30
|
+
# neuron.mju{ |connected_neuron| connected_neuron.mu }
|
31
|
+
# π§ββ±π§α΅’Κ²πβ±Ό is:
|
32
|
+
# nh.mju{ |ni| ni.mju{ |nj| nj.mu }}
|
33
|
+
def mju(&block)
|
34
|
+
@connections.sum { _1.mju * block[_1.neuron] }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Full recursive implementation of mju:
|
38
|
+
def self.mju(neuron)
|
39
|
+
return 0.0 if neuron.connections.empty?
|
40
|
+
|
41
|
+
neuron.mu + neuron.mju { |connected_neuron| Neuron.mju(connected_neuron) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# ππβπ = (1-βπ)βπ = (1-π)π = ππ
|
45
|
+
def derivative = Neuronet.derivative[@activation]
|
46
|
+
|
47
|
+
# π = πππ
|
48
|
+
def lamda = derivative * mu
|
49
|
+
|
50
|
+
# πΏ := π§ π' = πΎ ππ'π' = πΎ π'
|
51
|
+
# def kappa = mju(&:mu)
|
52
|
+
def kappa = @connections.sum(&:kappa)
|
53
|
+
|
54
|
+
# πΎ := π§ π§' π" = π§ πΏ'
|
55
|
+
def iota = mju(&:kappa)
|
56
|
+
|
57
|
+
# One can explicitly set the neuron's value, typically used to set the input
|
58
|
+
# neurons. The given "real world" value is squashed into the neuron's
|
59
|
+
# activation value.
|
60
|
+
def value=(value)
|
61
|
+
# If value is out of bounds, set it to the bound.
|
62
|
+
if value.abs > Neuronet.maxv
|
63
|
+
value = value.positive? ? Neuronet.maxv : -Neuronet.maxv
|
64
|
+
end
|
65
|
+
@activation = Neuronet.squash[value]
|
66
|
+
end
|
67
|
+
|
68
|
+
# The "real world" value of the neuron is the unsquashed activation value.
|
69
|
+
def value = Neuronet.unsquash[@activation]
|
70
|
+
|
71
|
+
# The initialize method sets the neuron's value, bias and connections.
|
72
|
+
def initialize(value = 0.0, bias: 0.0, connections: [])
|
73
|
+
self.value = value
|
74
|
+
@connections = connections
|
75
|
+
@bias = bias
|
76
|
+
@label = Neuron.label
|
77
|
+
Neuron.label = Neuron.label.next
|
78
|
+
end
|
79
|
+
|
80
|
+
# Updates the activation with the current value of bias and updated values
|
81
|
+
# of connections.
|
82
|
+
def update
|
83
|
+
return @activation if @connections.empty?
|
84
|
+
|
85
|
+
self.value = @bias + @connections.sum(&:update)
|
86
|
+
@activation
|
87
|
+
end
|
88
|
+
|
89
|
+
# For when connections are already updated, Neuron#partial updates the
|
90
|
+
# activation with the current values of bias and connections. It is not
|
91
|
+
# always necessary to burrow all the way down to the terminal input neuron
|
92
|
+
# to update the current neuron if it's connected neurons have all been
|
93
|
+
# updated. The implementation should set it's algorithm to use partial
|
94
|
+
# instead of update as update will most likely needlessly update previously
|
95
|
+
# updated neurons.
|
96
|
+
def partial
|
97
|
+
return @activation if @connections.empty?
|
98
|
+
|
99
|
+
self.value = @bias + @connections.sum(&:partial)
|
100
|
+
@activation
|
101
|
+
end
|
102
|
+
|
103
|
+
# The backpropagate method modifies the neuron's bias in proportion to the
|
104
|
+
# given error and passes on this error to each of its connection's
|
105
|
+
# backpropagate method. While updates flows from input to output, back-
|
106
|
+
# propagation of errors flows from output to input.
|
107
|
+
def backpropagate(error)
|
108
|
+
return self if @connections.empty?
|
109
|
+
|
110
|
+
@bias += Neuronet.noise[error]
|
111
|
+
if @bias.abs > Neuronet.maxb
|
112
|
+
@bias = @bias.positive? ? Neuronet.maxb : -Neuronet.maxb
|
113
|
+
end
|
114
|
+
@connections.each { |connection| connection.backpropagate(error) }
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# Connects the neuron to another neuron. The default weight=0 means there
|
119
|
+
# is no initial association. The connect method is how the implementation
|
120
|
+
# adds a connection, the way to connect a neuron to another. To connect
|
121
|
+
# "output" to "input", for example, it is:
|
122
|
+
# input = Neuronet::Neuron.new
|
123
|
+
# output = Neuronet::Neuron.new
|
124
|
+
# output.connect(input)
|
125
|
+
# Think "output" connects to "input".
|
126
|
+
def connect(neuron = Neuron.new, weight: 0.0)
|
127
|
+
@connections.push(Connection.new(neuron, weight:))
|
128
|
+
# Note that we're returning the connected neuron:
|
129
|
+
neuron
|
130
|
+
end
|
131
|
+
|
132
|
+
# Tacks on to neuron's inspect method to show the neuron's bias and
|
133
|
+
# connections.
|
134
|
+
def inspect
|
135
|
+
fmt = Neuronet.format
|
136
|
+
if @connections.empty?
|
137
|
+
"#{@label}:#{fmt % value}"
|
138
|
+
else
|
139
|
+
"#{@label}:#{fmt % value}|#{[(fmt % @bias), *@connections].join('+')}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# A neuron plainly puts itself as it's label.
|
144
|
+
def to_s = @label
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module
|
4
|
+
module Neuronet
|
5
|
+
# Neuronet::Scale is a class to help scale problems to fit within a network's
|
6
|
+
# "field of view". Given a list of values, it finds the minimum and maximum
|
7
|
+
# values and establishes a mapping to a scaled set of numbers between minus
|
8
|
+
# one and one (-1,1).
|
9
|
+
class Scale
|
10
|
+
attr_accessor :spread, :center
|
11
|
+
|
12
|
+
# If the value of center is provided, then
|
13
|
+
# that value will be used instead of
|
14
|
+
# calculating it from the values passed to method #set.
|
15
|
+
# Likewise, if spread is provided, that value of spread will be used.
|
16
|
+
def initialize(factor: 1.0, center: nil, spread: nil)
|
17
|
+
@factor = factor
|
18
|
+
@center = center
|
19
|
+
@spread = spread
|
20
|
+
end
|
21
|
+
|
22
|
+
def set(inputs)
|
23
|
+
min, max = inputs.minmax
|
24
|
+
@center ||= (max + min) / 2.0
|
25
|
+
@spread ||= (max - min) / 2.0
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset(inputs)
|
30
|
+
@center = @spread = nil
|
31
|
+
set(inputs)
|
32
|
+
end
|
33
|
+
|
34
|
+
def mapped(inputs)
|
35
|
+
factor = 1.0 / (@factor * @spread)
|
36
|
+
inputs.map { |value| factor * (value - @center) }
|
37
|
+
end
|
38
|
+
alias mapped_input mapped
|
39
|
+
alias mapped_output mapped
|
40
|
+
|
41
|
+
# Note that it could also unmap inputs, but
|
42
|
+
# outputs is typically what's being transformed back.
|
43
|
+
def unmapped(outputs)
|
44
|
+
factor = @factor * @spread
|
45
|
+
outputs.map { |value| (factor * value) + @center }
|
46
|
+
end
|
47
|
+
alias unmapped_input unmapped
|
48
|
+
alias unmapped_output unmapped
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Neuronet module
|
4
|
+
module Neuronet
|
5
|
+
# ScaledNetwork is a subclass of FeedForwardNetwork.
|
6
|
+
# It automatically scales the problem given to it
|
7
|
+
# by using a Scale type instance set in @distribution.
|
8
|
+
# The attribute, @distribution, is set to Neuronet::Gaussian.new by default,
|
9
|
+
# but one can change this to Scale, LogNormal, or one's own custom mapper.
|
10
|
+
class ScaledNetwork < FeedForward
|
11
|
+
attr_accessor :distribution, :reset
|
12
|
+
|
13
|
+
def initialize(layers, distribution: Gaussian.new, reset: false)
|
14
|
+
super(layers)
|
15
|
+
@distribution = distribution
|
16
|
+
@reset = reset
|
17
|
+
end
|
18
|
+
|
19
|
+
# ScaledNetwork set works just like FeedForwardNetwork's set method,
|
20
|
+
# but calls @distribution.set(values) first if @reset is true.
|
21
|
+
# Sometimes you'll want to set the distribution with the entire data set,
|
22
|
+
# and then there will be times you'll want to reset the distribution
|
23
|
+
# with each input.
|
24
|
+
def set(input)
|
25
|
+
@distribution.reset(input) if @reset
|
26
|
+
super(@distribution.mapped_input(input))
|
27
|
+
end
|
28
|
+
|
29
|
+
def input
|
30
|
+
@distribution.unmapped_input(super)
|
31
|
+
end
|
32
|
+
|
33
|
+
def output
|
34
|
+
@distribution.unmapped_output(super)
|
35
|
+
end
|
36
|
+
|
37
|
+
def *(_other)
|
38
|
+
@distribution.unmapped_output(super)
|
39
|
+
end
|
40
|
+
|
41
|
+
def train(target, mju = expected_mju)
|
42
|
+
super(@distribution.mapped_output(target), mju)
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect
|
46
|
+
distribution = @distribution.class.to_s.split(':').last
|
47
|
+
"#distribution:#{distribution} #reset:#{@reset}\n" + super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|