synaptical 0.0.1.pre.beta1
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/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +47 -0
- data/README.md +101 -0
- data/Rakefile +96 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/synaptical.rb +18 -0
- data/lib/synaptical/architect/perceptron.rb +32 -0
- data/lib/synaptical/connection.rb +31 -0
- data/lib/synaptical/cost/mse.rb +21 -0
- data/lib/synaptical/layer.rb +143 -0
- data/lib/synaptical/layer_connection.rb +74 -0
- data/lib/synaptical/network.rb +125 -0
- data/lib/synaptical/neuron.rb +312 -0
- data/lib/synaptical/serializer/json.rb +81 -0
- data/lib/synaptical/squash/logistic.rb +27 -0
- data/lib/synaptical/squash/tanh.rb +27 -0
- data/lib/synaptical/trainer.rb +89 -0
- data/lib/synaptical/version.rb +5 -0
- data/synaptical.gemspec +29 -0
- metadata +110 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Synaptical
|
4
|
+
# Representation of a connection between layers
|
5
|
+
class LayerConnection
|
6
|
+
attr_reader :id, :from, :to, :selfconnection, :type, :connections, :list,
|
7
|
+
:size, :gatedfrom
|
8
|
+
def initialize(from, to, type, weights)
|
9
|
+
@id = self.class.uid
|
10
|
+
@from = from
|
11
|
+
@to = to
|
12
|
+
@selfconnection = to == from
|
13
|
+
@type = type
|
14
|
+
@connections = {}
|
15
|
+
@list = []
|
16
|
+
@size = 0
|
17
|
+
@gatedfrom = []
|
18
|
+
|
19
|
+
init_type
|
20
|
+
|
21
|
+
connect!(weights)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initialize connection type if not provided
|
25
|
+
def init_type
|
26
|
+
return unless type.nil?
|
27
|
+
@type = if from == to
|
28
|
+
Synaptical::Layer::CONNECTION_TYPE[:ONE_TO_ONE]
|
29
|
+
else
|
30
|
+
Synaptical::Layer::CONNECTION_TYPE[:ALL_TO_ALL]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect!(weights)
|
35
|
+
if type == Synaptical::Layer::CONNECTION_TYPE[:ALL_TO_ALL] ||
|
36
|
+
type == Synaptical::Layer::CONNECTION_TYPE[:ALL_TO_ELSE]
|
37
|
+
from.list.each do |from|
|
38
|
+
to.list.each do |to|
|
39
|
+
if type == Synaptical::Layer::CONNECTION_TYPE[:ALL_TO_ELSE] &&
|
40
|
+
from == to
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
connection = from.project(to, weights)
|
45
|
+
@connections[connection.id] = connection
|
46
|
+
list.push(connection)
|
47
|
+
@size = list.size
|
48
|
+
end
|
49
|
+
end
|
50
|
+
elsif type == Synaptical::Layer::CONNECTION_TYPE[:ONE_TO_ONE]
|
51
|
+
from.list.each_with_index do |from, idx|
|
52
|
+
to = to.list[idx]
|
53
|
+
connection = from.project(to, weights)
|
54
|
+
|
55
|
+
@connections[connection.id] = connection
|
56
|
+
list.push(connection)
|
57
|
+
@size = list.size
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
from.connected_to << self
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
attr_reader :connections
|
66
|
+
|
67
|
+
def uid
|
68
|
+
@connections += 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@connections = 0
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Synaptical
|
4
|
+
# Representation of a network
|
5
|
+
class Network
|
6
|
+
Layers = Struct.new(:input, :hidden, :output)
|
7
|
+
|
8
|
+
attr_reader :optimized, :layers
|
9
|
+
|
10
|
+
def initialize(input:, hidden:, output:)
|
11
|
+
@layers = Layers.new(input, hidden, output)
|
12
|
+
@optimized = false
|
13
|
+
end
|
14
|
+
|
15
|
+
# Feed-forward activation of all the layers to produce an output
|
16
|
+
# @param input [Array<Numeric>] Input
|
17
|
+
#
|
18
|
+
# @return [Array<Numeric>] Output
|
19
|
+
def activate(input)
|
20
|
+
raise if optimized
|
21
|
+
layers.input.activate(input)
|
22
|
+
layers.hidden.each(&:activate)
|
23
|
+
layers.output.activate
|
24
|
+
end
|
25
|
+
|
26
|
+
# Back-propagate the error through the network
|
27
|
+
# @param rate [Float] Learning rate
|
28
|
+
# @param target [Array<Numeric>] Target values
|
29
|
+
def propagate(rate, target)
|
30
|
+
raise if optimized
|
31
|
+
layers.output.propagate(rate, target)
|
32
|
+
layers.hidden.each { |layer| layer.propagate(rate) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Project output onto another layer or network
|
36
|
+
# @param unit [Synaptical::Network, Synaptical::Layer] Object to project against
|
37
|
+
# @param type [type] [description]
|
38
|
+
# @param weights [type] [description]
|
39
|
+
def project(unit, type, weights)
|
40
|
+
raise if optimized
|
41
|
+
case unit
|
42
|
+
when Network
|
43
|
+
layers.output.project(unit.layers.input, type, weights)
|
44
|
+
when Layer
|
45
|
+
layers.output.project(unit, type, weights)
|
46
|
+
else
|
47
|
+
raise ArgumentError, 'Invalid argument'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def gate(connection, type)
|
52
|
+
raise if optimized
|
53
|
+
layers.output.gate(connection, type)
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
restore
|
58
|
+
([layers.input, layers.output] + layers.hidden).each(&:clear)
|
59
|
+
end
|
60
|
+
|
61
|
+
def reset
|
62
|
+
restore
|
63
|
+
([layers.input, layers.output] + layers.hidden).each(&:reset)
|
64
|
+
end
|
65
|
+
|
66
|
+
def optimize
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
|
70
|
+
def restore
|
71
|
+
raise if optimized
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return all neurons in all layers
|
75
|
+
#
|
76
|
+
# @return [Array<Hash>] A list of neurons and which layer they belong to
|
77
|
+
def neurons
|
78
|
+
layers.input.neurons.map { |n| { neuron: n, layer: 'input' } } +
|
79
|
+
layers.hidden
|
80
|
+
.flat_map(&:neurons)
|
81
|
+
.each_with_index
|
82
|
+
.map { |n, i| { neuron: n, layer: i } } +
|
83
|
+
layers.output.neurons.map { |n| { neuron: n, layer: 'output' } }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return number of inputs
|
87
|
+
#
|
88
|
+
# @return [Integer] Number of inputs
|
89
|
+
def inputs
|
90
|
+
layers.input.size
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return number of outputs
|
94
|
+
#
|
95
|
+
# @return [Integer] Number of outputs
|
96
|
+
def outputs
|
97
|
+
layers.output.size
|
98
|
+
end
|
99
|
+
|
100
|
+
def set
|
101
|
+
raise 'TODO'
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_optimize
|
105
|
+
raise 'TODO'
|
106
|
+
end
|
107
|
+
|
108
|
+
# Export the network as JSON
|
109
|
+
#
|
110
|
+
# @return [Hash] Hash ready for JSON serialization
|
111
|
+
def to_json
|
112
|
+
restore
|
113
|
+
|
114
|
+
Synaptical::Serializer::JSON.as_json(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_dot
|
118
|
+
raise 'TODO'
|
119
|
+
end
|
120
|
+
|
121
|
+
def from_json
|
122
|
+
raise 'TODO'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Synaptical
|
4
|
+
# Representation of a neuron
|
5
|
+
class Neuron
|
6
|
+
CONNECTION_TYPES = %i[inputs projected gated].freeze
|
7
|
+
|
8
|
+
Connections = Struct.new(:inputs, :projected, :gated)
|
9
|
+
Connection = Struct.new(:type, :connection)
|
10
|
+
Error = Struct.new(:responsibility, :projected, :gated)
|
11
|
+
Trace = Struct.new(:elegibility, :extended, :influences)
|
12
|
+
|
13
|
+
attr_reader :id, :connections, :error, :trace, :state, :old, :activation,
|
14
|
+
:selfconnection, :squash, :neighbors, :bias
|
15
|
+
# Creates an instance of a Neuron
|
16
|
+
def initialize
|
17
|
+
@id = self.class.uid
|
18
|
+
@connections = Connections.new({}, {}, {})
|
19
|
+
@error = Error.new(0.0, 0.0, 0.0)
|
20
|
+
@trace = Trace.new({}, {}, {})
|
21
|
+
|
22
|
+
@state = @old = @activation = 0.0
|
23
|
+
@selfconnection = Synaptical::Connection.new(self, self, 0.0)
|
24
|
+
@squash = Synaptical::Squash::Logistic
|
25
|
+
@neighbors = {}
|
26
|
+
@bias = rand * 0.2 - 0.1
|
27
|
+
end
|
28
|
+
|
29
|
+
# Activate the neuron
|
30
|
+
# @param input = nil [Numeric] input value
|
31
|
+
#
|
32
|
+
# @return [Numeric] output value
|
33
|
+
def activate(input = nil)
|
34
|
+
# Is neuron in input layer
|
35
|
+
unless input.nil?
|
36
|
+
@activation = input
|
37
|
+
@derivative = 0
|
38
|
+
@bias = 0
|
39
|
+
return activation
|
40
|
+
end
|
41
|
+
|
42
|
+
@old = @state
|
43
|
+
|
44
|
+
# eq. 15.
|
45
|
+
@state = selfconnection.gain * selfconnection.weight * state + bias
|
46
|
+
|
47
|
+
connections.inputs.each_value do |neuron|
|
48
|
+
@state += neuron.from.activation * neuron.weight * neuron.gain
|
49
|
+
end
|
50
|
+
|
51
|
+
# eq. 16.
|
52
|
+
@activation = squash.call(@state)
|
53
|
+
|
54
|
+
# f'(s)
|
55
|
+
@derivative = squash.derivate(@activation)
|
56
|
+
|
57
|
+
# Update traces
|
58
|
+
influences = []
|
59
|
+
trace.extended.each_key do |id|
|
60
|
+
neuron = @neighbors[id]
|
61
|
+
|
62
|
+
influence = neuron.selfconnection.gater == self ? neuron.old : 0
|
63
|
+
|
64
|
+
trace.influences[neuron.id].each do |incoming|
|
65
|
+
influence +=
|
66
|
+
trace.influences[neuron.id][incoming].weight *
|
67
|
+
trace.influences[id][incoming].from.activation
|
68
|
+
end
|
69
|
+
|
70
|
+
influences[neuron.id] = influence
|
71
|
+
end
|
72
|
+
|
73
|
+
connections.inputs.each_value do |input_neuron|
|
74
|
+
# elegibility trace - eq. 17
|
75
|
+
trace.elegibility[input_neuron.id] =
|
76
|
+
selfconnection.gain *
|
77
|
+
selfconnection.weight *
|
78
|
+
trace.elegibility[input_neuron.id] +
|
79
|
+
input_neuron.gain *
|
80
|
+
input_neuron.from.activation
|
81
|
+
|
82
|
+
trace.extended.each do |id, xtrace|
|
83
|
+
neuron = neighbors[id]
|
84
|
+
influence = influences[neuron.id]
|
85
|
+
|
86
|
+
xtrace[input_neuron.id] =
|
87
|
+
neuron.selfconnection.gain *
|
88
|
+
neuron.selfconnection.weight *
|
89
|
+
xtrace[input_neuron.id] +
|
90
|
+
@derivative *
|
91
|
+
trace.elegibility[input_neuron.id] *
|
92
|
+
influence
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Update gated connection's gains
|
97
|
+
connections.gated.each { |conn| conn.gain = @activation }
|
98
|
+
|
99
|
+
@activation
|
100
|
+
end
|
101
|
+
|
102
|
+
# Back propagate the error
|
103
|
+
# @param rate [Float] Learning rate
|
104
|
+
# @param target = nil [Numeric] Target value
|
105
|
+
def propagate(rate = 0.1, target = nil)
|
106
|
+
error = 0.0
|
107
|
+
|
108
|
+
# Is neuron in output layer
|
109
|
+
if !target.nil?
|
110
|
+
# Eq. 10.
|
111
|
+
@error.responsibility = @error.projected = target - @activation
|
112
|
+
else
|
113
|
+
# The rest of the neuron compute their error responsibilities by back-
|
114
|
+
# propagation
|
115
|
+
connections.projected.each_value do |connection|
|
116
|
+
neuron = connection.to
|
117
|
+
|
118
|
+
# Eq. 21.
|
119
|
+
error +=
|
120
|
+
neuron.error.responsibility * connection.gain * connection.weight
|
121
|
+
end
|
122
|
+
|
123
|
+
# Projected error responsibility
|
124
|
+
@error.projected = @derivative * error
|
125
|
+
|
126
|
+
error = 0.0
|
127
|
+
# Error responsibilities from all the connections gated by this neuron
|
128
|
+
trace.extended.each do |id, _|
|
129
|
+
neuron = @neighbors[id] # gated neuron
|
130
|
+
# If gated neuron's selfconnection is gated by this neuron
|
131
|
+
influence = neuron.selfconnection.gater == self ? neuron.old : 0.0
|
132
|
+
|
133
|
+
# Index runs over all th econnections to the gated neuron that are
|
134
|
+
# gated by this neuron
|
135
|
+
trace.influences[id].each do |input, infl|
|
136
|
+
# Captures the effect that the input connection of this neuron have,
|
137
|
+
# on a neuron which its input/s is/are gated by this neuron
|
138
|
+
influence +=
|
139
|
+
infl.weight *
|
140
|
+
trace.influences[neuron.id][input].from.activation
|
141
|
+
end
|
142
|
+
|
143
|
+
# Eq. 22.
|
144
|
+
error += neuron.error.responsibility * influence
|
145
|
+
end
|
146
|
+
|
147
|
+
# Gated error responsibility
|
148
|
+
@error.gated = @derivative * error
|
149
|
+
|
150
|
+
# Error responsibility - Eq. 23.
|
151
|
+
@error.responsibility = @error.projected + @error.gated
|
152
|
+
end
|
153
|
+
|
154
|
+
connections.inputs.each_value do |input_neuron|
|
155
|
+
# Eq. 24
|
156
|
+
gradient = @error.projected * trace.elegibility[input_neuron.id]
|
157
|
+
trace.extended.each do |id, _|
|
158
|
+
neuron = neighbors[id]
|
159
|
+
gradient += neuron.error.responsibility *
|
160
|
+
trace.extended[neuron.id][input_neuron.id]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Adjust weights - aka. learn
|
164
|
+
input_neuron.weight += rate * gradient
|
165
|
+
end
|
166
|
+
|
167
|
+
# Adjust bias
|
168
|
+
@bias += rate * @error.responsibility
|
169
|
+
end
|
170
|
+
|
171
|
+
# [project description]
|
172
|
+
# @param neuron [Synaptical::Neuron] Other neuron
|
173
|
+
# @param weight = nil [Float] Weight
|
174
|
+
#
|
175
|
+
# @return [Synaptical::Connection] Connection
|
176
|
+
def project(neuron, weight = nil)
|
177
|
+
if neuron == self
|
178
|
+
selfconnection.weight = 1
|
179
|
+
return selfconnection
|
180
|
+
end
|
181
|
+
|
182
|
+
# Check if connection already exists
|
183
|
+
connected = connected(neuron)
|
184
|
+
if connected && connected.type == :projected
|
185
|
+
# Update connection
|
186
|
+
connected.connection.weight = weight unless weight.nil?
|
187
|
+
return connected.connection
|
188
|
+
else
|
189
|
+
connection = ::Synaptical::Connection.new(self, neuron, weight)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Reference all te connections and traces
|
193
|
+
connections.projected[connection.id] = connection
|
194
|
+
neighbors[neuron.id] = neuron
|
195
|
+
neuron.connections.inputs[connection.id] = connection
|
196
|
+
neuron.trace.elegibility[connection.id] = 0
|
197
|
+
|
198
|
+
neuron.trace.extended.each do |_id, trace|
|
199
|
+
trace[connection.id] = 0
|
200
|
+
end
|
201
|
+
|
202
|
+
connection
|
203
|
+
end
|
204
|
+
|
205
|
+
# Add connection to gated list
|
206
|
+
# @param connection [Synaptical::Connection] Connection
|
207
|
+
def gate(connection)
|
208
|
+
connections.gated[connection.id] = connection
|
209
|
+
|
210
|
+
neuron = connection.to
|
211
|
+
unless trace.extended.key?(neuron.id)
|
212
|
+
# Extended trace
|
213
|
+
neighbors[neuron.id] = neuron
|
214
|
+
xtrace = trace.extended[neuron.id] = {}
|
215
|
+
connection.inputs.each_value do |input|
|
216
|
+
xtrace[input.id] = 0
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Keep track
|
221
|
+
if trace.influences.key?(neuron.id)
|
222
|
+
trace.influences[neuron.id] << connection
|
223
|
+
else
|
224
|
+
trace.influences[neuron.id] = [connection]
|
225
|
+
end
|
226
|
+
|
227
|
+
# Set gater
|
228
|
+
connection.gater = self
|
229
|
+
end
|
230
|
+
|
231
|
+
# Returns wheter the neuron is self connected
|
232
|
+
#
|
233
|
+
# @return [Boolean] true if self connected, false otherwise
|
234
|
+
def selfconnected?
|
235
|
+
!selfconnection.weight.zero?
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns whether the neuron is connected to another neuron
|
239
|
+
# @param neuron [Synaptical::Neuron] Other neuron
|
240
|
+
#
|
241
|
+
# @return [Boolean, Hash] Connection type if connected to other neuron,
|
242
|
+
# false otherwise
|
243
|
+
def connected(neuron)
|
244
|
+
result = Connection.new
|
245
|
+
|
246
|
+
if self == neuron
|
247
|
+
return nil unless selfconnected?
|
248
|
+
result.type = :selfconnection
|
249
|
+
result.connection = selfconnection
|
250
|
+
return result
|
251
|
+
end
|
252
|
+
|
253
|
+
CONNECTION_TYPES
|
254
|
+
.map { |ct| connections.send(ct).values }
|
255
|
+
.flatten
|
256
|
+
.each do |connection|
|
257
|
+
next unless connection.to == neuron || connection.from == neuron
|
258
|
+
result.type = type
|
259
|
+
result.connection = type
|
260
|
+
return result
|
261
|
+
end
|
262
|
+
|
263
|
+
nil
|
264
|
+
end
|
265
|
+
|
266
|
+
# Clear the context of the neuron, but keeps connections
|
267
|
+
def clear
|
268
|
+
trace.elegibility.transform_values { |_| 0 }
|
269
|
+
trace.extended.each_value do |ext|
|
270
|
+
ext.transform_values { |_| 0 }
|
271
|
+
end
|
272
|
+
|
273
|
+
error.responsibility = error.projected = error.gated = 0
|
274
|
+
end
|
275
|
+
|
276
|
+
# Clears traces and randomizes connections
|
277
|
+
def reset
|
278
|
+
clear
|
279
|
+
CONNECTION_TYPES.map { |ct| connections.send(ct) }.each do |conn_group|
|
280
|
+
conn_group.each_value { |conn| conn.weight = rand * 0.2 - 0.1 }
|
281
|
+
end
|
282
|
+
|
283
|
+
@bias = rand * 0.2 - 0.1
|
284
|
+
@old = @state = @activation = 0
|
285
|
+
end
|
286
|
+
|
287
|
+
# Hard codes the behavior of the neuron into an optimized function
|
288
|
+
# @param optimized [Hash] [description]
|
289
|
+
# @param layer [type] [description]
|
290
|
+
#
|
291
|
+
# @return [type] [description]
|
292
|
+
def optimize(_optimized, _layer)
|
293
|
+
raise 'TODO'
|
294
|
+
end
|
295
|
+
|
296
|
+
class << self
|
297
|
+
attr_reader :neurons
|
298
|
+
# Returns the next id in the sequence
|
299
|
+
#
|
300
|
+
# @return [type] [description]
|
301
|
+
def uid
|
302
|
+
@neurons += 1
|
303
|
+
end
|
304
|
+
|
305
|
+
def quantity
|
306
|
+
{ neurons: neurons, connections: Connection.connections }
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
@neurons = 0
|
311
|
+
end
|
312
|
+
end
|