stamina 0.3.0 → 0.3.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.
@@ -1,3 +1,18 @@
1
+ # 0.3.1 / 2011-03-24
2
+
3
+ * Major Enhancements
4
+
5
+ * Implemented the decoration algorithm of Damas10, allowing to decorate states
6
+ with information propagated from states to states until a fixpoint is reached.
7
+ * Added Automaton::Metrics module, automatically included, with useful metrics
8
+ like automaton depth, accepting ratio and so on.
9
+ * Added Scoring module and Classifier#classification_scoring(sample) method
10
+ with common measures from information retrieval.
11
+
12
+ * On the devel side
13
+
14
+ * Moved specific automaton tests under test/stamina/automaton/...
15
+
1
16
  # 0.3.0 / 2011-03-24
2
17
 
3
18
  * On the devel side
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stamina (0.3.0)
4
+ stamina (0.3.1)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -13,6 +13,8 @@ require 'stamina/sample'
13
13
  require 'stamina/input_string'
14
14
  require 'stamina/classifier'
15
15
  require 'stamina/automaton'
16
+ require 'stamina/scoring'
17
+ require 'stamina/utils'
16
18
  require 'stamina/induction/union_find'
17
19
  require 'stamina/induction/commons'
18
20
  require "stamina/induction/rpni"
@@ -1234,4 +1234,5 @@ module Stamina
1234
1234
  end # class Automaton
1235
1235
 
1236
1236
  end # module Stamina
1237
- require 'stamina/automaton/walking'
1237
+ require 'stamina/automaton/walking'
1238
+ require 'stamina/automaton/metrics'
@@ -0,0 +1,71 @@
1
+ require 'stamina/utils/decorate'
2
+ module Stamina
3
+ class Automaton
4
+ #
5
+ # Provides useful metric methods on automata.
6
+ #
7
+ # This module is automatically included by Automaton and is not intended
8
+ # to be used directly.
9
+ #
10
+ module Metrics
11
+
12
+ #
13
+ # Returns the number of letters of the alphabet.
14
+ #
15
+ def alphabet_size
16
+ alphabet.size
17
+ end
18
+
19
+ #
20
+ # Returns the average degree of states, that is,
21
+ # <code>edge_count/state_count</code>
22
+ #
23
+ def avg_degree
24
+ edge_count.to_f/state_count.to_f
25
+ end
26
+ alias :avg_out_degree :avg_degree
27
+ alias :avg_in_degree :avg_degree
28
+
29
+ #
30
+ # Number of accepting states over all states
31
+ #
32
+ def accepting_ratio
33
+ states.select{|s|s.accepting?}.size.to_f/state_count.to_f
34
+ end
35
+
36
+ #
37
+ # Number of error states over all states
38
+ #
39
+ def error_ratio
40
+ states.select{|s|s.error?}.size.to_f/state_count.to_f
41
+ end
42
+
43
+ #
44
+ # Computes the depth of the automaton.
45
+ #
46
+ # The depth of an automaton is defined as the length of the longest shortest
47
+ # path from the initial state to a state.
48
+ #
49
+ # This method has a side effect on state marks, as it keeps the depth of
50
+ # each state as a mark under _key_, which defaults to :depth.
51
+ #
52
+ def depth(key = :depth)
53
+ algo = Stamina::Utils::Decorate.new(key)
54
+ algo.set_suppremum do |d0,d1|
55
+ if d0.nil?
56
+ d1
57
+ elsif d1.nil?
58
+ d0
59
+ else
60
+ (d0 <= d1 ? d0 : d1)
61
+ end
62
+ end
63
+ algo.set_propagate {|d,e| d+1 }
64
+ algo.execute(self, nil, 0)
65
+ states.max{|s0,s1| s0[:depth] <=> s1[:depth]}[:depth]
66
+ end
67
+
68
+ end # module Metrics
69
+ include Metrics
70
+ end # class Automaton
71
+ end # module Stamina
@@ -20,6 +20,21 @@ module Stamina
20
20
  end
21
21
  signature
22
22
  end
23
+ alias :classification_signature :signature
24
+
25
+ #
26
+ # Classifies a sample then compute the classification scoring that is obtained
27
+ # by comparing the signature obtained by classification and the one of the sample
28
+ # itself. Returns an object responding to methods defined in Scoring module.
29
+ #
30
+ # This method is actually a convenient shortcut for:
31
+ #
32
+ # Stamina::Scoring.scoring(signature(sample), sample.signature)
33
+ #
34
+ def scoring(sample)
35
+ Stamina::Scoring.scoring(signature(sample), sample.signature)
36
+ end
37
+ alias :classification_scoring :scoring
23
38
 
24
39
  #
25
40
  # Checks if a labeled sample is correctly classified by the classifier.
@@ -34,4 +49,4 @@ module Stamina
34
49
  end
35
50
 
36
51
  end # module Classifier
