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,13 @@
1
+ module Mirlo
2
+ class AndDataSet < Dataset
3
+ def initialize
4
+ @feature_names = ['x', 'y']
5
+ @title = "Logical AND dataset"
6
+
7
+ samples = [[0,0], [0,1], [1,0], [1,1]]
8
+ targets = [ZERO, ZERO, ZERO, ONE]
9
+
10
+ super(samples: samples, targets: targets)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ module Mirlo
2
+ class DoubleMoonDataSet < Dataset
3
+ attr_reader :radius, :width, :distance
4
+
5
+ DEFAULT_RADIUS = 10
6
+ DEFAULT_WIDTH = 6
7
+ DEFAULT_DISTANCE = 2
8
+
9
+ UPPER_MOON = [1]
10
+ LOWER_MOON = [0]
11
+
12
+ def initialize(n_points: 500, radius: DEFAULT_RADIUS, width: DEFAULT_WIDTH, distance: DEFAULT_DISTANCE)
13
+ feature_names = ['x', 'y']
14
+ title = "Double Moon Dataset with radius:=#{radius}, width:=#{width}, distance:=#{distance}"
15
+ @radius, @width, @distance = radius, width, distance
16
+ samples = n_points.times.collect { random_point }
17
+
18
+ labels = {
19
+ UPPER_MOON => 'Upper moon',
20
+ LOWER_MOON => 'Lower moon'
21
+ }
22
+
23
+ super(samples: samples, feature_names: feature_names, title: title, labels: labels)
24
+ end
25
+
26
+ def random_point
27
+ angle_coord = rand * Math::PI
28
+ radial_coord = radius + width * rand(-0.5..0.5)
29
+
30
+ target = rand(2) == 1 ? UPPER_MOON : LOWER_MOON
31
+
32
+ if target == UPPER_MOON
33
+ x = radial_coord * Math.cos(angle_coord)
34
+ y = radial_coord * Math.sin(angle_coord)
35
+ else
36
+ x = radial_coord * Math.cos(angle_coord) + radius
37
+ y = - radial_coord * Math.sin(angle_coord) - distance
38
+ end
39
+
40
+ SampleWithBias.new(target: target, features: [x, y])
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ module Mirlo
2
+ class OrDataSet < Dataset
3
+ def initialize
4
+ @feature_names = ['x', 'y']
5
+ @title = "Logical OR dataset"
6
+
7
+ samples = [[0,0], [0,1], [1,0], [1,1]]
8
+ targets = [ZERO, ONE, ONE, ONE]
9
+
10
+ super(samples: samples, targets: targets)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Mirlo
2
+ class XorDataSet < Mirlo::Dataset
3
+ def initialize
4
+ @feature_names = ['x', 'y']
5
+ @title = "Logical XOR dataset"
6
+
7
+ samples = [[0,0], [0,1], [1,0], [1,1]]
8
+ targets = [ZERO, ONE, ONE, ZERO]
9
+
10
+ super(samples: samples, targets: targets)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require 'matrix'
2
+
3
+ class Matrix
4
+
5
+ def shape
6
+ [row_count, column_count]
7
+ end
8
+
9
+ #
10
+ # Public: given two matrices of equal dimensions, apply an operation elementwise.
11
+ #
12
+ # Returns a new matrix with the results of the operation.
13
+ #
14
+ def apply_elementwise(other, &op)
15
+ unless shape == other.shape
16
+ raise ArgumentError.new 'To perform an element wise operation, matrices must be of the same dimension.'
17
+ end
18
+
19
+ new_rows = row_count.times.collect do |row|
20
+ column_count.times.collect do |column|
21
+ op.call(self[row, column], other[row, column])
22
+ end
23
+ end
24
+
25
+ Matrix.rows(new_rows)
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ module Mirlo
2
+ module Plotting
3
+ def plot(x_feature = nil, y_feature = nil)
4
+ Gnuplot.open do |gp|
5
+ Gnuplot::Plot.new(gp) do |plot|
6
+ plot.title title
7
+ plot.xlabel 'x'
8
+ plot.ylabel 'y'
9
+
10
+ plot.data = to_gnu_plot_datasets
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def to_gnu_plot_datasets
18
+ target_set.each_with_index.collect do |target, i|
19
+ subset = subset_with_target(target)
20
+ x = subset.feature(0)
21
+ y = subset.feature(1)
22
+
23
+ Gnuplot::DataSet.new([x, y]) do |ds|
24
+ ds.title = label_for(target)
25
+ ds.with = "points ls #{i+1} lc rgb \"red\""
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ class Mirlo::Sample
2
+ attr_reader :target, :features
3
+
4
+ def initialize(target: [], features: [])
5
+ @target = target.is_a?(Array) ? target : [target]
6
+ @features = features
7
+ end
8
+
9
+ def [](index)
10
+ @features[index]
11
+ end
12
+
13
+ def has_features?(some_features)
14
+ features == some_features
15
+ end
16
+
17
+ def feature_size
18
+ features.size
19
+ end
20
+
21
+ def target_size
22
+ target.size
23
+ end
24
+
25
+ def biased?
26
+ false
27
+ end
28
+
29
+ def ==(other_sample)
30
+ target == other_sample.target &&
31
+ features == other_sample.features &&
32
+ biased? == other.biased?
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ class Mirlo::SampleWithBias < Mirlo::Sample
2
+
3
+ def initialize(target: [], features: [])
4
+ super(target: target)
5
+ @features = features.dup.unshift(-1)
6
+ end
7
+
8
+ def [](index)
9
+ super(index+1)
10
+ end
11
+
12
+ def has_features?(some_features)
13
+ features == some_features.dup.unshift(-1)
14
+ end
15
+
16
+ def biased?
17
+ true
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ class Mirlo::TestResult
2
+ attr_reader :n_samples
3
+
4
+ def initialize(possible_classes = [])
5
+ @possible_classes = possible_classes
6
+ @confusion_matrix = Hash.new { 0 }
7
+ @n_samples = 0
8
+ end
9
+
10
+ def add(sample, prediction)
11
+ @possible_classes << sample.target unless @possible_classes.include?(sample.target)
12
+ @confusion_matrix[[sample.target, prediction]] += 1
13
+ @n_samples += 1
14
+ end
15
+
16
+ def confusion_matrix(expected, prediction)
17
+ @confusion_matrix[[expected, prediction]]
18
+ end
19
+
20
+ def mean_squared_error
21
+ errors = @confusion_matrix.collect do |results, times|
22
+ expected, prediction = results
23
+ error_for(expected, prediction, times)
24
+ end
25
+
26
+ errors.inject(:+)
27
+ end
28
+
29
+ def n_errors
30
+ errors = @confusion_matrix.select do |results, times|
31
+ expected, prediction = results
32
+ expected != prediction
33
+ end
34
+
35
+ errors.collect { |results, times| times }.inject(:+)
36
+ end
37
+
38
+ def error_percentage
39
+ n_errors.to_f/n_samples
40
+ end
41
+
42
+ private
43
+
44
+ def error_for(expected, prediction, times)
45
+ diffs = expected.each_with_index.collect { |expected_val, i| expected_val - prediction[i] }
46
+ squared_errors = diffs.collect { |diff| diff ** 2 }
47
+ squared_errors.inject(:+) * times
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module Mirlo
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mirlo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mirlo"
8
+ spec.version = Mirlo::VERSION
9
+ spec.authors = ["Alberto F. Capel"]
10
+ spec.email = ["afcapel@gmail.com"]
11
+ spec.description = %q{Machine Learning experiments}
12
+ spec.summary = %q{Implementation of some Machine Learning algorithms}
13
+ spec.homepage = "https://github.com/afcapel/mirlo"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+
25
+ spec.add_dependency "gnuplot"
26
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ANN DSL' do
4
+ before :each do
5
+ @ann = Mirlo::ANN.build do
6
+ learning_rate 0.25
7
+
8
+ input_layer 3
9
+ hidden_layer 3
10
+ hidden_layer 2
11
+ output_layer 3
12
+ end
13
+ end
14
+
15
+ it "should build a multilayer perceptron" do
16
+ expect(@ann).to be_kind_of(Mirlo::MultilayerPerceptron)
17
+ end
18
+
19
+ it "can set the learning rate of the neural network" do
20
+ expect(@ann.learning_rate).to eq 0.25
21
+ end
22
+
23
+ it "can set the number of inputs on the input layer" do
24
+ expect(@ann.input_layer.size).to eq 4 # 3 inputs plus the bias
25
+ end
26
+
27
+ it "can define hidden layers on the network" do
28
+ expect(@ann.hidden_layers).to be_kind_of(Array)
29
+ expect(@ann.hidden_layers.size).to eq 2
30
+
31
+ expect(@ann.hidden_layers[0].size).to eq 3
32
+ expect(@ann.hidden_layers[1].size).to eq 2
33
+ end
34
+
35
+ it "set the connections between layers" do
36
+ expect(@ann.layers[0].next_layer).to eq @ann.layers[1]
37
+ expect(@ann.layers[1].next_layer).to eq @ann.layers[2]
38
+ expect(@ann.layers[2].next_layer).to eq @ann.layers[3]
39
+
40
+ expect(@ann.layers[1].previous_layer).to eq @ann.layers[0]
41
+ expect(@ann.layers[2].previous_layer).to eq @ann.layers[1]
42
+ expect(@ann.layers[3].previous_layer).to eq @ann.layers[2]
43
+ end
44
+
45
+ it "can set the number of outputs on the output layer" do
46
+ expect(@ann.output_layer.size).to eq 3
47
+ end
48
+
49
+ it "defines the weight matrices between layers" do
50
+ weights1 = @ann.layers[1].weights
51
+ expect(weights1.row_count).to eq 4 # 3 inputs plus the bias
52
+ expect(weights1.column_count).to eq 3
53
+
54
+
55
+
56
+ weights2 = @ann.layers[2].weights
57
+ expect(weights2.row_count).to eq 3
58
+ expect(weights2.column_count).to eq 2
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mirlo::MultilayerPerceptron do
4
+
5
+ let(:mlp) do
6
+ Mirlo::ANN.build do
7
+ input_layer 2
8
+ hidden_layer 3
9
+ output_layer 1
10
+ end
11
+ end
12
+
13
+ it "can classify all data points of the OR logical function" do
14
+ data_set = Mirlo::OrDataSet.new
15
+
16
+ mlp.train_until(data_set, max_error: 0.0, max_iterations: 50_000)
17
+
18
+ expect(mlp.classify([0,0])).to eq [0]
19
+ expect(mlp.classify([0,1])).to eq [1]
20
+ expect(mlp.classify([1,0])).to eq [1]
21
+ expect(mlp.classify([1,1])).to eq [1]
22
+
23
+ test_result = mlp.test_with(data_set)
24
+ expect(test_result.mean_squared_error).to eq 0.0
25
+ end
26
+
27
+ it "can classify all data points of the AND logical function" do
28
+ data_set = Mirlo::AndDataSet.new
29
+
30
+ mlp.train_until(data_set, max_error: 0.0, max_iterations: 50_000)
31
+
32
+ expect(mlp.classify([0,0])).to eq [0]
33
+ expect(mlp.classify([0,1])).to eq [0]
34
+ expect(mlp.classify([1,0])).to eq [0]
35
+ expect(mlp.classify([1,1])).to eq [1]
36
+
37
+ test_result = mlp.test_with(data_set)
38
+ expect(test_result.mean_squared_error).to eq 0.0
39
+ end
40
+
41
+ it "can classify all data points of the XOR logical function" do
42
+ data_set = Mirlo::XorDataSet.new
43
+
44
+ mlp.train_until(data_set, max_error: 0.0, max_iterations: 50_000)
45
+
46
+ expect(mlp.classify([0,0])).to eq [0]
47
+ expect(mlp.classify([0,1])).to eq [1]
48
+ expect(mlp.classify([1,0])).to eq [1]
49
+ expect(mlp.classify([1,1])).to eq [0]
50
+
51
+ test_result = mlp.test_with(data_set)
52
+ expect(test_result.mean_squared_error).to eq 0.0
53
+ end
54
+
55
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mirlo::NeuronLayer do
4
+ let(:previous_layer) do
5
+ input_layer = Mirlo::InputLayer.new(2)
6
+ input_layer.input = [0.5, 1] # A first -1 bias component will be added
7
+ input_layer
8
+ end
9
+
10
+ let(:hidden_layer) do
11
+ hidden_layer = Mirlo::NeuronLayer.new(2)
12
+ hidden_layer.previous_layer = previous_layer
13
+ hidden_layer
14
+ end
15
+
16
+ it "has a matrix of weights" do
17
+ expect(hidden_layer.weights.shape).to eq [3, 2]
18
+ end
19
+
20
+ context "with given weights" do
21
+ before :each do
22
+ hidden_layer.build_weight_function = -> { 0.5 }
23
+ end
24
+
25
+ it "allows to set a function to build the weights matrix" do
26
+ hidden_layer.weights.each do |elm|
27
+ expect(elm).to eq 0.5
28
+ end
29
+ end
30
+
31
+ it "can calculate the total input for each neuron" do
32
+ total_inputs = hidden_layer.inputs_matrix.row(0)
33
+
34
+ expect(total_inputs[0]).to be_within(0.00001).of 0.25
35
+ expect(total_inputs[1]).to be_within(0.00001).of 0.25
36
+ end
37
+
38
+ it "can calculate the activation of each neuron" do
39
+ activations = hidden_layer.activation_matrix.row(0)
40
+
41
+ expect(activations[0]).to be_within(0.00001).of 1.0/(1 + Math.exp(-0.25))
42
+ expect(activations[1]).to be_within(0.00001).of 1.0/(1 + Math.exp(-0.25))
43
+ end
44
+ end
45
+ end