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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +1 -0
- data/lib/mirlo.rb +33 -0
- data/lib/mirlo/ann/ann.rb +44 -0
- data/lib/mirlo/ann/hidden_layer.rb +11 -0
- data/lib/mirlo/ann/input_layer.rb +23 -0
- data/lib/mirlo/ann/multilayer_perceptron.rb +44 -0
- data/lib/mirlo/ann/neuron_layer.rb +53 -0
- data/lib/mirlo/ann/output_layer.rb +17 -0
- data/lib/mirlo/classifier.rb +37 -0
- data/lib/mirlo/classifiers/perceptron.rb +33 -0
- data/lib/mirlo/dataset.rb +103 -0
- data/lib/mirlo/datasets/and_dataset.rb +13 -0
- data/lib/mirlo/datasets/double_moon_dataset.rb +43 -0
- data/lib/mirlo/datasets/or_dataset.rb +13 -0
- data/lib/mirlo/datasets/xor_dataset.rb +13 -0
- data/lib/mirlo/extensions/matrix.rb +27 -0
- data/lib/mirlo/plotting.rb +30 -0
- data/lib/mirlo/sample.rb +34 -0
- data/lib/mirlo/sample_with_bias.rb +19 -0
- data/lib/mirlo/test_result.rb +49 -0
- data/lib/mirlo/version.rb +3 -0
- data/mirlo.gemspec +26 -0
- data/spec/ann/ann_spec.rb +60 -0
- data/spec/ann/multilayer_percetron_spec.rb +55 -0
- data/spec/ann/neuron_layer_spec.rb +45 -0
- data/spec/classifiers/perceptron_spec.rb +77 -0
- data/spec/dataset_spec.rb +52 -0
- data/spec/datasets/and_dataset_spec.rb +21 -0
- data/spec/datasets/double_moon_dataset_spec.rb +17 -0
- data/spec/extensions/matrix_spec.rb +18 -0
- data/spec/plots/double_moon.dat +100 -0
- data/spec/plotting_spec.rb +9 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/test_result_spec.rb +30 -0
- metadata +150 -0
@@ -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
|
data/lib/mirlo/sample.rb
ADDED
@@ -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
|
data/mirlo.gemspec
ADDED
@@ -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
|