37
- end # module Stamina
52
+ end # module Stamina
@@ -0,0 +1,176 @@
1
+ module Stamina
2
+ #
3
+ # Provides utility methods for scoring binary classifiers from signatures
4
+ #
5
+ module Scoring
6
+
7
+ #
8
+ # From the signatures of a learned model and a actual, returns an object
9
+ # responding to all instance methods defined in the Scoring module.
10
+ #
11
+ def self.scoring(learned, actual, max_size=nil)
12
+ unless learned.size==actual.size
13
+ raise ArgumentError, "Signatures must be of same size (#{learned.size} vs. #{actual.size})"
14
+ end
15
+ max_size ||= learned.size
16
+ max_size = learned.size if max_size > learned.size
17
+ tp, fn, fp, tn = 0, 0, 0, 0
18
+ (0...max_size).each do |i|
19
+ positive, labeled_as = actual[i..i]=='1', learned[i..i]=='1'
20
+ if positive==labeled_as
21
+ positive ? (tp += 1) : (tn += 1)
22
+ else
23
+ positive ? (fn += 1) : (fp += 1)
24
+ end
25
+ end
26
+ measures = { :true_positive => tp,
27
+ :true_negative => tn,
28
+ :false_positive => fp,
29
+ :false_negative => fn }
30
+ measures.extend(Scoring)
31
+ measures
32
+ end
33
+
34
+ #
35
+ # Returns the number of positive strings correctly labeled as positive
36
+ #
37
+ def true_positive
38
+ self[:true_positive]
39
+ end
40
+
41
+ #
42
+ # Returns the number of negative strings correctly labeled as negative.
43
+ #
44
+ def true_negative
45
+ self[:true_negative]
46
+ end
47
+
48
+ #
49
+ # Returns the number of negative strings incorrectly labeled as positive.
50
+ #
51
+ def false_positive
52
+ self[:false_positive]
53
+ end
54
+
55
+ #
56
+ # Returns the number of positive strings incorrectly labeled as negative.
57
+ #
58
+ def false_negative
59
+ self[:false_negative]
60
+ end
61
+
62
+ #
63
+ # Returns the percentage of positive predictions that are correct
64
+ #
65
+ def precision
66
+ true_positive.to_f/(true_positive + false_positive)
67
+ end
68
+ alias :positive_predictive_value :precision
69
+
70
+ #
71
+ # Returns the percentage of true negative over all negative
72
+ #
73
+ def negative_predictive_value
74
+ true_negative.to_f / (true_negative + false_negative)
75
+ end
76
+
77
+ #
78
+ # Returns the percentage of positive strings that were predicted as being
79
+ # positive
80
+ #
81
+ def recall
82
+ true_positive.to_f / (true_positive + false_negative)
83
+ end
84
+ alias :sensitivity :recall
85
+ alias :true_positive_rate :recall
86
+
87
+ #
88
+ # Returns the percentage of negative strings that were predicted as being
89
+ # negative
90
+ #
91
+ def specificity
92
+ true_negative.to_f / (true_negative + false_positive)
93
+ end
94
+ alias :true_negative_rate :specificity
95
+
96
+ #
97
+ # Returns the percentage of false positives
98
+ #
99
+ def false_positive_rate
100
+ false_positive.to_f / (false_positive + true_negative)
101
+ end
102
+
103
+ #
104
+ # Returns the percentage of false negatives
105
+ #
106
+ def false_negative_rate
107
+ false_negative.to_f / (true_positive + false_negative)
108
+ end
109
+
110
+ #
111
+ # Returns the likelihood that a predicted positive is an actual positive
112
+ #
113
+ def positive_likelihood
114
+ sensitivity / (1.0 - specificity)
115
+ end
116
+
117
+ #
118
+ # Returns the likelihood that a predicted negative is an actual negative
119
+ #
120
+ def negative_likelihood
121
+ (1.0 - sensitivity) / specificity
122
+ end
123
+
124
+ #
125
+ # Returns the percentage of predictions that are correct
126
+ #
127
+ def accuracy
128
+ num = (true_positive + true_negative).to_f
129
+ den = (true_positive + true_negative + false_positive + false_negative)
130
+ num / den
131
+ end
132
+
133
+ #
134
+ # Returns the error rate
135
+ #
136
+ def error_rate
137
+ num = (false_positive + false_negative).to_f
138
+ den = (true_positive + true_negative + false_positive + false_negative)
139
+ num / den
140
+ end
141
+
142
+ #
143
+ # Returns the harmonic mean between precision and recall
144
+ #
145
+ def f_measure
146
+ 2.0 * (precision * recall) / (precision + recall)
147
+ end
148
+
149
+ #
150
+ # Returns the balanced classification rate (arithmetic mean between
151
+ # sensitivity and specificity)
152
+ #
153
+ def balanced_classification_rate
154
+ 0.5 * (sensitivity + specificity)
155
+ end
156
+ alias :bcr :balanced_classification_rate
157
+
158
+ #
159
+ # Returns the balanced error rate (1 - bcr)
160
+ #
161
+ def balanced_error_rate
162
+ 1.0 - balanced_classification_rate
163
+ end
164
+ alias :ber :balanced_error_rate
165
+
166
+ #
167
+ # Returns the harmonic mean between sensitivity and specificity
168
+ #
169
+ def harmonic_balanced_classification_rate
170
+ 2.0 * (sensitivity * specificity) / (sensitivity + specificity)
171
+ end
172
+ alias :hbcr :harmonic_balanced_classification_rate
173
+ alias :harmonic_bcr :harmonic_balanced_classification_rate
174
+
175
+ end # module Scoring
176
+ end # module Stamina
@@ -0,0 +1 @@
1
+ require 'stamina/utils/decorate'
@@ -0,0 +1,81 @@
1
+ module Stamina
2
+ module Utils
3
+ #
4
+ # Decorates states of an automaton by applying a propagation rule
5
+ # until a fix point is reached.
6
+ #
7
+ class Decorate
8
+
9
+ # The key to use to maintain the decoration on states (:invariant
10
+ # is used by default)
11
+ attr_writer :decoration_key
12
+
13
+ # Creates a decoration algorithm instance
14
+ def initialize(decoration_key = :invariant)
15
+ @decoration_key = decoration_key
16
+ @suppremum = nil
17
+ @propagate = nil
18
+ end
19
+
20
+ # Installs a suppremum function through a block.
21
+ def set_suppremum(&block)
22
+ raise ArgumentError, 'Suppremum expected through a block' if block.nil?
23
+ raise ArgumentError, 'Block of arity 2 expected' unless block.arity==2
24
+ @suppremum = block
25
+ end
26
+
27
+ # Installs a propagate function through a block.
28
+ def set_propagate(&block)
29
+ raise ArgumentError, 'Propagate expected through a block' if block.nil?
30
+ raise ArgumentError, 'Block of arity 2 expected' unless block.arity==2
31
+ @propagate = block
32
+ end
33
+
34
+ # Computes the suppremum between two decorations. By default, this method
35
+ # looks for a suppremum function installed with set_suppremum. If not found,
36
+ # it tries calling a suppremum method on d0. If not found it raises an error.
37
+ # This method may be overriden.
38
+ def suppremum(d0, d1)
39
+ return @suppremum.call(d0, d1) if @suppremum
40
+ return d0.suppremum(d1) if d0.respond_to?(:suppremum)
41
+ raise "No suppremum function installed or implemented by decorations"
42
+ end
43
+
44
+ # Computes the propagation rule. By default, this method looks for a propagate
45
+ # function installed with set_propagate. If not found, it tries calling a +
46
+ # method on deco. If not found it raises an error.
47
+ # This method may be overriden.
48
+ def propagate(deco, edge)
49
+ return @propagate.call(deco, edge) if @propagate
50
+ return deco.+(edge) if deco.respond_to?(:+)
51
+ raise "No propagate function installed or implemented by decorations"
52
+ end
53
+
54
+ # Executes the propagation algorithm on a given automaton.
55
+ def execute(fa, bottom, d0)
56
+ # install initial decoration
57
+ fa.states.each do |s|
58
+ s[@decoration_key] = (s.initial? ? d0 : bottom)
59
+ end
60
+
61
+ # fix-point loop starting with initial states
62
+ to_explore = fa.initial_states
63
+ until to_explore.empty?
64
+ source = to_explore.pop
65
+ source.out_edges.each do |edge|
66
+ target = edge.target
67
+ p_decor = propagate(source[@decoration_key], edge)
68
+ p_decor = suppremum(target[@decoration_key], p_decor)
69
+ unless p_decor == target[@decoration_key]
70
+ target[@decoration_key] = p_decor
71
+ to_explore << target unless to_explore.include?(target)
72
+ end
73
+ end
74
+ end
75
+
76
+ fa
77
+ end
78
+
79
+ end # class Decorate
80
+ end # module Utils
81
+ end # module Stamina
@@ -3,7 +3,7 @@ module Stamina
3
3
 
