mirlo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d99924e52d6896e3346c45a30fb2c6d202ab2e63
4
+ data.tar.gz: 7e455ce32eb67d8ac4b1c239b5f87e0302ee4608
5
+ SHA512:
6
+ metadata.gz: c1c3e145df40a265b77e28b4f466dff0b7f53d16efb432dc28c7b1184051a43be8cc6437c1019bc54606eddc99f8d46072ba11509073f82cd41a0b92f3f8dfdf
7
+ data.tar.gz: 6e6805bd9b68c22fe8b48b85dc19b671e19db0522a5359968424f7dc7a887daa5fa40fe0f7514306011a921b2dd8f1ba7b5e7aaac87ea65ac657f4b11726f89a
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .DS_Store
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mirlo.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'debugger'
8
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alberto F. Capel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Mirlo
2
+
3
+ Some Machine Learning algorithms implemented in Ruby.
4
+
5
+ Currently implemented:
6
+
7
+ * Perceptron
8
+ * Multilayer Perceptron. Batch update of neuron weights with momentum.
9
+
10
+
11
+ ## Example
12
+
13
+ ```ruby
14
+
15
+ mlp = Mirlo::ANN.build do
16
+ input_layer 2
17
+ hidden_layer 3
18
+ output_layer 1
19
+ end
20
+ # => #<Mirlo::MultilayerPerceptron:0x007fa0e997eff0 ...>
21
+
22
+ data_set = Mirlo::XorDataSet.new
23
+ # => #<Mirlo::XorDataSet:0x007fa0e9995430 ...>
24
+
25
+ mlp.train_until(data_set, max_error: 0.0)
26
+
27
+ mlp.classify([0,0])
28
+ # => [0]
29
+
30
+ mlp.classify([1,0])
31
+ # => [1]
32
+
33
+ mlp.classify([0,1])
34
+ # => [1]
35
+
36
+ mlp.classify([1,1])
37
+ # => [0]
38
+
39
+ ```
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,33 @@
1
+ module Mirlo
2
+ ZERO = [0]
3
+ ONE = [1]
4
+ POSITIVE = ONE
5
+ NEGATIVE = [-1]
6
+
7
+ DEFAULT_LEARNING_RATE = 0.05
8
+ DEFAULT_N_ITERATIONS = 1000
9
+
10
+ require "gnuplot"
11
+ require_relative "mirlo/version"
12
+ require_relative "mirlo/plotting"
13
+ require_relative "mirlo/extensions/matrix"
14
+ require_relative "mirlo/sample"
15
+ require_relative "mirlo/sample_with_bias"
16
+ require_relative "mirlo/dataset"
17
+ require_relative "mirlo/test_result"
18
+ require_relative "mirlo/classifier"
19
+ require_relative "mirlo/ann/input_layer"
20
+ require_relative "mirlo/ann/neuron_layer"
21
+ require_relative "mirlo/ann/hidden_layer"
22
+ require_relative "mirlo/ann/output_layer"
23
+ require_relative "mirlo/ann/multilayer_perceptron"
24
+ require_relative "mirlo/ann/ann"
25
+
26
+ Dir.glob(File.expand_path('./mirlo/classifiers/*.rb', File.dirname(__FILE__))).each do |f|
27
+ require f
28
+ end
29
+
30
+ Dir.glob(File.expand_path('./mirlo/datasets/*.rb', File.dirname(__FILE__))).each do |f|
31
+ require f
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ class Mirlo::ANN
2
+ attr_reader :ann
3
+
4
+ def self.build(*args, &block)
5
+ instance = new(*args)
6
+ instance.instance_eval(&block)
7
+ instance.ann
8
+ end
9
+
10
+ def initialize(*args)
11
+ @ann = Mirlo::MultilayerPerceptron.new(*args)
12
+ end
13
+
14
+ def learning_rate(l_rate)
15
+ @ann.learning_rate = l_rate
16
+ end
17
+
18
+ def input_layer(n_inputs)
19
+ @ann.input_layer = Mirlo::InputLayer.new(n_inputs)
20
+ end
21
+
22
+ def hidden_layer(n_neurons)
23
+ hidden_layer = Mirlo::HiddenLayer.new(n_neurons)
24
+
25
+ connect_with_last_layer(hidden_layer)
26
+
27
+ @ann.hidden_layers << hidden_layer
28
+ end
29
+
30
+ def output_layer(n_outputs)
31
+ output_layer = Mirlo::OutputLayer.new(n_outputs)
32
+
33
+ connect_with_last_layer(output_layer)
34
+
35
+ @ann.output_layer = output_layer
36
+ end
37
+
38
+ def connect_with_last_layer(layer)
39
+ previous_layer = @ann.layers.last
40
+
41
+ previous_layer.next_layer = layer
42
+ layer.previous_layer = previous_layer
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ class Mirlo::HiddenLayer < Mirlo::NeuronLayer
2
+ attr_accessor :next_layer, :errors
3
+
4
+ def calculate_errors
5
+ error_signal = next_layer.errors * next_layer.weights.transpose
6
+
7
+ @errors = @activations.apply_elementwise error_signal do |activation, delta|
8
+ activation * (1.0 - activation) * delta
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module Mirlo
2
+ class InputLayer
3
+ attr_accessor :next_layer, :inputs
4
+
5
+ def initialize(input_size)
6
+ @input_size = input_size
7
+ @inputs = Matrix.zero(1, input_size)
8
+ end
9
+
10
+ def size
11
+ @input_size + 1
12
+ end
13
+
14
+ def input=(input)
15
+ sample = input.is_a?(Mirlo::Sample) ? input : Mirlo::SampleWithBias.new(features: input)
16
+ @inputs = Matrix.row_vector(sample.features)
17
+ end
18
+
19
+ def activation_matrix
20
+ @inputs
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ module Mirlo
2
+ class MultilayerPerceptron < Classifier
3
+
4
+ attr_accessor :learning_rate, :momentum, :input_layer, :hidden_layers, :output_layer
5
+
6
+ def initialize(learning_rate: DEFAULT_LEARNING_RATE, momentum: 0.9)
7
+ @learning_rate, @momentum = learning_rate, momentum
8
+ @hidden_layers = []
9
+ end
10
+
11
+ def iterate
12
+ # train_set.shuffle!
13
+
14
+ input_layer.inputs = train_set.input_matrix
15
+ output_layer.expected_targets = train_set.target_matrix
16
+
17
+ move_forward
18
+ move_backward
19
+ end
20
+
21
+ def move_forward(inputs = nil)
22
+ hidden_layers.each(&:calculate_activations)
23
+ output_layer.calculate_activations
24
+ end
25
+
26
+ def move_backward
27
+ output_layer.calculate_errors
28
+ hidden_layers.reverse.each { |layer| layer.calculate_errors }
29
+
30
+ output_layer.update_weights(learning_rate, momentum)
31
+ hidden_layers.reverse.each { |layer| layer.update_weights(learning_rate, momentum) }
32
+ end
33
+
34
+ def classify(input)
35
+ input_layer.input = input
36
+ move_forward
37
+ output_layer.outputs.first.collect(&:round)
38
+ end
39
+
40
+ def layers
41
+ [input_layer, hidden_layers, output_layer].flatten.compact
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ class Mirlo::NeuronLayer
2
+ attr_accessor :previous_layer, :size, :activations, :errors, :build_weight_function
3
+
4
+ def initialize(size)
5
+ @size = size
6
+ @errors = Array.new(size, 0)
7
+ end
8
+
9
+ def inputs_matrix
10
+ # debugger
11
+ previous_layer.activation_matrix * weights
12
+ end
13
+
14
+ def calculate_activations
15
+ @activations = activation_matrix
16
+ end
17
+
18
+ def activation_matrix
19
+ inputs_matrix.collect { |i| activation_function(i) }
20
+ end
21
+
22
+ def error_matrix
23
+ Matrix.row_vector(@errors)
24
+ end
25
+
26
+ def activation_function(input)
27
+ 1.0/(1 + Math.exp(-input))
28
+ end
29
+
30
+ def weights
31
+ @weights ||= Matrix.build(previous_layer.size, size) { build_weight }
32
+ end
33
+
34
+ def update_weights(learning_rate, momentum = 0)
35
+ has_momentum = @weights_update && momentum > 0
36
+
37
+ if has_momentum
38
+ momentum_matrix = @weights_update.collect { |u| u * momentum }
39
+ end
40
+
41
+ @weights_update = learning_rate * (previous_layer.activation_matrix.transpose * errors)
42
+
43
+ if has_momentum
44
+ @weights_update = @weights_update + momentum_matrix
45
+ end
46
+
47
+ @weights = @weights + @weights_update
48
+ end
49
+
50
+ def build_weight
51
+ @build_weight_function ? @build_weight_function.call : rand(-0.5..0.5)
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ class Mirlo::OutputLayer < Mirlo::NeuronLayer
2
+ attr_accessor :errors, :previous_layer
3
+
4
+ def expected_targets=(target_matrix)
5
+ @expected_targets = target_matrix
6
+ end
7
+
8
+ def outputs
9
+ @activations.row_vectors.collect(&:to_a)
10
+ end
11
+
12
+ def calculate_errors
13
+ num_samples = @expected_targets.row_count
14
+
15
+ @errors = (@expected_targets - @activations).collect { |elm| elm/num_samples }
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ module Mirlo
2
+ class ClassifyError < StandardError; end
3
+
4
+ class Classifier
5
+ attr_accessor :train_set
6
+
7
+ def train(train_set, n_iterations = Mirlo::DEFAULT_N_ITERATIONS)
8
+ @train_set = train_set
9
+
10
+ n_iterations.times { |i| iterate }
11
+ end
12
+
13
+ def train_until(train_set, max_error: 0.01, max_iterations: Mirlo::DEFAULT_N_ITERATIONS)
14
+ @train_set = train_set
15
+
16
+ max_iterations.times do |i|
17
+ iterate
18
+ test_result = test_with(train_set)
19
+
20
+ break if test_result.mean_squared_error <= max_error
21
+
22
+ if i == max_iterations - 1
23
+ raise ClassifyError.new("Could not reach a standard error of #{max_error} after #{max_iterations} iterations")
24
+ end
25
+ end
26
+ end
27
+
28
+ def test_with(test_set)
29
+ TestResult.new.tap do |tr|
30
+ test_set.samples.each do |sample|
31
+ prediction = classify(sample)
32
+ tr.add(sample, prediction)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ module Mirlo
2
+ class Perceptron < Classifier
3
+ attr_accessor :learning_rate
4
+
5
+ def initialize(learning_rate = DEFAULT_LEARNING_RATE)
6
+ @learning_rate = learning_rate
7
+ end
8
+
9
+ def activations(inputs = train_set.input_matrix)
10
+ (inputs * weights).collect { |v| v > 0 ? 1 : 0 }
11
+ end
12
+
13
+ def classify(input)
14
+ input = SampleWithBias.new(features: input) unless input.is_a?(Mirlo::Sample)
15
+
16
+ input_vector = Matrix[input.features]
17
+ result = (input_vector * weights).row(0).to_a
18
+ result.collect { |v| v > 0 ? 1 : 0 }
19
+ end
20
+
21
+ def weight_updates
22
+ train_set.input_matrix.transpose * (train_set.target_matrix - activations)
23
+ end
24
+
25
+ def iterate
26
+ @weights = weights + learning_rate * weight_updates
27
+ end
28
+
29
+ def weights
30
+ @weights ||= Matrix.build(train_set.num_features, train_set.num_outputs) { rand(-0.05..0.05) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,103 @@
1
+ module Mirlo
2
+
3
+ # Public: Dataset class to store a set of samples with their associated targets.
4
+ #
5
+ class Dataset
6
+ include Enumerable
7
+ include Plotting
8
+
9
+ DEFAULT_LABELS = {
10
+ [0] => 'Zero',
11
+ [1] => 'Positive',
12
+ [-1] => 'Negative'
13
+ }
14
+
15
+ attr_reader :title, :samples, :feature_names
16
+
17
+ def initialize(samples: [], targets: nil, feature_names: [], title: "Dataset", add_bias: true, labels: DEFAULT_LABELS)
18
+ @feature_names ||= feature_names
19
+ @title ||= title
20
+ @labels ||= labels
21
+
22
+ @samples = if targets.nil?
23
+ samples
24
+ else
25
+ build_from_samples_and_targets(samples, targets)
26
+ end
27
+ end
28
+
29
+ def feature(feature_name_or_index)
30
+ index = if feature_names.include?(feature_name_or_index)
31
+ feature_names.index(feature_name_or_index)
32
+ else
33
+ feature_name_or_index
34
+ end
35
+
36
+ samples.collect { |sample| sample[index] }
37
+ end
38
+
39
+ def subset_with_target(target)
40
+ matching_samples = samples.find_all { |s| s.target == target }
41
+ Dataset.new(samples: matching_samples, feature_names: feature_names, title: target)
42
+ end
43
+
44
+ def targets_for(feature_values)
45
+ samples.select { |s| s.has_features?(feature_values) }.collect(&:target)
46
+ end
47
+
48
+ def label_for(val)
49
+ @labels[val] || val
50
+ end
51
+
52
+ def target_set
53
+ targets.uniq.sort
54
+ end
55
+
56
+ def targets
57
+ samples.collect(&:target)
58
+ end
59
+
60
+ def size
61
+ @samples.size
62
+ end
63
+
64
+ def each(*args, &block)
65
+ @samples.each(*args, &block)
66
+ end
67
+
68
+ def num_features
69
+ @num_features ||= samples.first.feature_size
70
+ end
71
+
72
+ def num_outputs
73
+ @num_outputs ||= samples.first.target_size
74
+ end
75
+
76
+ def input_matrix
77
+ @input_matrix ||= Matrix.rows(samples.collect(&:features), false)
78
+ end
79
+
80
+ def target_matrix
81
+ @target_matrix ||= Matrix.rows(samples.collect(&:target), false)
82
+ end
83
+
84
+ def shuffle!
85
+ @input_matrix = @target_matrix = nil
86
+
87
+ shuffled_positions = (0..size-1).to_a.shuffle
88
+
89
+ shuffled_samples = shuffled_positions.collect { |i| samples[i] }
90
+ shuffled_targets = shuffled_positions.collect { |i| targets[i] }
91
+
92
+ @samples, @targets = shuffled_samples, shuffled_targets
93
+ end
94
+
95
+ private
96
+
97
+ def build_from_samples_and_targets(samples, targets)
98
+ samples.each_with_index.collect do |sample, index|
99
+ SampleWithBias.new(target: targets[index], features: sample)
100
+ end
101
+ end
102
+ end
103
+ end