File Coverage

blib/lib/AI/FANN/Evolving/TrainData.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package AI::FANN::Evolving::TrainData;
2 3     3   40993 use strict;
  3         8  
  3         153  
3 3     3   19 use List::Util 'shuffle';
  3         7  
  3         340  
4 3     3   2940 use AI::FANN ':all';
  0            
  0            
5             use Algorithm::Genetic::Diploid::Base;
6             use base 'Algorithm::Genetic::Diploid::Base';
7              
8             our $AUTOLOAD;
9             my $log = __PACKAGE__->logger;
10              
11             =head1 NAME
12              
13             AI::FANN::Evolving::TrainData - wrapper class for FANN data
14              
15             =head1 METHODS
16              
17             =over
18              
19             =item new
20              
21             Constructor takes named arguments. By default, ignores column
22             named ID and considers column named CLASS as classifier.
23              
24             =cut
25              
26             sub new {
27             my $self = shift->SUPER::new(
28             'ignore' => [ 'ID' ],
29             'dependent' => [ 'CLASS' ],
30             'header' => {},
31             'table' => [],
32             @_
33             );
34             my %args = @_;
35             $self->read_data($args{'file'}) if $args{'file'};
36             $self->trim_data if $args{'trim'};
37             return $self;
38             }
39              
40             =item ignore_columns
41              
42             Getter/setter for column names to ignore in the train data structure.
43             For example: an identifier columns named 'ID'
44              
45             =cut
46              
47             sub ignore_columns {
48             my $self = shift;
49             $self->{'ignore'} = \@_ if @_;
50             return @{ $self->{'ignore'} };
51             }
52              
53             =item dependent_columns
54              
55             Getter/setter for column name(s) of the output value(s).
56              
57             =cut
58              
59             sub dependent_columns {
60             my $self = shift;
61             $self->{'dependent'} = \@_ if @_;
62             return @{ $self->{'dependent'} };
63             }
64              
65             =item predictor_columns
66              
67             Getter for column name(s) of input value(s)
68              
69             =cut
70              
71             sub predictor_columns {
72             my $self = shift;
73             my @others = ( $self->ignore_columns, $self->dependent_columns );
74             my %skip = map { $_ => 1 } @others;
75             return grep { ! $skip{$_} } keys %{ $self->{'header'} };
76             }
77              
78             =item predictor_data
79              
80             Getter for rows of input values
81              
82             =cut
83              
84             sub predictor_data {
85             my ( $self, %args ) = @_;
86             my $i = $args{'row'};
87             my @cols = $args{'cols'} ? @{ $args{'cols'} } : $self->predictor_columns;
88            
89             # build hash of indices to keep
90             my %keep = map { $self->{'header'}->{$_} => 1 } @cols;
91            
92             # only return a single row
93             if ( defined $i ) {
94             my @pred;
95             for my $j ( 0 .. $#{ $self->{'table'}->[$i] } ) {
96             push @pred, $self->{'table'}->[$i]->[$j] if $keep{$j};
97             }
98             return \@pred;
99             }
100             else {
101             my @preds;
102             my $max = $self->size - 1;
103             for my $j ( 0 .. $max ) {
104             push @preds, $self->predictor_data( 'row' => $j, 'cols' => \@cols);
105             }
106             return @preds;
107             }
108             }
109              
110             =item dependent_data
111              
112             Getter for dependent (classifier) data
113              
114             =cut
115              
116             sub dependent_data {
117             my ( $self, $i ) = @_;
118             my @dc = map { $self->{'header'}->{$_} } $self->dependent_columns;
119             if ( defined $i ) {
120             return [ map { $self->{'table'}->[$i]->[$_] } @dc ];
121             }
122             else {
123             my @dep;
124             for my $j ( 0 .. $self->size - 1 ) {
125             push @dep, $self->dependent_data($j);
126             }
127             return @dep;
128             }
129             }
130              
131             =item read_data
132              
133             Reads provided input file
134              
135             =cut
136              
137             sub read_data {
138             my ( $self, $file ) = @_; # file is tab-delimited
139             $log->debug("reading data from file $file");
140             open my $fh, '<', $file or die "Can't open $file: $!";
141             my ( %header, @table );
142             while(<$fh>) {
143             chomp;
144             next if /^\s*$/;
145             my @fields = split /\t/, $_;
146             if ( not %header ) {
147             my $i = 0;
148             %header = map { $_ => $i++ } @fields;
149             }
150             else {
151             push @table, \@fields;
152             }
153             }
154             $self->{'header'} = \%header;
155             $self->{'table'} = \@table;
156             return $self;
157             }
158              
159             =item write_data
160              
161             Writes to provided output file
162              
163             =cut
164              
165             sub write_data {
166             my ( $self, $file ) = @_;
167            
168             # use file or STDOUT
169             my $fh;
170             if ( $file ) {
171             open $fh, '>', $file or die "Can't write to $file: $!";
172             $log->info("writing data to $file");
173             }
174             else {
175             $fh = \*STDOUT;
176             $log->info("writing data to STDOUT");
177             }
178            
179             # print header
180             my $h = $self->{'header'};
181             print $fh join "\t", sort { $h->{$a} <=> $h->{$b} } keys %{ $h };
182             print $fh "\n";
183            
184             # print rows
185             for my $row ( @{ $self->{'table'} } ) {
186             print $fh join "\t", @{ $row };
187             print $fh "\n";
188             }
189             }
190              
191             =item trim_data
192              
193             Trims sparse rows with missing values
194              
195             =cut
196              
197             sub trim_data {
198             my $self = shift;
199             my @trimmed;
200             ROW: for my $row ( @{ $self->{'table'} } ) {
201             next ROW if grep { not defined $_ } @{ $row };
202             push @trimmed, $row;
203             }
204             my $num = $self->{'size'} - scalar @trimmed;
205             $log->info("removed $num incomplete rows");
206             $self->{'table'} = \@trimmed;
207             }
208              
209             =item sample_data
210              
211             Sample a fraction of the data
212              
213             =cut
214              
215             sub sample_data {
216             my $self = shift;
217             my $sample = shift || 0.5;
218             my $clone1 = $self->clone;
219             my $clone2 = $self->clone;
220             my $size = $self->size;
221             my @sample;
222             $clone2->{'table'} = \@sample;
223             while( scalar(@sample) < int( $size * $sample ) ) {
224             my @shuffled = shuffle( @{ $clone1->{'table'} } );
225             push @sample, shift @shuffled;
226             $clone1->{'table'} = \@shuffled;
227             }
228             return $clone2, $clone1;
229             }
230              
231             =item partition_data
232              
233             Creates two clones that partition the data according to the provided ratio.
234              
235             =cut
236              
237             sub partition_data {
238             my $self = shift;
239             my $sample = shift || 0.5;
240             my $clone1 = $self->clone;
241             my $clone2 = $self->clone;
242             my $remain = 1 - $sample;
243             $log->info("going to partition into $sample : $remain");
244            
245             # compute number of different dependent patterns and ratios of each
246             my @dependents = $self->dependent_data;
247             my %seen;
248             for my $dep ( @dependents ) {
249             my $key = join '/', @{ $dep };
250             $seen{$key}++;
251             }
252            
253             # adjust counts to sample size
254             for my $key ( keys %seen ) {
255             $log->debug("counts: $key => $seen{$key}");
256             $seen{$key} = int( $seen{$key} * $sample );
257             $log->debug("rescaled: $key => $seen{$key}");
258             }
259              
260             # start the sampling
261             my @dc = map { $self->{'header'}->{$_} } $self->dependent_columns;
262             my @new_table; # we will populate this
263             my @table = @{ $clone1->{'table'} }; # work on cloned instance
264            
265             # as long as there is still sampling to do
266             SAMPLE: while( grep { !!$_ } values %seen ) {
267             for my $i ( 0 .. $#table ) {
268             my @r = @{ $table[$i] };
269             my $key = join '/', @r[@dc];
270             if ( $seen{$key} ) {
271             my $rand = rand(1);
272             if ( $rand < $sample ) {
273             push @new_table, \@r;
274             splice @table, $i, 1;
275             $seen{$key}--;
276             $log->debug("still to go for $key: $seen{$key}");
277             next SAMPLE;
278             }
279             }
280             }
281             }
282             $clone2->{'table'} = \@new_table;
283             $clone1->{'table'} = \@table;
284             return $clone2, $clone1;
285             }
286              
287             =item size
288              
289             Returns the number of data records
290              
291             =cut
292              
293             sub size { scalar @{ shift->{'table'} } }
294              
295             =item to_fann
296              
297             Packs data into an L TrainData structure
298              
299             =cut
300              
301             sub to_fann {
302             $log->debug("encoding data as FANN struct");
303             my $self = shift;
304             my @cols = @_ ? @_ : $self->predictor_columns;
305             my @deps = $self->dependent_data;
306             my @pred = $self->predictor_data( 'cols' => \@cols );
307             my @interdigitated;
308             for my $i ( 0 .. $#deps ) {
309             push @interdigitated, $pred[$i], $deps[$i];
310             }
311             return AI::FANN::TrainData->new(@interdigitated);
312             }
313              
314             =back
315              
316             =cut
317              
318             1;