4
4
  MAJOR = 0
5
5
  MINOR = 3
6
- TINY = 0
6
+ TINY = 1
7
7
 
8
8
  def self.to_s
9
9
  [ MAJOR, MINOR, TINY ].join('.')
@@ -9,7 +9,7 @@ variables:
9
9
  upper:
10
10
  Stamina
11
11
  version:
12
- 0.3.0
12
+ 0.3.1
13
13
  summary: |-
14
14
  Automaton and Regular Inference Toolkit
15
15
  description: |-
@@ -150,6 +150,110 @@ module Stamina
150
150
  assert_equal(false, @small_nfa.correctly_classify?(sample))
151
151
  end
152
152
 
153
+ def test_scoring_on_valid_sample
154
+ sample = ADL::parse_sample <<-SAMPLE
155
+ -
156
+ + b
157
+ + b c
158
+ - b c a
159
+ - b c a c
160
+ - b c a c a
161
+ - b c a a
162
+ + b c a b
163
+ + b c a b c a c b
164
+ - z
165
+ - b z
166
+ SAMPLE
167
+ measures = @small_dfa.scoring(sample)
168
+ assert_equal(sample.positive_count, measures.true_positive)
169
+ assert_equal(0, measures.false_positive)
170
+ assert_equal(sample.negative_count, measures.true_negative)
171
+ assert_equal(0, measures.false_negative)
172
+ assert_equal(1.0, measures.precision)
173
+ assert_equal(1.0, measures.recall)
174
+ assert_equal(1.0, measures.sensitivity)
175
+ assert_equal(1.0, measures.specificity)
176
+ assert_equal(1.0, measures.accuracy)
177
+ end
178
+
179
+ def test_scoring_on_invalid_sample
180
+ sample = ADL::parse_sample <<-SAMPLE
181
+ +
182
+ - b
183
+ - b c
184
+ + b c a
185
+ + b c a c
186
+ + b c a c a
187
+ + b c a a
188
+ - b c a b
189
+ - b c a b c a c b
190
+ + z
191
+ + b z
192
+ SAMPLE
193
+ measures = @small_dfa.scoring(sample)
194
+ assert_equal(0.0, measures.true_positive)
195
+ assert_equal(sample.negative_count, measures.false_positive)
196
+ assert_equal(0.0, measures.true_negative)
197
+ assert_equal(sample.positive_count, measures.false_negative)
198
+ assert_equal(0.0, measures.precision)
199
+ assert_equal(0.0, measures.recall)
200
+ assert_equal(0.0, measures.sensitivity)
201
+ assert_equal(0.0, measures.specificity)
202
+ assert_equal(0.0, measures.accuracy)
203
+ end
204
+
205
+ def test_scoring_with_positive_only
206
+ sample = ADL::parse_sample <<-SAMPLE
207
+ +
208
+ + b
209
+ + b c
210
+ + b c a
211
+ + b c a c
212
+ + b c a c a
213
+ + b c a a
214
+ + b c a b
215
+ + b c a b c a c b
216
+ + z
217
+ + b z
218
+ SAMPLE
219
+ measures = @small_dfa.scoring(sample)
220
+ assert_equal(4.0, measures.true_positive)
221
+ assert_equal(sample.size-sample.positive_count, measures.false_positive)
222
+ assert_equal(0, measures.true_negative)
223
+ assert_equal(sample.size-4.0, measures.false_negative)
224
+ assert_equal(1.0, measures.precision)
225
+ assert_equal(4.0/sample.size, measures.recall)
226
+ assert_equal(4.0/sample.size, measures.sensitivity)
227
+ #assert_equal(0.0/0.0, measures.specificity)
228
+ assert_equal(4.0/sample.size, measures.accuracy)
229
+ end
230
+
231
+ def test_scoring_with_negative_only
232
+ sample = ADL::parse_sample <<-SAMPLE
233
+ -
234
+ - b
235
+ - b c
236
+ - b c a
237
+ - b c a c
238
+ - b c a c a
239
+ - b c a a
240
+ - b c a b
241
+ - b c a b c a c b
242
+ - z
243
+ - b z
244
+ SAMPLE
245
+ measures = @small_dfa.scoring(sample)
246
+ assert_equal(0.0, measures.true_positive)
247
+ assert_equal(4.0, measures.false_positive)
248
+ assert_equal(sample.size-4.0, measures.true_negative)
249
+ assert_equal(0.0, measures.false_negative)
250
+ assert_equal(0.0, measures.precision)
251
+ #assert_equal(0.0, measures.recall)
252
+ #assert_equal(0.0, measures.sensitivity)
253
+ assert_equal((sample.size-4.0)/sample.size, measures.specificity)
254
+ assert_equal((sample.size-4.0)/sample.size, measures.accuracy)
255
+ end
256
+
153
257
  end # class ClassifierTest
