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
data/README.md
CHANGED
@@ -1,786 +1,137 @@
|
|
1
|
-
# Neuronet
|
1
|
+
# Neuronet
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
*
|
6
|
-
* Git: <https://github.com/carlosjhr64/neuronet>
|
7
|
-
* Author: <carlosjhr64@gmail.com>
|
8
|
-
* Copyright: 2013
|
9
|
-
* License: [GPL](http://www.gnu.org/licenses/gpl.html)
|
10
|
-
|
11
|
-
## Installation
|
12
|
-
|
13
|
-
gem install neuronet
|
14
|
-
|
15
|
-
## Synopsis
|
16
|
-
|
17
|
-
Given some set of inputs (of at least length 3) and
|
18
|
-
targets that are Array's of Float's. Then:
|
19
|
-
|
20
|
-
# data = [ [input, target], ... }
|
21
|
-
# n = input.length # > 3
|
22
|
-
# t = target.length
|
23
|
-
# m = n + t
|
24
|
-
# l = data.length
|
25
|
-
# Then:
|
26
|
-
# Create a general purpose neuronet
|
27
|
-
|
28
|
-
neuronet = Neuronet::ScaledNetwork.new([n, m, t])
|
29
|
-
|
30
|
-
# "Bless" it as a TaoYinYang,
|
31
|
-
# a perceptron hybrid with the middle layer
|
32
|
-
# initially mirroring the input layer and
|
33
|
-
# mirrored by the output layer.
|
34
|
-
|
35
|
-
Neuronet::TaoYinYang.bless(neuronet)
|
36
|
-
|
37
|
-
# The following sets the learning constant
|
38
|
-
# to something I think is reasonable.
|
39
|
-
|
40
|
-
neuronet.num(l)
|
41
|
-
|
42
|
-
# Start training
|
43
|
-
|
44
|
-
MANY.times do
|
45
|
-
data.shuffle.each do |input, target|
|
46
|
-
neuronet.reset(input)
|
47
|
-
neuronet.train!(target)
|
48
|
-
end
|
49
|
-
end # or until some small enough error
|
50
|
-
|
51
|
-
# See how well the training went
|
52
|
-
|
53
|
-
require 'pp'
|
54
|
-
data.each do |input, target|
|
55
|
-
puts "Input:"
|
56
|
-
pp input
|
57
|
-
puts "Output:"
|
58
|
-
neuronet.reset(input) # sets the input values
|
59
|
-
pp neuronet.output # gets the output values
|
60
|
-
puts "Target:"
|
61
|
-
pp target
|
62
|
-
end
|
63
|
-
|
64
|
-
## Introduction
|
65
|
-
|
66
|
-
Neuronet is a pure Ruby 1.9, sigmoid squashed, neural network building library.
|
67
|
-
It allows one to build a network by connecting one neuron at a time, or a layer at a time,
|
68
|
-
or up to a full feed forward network that automatically scales the inputs and outputs.
|
69
|
-
|
70
|
-
I chose a TaoYinYang'ed ScaledNetwork neuronet for the synopsis because
|
71
|
-
it will probably handle most anything with 3 or more input variables you'd throw at it.
|
72
|
-
But there's a lot you can do to the data before throwing it at a neuronet.
|
73
|
-
And you can build a neuronet specifically to solve a particular kind of problem.
|
74
|
-
Properly transforming the data and choosing the right neuronet architecture
|
75
|
-
can greatly reduce the amount of training time the neuronet will require.
|
76
|
-
A neuronet with the wrong architecture for a problem will be unable to solve it.
|
77
|
-
Raw data without hints as to what's important in the data will take longer to solve.
|
78
|
-
|
79
|
-
As an analogy, think of what you can do with
|
80
|
-
[linear regression](http://en.wikipedia.org/wiki/Linear_regression).
|
81
|
-
Your raw data might not be linear, but if a transform converts it to a linear form,
|
82
|
-
you can use linear regression to find the best fit line, and
|
83
|
-
from that deduce the properties of the untransformed data.
|
84
|
-
Likewise, if you can transform the data into something the neuronet can solve,
|
85
|
-
you can by inverse get back the answer you're lookin for.
|
86
|
-
|
87
|
-
# Examples
|
88
|
-
|
89
|
-
## Time Series
|
90
|
-
|
91
|
-
A common use for a neural-net is to attempt to forecast future set of data points
|
92
|
-
based on past set of data points, [Time series](http://en.wikipedia.org/wiki/Time_series).
|
93
|
-
To demonstrate, I'll train a network with the following function:
|
94
|
-
|
95
|
-
f(t) = A + B sine(C + D t), t in [0,1,2,3,...]
|
96
|
-
|
97
|
-
I'll set A, B, C, and D with random numbers and see
|
98
|
-
if eventually the network can predict the next set of values based on previous values.
|
99
|
-
I'll try:
|
100
|
-
|
101
|
-
[f(n),...,f(n+19)] => [f(n+20),...,f(n+24)]
|
102
|
-
|
103
|
-
That is... given 20 consecutive values, give the next 5 in the series.
|
104
|
-
There is no loss, and probably greater generality,
|
105
|
-
if I set at random the phase (C above), so that for any given random phase we want:
|
106
|
-
|
107
|
-
[f(0),...,f(19)] => [f(20),...,f(24)]
|
108
|
-
|
109
|
-
I'll be using [Neuronet::ScaledNetwork](http://rubydoc.info/gems/neuronet/Neuronet/ScaledNetwork).
|
110
|
-
Also note that the Sine function is entirely defined within a cycle ( 2 Math::PI ) and
|
111
|
-
so parameters (particularly C) need only to be set within this cycle.
|
112
|
-
After a lot of testing, I've verified that a
|
113
|
-
[Perceptron](http://en.wikipedia.org/wiki/Perceptron) is enough to solve the problem.
|
114
|
-
The Sine function is [Linearly separable](http://en.wikipedia.org/wiki/Linearly_separable).
|
115
|
-
Adding hidden layers needlessly adds training time, but does converge.
|
116
|
-
|
117
|
-
The gist of the
|
118
|
-
[example code](https://github.com/carlosjhr64/neuronet/blob/master/examples/sine_series.rb)
|
119
|
-
is:
|
120
|
-
|
121
|
-
...
|
122
|
-
# The constructor
|
123
|
-
neuronet = Neuronet::ScaledNetwork.new([INPUTS, OUTPUTS])
|
124
|
-
...
|
125
|
-
# Setting learning constant
|
126
|
-
neuronet.num(1.0)
|
127
|
-
...
|
128
|
-
# Setting the input values
|
129
|
-
neuronet.reset(input)
|
130
|
-
...
|
131
|
-
# Getting the neuronet's output
|
132
|
-
output = neuronet.output
|
133
|
-
...
|
134
|
-
# Training the target
|
135
|
-
neuronet.train!(target)
|
136
|
-
...
|
137
|
-
|
138
|
-
Heres a sample output:
|
139
|
-
|
140
|
-
f(phase, t) = 3.002 + 3.28*Sin(phase + 1.694*t)
|
141
|
-
Cycle step = 0.27
|
142
|
-
|
143
|
-
Iterations: 1738
|
144
|
-
Relative Error (std/B): 0.79% Standard Deviation: 0.026
|
145
|
-
Examples:
|
146
|
-
|
147
|
-
Input: 0.522, 1.178, 5.932, 4.104, -0.199, 2.689, 6.28, 2.506, -0.154, 4.276, 5.844, 1.028, 0.647, 5.557, 4.727, 0.022, 2.011, 6.227, 3.198, -0.271
|
148
|
-
Target: 3.613, 6.124, 1.621, 0.22, 5.069
|
149
|
-
Output: 3.575, 6.101, 1.664, 0.227, 5.028
|
150
|
-
|
151
|
-
Input: 5.265, 5.079, 0.227, 1.609, 6.12, 3.626, -0.27, 3.184, 6.229, 2.024, 0.016, 4.716, 5.565, 0.656, 1.017, 5.837, 4.288, -0.151, 2.493, 6.28
|
152
|
-
Target: 2.703, -0.202, 4.091, 5.938, 1.189
|
153
|
-
Output: 2.728, -0.186, 4.062, 5.931, 1.216
|
154
|
-
|
155
|
-
Input: 5.028, 0.193, 1.669, 6.14, 3.561, -0.274, 3.25, 6.217, 1.961, 0.044, 4.772, 5.524, 0.61, 1.07, 5.87, 4.227, -0.168, 2.558, 6.281, 2.637
|
156
|
-
Target: -0.188, 4.153, 5.908, 1.135, 0.557
|
157
|
-
Output: -0.158, 4.112, 5.887, 1.175, 0.564
|
158
|
-
|
159
|
-
ScaledNetwork automatically scales each input via
|
160
|
-
[Neuronet::Gaussian](http://rubydoc.info/gems/neuronet/Neuronet/Gaussian),
|
161
|
-
so the input needs to be many variables and
|
162
|
-
the output entirely determined by the shape of the input and not it's scale.
|
163
|
-
That is, two inputs that are different only in scale should
|
164
|
-
produce outputs that are different only in scale.
|
165
|
-
The input must have at least three points.
|
166
|
-
|
167
|
-
You can tackle many problems just with
|
168
|
-
[Neuronet::ScaledNetwork](http://rubydoc.info/gems/neuronet/Neuronet/ScaledNetwork)
|
169
|
-
as described above.
|
170
|
-
|
171
|
-
# Component Architecture
|
172
|
-
|
173
|
-
## Nodes and Neurons
|
174
|
-
|
175
|
-
[Nodes](http://rubydoc.info/gems/neuronet/Neuronet/Node)
|
176
|
-
are used to set inputs while
|
177
|
-
[Neurons](http://rubydoc.info/gems/neuronet/Neuronet/Neuron)
|
178
|
-
are used for outputs and middle layers.
|
179
|
-
It's easy to create and connect Nodes and Neurons.
|
180
|
-
You can assemble custom neuronets one neuron at a time.
|
181
|
-
Too illustrate, here's a simple network that adds two random numbers.
|
182
|
-
|
183
|
-
require 'neuronet'
|
184
|
-
include Neuronet
|
185
|
-
|
186
|
-
def random
|
187
|
-
rand - rand
|
188
|
-
end
|
189
|
-
|
190
|
-
# create the input nodes
|
191
|
-
a = Node.new
|
192
|
-
b = Node.new
|
193
|
-
|
194
|
-
# create the output neuron
|
195
|
-
sum = Neuron.new
|
196
|
-
|
197
|
-
# and a neuron on the side
|
198
|
-
adjuster = Neuron.new
|
199
|
-
|
200
|
-
# connect the adjuster to a and b
|
201
|
-
adjuster.connect(a)
|
202
|
-
adjuster.connect(b)
|
203
|
-
|
204
|
-
# connect sum to a and b
|
205
|
-
sum.connect(a)
|
206
|
-
sum.connect(b)
|
207
|
-
# and to the adjuster
|
208
|
-
sum.connect(adjuster)
|
209
|
-
|
210
|
-
# The learning constant is about...
|
211
|
-
learning = 0.1
|
212
|
-
|
213
|
-
# Train the tiny network
|
214
|
-
10_000.times do
|
215
|
-
a.value = x = random
|
216
|
-
b.value = y = random
|
217
|
-
target = x+y
|
218
|
-
output = sum.update
|
219
|
-
sum.backpropagate(learning*(target-output))
|
220
|
-
end
|
221
|
-
|
222
|
-
# Let's see how well the training went
|
223
|
-
10.times do
|
224
|
-
a.value = x = random
|
225
|
-
b.value = y = random
|
226
|
-
target = x+y
|
227
|
-
output = sum.update
|
228
|
-
puts "#{x.round(3)} + #{y.round(3)} = #{target.round(3)}"
|
229
|
-
puts " Neuron says #{output.round(3)}, #{(100.0*(target-output)/target).round(2)}% error."
|
230
|
-
end
|
231
|
-
|
232
|
-
|
233
|
-
Here's a sample output:
|
234
|
-
|
235
|
-
0.003 + -0.413 = -0.41
|
236
|
-
Neuron says -0.413, -0.87% error.
|
237
|
-
-0.458 + 0.528 = 0.07
|
238
|
-
Neuron says 0.07, -0.45% error.
|
239
|
-
0.434 + -0.125 = 0.309
|
240
|
-
Neuron says 0.313, -1.43% error.
|
241
|
-
-0.212 + 0.34 = 0.127
|
242
|
-
Neuron says 0.131, -2.83% error.
|
243
|
-
-0.364 + 0.659 = 0.294
|
244
|
-
Neuron says 0.286, 2.86% error.
|
245
|
-
0.045 + 0.323 = 0.368
|
246
|
-
Neuron says 0.378, -2.75% error.
|
247
|
-
0.545 + 0.901 = 1.446
|
248
|
-
Neuron says 1.418, 1.9% error.
|
249
|
-
-0.451 + -0.486 = -0.937
|
250
|
-
Neuron says -0.944, -0.77% error.
|
251
|
-
-0.008 + 0.219 = 0.211
|
252
|
-
Neuron says 0.219, -3.58% error.
|
253
|
-
0.61 + 0.554 = 1.163
|
254
|
-
Neuron says 1.166, -0.25% error.
|
255
|
-
|
256
|
-
Note that the tiny neuronet has a limit on how precisely it can match the target, and
|
257
|
-
even after a million times training it won't do any beter than when it trains a few thousands.
|
258
|
-
[code](https://github.com/carlosjhr64/neuronet/blob/master/examples/neurons.rb)
|
259
|
-
|
260
|
-
|
261
|
-
## InputLayer and Layer
|
262
|
-
|
263
|
-
Instead of working with individual neurons, you can work with layers.
|
264
|
-
Here we build a [Perceptron](http://en.wikipedia.org/wiki/Perceptron):
|
265
|
-
|
266
|
-
in = InputLayer.new(9)
|
267
|
-
out = Layer.new(1)
|
268
|
-
out.connect(in)
|
269
|
-
|
270
|
-
When making connections keep in mind "outputs connects to inputs",
|
271
|
-
not the other way around.
|
272
|
-
You can set the input values and update this way:
|
273
|
-
|
274
|
-
in.set([1,2,3,4,5,6,7,8,9])
|
275
|
-
out.partial
|
276
|
-
|
277
|
-
Partial means the update wont travel further than the current layer,
|
278
|
-
which is all we have in this case anyways.
|
279
|
-
You get the output this way:
|
280
|
-
|
281
|
-
output = out.output # returns an array of values
|
282
|
-
|
283
|
-
You train this way:
|
284
|
-
|
285
|
-
target = [1] #<= whatever value you want in the array
|
286
|
-
learning = 0.1
|
287
|
-
out.train(target, learning)
|
288
|
-
|
289
|
-
## FeedForward Network
|
290
|
-
|
291
|
-
Most of the time, you'll just use a network created with the
|
292
|
-
[FeedForward](http://rubydoc.info/gems/neuronet/Neuronet/FeedForward) class,
|
293
|
-
or a modified version or subclass of it.
|
294
|
-
Here we build a neuronet with four layers.
|
295
|
-
The input layer has four neurons, and the output has three.
|
296
|
-
Then we train it with a list of inputs and targets
|
297
|
-
using the method [#exemplar](http://rubydoc.info/gems/neuronet/Neuronet/FeedForward:exemplar):
|
298
|
-
|
299
|
-
neuronet = Neuronet::FeedForward.new([4,5,6,3])
|
300
|
-
LIST.each do |input, target|
|
301
|
-
neuronet.exemplar(input, target)
|
302
|
-
# you could also train this way:
|
303
|
-
# neuronet.set(input)
|
304
|
-
# neuronet.train!(target)
|
305
|
-
end
|
306
|
-
|
307
|
-
The first layer is the input layer and the last layer is the output layer.
|
308
|
-
Neuronet also names the second and second last layer.
|
309
|
-
The second layer is called yin.
|
310
|
-
The second last layer is called yang.
|
311
|
-
For the example above, we can check their lengths.
|
312
|
-
|
313
|
-
puts neuronet.in.length #=> 4
|
314
|
-
puts neuronet.yin.length #=> 5
|
315
|
-
puts neuronet.yang.length #=> 6
|
316
|
-
puts neuronet.out.length #=> 3
|
317
|
-
|
318
|
-
## Tao, Yin, Yang, and Brahma
|
319
|
-
|
320
|
-
Tao
|
321
|
-
: The absolute principle underlying the universe,
|
322
|
-
combining within itself the principles of yin and yang and
|
323
|
-
signifying the way, or code of behavior,
|
324
|
-
that is in harmony with the natural order.
|
325
|
-
|
326
|
-
Perceptrons are already very capable and quick to train.
|
327
|
-
By connecting the input layer to the output layer of a multilayer FeedForward network,
|
328
|
-
you'll get the Perceptron solution quicker while the middle layers work on the harder problem.
|
329
|
-
You can do that this way:
|
330
|
-
|
331
|
-
neronet.out.connect(neuronet.in)
|
332
|
-
|
333
|
-
But giving that a name, [Tao](http://rubydoc.info/gems/neuronet/Neuronet/Tao),
|
334
|
-
and using a prototype pattern to modify the instance is more fun:
|
335
|
-
|
336
|
-
Tao.bless(neuronet)
|
337
|
-
|
338
|
-
Yin
|
339
|
-
: The passive female principle of the universe, characterized as female and
|
340
|
-
sustaining and associated with earth, dark, and cold.
|
341
|
-
|
342
|
-
Initially FeedForward sets the weights of all connections to zero.
|
343
|
-
That is, there is no association made from input to ouput.
|
344
|
-
Changes in the inputs have no effect on the output.
|
345
|
-
Training begins the process that sets the weights to associate the two.
|
346
|
-
But you can also manually set the initial weights.
|
347
|
-
One useful way to initially set the weigths is to have one layer mirror another.
|
348
|
-
The [Yin](http://rubydoc.info/gems/neuronet/Neuronet/Yin) bless makes yin mirror the input.
|
349
|
-
The length of yin must be at least that of in.
|
350
|
-
The pairing starts with in.first and yin.first on up.
|
351
|
-
|
352
|
-
Yin.bless(neuronet)
|
353
|
-
|
354
|
-
Yang
|
355
|
-
: The active male principle of the universe, characterized as male and
|
356
|
-
creative and associated with heaven, heat, and light.
|
357
|
-
|
358
|
-
On the other hand, the [Yang](http://rubydoc.info/gems/neuronet/Neuronet/Yang)
|
359
|
-
bless makes the output mirror yang.
|
360
|
-
The length of yang must be a least that of out.
|
361
|
-
The pairing starts from yang.last and out.last on down.
|
362
|
-
|
363
|
-
Yang.bless(neuronet)
|
364
|
-
|
365
|
-
Brahma
|
366
|
-
: The creator god in later Hinduism, who forms a triad with Vishnu the preserver and Shiva the destroyer.
|
367
|
-
|
368
|
-
[Brahma](http://rubydoc.info/gems/neuronet/Neuronet/Brahma)
|
369
|
-
pairs each input node with two yin neurons sending them respectively the positive and negative value of its activation.
|
370
|
-
I'd say then that yin both mirrors and shadows input.
|
371
|
-
The length of yin must be at least twice that of in.
|
372
|
-
The pairing starts with in.first and yin.first on up.
|
373
|
-
|
374
|
-
Brahma.bless(neuronet)
|
375
|
-
|
376
|
-
Bless
|
377
|
-
: Pronounce words in a religious rite, to confer or invoke divine favor upon.
|
378
|
-
|
379
|
-
The reason Tao, Yin, and Yang are not classes onto themselves is that
|
380
|
-
you can combine these, and a protoptype pattern (bless) works better in this case.
|
381
|
-
Bless is the keyword used in [Perl](http://www.perl.org/) to create objects,
|
382
|
-
so it's not without precedent.
|
383
|
-
To combine all three features, Tao, Yin, and Yang, do this:
|
384
|
-
|
385
|
-
Tao.bless Yin.bless Yang.bless neuronet
|
386
|
-
|
387
|
-
To save typing, the library provides the possible combinations.
|
388
|
-
For example:
|
389
|
-
|
390
|
-
TaoYinYang.bless neuronet
|
391
|
-
|
392
|
-
# Scaling The Problem
|
3
|
+
* [VERSION 7.0.230416](https://github.com/carlosjhr64/neuronet/releases)
|
4
|
+
* [github](https://github.com/carlosjhr64/neuronet)
|
5
|
+
* [rubygems](https://rubygems.org/gems/neuronet)
|
393
6
|
|
394
|
-
|
395
|
-
to the segment zero to one (0,1).
|
396
|
-
But for the sake of computation in a neural net,
|
397
|
-
sigmoid works best if the problem is scaled to numbers
|
398
|
-
between negative one and positive one (-1, 1).
|
399
|
-
Study the following table and see if you can see why:
|
7
|
+
## DESCRIPTION:
|
400
8
|
|
401
|
-
|
402
|
-
9 => 0.99987...
|
403
|
-
3 => 0.95257...
|
404
|
-
2 => 0.88079...
|
405
|
-
1 => 0.73105...
|
406
|
-
0 => 0.50000...
|
407
|
-
-1 => 0.26894...
|
408
|
-
-2 => 0.11920...
|
409
|
-
-3 => 0.04742...
|
410
|
-
-9 => 0.00012...
|
411
|
-
|
412
|
-
As x gets much higher than 3, sigmoid(x) gets to be pretty close to just 1, and
|
413
|
-
as x gets much lower than -3, sigmoid(x) gets to be pretty close to 0.
|
414
|
-
Note that sigmoid is centered about 0.5 which maps to 0.0 in problem space.
|
415
|
-
It is for this reason that I suggest the problem be displaced (subtracted)
|
416
|
-
by it's average to be centered about zero and scaled (divided) by it standard deviation.
|
417
|
-
Try to get most of the data to fit within sigmoid's central "field of view" (-1, 1).
|
418
|
-
|
419
|
-
## Scale, Gaussian, and Log Normal
|
420
|
-
|
421
|
-
Neuronet provides three classes to help scale the problem space.
|
422
|
-
[Neuronet::Scale](http://rubydoc.info/gems/neuronet/Neuronet/Scale)
|
423
|
-
is the simplest most straight forward.
|
424
|
-
It finds the range and center of a list of values, and
|
425
|
-
linearly tranforms it to a range of (-1,1) centered at 0.
|
426
|
-
For example:
|
427
|
-
|
428
|
-
scale = Neuronet::Scale.new
|
429
|
-
values = [ 1, -3, 5, -2 ]
|
430
|
-
scale.set( values )
|
431
|
-
mapped = scale.mapped( values )
|
432
|
-
puts mapped.join(', ') # 0.0, -1.0, 1.0, -0.75
|
433
|
-
puts scale.unmapped( mapped ).join(', ') # 1.0, -3.0, 5.0, -2.0
|
434
|
-
|
435
|
-
The mapping is the following:
|
436
|
-
|
437
|
-
center = (maximum + minimum) / 2.0 if center.nil? # calculate center if not given
|
438
|
-
spread = (maximum - minimum) / 2.0 if spread.nil? # calculate spread if not given
|
439
|
-
inputs.map{ |value| (value - center) / (factor * spread) }
|
440
|
-
|
441
|
-
One can change the range of the map to (-1/factor, 1/factor)
|
442
|
-
where factor is the spread multiplier and force
|
443
|
-
a (perhaps pre-calculated) value for center and spread.
|
444
|
-
The constructor is:
|
445
|
-
|
446
|
-
scale = Neuronet::Scale.new( factor=1.0, center=nil, spread=nil )
|
447
|
-
|
448
|
-
In the constructor, if the value of center is provided, then
|
449
|
-
that value will be used instead of it being calculated from the values passed to method set.
|
450
|
-
Likewise, if spread is provided, that value of spread will be used.
|
451
|
-
|
452
|
-
[Neuronet::Gaussian](http://rubydoc.info/gems/neuronet/Neuronet/Gaussian)
|
453
|
-
works the same way, except that it uses the average value of the list given
|
454
|
-
for the center, and the standard deviation for the spread.
|
455
|
-
|
456
|
-
And [Neuronet::LogNormal](http://rubydoc.info/gems/neuronet/Neuronet/LogNormal)
|
457
|
-
is just like Gaussian except that it first pipes values through a logarithm, and
|
458
|
-
then pipes the output back through exponentiation.
|
459
|
-
|
460
|
-
## ScaledNetwork
|
461
|
-
|
462
|
-
[Neuronet::ScaledNetwork](http://rubydoc.info/gems/neuronet/Neuronet/ScaledNetwork)
|
463
|
-
automates the problem space scaling.
|
464
|
-
You can choose to do your scaling over the entire data set if you think
|
465
|
-
the relative scale of the individual inputs matter.
|
466
|
-
For example if in the problem one apple is good but two is to many...
|
467
|
-
In that case do this:
|
468
|
-
|
469
|
-
scaled_network.distribution.set( data_set.flatten )
|
470
|
-
data_set.each do |inputs,outputs|
|
471
|
-
# ... do your stuff using scaled_network.set( inputs )
|
472
|
-
end
|
473
|
-
|
474
|
-
If on the other hand the scale of the individual inputs is not the relevant feature,
|
475
|
-
you can you your scaling per individual input.
|
476
|
-
For example a small apple is an apple, and so is the big one. They're both apples.
|
477
|
-
Then do this:
|
478
|
-
|
479
|
-
data_set.each do |inputs,outputs|
|
480
|
-
# ... do your stuff using scaled_network.reset( inputs )
|
481
|
-
end
|
482
|
-
|
483
|
-
Note that in the first case you are using
|
484
|
-
[#set](http://rubydoc.info/gems/neuronet/Neuronet/ScaledNetwork:set)
|
485
|
-
and in the second case you are using
|
486
|
-
[#reset](http://rubydoc.info/gems/neuronet/Neuronet/ScaledNetwork:reset).
|
487
|
-
|
488
|
-
# Pit Falls
|
489
|
-
|
490
|
-
When sub-classing a Neuronet::Scale type class,
|
491
|
-
make sure mapped\_input, mapped\_output, unmapped\_input,
|
492
|
-
and unmapped\_output are defined as you intended.
|
493
|
-
If you don't override them, they will point to the first ancestor that defines them.
|
494
|
-
Overriding #mapped does not piggyback the aliases and
|
495
|
-
they will continue to point to the original #mapped method.
|
496
|
-
|
497
|
-
Another pitfall is confusing the input/output flow in connections and back-propagation.
|
498
|
-
Remember to connect outputs to inputs (out.connect(in)) and
|
499
|
-
to back-propagate from outputs to inputs (out.train(targets)).
|
500
|
-
|
501
|
-
# Interesting Custom Networks
|
502
|
-
|
503
|
-
Note that a particularly interesting YinYang with n inputs and m outputs
|
504
|
-
would be constructed this way:
|
505
|
-
|
506
|
-
yinyang = YinYang.bless FeedForward.new( [n, n+m, m] )
|
507
|
-
|
508
|
-
Here yinyang's hidden layer (which is both yin and yang)
|
509
|
-
initially would have the first n neurons mirror the input and
|
510
|
-
the last m neurons be mirrored by the output.
|
511
|
-
Another interesting YinYang would be an input to output mirror:
|
512
|
-
|
513
|
-
yinyang = YinYang.bless FeedForward.new( [n, n, n] )
|
514
|
-
|
515
|
-
# Theory
|
516
|
-
|
517
|
-
## The Biological Description of a Neuron
|
518
|
-
|
519
|
-
Usually a neuron is described as being either on or off.
|
520
|
-
I think it is more useful to describe a neuron as having a pulse rate.
|
521
|
-
A neuron would either have a high or a low pulse rate.
|
522
|
-
In absence of any stimuli from neighboring neurons, the neuron may also have a rest pulse rate.
|
523
|
-
A neuron receives stimuli from other neurons through the axons that connects them.
|
524
|
-
These axons communicate to the receiving neuron the pulse rates of the transmitting neurons.
|
525
|
-
The signal from other neurons are either strengthen or weakened at the synapse, and
|
526
|
-
might either inhibit or excite the receiving neuron.
|
527
|
-
Regardless of how much stimuli the neuron gets,
|
528
|
-
a neuron has a maximum pulse it cannot exceed.
|
529
|
-
|
530
|
-
## The Mathematical Model of a Neuron
|
531
|
-
|
532
|
-
Since my readers here are probably Ruby programmers, I'll write the math in a Ruby-ish way.
|
533
|
-
Allow me to sum this way:
|
534
|
-
|
535
|
-
module Enumerable
|
536
|
-
def sum
|
537
|
-
map{|a| yield(a)}.inject(0, :+)
|
538
|
-
end
|
539
|
-
end
|
540
|
-
[1,2,3].sum{|i| 2*i} == 2+4+6 # => true
|
541
|
-
|
542
|
-
Can I convince you that taking the derivative of a function looks like this?
|
543
|
-
|
544
|
-
def d(x)
|
545
|
-
dx = SMALL
|
546
|
-
f = yield(x)
|
547
|
-
(yield(x+dx) - f)/dx
|
548
|
-
end
|
549
|
-
dfdx = d(a){|x| f(x)}
|
550
|
-
|
551
|
-
So the Ruby-ish way to write one of the rules of Calculus is:
|
552
|
-
|
553
|
-
d{|x| Ax^n} == nAx^(n-1)
|
554
|
-
|
555
|
-
We won't bother distinguishing integers from floats.
|
556
|
-
The sigmoid function is:
|
557
|
-
|
558
|
-
def sigmoid(x)
|
559
|
-
1/(1+exp(-x))
|
560
|
-
end
|
561
|
-
sigmoid(a) == 1/(1+exp(a))
|
562
|
-
|
563
|
-
A neuron's pulserate increases with increasing stimulus, so
|
564
|
-
we need a model that adds up all the stimuli a neuron gets.
|
565
|
-
The sum of all stimuli we will call the neuron's value.
|
566
|
-
(I find this confusing, but
|
567
|
-
it works out that it is this sum that will give us the problem space value.)
|
568
|
-
To model the neuron's rest pulse, we'll say that it has a bias value, it's own stimuli.
|
569
|
-
Stimuli from other neurons comes through the connections,
|
570
|
-
so there is a sum over all the connections.
|
571
|
-
The stimuli from other transmitting neurons is be proportional to their own pulsetates and
|
572
|
-
the weight the receiving neuron gives them.
|
573
|
-
In the model we will call the pulserate the neuron's activation.
|
574
|
-
Lastly, to more closely match the code, a neuron is a node.
|
575
|
-
This is what we have so far:
|
576
|
-
|
577
|
-
value = bias + connections.sum{|connection| connection.weight * connection.node.activation }
|
578
|
-
|
579
|
-
# or by their biological synonyms
|
580
|
-
|
581
|
-
stimulus = unsquashed_rest_pulse_rate +
|
582
|
-
connections.sum{|connection| connection.weight * connection.neuron.pulserate}
|
583
|
-
|
584
|
-
Unsquashed rest pulse rate? Yeah, I'm about to close the loop here.
|
585
|
-
As described, a neuron can have a very low pulse rate, effectively zero,
|
586
|
-
and a maximum pulse which I will define as being one.
|
587
|
-
The sigmoid function will take any amount it gets and
|
588
|
-
squashes it to a number between zero and one,
|
589
|
-
which is what we need to model the neuron's behavior.
|
590
|
-
To get the node's activation (aka neuron's pulserate)
|
591
|
-
from the node's value (aka neuron's stimulus),
|
592
|
-
we squash the value with the sigmoid function.
|
593
|
-
|
594
|
-
# the node's activation from it's value
|
595
|
-
activation = sigmoid(value)
|
596
|
-
|
597
|
-
# or by their biological synonyms
|
598
|
-
|
599
|
-
# the neuron's pulserate from its stimulus
|
600
|
-
pulserate = sigmoid(stimulus)
|
601
|
-
|
602
|
-
So the "rest pulse rate" is sigmoid("unsquashed rest pulse rate").
|
603
|
-
|
604
|
-
## Backpropagation of Errors
|
605
|
-
|
606
|
-
There's a lot of really complicated math in understanding how neural networks work.
|
607
|
-
But if we concentrate on just the part pertinent to the bacpkpropagation code, it's not that bad.
|
608
|
-
The trick is to do the analysis in the problem space (otherwise things get real ugly).
|
609
|
-
When we train a neuron, we want the neuron's value to match a target as closely as possible.
|
610
|
-
The deviation from the target is the error:
|
611
|
-
|
612
|
-
error = target - value
|
613
|
-
|
614
|
-
Where does the error come from?
|
615
|
-
It comes from deviations from the ideal bias and weights the neuron should have.
|
616
|
-
|
617
|
-
target = value + error
|
618
|
-
target = bias + bias_error +
|
619
|
-
connections.sum{|connection| (connection.weight + weight_error) * connection.node.activation }
|
620
|
-
error = bias_error + connections.sum{|connection| weight_error * connection.node.activation }
|
621
|
-
|
622
|
-
Next we assume that the errors are equally likely everywhere,
|
623
|
-
so that the bias error is expected to be same on average as weight error.
|
624
|
-
That's where the learning constant comes in.
|
625
|
-
We need to divide the error equally among all contributors, say 1/N.
|
626
|
-
Then:
|
627
|
-
|
628
|
-
error = error/N + connections.sum{|connection| error/N * connection.node.activation }
|
629
|
-
|
630
|
-
Note that if the equation above represents the entire network, then
|
631
|
-
|
632
|
-
N = 1 + connections.length
|
633
|
-
|
634
|
-
So now that we know the error, we can modify the bias and weights.
|
635
|
-
|
636
|
-
bias += error/N
|
637
|
-
connection.weight += connection.node.activation * error/N
|
638
|
-
|
639
|
-
The Calculus is:
|
640
|
-
|
641
|
-
d{|bias| bias + connections.sum{|connection| connection.weight * connection.node.activation }}
|
642
|
-
== d{|bias| bias}
|
643
|
-
|
644
|
-
d{|connection.weight| bias + connections.sum{|connection| connection.weight * connection.node.activation }}
|
645
|
-
== connection.node.activation * d{|weight| connection.weight }
|
646
|
-
|
647
|
-
So what's all the ugly math you'll see elsewhere?
|
648
|
-
Well, you can try to do the above analysis in neuron space.
|
649
|
-
Then you're inside the squash function.
|
650
|
-
I'll just show derivative of the sigmoid function:
|
651
|
-
|
652
|
-
d{|x| sigmoid(x)} ==
|
653
|
-
d{|x| 1/(1+exp(-x))} ==
|
654
|
-
1/(1+exp(-x))^2 * d{|x|(1+exp(-x)} ==
|
655
|
-
1/(1+exp(-x))^2 * d{|x|(exp(-x)} ==
|
656
|
-
1/(1+exp(-x))^2 * d{|x| -x}*exp(-x) ==
|
657
|
-
1/(1+exp(-x))^2 * (-1)*exp(-x) ==
|
658
|
-
-exp(-x)/(1+exp(-x))^2 ==
|
659
|
-
(1 -1 - exp(-x))/(1+exp(-x))^2 ==
|
660
|
-
(1 - (1 + exp(-x)))/(1+exp(-x))^2 ==
|
661
|
-
(1 - 1/sigmoid(x)) * sigmoid^2(x) ==
|
662
|
-
(sigmoid(x) - 1) * sigmoid(x) ==
|
663
|
-
sigmoid(x)*(sigmoid(x) - 1)
|
664
|
-
# =>
|
665
|
-
d{|x| sigmoid(x)} == sigmoid(x)*(sigmoid(x) - 1)
|
666
|
-
|
667
|
-
From there you try to find the errors from the point of view of the activation instead of the value.
|
668
|
-
But as the code clearly shows, the analysis need not get this deep.
|
669
|
-
|
670
|
-
## Learning Constant
|
671
|
-
|
672
|
-
One can think of a neural network as a sheet of very elastic rubber
|
673
|
-
which one pokes and pulls to fit the training data while
|
674
|
-
otherwise keeping the sheet as smooth as possible.
|
675
|
-
One concern is that the training data may contain noise, random errors.
|
676
|
-
So the training of the network should add up the true signal in the data
|
677
|
-
while canceling out the noise. This balance is set via the learning constant.
|
678
|
-
|
679
|
-
neuronet.learning
|
680
|
-
# Returns the current value of the network's learning constant
|
681
|
-
|
682
|
-
neuronet.learning = float
|
683
|
-
# where float is greater than zero but less than one.
|
684
|
-
|
685
|
-
By default, Neuronet::FeedForward sets the learning constant to 1/N, where
|
686
|
-
N is the number of biases and weights in the network
|
687
|
-
(plus one, just because...). You can get the vale of N with
|
688
|
-
[#mu](http://rubydoc.info/gems/neuronet/Neuronet/FeedForward:mu).
|
689
|
-
|
690
|
-
So I'm now making up a few more names for stuff.
|
691
|
-
The number of contributors to errors in the network is #mu.
|
692
|
-
The learning constant based on #mu is
|
693
|
-
[#muk](http://rubydoc.info/gems/neuronet/Neuronet/FeedForward:muk).
|
694
|
-
You can modify the learning constant to some fraction of muk, say 0.7, this way:
|
695
|
-
|
696
|
-
neuronet.muk(0.7)
|
697
|
-
|
698
|
-
I've not come across any hard rule for the learning constant.
|
699
|
-
I have my own intuition derived from the behavior of random walks.
|
700
|
-
The distance away from a starting point in a random walk is
|
701
|
-
proportional to the square root of the number of steps.
|
702
|
-
I conjecture that the number of training data points is related to
|
703
|
-
the optimal learning constant in the same way.
|
704
|
-
So I provide a way to set the learning constant based on the size of the data with
|
705
|
-
[#num](http://rubydoc.info/gems/neuronet/Neuronet/FeedForward:num)
|
706
|
-
|
707
|
-
neuronet.num(n)
|
708
|
-
|
709
|
-
The value of #num(n) is #muk(1.0)/Math.sqrt(n)).
|
710
|
-
|
711
|
-
## Mirroring
|
712
|
-
|
713
|
-
Because the squash function is not linear, mirroring is going to be warped.
|
714
|
-
Nonetheless, I'd like to map zeroes to zeroes and ones to ones.
|
715
|
-
That gives us the following two equations:
|
716
|
-
|
717
|
-
weight*sigmoid(1.0) + bias = 1.0
|
718
|
-
weight*sigmoid(0.0) + bias = 0.0
|
719
|
-
|
720
|
-
We can solve that! Consider the zeroes to zeroes map:
|
721
|
-
|
722
|
-
weight*sigmoid(0.0) + bias = 0.0
|
723
|
-
weight*sigmoid(0.0) = -bias
|
724
|
-
weight*0.5 = -bias
|
725
|
-
weight = -2*bias
|
726
|
-
|
727
|
-
Now the ones to ones:
|
728
|
-
|
729
|
-
weight*sigmoid(1.0) + bias = 1.0
|
730
|
-
-2.0*bias*sigmoid(1.0) + bias = 1.0
|
731
|
-
bias*(-2.0*sigmoid(1.0) + 1.0) = 1.0
|
732
|
-
bias = 1.0 / (1.0 - 2.0*sigmoid(1.0))
|
733
|
-
|
734
|
-
We get the numerical values:
|
735
|
-
|
736
|
-
bias = -2.163953413738653 # BZERO
|
737
|
-
weight = 4.327906827477306 # WONE
|
738
|
-
|
739
|
-
In the code I call this bias and weight BZERO and WONE respectively.
|
740
|
-
What about "shadowing"?
|
741
|
-
|
742
|
-
weight*sigmoid(1.0) + bias = -1.0
|
743
|
-
weight*sigmoid(0.0) + bias = 0.0
|
744
|
-
|
745
|
-
weight = -2.0*bias # <== same a before
|
746
|
-
|
747
|
-
weight*sigmoid(1.0) + bias = -1.0
|
748
|
-
-2.0*bias*sigmoid(1.0) + bias = -1.0
|
749
|
-
bias*(-2.0*sigmoid(1.0) + 1.0) = -1.0
|
750
|
-
bias = -1.0 / (-2.0*sigmoid(1.0) + 1.0)
|
751
|
-
bias = 1.0 / (2.0*sigmoid(1.0) - 1.0)
|
752
|
-
# ^== this is just negative what we got before.
|
753
|
-
|
754
|
-
Shadowing is just the negative of mirroring.
|
755
|
-
There's a test, [tests/mirror.rb](https://github.com/carlosjhr64/neuronet/blob/master/tests/mirror.rb),
|
756
|
-
which demostrates mirroring. Here's the output:
|
757
|
-
|
758
|
-
### YinYang ###
|
759
|
-
Input:
|
760
|
-
-1.0, 0.0, 1.0
|
761
|
-
In:
|
762
|
-
0.2689414213699951, 0.5, 0.7310585786300049
|
763
|
-
Yin/Yang:
|
764
|
-
0.2689414213699951, 0.5, 0.7310585786300049
|
765
|
-
0.2689414213699951, 0.5, 0.7310585786300049
|
766
|
-
Out:
|
767
|
-
0.2689414213699951, 0.5, 0.7310585786300049
|
768
|
-
Output:
|
769
|
-
-1.0000000000000002, 0.0, 1.0
|
770
|
-
|
771
|
-
### BrahmaYang ###
|
772
|
-
Input:
|
773
|
-
-1.0, 0.0, 1.0
|
774
|
-
In:
|
775
|
-
0.2689414213699951, 0.5, 0.7310585786300049
|
776
|
-
Yin/Yang:
|
777
|
-
0.2689414213699951, 0.7310585786300049, 0.5, 0.5, 0.7310585786300049, 0.2689414213699951
|
778
|
-
0.2689414213699951, 0.7310585786300049, 0.5, 0.5, 0.7310585786300049, 0.2689414213699951
|
779
|
-
Out:
|
780
|
-
0.2689414213699951, 0.7310585786300049, 0.5, 0.5, 0.7310585786300049, 0.2689414213699951
|
781
|
-
Output:
|
782
|
-
-1.0000000000000002, 1.0, 0.0, 0.0, 1.0, -1.0000000000000002
|
783
|
-
|
784
|
-
# Questions?
|
9
|
+
Library to create neural networks.
|
785
10
|
|
786
|
-
|
11
|
+
This is primarily a math project meant to be used to investigate the behavior of
|
12
|
+
different small neural networks.
|
13
|
+
|
14
|
+
## INSTALL:
|
15
|
+
```console
|
16
|
+
gem install neuronet
|
17
|
+
```
|
18
|
+
## SYNOPSIS:
|
19
|
+
|
20
|
+
The library is meant to be read, but here is a motivating example:
|
21
|
+
```ruby
|
22
|
+
require 'neuronet'
|
23
|
+
include Neuronet
|
24
|
+
|
25
|
+
ff = FeedForward.new([3,3])
|
26
|
+
# It can mirror, equivalent to "copy":
|
27
|
+
ff.last.mirror
|
28
|
+
values = ff * [-1, 0, 1]
|
29
|
+
values.map { '%.13g' % _1 } #=> ["-1", "0", "1"]
|
30
|
+
# It can anti-mirror, equivalent to "not":
|
31
|
+
ff.last.mirror(-1)
|
32
|
+
values = ff * [-1, 0, 1]
|
33
|
+
values.map { '%.13g' % _1 } #=> ["1", "0", "-1"]
|
34
|
+
|
35
|
+
# It can "and";
|
36
|
+
ff = FeedForward.new([2,2,1])
|
37
|
+
ff[1].mirror(-1)
|
38
|
+
ff.last.connect(ff.first)
|
39
|
+
ff.last.average
|
40
|
+
# Training "and" pairs:
|
41
|
+
pairs = [
|
42
|
+
[[1, 1], [1]],
|
43
|
+
[[-1, 1], [-1]],
|
44
|
+
[[1, -1], [-1]],
|
45
|
+
[[-1, -1], [-1]],
|
46
|
+
]
|
47
|
+
# Train until values match:
|
48
|
+
ff.pairs(pairs) do
|
49
|
+
pairs.any? { |input, target| (ff * input).map { _1.round(1) } != target }
|
50
|
+
end
|
51
|
+
(ff * [-1, -1]).map{ _1.round } #=> [-1]
|
52
|
+
(ff * [-1, 1]).map{ _1.round } #=> [-1]
|
53
|
+
(ff * [ 1, -1]).map{ _1.round } #=> [-1]
|
54
|
+
(ff * [ 1, 1]).map{ _1.round } #=> [1]
|
55
|
+
|
56
|
+
# It can "or";
|
57
|
+
ff = FeedForward.new([2,2,1])
|
58
|
+
ff[1].mirror(-1)
|
59
|
+
ff.last.connect(ff.first)
|
60
|
+
ff.last.average
|
61
|
+
# Training "or" pairs:
|
62
|
+
pairs = [
|
63
|
+
[[1, 1], [1]],
|
64
|
+
[[-1, 1], [1]],
|
65
|
+
[[1, -1], [1]],
|
66
|
+
[[-1, -1], [-1]],
|
67
|
+
]
|
68
|
+
# Train until values match:
|
69
|
+
ff.pairs(pairs) do
|
70
|
+
pairs.any? { |input, target| (ff * input).map { _1.round(1) } != target }
|
71
|
+
end
|
72
|
+
(ff * [-1, -1]).map{ _1.round } #=> [-1]
|
73
|
+
(ff * [-1, 1]).map{ _1.round } #=> [1]
|
74
|
+
(ff * [ 1, -1]).map{ _1.round } #=> [1]
|
75
|
+
(ff * [ 1, 1]).map{ _1.round } #=> [1]
|
76
|
+
```
|
77
|
+
## CONTENTS:
|
78
|
+
|
79
|
+
* [Neuronet wiki](https://github.com/carlosjhr64/neuronet/wiki)
|
80
|
+
|
81
|
+
### Mju
|
82
|
+
|
83
|
+
Mju is a Marklar which value depends on which Marklar is asked.
|
84
|
+
Other known Marklars are Mu and Kappa.
|
85
|
+
Hope it's not confusing...
|
86
|
+
I tried to give related Marklars the same name.
|
87
|
+

|
88
|
+
|
89
|
+
### Marshal
|
90
|
+
|
91
|
+
Marshal works with Neuronet to save your networks:
|
92
|
+
```ruby
|
93
|
+
dump = Marshal.dump ff
|
94
|
+
ff2 = Marshal.load dump
|
95
|
+
ff2.inspect == ff.inspect #=> true
|
96
|
+
```
|
97
|
+
### Base
|
98
|
+
|
99
|
+
* [Requires and autoloads](lib/neuronet.rb)
|
100
|
+
* [Constants and lambdas](lib/neuronet/constants.rb)
|
101
|
+
* [Connection](lib/neuronet/connection.rb)
|
102
|
+
* [Neuron](lib/neuronet/neuron.rb)
|
103
|
+
* [Layer](lib/neuronet/layer.rb)
|
104
|
+
* [FeedForward](lib/neuronet/feed_forward.rb)
|
105
|
+
|
106
|
+
### Scaled
|
107
|
+
|
108
|
+
* [Scale](lib/neuronet/scale.rb)
|
109
|
+
* [Gaussian](lib/neuronet/gaussian.rb)
|
110
|
+
* [LogNormal](lib/neuronet/log_normal.rb)
|
111
|
+
* [ScaledNetwork](lib/neuronet/scaled_network.rb)
|
112
|
+
|
113
|
+
## LICENSE:
|
114
|
+
|
115
|
+
Copyright (c) 2023 CarlosJHR64
|
116
|
+
|
117
|
+
Permission is hereby granted, free of charge,
|
118
|
+
to any person obtaining a copy of this software and
|
119
|
+
associated documentation files (the "Software"),
|
120
|
+
to deal in the Software without restriction,
|
121
|
+
including without limitation the rights
|
122
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
123
|
+
copies of the Software, and
|
124
|
+
to permit persons to whom the Software is furnished to do so,
|
125
|
+
subject to the following conditions:
|
126
|
+
|
127
|
+
The above copyright notice and this permission notice
|
128
|
+
shall be included in all copies or substantial portions of the Software.
|
129
|
+
|
130
|
+
THE SOFTWARE IS PROVIDED "AS IS",
|
131
|
+
WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
132
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
133
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
134
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
135
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
136
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
137
|
+
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|