mirlo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|