154
258
  end # class Automaton
155
- end # module Stamina
259
+ end # module Stamina
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require 'stamina/adl'
3
+ require 'stamina/stamina_test'
4
+ module Stamina
5
+ class Automaton
6
+ class MetricsTest < StaminaTest
7
+
8
+ def test_alphabet_size
9
+ assert_equal 3, @small_dfa.alphabet_size
10
+ end
11
+
12
+ def test_avg_degree
13
+ assert_equal 6.to_f/4, @small_dfa.avg_degree
14
+ end
15
+
16
+ def test_avg_out_degree
17
+ assert_equal 6.to_f/4, @small_dfa.avg_out_degree
18
+ end
19
+
20
+ def test_avg_in_degree
21
+ assert_equal 6.to_f/4, @small_dfa.avg_in_degree
22
+ end
23
+
24
+ def test_accepting_ratio
25
+ assert_equal 0.5, @small_dfa.accepting_ratio
26
+ end
27
+
28
+ def test_depth
29
+ assert_equal 3, @small_dfa.depth
30
+ assert_equal 2, @small_nfa.depth
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+
@@ -3,216 +3,213 @@ require 'stamina/errors'
3
3
  require 'stamina/stamina_test'
4
4
  require 'stamina/sample'
5
5
  module Stamina
