stamina 0.3.0 → 0.3.1

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