mirlo 0.0.1

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.
@@ -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