6
-
7
- # Tests Sample class
8
- class SampleTest < StaminaTest
9
-
10
- # Converts a String to an InputString
11
- def s(str)
12
- Stamina::ADL::parse_string(str)
13
- end
14
-
15
- # Tests Sample#empty?
16
- def test_empty
17
- assert_equal(true, Sample.new.empty?)
18
- assert_equal(true, Sample[].empty?)
19
- assert_equal(false, Sample['?'].empty?)
20
- assert_equal(false, Sample['-'].empty?)
21
- assert_equal(false, Sample['+'].empty?)
22
- assert_equal(false, Sample['+ a b'].empty?)
23
- assert_equal(false, Sample['+ a b', '- a'].empty?)
24
- assert_equal(false, Sample['- a b'].empty?)
25
- end
26
-
27
- # Tests Sample#size
28
- def test_size_and_counts
29
- s = Sample.new
30
- assert_equal(0, s.size)
31
- assert_equal(0, s.positive_count)
32
- assert_equal(0, s.negative_count)
33
- s << '+ a b'
34
- assert_equal(1, s.size)
35
- assert_equal(1, s.positive_count)
36
- assert_equal(0, s.negative_count)
37
- s << '+ a b'
38
- assert_equal(2, s.size)
39
- assert_equal(2, s.positive_count)
40
- assert_equal(0, s.negative_count)
41
- s << '+ a'
42
- assert_equal(3, s.size)
43
- assert_equal(3, s.positive_count)
44
- assert_equal(0, s.negative_count)
45
- s << '- a b c'
46
- assert_equal(4, s.size)
47
- assert_equal(3, s.positive_count)
48
- assert_equal(1, s.negative_count)
49
- end
50
-
51
- def test_same_string_can_be_added_many_times
52
- s = Sample.new
53
- 10.times {|i| s << "+ a b"}
54
- assert_equal(10, s.size)
55
- assert_equal(10, s.positive_count)
56
- assert_equal(0, s.negative_count)
57
- strings = s.collect{|s| s}
58
- assert_equal 10, strings.size
59
- end
60
-
61
- # Tests Sample#<<
62
- def test_append
63
- s = Sample.new
64
- assert_equal(s,s << '+',"Accepts empty string")
65
- assert_equal(s,s << '+ a b a b a',"Accepts positive string")
66
- assert_equal(s,s << '- a',"Accepts negative string")
67
- assert_equal(s,s << '? a',"Accepts unlabeled string")
68
- end
69
-
70
- # Tests Sample#include? on every kind of arguments it announce
71
- def test_append_accepts_arguments_it_annouce
72
- expected = Sample[
73
- '+ a b a b',
74
- '+ a b',
75
- '-',
76
- '- a',
77
- '+ a b a b a b'
78
- ]
79
- s = Sample.new
80
- s << '+ a b a b'
81
- s << ['+ a b', '-']
82
- s << InputString.new('a', false)
83
- s << Sample['+ a b a b a b', '-']
84
- assert_equal(expected,s)
85
- end
86
-
87
- # Tests that Sample#<< detects inconsistencies
88
- # def test_append_detects_inconsistency
89
- # s = Sample.new
90
- # s << '+ a b'
91
- # s << '+ a b a b'
92
- # assert_raise InconsistencyError do
93
- # s << '- a b a b'
94
- # end
95
- # end
96
-
97
- # Tests that Sample#<< detects inconsistencies
98
- def test_append_detects_real_inconsistencies_only
99
- s = Sample.new
100
- s << '+ a b'
101
- s << '+ a b a b'
102
- assert_nothing_raised do
103
- s << '- b'
104
- s << '- a'
105
- s << '- a b a'
6
+ class SampleTest < StaminaTest
7
+
8
+ # Converts a String to an InputString
9
+ def s(str)
10
+ Stamina::ADL::parse_string(str)
11
+ end
12
+
13
+ # Tests Sample#empty?
14
+ def test_empty
15
+ assert_equal(true, Sample.new.empty?)
16
+ assert_equal(true, Sample[].empty?)
17
+ assert_equal(false, Sample['?'].empty?)
18
+ assert_equal(false, Sample['-'].empty?)
19
+ assert_equal(false, Sample['+'].empty?)
20
+ assert_equal(false, Sample['+ a b'].empty?)
21
+ assert_equal(false, Sample['+ a b', '- a'].empty?)
22
+ assert_equal(false, Sample['- a b'].empty?)
23
+ end
24
+
25
+ # Tests Sample#size
26
+ def test_size_and_counts
27
+ s = Sample.new
28
+ assert_equal(0, s.size)
29
+ assert_equal(0, s.positive_count)
30
+ assert_equal(0, s.negative_count)
31
+ s << '+ a b'
32
+ assert_equal(1, s.size)
33
+ assert_equal(1, s.positive_count)
34
+ assert_equal(0, s.negative_count)
35
+ s << '+ a b'
36
+ assert_equal(2, s.size)
37
+ assert_equal(2, s.positive_count)
38
+ assert_equal(0, s.negative_count)
39
+ s << '+ a'
40
+ assert_equal(3, s.size)
41
+ assert_equal(3, s.positive_count)
42
+ assert_equal(0, s.negative_count)
43
+ s << '- a b c'
44
+ assert_equal(4, s.size)
45
+ assert_equal(3, s.positive_count)
46
+ assert_equal(1, s.negative_count)
47
+ end
48
+
49
+ def test_same_string_can_be_added_many_times
50
+ s = Sample.new
51
+ 10.times {|i| s << "+ a b"}
52
+ assert_equal(10, s.size)
53
+ assert_equal(10, s.positive_count)
54
+ assert_equal(0, s.negative_count)
55
+ strings = s.collect{|s| s}
56
+ assert_equal 10, strings.size
57
+ end
58
+
59
+ # Tests Sample#<<
60
+ def test_append
61
+ s = Sample.new
62
+ assert_equal(s,s << '+',"Accepts empty string")
63
+ assert_equal(s,s << '+ a b a b a',"Accepts positive string")
64
+ assert_equal(s,s << '- a',"Accepts negative string")
65
+ assert_equal(s,s << '? a',"Accepts unlabeled string")
66
+ end
67
+
68
+ # Tests Sample#include? on every kind of arguments it announce
69
+ def test_append_accepts_arguments_it_annouce
70
+ expected = Sample[
71
+ '+ a b a b',
72
+ '+ a b',
73
+ '-',
74
+ '- a',
75
+ '+ a b a b a b'
76
+ ]
77
+ s = Sample.new
78
+ s << '+ a b a b'
79
+ s << ['+ a b', '-']
80
+ s << InputString.new('a', false)
81
+ s << Sample['+ a b a b a b', '-']
82
+ assert_equal(expected,s)
106
83
  end
107
- end
108
-
109
- # Tests each
110
- def test_each
111
- strings = ['+ a b a b', '+ a b', '+ a b', '- a', '+']
112
- strings = strings.collect{|s| ADL::parse_string(s)}
113
- s = Sample.new << strings
114
- count = 0
115
- s.each do |str|
116
- assert_equal(true, strings.include?(str))
117
- count += 1
84
+
85
+ # Tests that Sample#<< detects inconsistencies
86
+ # def test_append_detects_inconsistency
87
+ # s = Sample.new
88
+ # s << '+ a b'
89
+ # s << '+ a b a b'
90
+ # assert_raise InconsistencyError do
91
+ # s << '- a b a b'
92
+ # end
93
+ # end
94
+
95
+ # Tests that Sample#<< detects inconsistencies
96
+ def test_append_detects_real_inconsistencies_only
97
+ s = Sample.new
98
+ s << '+ a b'
99
+ s << '+ a b a b'
100
+ assert_nothing_raised do
101
+ s << '- b'
102
+ s << '- a'
103
+ s << '- a b a'
104
+ end
105
+ end
106
+
107
+ # Tests each
108
+ def test_each
109
+ strings = ['+ a b a b', '+ a b', '+ a b', '- a', '+']
110
+ strings = strings.collect{|s| ADL::parse_string(s)}
111
+ s = Sample.new << strings
112
+ count = 0
113
+ s.each do |str|
114
+ assert_equal(true, strings.include?(str))
115
+ count += 1
116
+ end
117
+ assert_equal(strings.size, count)
118
118
  end
