File Coverage

blib/lib/AI/FuzzyInference.pm
Criterion Covered Total %
statement 9 145 6.2
branch 0 20 0.0
condition 0 18 0.0
subroutine 3 21 14.2
pod 11 12 91.6
total 23 216 10.6


line stmt bran cond sub pod time code
1              
2             # A module to implement a fuzzy inference system.
3             #
4             # Copyright Ala Qumsieh (aqumsieh@cpan.org) 2002.
5             # This program is distributed under the same terms as Perl itself.
6              
7             package AI::FuzzyInference;
8 1     1   125125 use strict;
  1         3  
  1         50  
9              
10 1     1   6 use vars qw/$VERSION/;
  1         1  
  1         60  
11             $VERSION = 0.05;
12              
13 1     1   1294 use AI::FuzzyInference::Set;
  1         4  
  1         5217  
14              
15             ############################################
16             #
17             # First some global vars.
18             #
19             ############################################
20              
21             # this hash defines the possible interpretations of the
22             # standard fuzzy logic operations.
23             my %_operations = (
24             '&' => {
25             min => sub { (sort {$a <=> $b} @_)[0] },
26             product => sub { my $p = 1; $p *= $_ for @_; $p },
27             default => 'min',
28             },
29             '|' => {
30             max => sub { (sort {$a <=> $b} @_)[-1] },
31             sum => sub { my $s = 0; $s += $_ for @_; $s > 1 ? 1 : $s },
32             default => 'max',
33             },
34             '!' => {
35             complement => sub { 1 - $_[0] },
36             custom => sub {},
37             default => 'complement',
38             },
39             );
40              
41             # this hash defines the currently implemented implication methods.
42             my %_implication = qw(
43             clip 1
44             scale 1
45             default clip
46             );
47              
48             # this hash defines the currently implemented aggregation methods.
49             my %_aggregation = qw(
50             max 1
51             default max
52             );
53              
54             # this hash defines the currently implemented defuzzification methods.
55             my %_defuzzification = qw(
56             centroid 1
57             default centroid
58             );
59              
60             # sub new() - constructor.
61             #
62             # doesn't take any arguments. Returns an initialized AI::FuzzyInference object.
63              
64             sub new {
65 0     0 1   my $self = shift;
66 0   0       my $class = ref($self) || $self;
67              
68 0           my $obj = bless {} => $class;
69              
70 0           $obj->_init;
71              
72 0           return $obj;
73             }
74              
75             # sub _init() - private method.
76             #
77             # no arguments. Initializes the data structures we will need.
78             # It also defines the default logic operations we might need.
79              
80             sub _init {
81 0     0     my $self = shift;
82              
83 0           $self->{SET} = new AI::FuzzyInference::Set;
84 0           $self->{INVARS} = {};
85 0           $self->{OUTVARS} = {};
86 0           $self->{RULES} = [];
87 0           $self->{RESULTS} = {};
88              
89 0           $self->{IMPLICATION} = $_implication{default};
90 0           $self->{AGGREGATION} = $_aggregation{default};
91 0           $self->{DEFUZZIFICATION} = $_defuzzification{default};
92              
93 0           for my $op (qw/& | !/) {
94 0           $self->{OPERATIONS}{$op} = $_operations{$op}{default};
95             }
96             }
97              
98             # sub implication() - public method.
99             #
100             # one optional argument: has to match one of the keys of the %_implication hash.
101             # used to query/set the implication method.
102              
103             sub implication {
104 0     0 1   my ($self,
105             $new,
106             ) = @_;
107              
108 0 0 0       if (defined $new and exists $_implication{$new}) {
109 0           $self->{IMPLICATION} = $new;
110             }
111              
112 0           return $self->{IMPLICATION};
113             }
114              
115             # sub aggregation() - public method.
116             #
117             # one optional argument: has to match one of the keys of the %_aggregation hash.
118             # used to query/set the aggregation method.
119              
120             sub aggregation {
121 0     0 1   my ($self,
122             $new,
123             ) = @_;
124              
125 0 0 0       if (defined $new and exists $_aggregation{$new}) {
126 0           $self->{AGGREGATION} = $new;
127             }
128              
129 0           return $self->{AGGREGATION};
130             }
131              
132             # sub defuzzification() - public method.
133             #
134             # one optional argument: has to match one of the keys of the %_defuzzification hash.
135             # used to query/set the defuzzification method.
136              
137             sub defuzzification {
138 0     0 1   my ($self,
139             $new,
140             ) = @_;
141              
142 0 0 0       if (defined $new and exists $_defuzzification{$new}) {
143 0           $self->{DEFUZZIFICATION} = $new;
144             }
145              
146 0           return $self->{DEFUZZIFICATION};
147             }
148              
149             # sub operation() - public method.
150             #
151             # two arguments: first one mandatory and specifies the logic operation
152             # in question. Second one is optional and has to match one of the keys
153             # of the %{$_operations{$first_arg}} hash.
154             # Used to query/set the logic operations method.
155              
156             sub operation {
157 0     0 1   my ($self,
158             $op,
159             $new,
160             ) = @_;
161              
162 0 0 0       return unless defined $op && exists $_operations{$op};
163              
164 0 0 0       if (defined $new and exists $_operations{$op}{$new}) {
165 0           $self->{OPERATIONS}{$op} = $new;
166             }
167              
168 0           return $self->{OPERATIONS}{$op};
169             }
170              
171             # sub inVar() - public method.
172             #
173             # 4 arguments or more : First is a name of a new input variable.
174             # Second and third are the min and max values of that variable.
175             # These define the universe of discourse for that variable.
176             # Additional argumets constitute a hash. The keys of the hash
177             # are term set names defined for the given variable. The values
178             # are the coordinates of the vertices of the term sets.
179             #
180             # ex. $obj->inVar('height',
181             # 5, 8, # xmin, xmax (in feet, say)
182             # 'tall' => [0, 0,
183             # 5, 1,
184             # 10,0],
185             # ....);
186              
187             sub inVar {
188 0     0 1   my ($self,
189             $var,
190             $xmin,
191             $xmax,
192             @sets,
193             ) = @_;
194              
195 0           $self->{INVARS}{$var} = [$xmin, $xmax];
196              
197 0           while (@sets) {
198 0           my $s = shift @sets;
199 0           my $c = shift @sets;
200              
201 0           $self->{SET}->add("$var:$s", $xmin, $xmax, @$c);
202             }
203             }
204              
205             # sub outVar() - public method.
206             #
207             # 4 arguments or more : First is a name of a new output variable.
208             # Second and third are the min and max values of that variable.
209             # These define the universe of discourse for that variable.
210             # Additional argumets constitute a hash. The keys of the hash
211             # are term set names defined for the given variable. The values
212             # are the coordinates of the vertices of the term sets.
213              
214             sub outVar {
215 0     0 1   my ($self,
216             $var,
217             $xmin,
218             $xmax,
219             @sets,
220             ) = @_;
221              
222 0           $self->{OUTVARS}{$var} = [$xmin, $xmax];
223              
224 0           while (@sets) {
225 0           my $s = shift @sets;
226 0           my $c = shift @sets;
227              
228 0           $self->{SET}->add("$var:$s", $xmin, $xmax, @$c);
229             }
230             }
231              
232             # sub addRule() - public method.
233             #
234             # Adds fuzzy if-then inference rules.
235             #
236             # $obj->addRule('x=medium' => 'z = slow',
237             # 'x=low & y=small' => 'z = fast',
238             # 'x=high & y=tiny' => 'z=veryfast');
239             # spaces are optional. The characters [&=|] are special.
240              
241             sub addRule {
242 0     0 1   my ($self, %rules) = @_;
243              
244 0           for my $k (keys %rules) {
245 0           my $v = $rules{$k};
246 0           s/\s+//g for $v, $k;
247              
248 0           push @{$self->{RULES}} => [$k, $v];
  0            
249             }
250              
251 0           return 1;
252             }
253              
254             # sub show() - public method.
255             #
256             # This method displays the computed values of all
257             # output variables.
258             # It is ugly, and will be removed. Here for debugging.
259              
260             sub show {
261 0     0 0   my $self = shift;
262              
263 0           for my $var (keys %{$self->{RESULTS}}) {
  0            
264 0           print "Var $var = $self->{RESULTS}{$var}.\n";
265             }
266             }
267              
268             # sub value() - public method.
269             #
270             # one argument: the name of an output variable.
271             # This method returns the computed value of a given output var.
272              
273             sub value {
274 0     0 1   my ($self,
275             $var,
276             ) = @_;
277              
278 0 0         return undef unless exists $self->{RESULTS}{$var};
279 0           return $self->{RESULTS}{$var};
280             }
281              
282             # sub reset() - public method
283             #
284             # cleans the data structures used.
285              
286             sub reset {
287 0     0 1   my $self = shift;
288              
289 0           my @list = $self->{SET}->listMatching(q|:implicated$|);
290 0           push @list => $self->{SET}->listMatching(q|:aggregated$|);
291              
292 0           $self->{SET}->delete($_) for @list;
293              
294 0           $self->{RESULTS} = {};
295             }
296              
297             # sub compute() - public method
298             #
299             # This method takes as input crisp values for each
300             # of the input vars, and produces a crisp output value
301             # based on the application of the fuzzy if-then rules.
302             # ex.
303             # $z = $obj->compute(x => 5,
304             # y => 24);
305              
306             sub compute {
307 0     0 1   my ($self,
308             %vars,
309             ) = @_;
310              
311 0           $self->reset();
312              
313             # First thing we do is to fuzzify the inputs.
314 0           $self->_fuzzify(%vars);
315              
316             # Now, apply the rules to see which ones fire.
317 0           $self->_infer;
318              
319             # implicate
320 0           $self->_implicate;
321              
322             # aggregate
323 0           $self->_aggregate;
324              
325             # defuzzify .. final step.
326 0           $self->_defuzzify;
327              
328 0           return 1;
329             }
330              
331             # sub _defuzzify() - private method.
332             #
333             # no arguments. This method applies the defuzzification technique
334             # to get a crisp value out of the aggregated set of each output
335             # var.
336              
337             sub _defuzzify {
338 0     0     my $self = shift;
339              
340 0           my $_defuzzification = $self->{DEFUZZIFICATION};
341              
342             # iterate through all output vars.
343 0           for my $var (keys %{$self->{OUTVARS}}) {
  0            
344              
345 0           my $result = 0;
346 0 0         if ($self->{SET}->exists("$var:aggregated")) {
347 0           $result = $self->{SET}->$_defuzzification("$var:aggregated");
348             }
349              
350 0           $self->{RESULTS}{$var} = $result;
351             }
352             }
353              
354             # sub _aggregate() - private method.
355             #
356             # no arguments. This method applies the aggregation technique to get
357             # one fuzzy set out of the implicated sets of each output var.
358              
359             sub _aggregate {
360 0     0     my $self = shift;
361              
362 0           my $_aggregation = $self->{AGGREGATION};
363              
364             # iterate through all output vars.
365 0           for my $var (keys %{$self->{OUTVARS}}) {
  0            
366              
367             # get implicated sets.
368 0           my @list = $self->{SET}->listMatching("\Q$var\E:.*:implicated\$");
369              
370 0 0         next unless @list;
371              
372 0           my $i = 0;
373 0           my $current = shift @list;
374              
375             # aggregate everything together.
376 0           while (@list) {
377 0           my $new = shift @list;
378 0           my $name = "temp" . $i++;
379              
380 0           my @c = $self->{SET}->$_aggregation($current, $new);
381 0           $self->{SET}->add($name, @{$self->{OUTVARS}{$var}}, @c);
  0            
382 0           $current = $name;
383             }
384              
385             # rename the final aggregated set.
386 0           my @c = $self->{SET}->coords($current);
387 0           $self->{SET}->add("$var:aggregated", @{$self->{OUTVARS}{$var}}, @c);
  0            
388              
389             # delete the temporary sets.
390 0           for my $j (0 .. $i - 1) {
391 0           $self->{SET}->delete("temp$j");
392             }
393             }
394             }
395              
396             # sub _implicate() - private method.
397             #
398             # no arguments. This method applies the implication technique
399             # to all the fired rules to find a support value for each
400             # output variable.
401              
402             sub _implicate {
403 0     0     my $self = shift;
404              
405 0           my $_implication = $self->{IMPLICATION};
406              
407 0           my %ind;
408              
409 0           for my $ref (@{$self->{FIRED}}) {
  0            
410 0           my ($i, $val) = @$ref;
411 0           my ($var, $ts) = split /=/, $self->{RULES}[$i][1];
412              
413 0 0         if ($val > 0) {
414 0           $ind{$var}{$ts}++;
415 0           my @c = $self->{SET}->$_implication("$var:$ts", $val);
416 0           my @u = @{$self->{OUTVARS}{$var}}; # the universe
  0            
417 0           $self->{SET}->add("$var:$ts:$ind{$var}{$ts}:implicated", @u, @c);
418             }
419             }
420             }
421              
422             # sub _fuzzify() - private method.
423             #
424             # one argument: a hash. The keys are input variables. The
425             # values are the crisp values of the input variables (same arguments
426             # as compute()). It finds the degree of membership of each input
427             # variable in each of its term sets.
428              
429             sub _fuzzify {
430 0     0     my ($self, %vars) = @_;
431              
432 0           my %terms;
433              
434 0           for my $var (keys %vars) {
435 0           my $val = $vars{$var};
436              
437 0           for my $ts ($self->{SET}->listMatching("\Q$var\E")) {
438 0           my $deg = $self->{SET}->membership($ts, $val);
439              
440 0           $terms{$var}{$ts} = $deg;
441             }
442             }
443              
444 0           $self->{FUZZIFY} = \%terms;
445             }
446              
447             # sub _infer() - private method.
448             #
449             # no arguments. This method applies the logic operations to combine
450             # multiple parts of the antecedent of a rule to get one crisp value
451             # that is the degree of support of that rule.
452             # Rules with positive support "fire".
453              
454             sub _infer {
455 0     0     my $self = shift;
456              
457 0           my @fired; # keep list of fired rules.
458              
459 0           for my $i (0 .. $#{$self->{RULES}}) {
  0            
460 0           my $r = $self->{RULES}[$i][0]; # precedent
461              
462 0           my $val = 0;
463 0           while ($r =~ /([&|]?)([^&|]+)/g) {
464 0           my ($op, $ant) = ($1, $2);
465 0           my ($var, $ts) = split /=/ => $ant;
466              
467 0           $ts = "$var:$ts";
468              
469 0 0         if ($op) {
470             #$val = $self->{LOGIC}{$op}{SUB}->($val, $self->{FUZZIFY}{$var}{$ts});
471 0           $val = $_operations{$op}{$self->{OPERATIONS}{$op}}->($val, $self->{FUZZIFY}{$var}{$ts});
472             } else {
473 0           $val = $self->{FUZZIFY}{$var}{$ts};
474             }
475             }
476              
477             # We only care about positive values.
478 0           push @fired => [$i, $val];
479             }
480              
481 0           $self->{FIRED} = \@fired;
482             }
483              
484             __END__