File Coverage

blib/lib/Lingua/YALI/Identifier.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Lingua::YALI::Identifier;
2             # ABSTRACT: Module for language identification with custom models.
3              
4 19     19   278683 use strict;
  19         46  
  19         906  
5 19     19   108 use warnings;
  19         42  
  19         642  
6 19     19   7771 use Moose;
  19         4895611  
  19         154  
7 19     19   192993 use Carp;
  19         241  
  19         1706  
8 19     19   11243 use PerlIO::gzip;
  0            
  0            
9             use Lingua::YALI;
10              
11             our $VERSION = '0.015'; # VERSION
12              
13             # hash with paths to models
14             # format: { 'class1' => 'file1', 'class2' => 'file2' }
15             has '_model_file' => (
16             is => 'rw',
17             isa => 'HashRef'
18             );
19              
20             # hash with n-gram frequencies retrieved from models
21             # format: { 'ngram1' => { 'class1' => 0.5, 'class2' => 0.3}, 'ngram2' => ...}
22             has '_frequency' => (
23             is => 'rw',
24             isa => 'HashRef'
25             );
26              
27             # n-gram size
28             has '_ngram' => (
29             is => 'rw',
30             isa => 'Int'
31             );
32              
33             # list of identified classes
34             has '_classes' => (
35             is => 'rw',
36             isa => 'ArrayRef'
37             );
38              
39              
40              
41             sub BUILD
42             {
43             my $self = shift;
44             my %frequency = ();
45             my @classes = ();
46             $self->{_frequency} = \%frequency;
47             $self->{_classes} = \@classes;
48              
49             return;
50             }
51              
52              
53             sub add_class
54             {
55             my ( $self, $class, $file ) = @_;
56              
57             if ( defined( $self->{_model_file}->{$class} ) ) {
58             return 0;
59             }
60              
61             # parameter check
62             if ( ! defined($file) ) {
63             croak("Model has to be specified.");
64             }
65              
66             if ( ! -r $file ) {
67             croak("Model $file is not readable.");
68             }
69              
70             $self->_load_model($class, $file);
71              
72             return 1;
73             }
74              
75              
76             sub remove_class
77             {
78             my ( $self, $class ) = @_;
79              
80             if ( defined( $self->{_model_file}->{$class} ) ) {
81             $self->_unload_model($class);
82              
83             return 1;
84             }
85              
86             return 0;
87             }
88              
89              
90             sub get_classes
91             {
92             my $self = shift;
93             return $self->{_classes};
94             }
95              
96              
97             sub identify_file
98             {
99             my ( $self, $file ) = @_;
100              
101             if ( ! defined($file) ) {
102             return;
103             }
104              
105             my $fh = Lingua::YALI::_open($file);
106              
107             return $self->identify_handle($fh);
108             }
109              
110              
111             sub identify_string
112             {
113             my ( $self, $string ) = @_;
114             open(my $fh, "<", \$string) or croak $!;
115              
116             if ( ! defined($string) ) {
117             return;
118             }
119              
120             my $result = $self->identify_handle($fh);
121              
122             close($fh);
123              
124             return $result;
125             }
126              
127              
128             sub identify_handle
129             {
130             my ($self, $fh) = @_;
131             my %actRes = ();
132              
133             # parameter check
134             my $ngram = $self->{_ngram};
135             if ( ! defined($ngram) ) {
136             croak("At least one class must be specified.");
137             }
138              
139             if ( ! defined($fh) ) {
140             return;
141             } elsif ( ref $fh ne "GLOB" ) {
142             croak("Expected file handler but " . (ref $fh) . " was used.");
143             }
144              
145             # read input file
146             while ( <$fh> ) {
147             chomp;
148             s/ +/ /g;
149             s/^ +//g;
150             s/ +$//g;
151             if ( ! $_ ) {
152             next;
153             }
154              
155             # $_ = $padding . $_ . $padding;
156              
157             {
158             use bytes;
159             for my $i (0 .. bytes::length($_) - $ngram) {
160             my $w = substr($_, $i, $ngram);
161              
162             if ( defined($self->{_frequency}->{$w}) ) {
163             for my $lang (keys %{$self->{_frequency}->{$w}}) {
164             # print STDERR "$w - $lang - $frequency{$w}{$lang}\n";
165             $actRes{$lang} += $self->{_frequency}->{$w}{$lang};
166             # print STDERR "Lang: $lang - $actRes{$lang}\n";
167             }
168             }
169             }
170             }
171             }
172              
173             # sum scores of all classifiers
174             my @allLanguages = @ { $self->get_classes() };
175              
176             my $sum = 0;
177             for my $l (@allLanguages) {
178             my $score = 0;
179             if ( defined($actRes{$l}) ) {
180             $score = $actRes{$l};
181             }
182             $sum += $score;
183             }
184              
185             # normalize results
186             my @res = ();
187              
188             for my $l (@allLanguages) {
189             my $score = 0;
190             if ( defined($actRes{$l}) ) {
191             $score = $actRes{$l} / $sum;
192             }
193             my @pair = ($l, $score);
194             push(@res, \@pair);
195             }
196              
197             # sort according to score
198             my @sortedRes = sort { $b->[1] <=> $a->[1] } @res;
199              
200             return \@sortedRes;
201             }
202              
203             # recompute classes after manipulation with classes
204             sub _compute_classes
205             {
206             my $self = shift;
207             my @classes = keys %{ $self->{_model_file} };
208              
209             $self->{_classes} = \@classes;
210              
211             return;
212             }
213              
214             # load model
215             sub _load_model
216             {
217             my ($self, $class, $file) = @_;
218              
219             if ( $self->{_model_file}->{$class} ) {
220             return;
221             }
222              
223             open(my $fh, "<:gzip:bytes", $file) or croak($!);
224             my $ngram = <$fh>;
225             my $total_line = <$fh>;
226              
227             if ( ! defined($self->{_ngram}) ) {
228             $self->{_ngram} = $ngram;
229             } else {
230             if ( $ngram != $self->{_ngram} ) {
231             croak("Incompatible model for '$class'. Expected $self->{_ngram}-grams, but was $ngram-gram.");
232             }
233             }
234              
235             my $sum = 0;
236             while ( <$fh> ) {
237             chomp;
238             my @p = split(/\t/, $_);
239             my $word = $p[0];
240             $self->{_frequency}->{$word}{$class} = $p[1];
241             $sum += $p[1];
242             }
243              
244             for my $word (keys %{$self->{_frequency}}) {
245             if ( defined($self->{_frequency}->{$word}{$class}) ) {
246             $self->{_frequency}->{$word}{$class} /= $sum;
247             }
248             }
249              
250             close($fh);
251              
252             $self->{_model_file}->{$class} = $file;
253             $self->_compute_classes();
254              
255             return;
256             }
257              
258             # unload model
259             sub _unload_model
260             {
261             my ($self, $class) = @_;
262              
263             if ( ! $self->{_model_file}->{$class} ) {
264             return;
265             }
266              
267             delete( $self->{_model_file}->{$class} );
268             $self->_compute_classes();
269              
270             my $classes = $self->get_classes();
271             # print STDERR "\nX=removing $class\n" . (join("\t", @$classes)) . "\n" . (scalar @$classes) . "\nX\n";
272             if ( scalar @$classes == 0 ) {
273             delete($self->{_ngram});
274             $self->{_ngram} = undef;
275             }
276              
277              
278              
279              
280             return;
281             }
282              
283              
284             1;
285              
286             __END__
287              
288             =pod
289              
290             =encoding UTF-8
291              
292             =head1 NAME
293              
294             Lingua::YALI::Identifier - Module for language identification with custom models.
295              
296             =head1 VERSION
297              
298             version 0.015
299              
300             =head1 SYNOPSIS
301              
302             This modul identify languages with moduls provided by the user. If you want to use pretrained models use L<Lingua::YALI::LanguageIdentifier|Lingua::YALI::LanguageIdentifier>.
303              
304             Models trained on texts from specific domain outperforms the general ones.
305              
306             use Lingua::YALI::Builder;
307             use Lingua::YALI::Identifier;
308              
309             # create models
310             my $builder_a = Lingua::YALI::Builder->new(ngrams=>[2]);
311             $builder_a->train_string("aaaaa aaaa aaa aaa aaa aaaaa aa");
312             $builder_a->store("model_a.2_all.gz", 2);
313              
314             my $builder_b = Lingua::YALI::Builder->new(ngrams=>[2]);
315             $builder_b->train_string("bbbbbb bbbb bbbb bbb bbbb bbbb bbb");
316             $builder_b->store("model_b.2_all.gz", 2);
317              
318             # create identifier and load models
319             my $identifier = Lingua::YALI::Identifier->new();
320             $identifier->add_class("a", "model_a.2_all.gz");
321             $identifier->add_class("b", "model_b.2_all.gz");
322              
323             # identify strings
324             my $result1 = $identifier->identify_string("aaaaaaaaaaaaaaaaaaa");
325             print $result1->[0]->[0] . "\t" . $result1->[0]->[1];
326             # prints out a 1
327              
328             my $result2 = $identifier->identify_string("bbbbbbbbbbbbbbbbbbb");
329             print $result2->[0]->[0] . "\t" . $result2->[0]->[1];
330             # prints out b 1
331              
332             More examples is presented in L<Lingua::YALI::Examples|Lingua::YALI::Examples>.
333              
334             =head1 METHODS
335              
336             =head2 BUILD
337              
338             Initializes internal variables.
339              
340             # create identifier
341             my $identifier = Lingua::YALI::Identifier->new();
342              
343             =head2 add_class
344              
345             $added = $identifier->add_class($class, $model)
346              
347             Adds model stored in file C<$model> with class C<$class> and
348             returns whether it was added or not.
349              
350             print $identifier->add_class("a", "model.a1.gz") . "\n";
351             # prints out 1
352             print $identifier->add_class("a", "model.a2.gz") . "\n";
353             # prints out 0 - class a was already added
354              
355             =head2 remove_class
356              
357             my $removed = $identifier->remove_class($class);
358              
359             Removes model for class C<$class>.
360              
361             $identifier->add_class("a", "model.a1.gz");
362             print $identifier->remove_class("a") . "\n";
363             # prints out 1
364             print $identifier->remove_class("a") . "\n";
365             # prints out 0 - class a was already removed
366              
367             =head2 get_classes
368              
369             my \@classes = $identifier->get_classes();
370              
371             Returns all registered classes.
372              
373             =head2 identify_file
374              
375             my $result = $identifier->identify_file($file)
376              
377             Identifies class for file C<$file>.
378              
379             =over
380              
381             =item * It returns undef if C<$file> is undef.
382              
383             =item * It croaks if the file C<$file> does not exist or is not readable.
384              
385             =item * Otherwise look for more details at method L</identify_handle>.
386              
387             =back
388              
389             =head2 identify_string
390              
391             my $result = $identifier->identify_string($string)
392              
393             Identifies class for string C<$string>.
394              
395             =over
396              
397             =item * It returns undef if C<$string> is undef.
398              
399             =item * Otherwise look for more details at method L</identify_handle>.
400              
401             =back
402              
403             =head2 identify_handle
404              
405             my $result = $identifier->identify_handle($fh)
406              
407             Identifies class for file handle C<$fh> and returns:
408              
409             =over
410              
411             =item * It returns undef if C<$fh> is undef.
412              
413             =item * It croaks if the C<$fh> is not file handle.
414              
415             =item * It returns array reference in format [ ['class1', score1], ['class2', score2], ...] sorted
416             according to score descendently, so the most probable class is the first.
417              
418             =back
419              
420             =head1 SEE ALSO
421              
422             =over
423              
424             =item * Identifier with pretrained models for language identification is L<Lingua::YALI::LanguageIdentifier|Lingua::YALI::LanguageIdentifier>.
425              
426             =item * Builder for these models is L<Lingua::YALI::Builder|Lingua::YALI::Builder>.
427              
428             =item * There is also command line tool L<yali-identifier|Lingua::YALI::yali-identifier> with similar functionality.
429              
430             =item * Source codes are available at L<https://github.com/martin-majlis/YALI>.
431              
432             =back
433              
434             =head1 AUTHOR
435              
436             Martin Majlis <martin@majlis.cz>
437              
438             =head1 COPYRIGHT AND LICENSE
439              
440             This software is Copyright (c) 2012 by Martin Majlis.
441              
442             This is free software, licensed under:
443              
444             The (three-clause) BSD License
445              
446             =head1 AUTHOR
447              
448             Martin Majlis <martin@majlis.cz>
449              
450             =head1 COPYRIGHT AND LICENSE
451              
452             This software is Copyright (c) 2012 by Martin Majlis.
453              
454             This is free software, licensed under:
455              
456             The (three-clause) BSD License
457              
458             =cut