119
- assert_equal(strings.size, count)
120
- end
121
-
122
- # Tests each_positive
123
- def test_each_positive
124
- sample = Sample[
125
- '+',
126
- '- b',
127
- '+ a b a b',
128
- '- a b a a'
129
- ]
130
- count = 0
131
- sample.each_positive do |str|
132
- assert str.positive?
133
- count += 1
119
+
120
+ # Tests each_positive
121
+ def test_each_positive
122
+ sample = Sample[
123
+ '+',
124
+ '- b',
125
+ '+ a b a b',
126
+ '- a b a a'
127
+ ]
128
+ count = 0
129
+ sample.each_positive do |str|
130
+ assert str.positive?
131
+ count += 1
132
+ end
133
+ assert_equal 2, count
134
+ positives = sample.positive_enumerator.collect{|s| s}
135
+ assert_equal 2, positives.size
136
+ [s('+'), s('+ a b a b')].each do |str|
137
+ assert positives.include?(str)
138
+ end
134
139
  end
135
- assert_equal 2, count
136
- positives = sample.positive_enumerator.collect{|s| s}
137
- assert_equal 2, positives.size
138
- [s('+'), s('+ a b a b')].each do |str|
139
- assert positives.include?(str)
140
+
141
+ # Tests each_negative
142
+ def test_each_negative
143
+ sample = Sample[
144
+ '+',
145
+ '- b',
146
+ '+ a b a b',
147
+ '- a b a a'
148
+ ]
149
+ count = 0
150
+ sample.each_negative do |str|
151
+ assert str.negative?
152
+ count += 1
153
+ end
154
+ assert_equal 2, count
155
+ negatives = sample.negative_enumerator.collect{|s| s}
156
+ assert_equal 2, negatives.size
157
+ [s('- b'), s('- a b a a')].each do |str|
158
+ assert negatives.include?(str)
159
+ end
140
160
  end
141
- end
142
-
143
- # Tests each_negative
144
- def test_each_negative
145
- sample = Sample[
146
- '+',
147
- '- b',
148
- '+ a b a b',
149
- '- a b a a'
150
- ]
151
- count = 0
152
- sample.each_negative do |str|
153
- assert str.negative?
154
- count += 1
161
+
162
+ # Tests Sample#include?
163
+ def test_include
164
+ strings = ['+ a b a b', '+ a b', '- a', '+']
165
+ s = Sample.new << strings
166
+ strings.each do |str|
167
+ assert_equal(true, s.include?(str))
168
+ end
169
+ assert_equal(true, s.include?(strings))
170
+ assert_equal(true, s.include?(s))
171
+ assert_equal(false, s.include?('+ a'))
172
+ assert_equal(false, s.include?('-'))
173
+ assert_equal(false, s.include?('+ a b a'))
155
174
  end
156
- assert_equal 2, count
157
- negatives = sample.negative_enumerator.collect{|s| s}
158
- assert_equal 2, negatives.size
159
- [s('- b'), s('- a b a a')].each do |str|
160
- assert negatives.include?(str)
175
+
176
+ # Tests Sample#include? on every kind of arguments it announce
177
+ def test_include_accepts_arguments_it_annouce
178
+ s = Sample.new << ['+ a b a b', '+ a b', '- a', '+']
179
+ assert_equal true, s.include?('+ a b a b')
180
+ assert_equal true, s.include?(InputString.new('a b a b',true))
181
+ assert_equal true, s.include?(ADL::parse_string('+ a b a b'))
182
+ assert_equal true, s.include?(['+ a b a b', '+ a b'])
183
+ assert_equal true, s.include?(s)
161
184
  end
162
- end
163
-
164
- # Tests Sample#include?
165
- def test_include
166
- strings = ['+ a b a b', '+ a b', '- a', '+']
167
- s = Sample.new << strings
168
- strings.each do |str|
169
- assert_equal(true, s.include?(str))
185
+
186
+ # Tests Sample#==
187
+ def test_equal
188
+ s1 = Sample['+ a b a b', '+', '- a']
189
+ s2 = Sample['+ a b a b', '+', '+ a']
190
+ assert_equal(true, s1==s1)
191
+ assert_equal(true, s2==s2)
192
+ assert_equal(false, s1==s2)
193
+ assert_equal(false, s1==Sample.new)
194
+ assert_equal(false, s2==Sample.new)
170
195
  end
171
- assert_equal(true, s.include?(strings))
172
- assert_equal(true, s.include?(s))
173
- assert_equal(false, s.include?('+ a'))
174
- assert_equal(false, s.include?('-'))
175
- assert_equal(false, s.include?('+ a b a'))
176
- end
177
-
178
- # Tests Sample#include? on every kind of arguments it announce
179
- def test_include_accepts_arguments_it_annouce
180
- s = Sample.new << ['+ a b a b', '+ a b', '- a', '+']
181
- assert_equal true, s.include?('+ a b a b')
182
- assert_equal true, s.include?(InputString.new('a b a b',true))
183
- assert_equal true, s.include?(ADL::parse_string('+ a b a b'))
184
- assert_equal true, s.include?(['+ a b a b', '+ a b'])
185
- assert_equal true, s.include?(s)
186
- end
187
-
188
- # Tests Sample#==
189
- def test_equal
190
- s1 = Sample['+ a b a b', '+', '- a']
191
- s2 = Sample['+ a b a b', '+', '+ a']
192
- assert_equal(true, s1==s1)
193
- assert_equal(true, s2==s2)
194
- assert_equal(false, s1==s2)
195
- assert_equal(false, s1==Sample.new)
196
- assert_equal(false, s2==Sample.new)
197
- end
198
-
199
- # Test the signature
200
- def test_signature
201
- s = Sample.new
202
- assert_equal '', s.signature
203
- s = Sample.new << ['+ a b a b', '+ a b', '- a', '+']
204
- assert_equal '1101', s.signature
205
- s = Sample.new << ['+ a b a b', '+ a b', '- a', '?']
206
- assert_equal '110?', s.signature
207
- s = Stamina::ADL.parse_sample <<-SAMPLE
208
- +
209
- + a b
210
- - a c
211
- ? a d
212
- SAMPLE
213
- assert_equal '110?', s.signature
214
- end
215
-
216
- end # class SampleTest
217
-
218
- end # module Stamina
196
+
197
+ # Test the signature
198
+ def test_signature
199
+ s = Sample.new
200
+ assert_equal '', s.signature
201
+ s = Sample.new << ['+ a b a b', '+ a b', '- a', '+']
202
+ assert_equal '1101', s.signature
203
+ s = Sample.new << ['+ a b a b', '+ a b', '- a', '?']
204
+ assert_equal '110?', s.signature
205
+ s = Stamina::ADL.parse_sample <<-SAMPLE
206
+ +
207
+ + a b
208
+ - a c
209
+ ? a d
210
+ SAMPLE
211
+ assert_equal '110?', s.signature
212
+ end
213
+
214
+ end # class SampleTest
215
+ end # module Stamina
@@ -0,0 +1,63 @@
1
+ require 'test/unit'
2
+ require 'stamina/errors'
3
+ require 'stamina/stamina_test'
4
+ require 'stamina/scoring'
5
+ module Stamina
6
+ class ScoringTest < StaminaTest
7
+
8
+ def assert_almost_equal(x, y)
9
+ assert (x.to_f - y.to_f).abs <= 0.0001
10
+ end
11
+
12
+ def test_scoring_on_exact
13
+ learned, reference = "11010", "11010"
14
+ scoring = Scoring.scoring(learned, reference)
15
+
16
+ # It looks like a Scoring object
17
+ assert scoring.respond_to?(:false_positive)
18
+ assert scoring.respond_to?(:recall)
19
+
20
+ # four measures are ok
21
+ assert_equal 3, scoring.true_positive
22
+ assert_equal 2, scoring.true_negative
23
+ assert_equal 0, scoring.false_positive
24
+ assert_equal 0, scoring.false_negative
25
+
26
+ # precision and recall are ok
27
+ assert_equal (3.0 / (3.0 + 0.0)), scoring.precision
28
+ assert_equal (3.0 / (3.0 + 0.0)), scoring.recall
29
+
30
+ # sensitivity and specificity are ok
31
+ assert_equal (3.0 / (3.0 + 0.0)), scoring.sensitivity
32
+ assert_equal (3.0 / (3.0 + 0.0)), scoring.specificity
33
+
34
+ #
35
+ assert_equal 1.0, scoring.accuracy
36
+ assert_equal 1.0, scoring.bcr
37
+ assert_equal 1.0, scoring.f_measure
38
+ assert_equal 1.0, scoring.hbcr
39
+ end
40
+
41
+ def test_on_wikipedia_example
42
+ hash = {
43
+ :true_positive => 2,
44
+ :false_positive => 18,
45
+ :true_negative => 182,
46
+ :false_negative => 1
47
+ }
48
+ hash.extend(Scoring)
49
+ assert_equal (2.0 / (2 + 18)), hash.positive_predictive_value
50
+ assert_equal (182.0 / (1 + 182)), hash.negative_predictive_value
51
+ assert_equal (2.0 / (2 + 1)), hash.sensitivity
52
+ assert_equal (182.0 / (18 + 182)), hash.specificity
53
+ assert_equal (18.0 / (18 + 182)), hash.false_positive_rate
54
+ assert_equal (1.0 / (2 + 1)), hash.false_negative_rate
55
+ #
56
+ assert_almost_equal (1.0 - hash.specificity), hash.false_positive_rate
57
+ assert_almost_equal (1.0 - hash.sensitivity), hash.false_negative_rate
58
+ assert_almost_equal hash.sensitivity / (1.0 - hash.specificity), hash.positive_likelihood
59
+ assert_almost_equal (1.0 - hash.sensitivity) / hash.specificity, hash.negative_likelihood
60
+ end
61
+
62
+ end # class ScoringTest
63
+ end # module Stamina
@@ -0,0 +1,65 @@
1
+ require 'stamina'
2
+ require 'stamina/utils/decorate'
3
+ require 'stamina/stamina_test'
4
+ require 'test/unit'
5
+ module Stamina
6
+ module Utils
7
+ class DecorateTest < ::Stamina::StaminaTest
8
+
9
+ module Reachability
10
+ def suppremum(d0, d1) d0 || d1; end
11
+ def propagate(deco, edge) deco; end
12
+ end
13
+
14
+ module Depth
15
+ def suppremum(d0, d1) (d0 < d1 ? d0 : d1) end
16
+ def propagate(deco, edge) deco+1; end
17
+ end
18
+
19
+ module ShortPrefix
20
+ def suppremum(d0, d1)
21
+ return d0 if d1.nil?
22
+ return d1 if d0.nil?
23
+ d0.size <= d1.size ? d0 : d1
24
+ end
25
+ def propagate(deco, edge)
26
+ deco.dup << edge.symbol
27
+ end
28
+ end
29
+
30
+ def test_reachability_on_small_dfa
31
+ algo = Stamina::Utils::Decorate.new(:reachable)
32
+ algo.set_suppremum {|d0,d1| d0 || d1 }
33
+ algo.set_propagate {|deco,edge| deco }
34
+ algo.execute(@small_dfa, false, true)
35
+ assert_equal @small_dfa.states.select {|s| s[:reachable]==true}, @small_dfa.states
36
+
37
+ algo = Stamina::Utils::Decorate.new(:reachable)
38
+ algo.extend(Reachability)
39
+ algo.execute(@small_dfa, false, true)
40
+ assert_equal @small_dfa.states.select {|s| s[:reachable]==true}, @small_dfa.states
41
+ end
42
+
43
+ def test_depth_on_small_dfa
44
+ algo = Stamina::Utils::Decorate.new(:depth)
45
+ algo.extend(Depth)
46
+ algo.execute(@small_dfa, 1000000, 0)
47
+ assert_equal 0, @small_dfa.ith_state(3)[:depth]
48
+ assert_equal 1, @small_dfa.ith_state(2)[:depth]
49
+ assert_equal 2, @small_dfa.ith_state(0)[:depth]
50
+ assert_equal 3, @small_dfa.ith_state(1)[:depth]
51
+ end
52
+
53
+ def test_depth_on_small_dfa
54
+ algo = Stamina::Utils::Decorate.new(:short_prefix)
55
+ algo.extend(ShortPrefix)
56
+ algo.execute(@small_dfa, nil, [])
57
+ assert_equal [], @small_dfa.ith_state(3)[:short_prefix]
58
+ assert_equal ['b'], @small_dfa.ith_state(2)[:short_prefix]
59
+ assert_equal ['b', 'c'], @small_dfa.ith_state(0)[:short_prefix]
60
+ assert_equal ['b', 'c', 'a'], @small_dfa.ith_state(1)[:short_prefix]
61
+ end
62
+
63
+ end
64
+ end
65
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stamina
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 3
9
- - 0
10
- version: 0.3.0
9
+ - 1
10
+ version: 0.3.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Bernard Lambeau
@@ -144,6 +144,7 @@ files:
144
144
  - lib/stamina/adl.rb
145
145
  - lib/stamina/automaton.rb
146
146
  - lib/stamina/automaton/walking.rb
147
+ - lib/stamina/automaton/metrics.rb
147
148
  - lib/stamina/classifier.rb
148
149
  - lib/stamina/command/adl2dot_command.rb
149
150
  - lib/stamina/command/classify_command.rb
@@ -160,6 +161,9 @@ files:
160
161
  - lib/stamina/sample.rb
161
162
  - lib/stamina/version.rb
162
163
  - lib/stamina/loader.rb
164
+ - lib/stamina/scoring.rb
165
+ - lib/stamina/utils/decorate.rb
166
+ - lib/stamina/utils.rb
163
167
  - tasks/yard.rake
164
168
  - tasks/debug_mail.txt
165
169
  - tasks/gem.rake
@@ -168,10 +172,8 @@ files:
168
172
  - tasks/spec_test.rake
169
173
  - test/stamina/adl_test.rb
170
174
  - test/stamina/automaton_additional_test.rb
171
- - test/stamina/automaton_classifier_test.rb
172
175
  - test/stamina/automaton_test.rb
173
- - test/stamina/automaton_to_dot_test.rb
174
- - test/stamina/automaton_walking_test.rb
176
+ - test/stamina/scoring_test.rb
175
177
  - test/stamina/exit.rb
176
178
  - test/stamina/induction/induction_test.rb
177
179
  - test/stamina/induction/redblue_mergesamestatebug_expected.adl
@@ -198,6 +200,11 @@ files:
198
200
  - test/stamina/small_nfa.dot
199
201
  - test/stamina/small_nfa.gif
200
202
  - test/stamina/stamina_test.rb
203
+ - test/stamina/utils/decorate_test.rb
204
+ - test/stamina/automaton/classifier_test.rb
205
+ - test/stamina/automaton/walking_test.rb
206
+ - test/stamina/automaton/to_dot_test.rb
207
+ - test/stamina/automaton/metrics_test.rb
201
208
  - test/test_all.rb
202
209
  - .gemtest
203
210
  - CHANGELOG.md
@@ -246,10 +253,8 @@ summary: Automaton and Regular Inference Toolkit
246
253
  test_files:
247
254
  - test/stamina/adl_test.rb
248
255
  - test/stamina/automaton_additional_test.rb
249
- - test/stamina/automaton_classifier_test.rb
250
256
  - test/stamina/automaton_test.rb
251
- - test/stamina/automaton_to_dot_test.rb
252
- - test/stamina/automaton_walking_test.rb
257
+ - test/stamina/scoring_test.rb
253
258
  - test/stamina/exit.rb
254
259
  - test/stamina/induction/induction_test.rb
255
260
  - test/stamina/induction/redblue_mergesamestatebug_expected.adl
@@ -276,4 +281,9 @@ test_files:
276
281
  - test/stamina/small_nfa.dot
277
282
  - test/stamina/small_nfa.gif
278
283
  - test/stamina/stamina_test.rb
284
+ - test/stamina/utils/decorate_test.rb
285
+ - test/stamina/automaton/classifier_test.rb
286
+ - test/stamina/automaton/walking_test.rb
287
+ - test/stamina/automaton/to_dot_test.rb
288
+ - test/stamina/automaton/metrics_test.rb
279
289
  - test/test_all.rb