mirror of
https://github.com/cmusphinx/sphinxtrain.git
synced 2026-06-16 13:14:30 +00:00
406 lines
12 KiB
Perl
406 lines
12 KiB
Perl
#!/usr/bin/perl -w
|
|
|
|
# word_align.pl - Calculate word error and accuracy for a recognition
|
|
# hypothesis file vs. a reference transcription
|
|
#
|
|
# Written by David Huggins-Daines <dhuggins@cs.cmu.edu> for Speech
|
|
# Recognition and Understanding 11-751, Carnegie Mellon University,
|
|
# October 2004.
|
|
|
|
use strict;
|
|
use Pod::Usage;
|
|
use Getopt::Long;
|
|
|
|
use open ':encoding(utf8)';
|
|
binmode(STDOUT, ":utf8");
|
|
|
|
use constant INS => 1;
|
|
use constant DEL => 2;
|
|
use constant MATCH => 3;
|
|
use constant SUBST => 4;
|
|
use constant BIG_NUMBER => 1e50;
|
|
|
|
use vars qw($Help $Verbose $CER $IgnoreUttID $SkipComments $RefHypMatch);
|
|
|
|
# Defaults
|
|
$IgnoreUttID = 0;
|
|
$SkipComments = 0;
|
|
$RefHypMatch = 1;
|
|
|
|
GetOptions(
|
|
'help|?' => \$Help,
|
|
'verbose|v' => \$Verbose,
|
|
'cer|c' => \$CER,
|
|
'ignore-uttid|i' => \$IgnoreUttID,
|
|
'comments!' => \$SkipComments,
|
|
'ref-hyp-match!' => \$RefHypMatch
|
|
|
|
) or pod2usage(1);
|
|
|
|
pod2usage(1) if $Help;
|
|
pod2usage(2) unless @ARGV == 2;
|
|
|
|
my %hyphash;
|
|
my ($ref, $hyp) = @ARGV;
|
|
my ($total_cost, $total_words, $total_hyp);
|
|
my ($total_ins, $total_del, $total_subst, $total_match);
|
|
|
|
# Build hypothesis hash (lookup by uttid)
|
|
open HYP, "<$hyp" or die "Failed to open $hyp: $!";
|
|
while (defined(my $hyp_utt=<HYP>)){
|
|
my $hyp_uttid;
|
|
($hyp_utt, $hyp_uttid) = s3_magic_norm($hyp_utt);
|
|
$hyphash{$hyp_uttid} = "$hyp_utt ($hyp_uttid)";
|
|
}
|
|
close HYP;
|
|
|
|
die "No hypotheses in: $hyp" unless %hyphash;
|
|
|
|
open REF, "<$ref" or die "Failed to open $ref: $!";
|
|
open HYP, "<$hyp" or die "Failed to open $hyp: $!";
|
|
|
|
# Main loop
|
|
while (defined(my $ref_utt = <REF>)) {
|
|
my (@res, $ref_uttid, $hyp_utt, $hyp_uttid);
|
|
|
|
last unless defined $ref_utt;
|
|
|
|
# Normalize reference utterance
|
|
@res = s3_magic_norm($ref_utt);
|
|
|
|
# Skip empty and commented lines; s3_magic_norm() returns undef.
|
|
next unless grep defined, @res;
|
|
|
|
($ref_utt, $ref_uttid) = @res;
|
|
|
|
if ($IgnoreUttID) {
|
|
$hyp_utt = <HYP>;
|
|
die "UTTID is ignored when matching reference and hypothesis ",
|
|
"files and line count of files don't match. If UTTID is ignored, ",
|
|
"then files should match not only in line count, but also line ",
|
|
"by line.\n"
|
|
unless defined($hyp_utt);
|
|
} else {
|
|
$hyp_utt = $hyphash{$ref_uttid};
|
|
if (!defined($hyp_utt)) {
|
|
next unless $RefHypMatch;
|
|
die "UTTID '$ref_uttid' from the reference file '$ref' (line $.) ",
|
|
"could not be found in the hypothesis file '$hyp'.\n";
|
|
}
|
|
}
|
|
|
|
# Normalize hypothesis utterance
|
|
($hyp_utt, $hyp_uttid) = s3_magic_norm($hyp_utt);
|
|
|
|
# Redundant: if we are here after the above check, then uttid matches.
|
|
# if(! $IgnoreUttID) {
|
|
# die "Utterance ID mismatch on line $.: $ref_uttid != $hyp_uttid"
|
|
# unless $ref_uttid eq $hyp_uttid;
|
|
# }
|
|
|
|
# Split the text into an array of words
|
|
my @ref_words = split ' ', $ref_utt;
|
|
my @hyp_words = split ' ', $hyp_utt;
|
|
if ($CER) {
|
|
# Split the text into an array of characters
|
|
@ref_words = map { split "" } @ref_words;
|
|
@hyp_words = map { split "" } @hyp_words;
|
|
}
|
|
|
|
# Initialize the alignment and backtrace matrices
|
|
my (@align_matrix, @backtrace_matrix);
|
|
initialize(\@ref_words, \@hyp_words, \@align_matrix, \@backtrace_matrix);
|
|
|
|
# Do DP alignment maintaining backtrace pointers
|
|
my $cost = align(\@ref_words, \@hyp_words,
|
|
\@align_matrix, \@backtrace_matrix);
|
|
|
|
# Find the backtrace
|
|
my ($alignment, $ins, $del, $subst, $match) =
|
|
backtrace(\@ref_words, \@hyp_words,
|
|
\@align_matrix, \@backtrace_matrix);
|
|
|
|
# Format the alignment nicely
|
|
my ($ref_align, $hyp_align) = ("", "");
|
|
foreach (@$alignment) {
|
|
my ($ref, $hyp) = @$_;
|
|
my $width = 0;
|
|
|
|
if (defined($ref) and defined($hyp)) {
|
|
if ($CER or
|
|
($ref =~ /\p{InCJKUnifiedIdeographs}/ or
|
|
$ref =~ /\p{Han}/ or
|
|
$hyp =~ /\p{Han}/)) {
|
|
# Assume this is Chinese, no capitalization
|
|
# so put ** around errors
|
|
if ($ref ne $hyp) {
|
|
$ref = "*$ref*";
|
|
$hyp = "*$hyp*";
|
|
}
|
|
} elsif ($ref eq $hyp) {
|
|
# Capitalize errors (they already are...), lowercase matches
|
|
$ref = lc $ref;
|
|
$hyp = lc $hyp;
|
|
}
|
|
}
|
|
|
|
# Replace deletions with ***
|
|
foreach ($ref, $hyp) { $_ = "***" unless defined $_ };
|
|
|
|
# Find the width of this column
|
|
foreach ($ref, $hyp) { $width = length if length > $width };
|
|
$width = 3 if $width < 3; # Make it long enough for ***
|
|
|
|
# Space out the words and concatenate them to the output
|
|
$ref_align .= sprintf("%-*s ", $width, $ref);
|
|
$hyp_align .= sprintf("%-*s ", $width, $hyp);
|
|
}
|
|
$ref_uttid = "" unless defined $ref_uttid; # avoid warnings
|
|
print "$ref_align ($ref_uttid)\n$hyp_align ($hyp_uttid)\n";
|
|
|
|
# Print out the word error and accuracy rates
|
|
my $acc = @ref_words == 0 ? 0 : $match/@ref_words;
|
|
my $error = @ref_words == 0 ? 1 : $cost/@ref_words;
|
|
printf("Words: %d Correct: %d Errors: %d Percent correct = %.2f%% " .
|
|
"Error = %.2f%% Accuracy = %.2f%%\n",
|
|
scalar(@ref_words), $match, $cost, $acc*100,
|
|
$error*100, 100-$error*100);
|
|
print "Insertions: $ins Deletions: $del Substitutions: $subst\n";
|
|
|
|
$total_cost += $cost;
|
|
$total_ins += $ins;
|
|
$total_del += $del;
|
|
$total_subst += $subst;
|
|
$total_match += $match;
|
|
$total_hyp += @hyp_words;
|
|
$total_words += @ref_words;
|
|
}
|
|
|
|
# Print out the total word error and accuracy rates
|
|
my ($acc, $error);
|
|
if ($total_words == 0) {
|
|
$acc = $total_match/$total_hyp;
|
|
$error = $total_cost/$total_hyp;
|
|
} else {
|
|
$acc = $total_match/$total_words;
|
|
$error = $total_cost/$total_words;
|
|
}
|
|
printf("TOTAL Words: %d Correct: %d Errors: %d\n" .
|
|
"TOTAL Percent correct = %.2f%% Error = %.2f%% Accuracy = %.2f%%\n",
|
|
$total_words, $total_match, $total_cost,
|
|
$acc*100, $error*100, 100-$error*100);
|
|
print("TOTAL Insertions: $total_ins Deletions: $total_del ",
|
|
"Substitutions: $total_subst\n");
|
|
|
|
# This function normalizes a line of a match file.
|
|
sub s3_magic_norm{
|
|
my ($word)=@_;
|
|
my $uttid;
|
|
|
|
# Skip empty lines
|
|
return undef if $word =~ /^\s*$/;
|
|
|
|
# Skip commented lines
|
|
if ($SkipComments) {
|
|
return undef if $word =~ /^\s*#/;
|
|
}
|
|
|
|
# Remove line endings
|
|
$word =~ s/[\n\r]+$//; # the agnostic way...
|
|
|
|
# This computes the uttid and remove it from a line.
|
|
$word =~ s/\(([^) ]+)[^)]*\)$// ;
|
|
$uttid = $1;
|
|
|
|
# Normalize case
|
|
$word = uc $word;
|
|
# Remove filler words and context cues
|
|
$word =~ s/<[^>]+>//g;
|
|
$word =~ s/\+\+[^+]+\+\+//g;
|
|
$word =~ s/\+[^+]+\+//g;
|
|
|
|
# Remove alternative pronunciations
|
|
$word =~ s/\([1-9]\)//g;
|
|
|
|
# Remove class tags
|
|
$word =~ s/:\S+//g;
|
|
|
|
# Split apart compound words and acronyms
|
|
$word =~ tr/-_./ /;
|
|
|
|
return ($word, $uttid);
|
|
}
|
|
|
|
sub initialize {
|
|
my ($ref_words, $hyp_words, $align_matrix, $backtrace_matrix) = @_;
|
|
|
|
# All initial costs along the j axis are insertions
|
|
for (my $j = 0; $j <= @$hyp_words; ++$j) {
|
|
$$align_matrix[0][$j] = $j;
|
|
}
|
|
for (my $j = 0; $j <= @$hyp_words; ++$j) {
|
|
$$backtrace_matrix[0][$j] = INS;
|
|
}
|
|
# All initial costs along the i axis are deletions
|
|
for (my $i = 0; $i <= @$ref_words; ++$i) {
|
|
$$align_matrix[$i][0] = $i;
|
|
}
|
|
for (my $i = 0; $i <= @$ref_words; ++$i) {
|
|
$$backtrace_matrix[$i][0] = DEL;
|
|
}
|
|
}
|
|
|
|
sub align {
|
|
my ($ref_words, $hyp_words, $align_matrix, $backtrace_matrix) = @_;
|
|
|
|
for (my $i = 1; $i <= @$ref_words; ++$i) {
|
|
for (my $j = 1; $j <= @$hyp_words; ++$j) {
|
|
# Find insertion, deletion, substitution scores
|
|
my ($ins, $del, $subst);
|
|
|
|
# Cost of a substitution (0 if they are equal)
|
|
my $cost = $$ref_words[$i-1] ne $$hyp_words[$j-1];
|
|
|
|
# Find insertion, deletion, substitution costs
|
|
$ins = $$align_matrix[$i][$j-1] + 1;
|
|
$del = $$align_matrix[$i-1][$j] + 1;
|
|
$subst = $$align_matrix[$i-1][$j-1] + $cost;
|
|
print "Costs at $i $j: INS $ins DEL $del SUBST $subst\n"
|
|
if $Verbose;
|
|
|
|
# Get the minimum one
|
|
my $min = BIG_NUMBER;
|
|
foreach ($ins, $del, $subst) {
|
|
if ($_ < $min) {
|
|
$min = $_;
|
|
}
|
|
}
|
|
$$align_matrix[$i][$j] = $min;
|
|
|
|
# If the costs are equal, prefer match or substitution
|
|
# (keep the path diagonal).
|
|
if ($min == $subst) {
|
|
$$backtrace_matrix[$i][$j] = MATCH + $cost;
|
|
print(($cost ? "SUBSTITUTION" : "MATCH"),
|
|
"($$ref_words[$i-1] <=> $$hyp_words[$j-1])\n")
|
|
if $Verbose;
|
|
}
|
|
elsif ($min == $ins) {
|
|
$$backtrace_matrix[$i][$j] = INS;
|
|
print "INSERTION (0 => $$hyp_words[$j-1])\n" if $Verbose;
|
|
}
|
|
elsif ($min == $del) {
|
|
$$backtrace_matrix[$i][$j] = DEL;
|
|
print "DELETION ($$ref_words[$i-1] => 0)\n" if $Verbose;
|
|
}
|
|
}
|
|
}
|
|
return $$align_matrix[@$ref_words][@$hyp_words];
|
|
}
|
|
|
|
sub backtrace {
|
|
my ($ref_words, $hyp_words, $align_matrix, $backtrace_matrix) = @_;
|
|
|
|
# Backtrace to find number of ins/del/subst
|
|
my @alignment;
|
|
my $i = @$ref_words;
|
|
my $j = @$hyp_words;
|
|
my ($inspen, $delpen, $substpen, $match) = (0, 0, 0, 0);
|
|
|
|
while (!($i == 0 and $j == 0)) {
|
|
my $pointer = $$backtrace_matrix[$i][$j];
|
|
print "Cost at $i $j: $$align_matrix[$i][$j]\n" if $Verbose;
|
|
if ($pointer == INS) {
|
|
print "INSERTION (0 => $$hyp_words[$j-1])" if $Verbose;
|
|
# Append the pair 0:hyp[j] to the front of the alignment
|
|
unshift @alignment, [undef, $$hyp_words[$j-1]];
|
|
++$inspen;
|
|
--$j;
|
|
print " - moving to $i $j\n" if $Verbose;
|
|
}
|
|
elsif ($pointer == DEL) {
|
|
print "DELETION ($$ref_words[$i-1] => 0)" if $Verbose;
|
|
# Append the pair ref[i]:0 to the front of the alignment
|
|
unshift @alignment, [$$ref_words[$i-1], undef];
|
|
++$delpen;
|
|
--$i;
|
|
print " - moving to $i $j\n" if $Verbose;
|
|
}
|
|
elsif ($pointer == MATCH) {
|
|
print "MATCH ($$ref_words[$i-1] <=> $$hyp_words[$j-1])" if $Verbose;
|
|
# Append the pair ref[i]:hyp[j] to the front of the alignment
|
|
unshift @alignment, [$$ref_words[$i-1], $$hyp_words[$j-1]];
|
|
++$match;
|
|
--$j;
|
|
--$i;
|
|
print " - moving to $i $j\n" if $Verbose;
|
|
}
|
|
elsif ($pointer == SUBST) {
|
|
print "SUBSTITUTION ($$ref_words[$i-1] <=> $$hyp_words[$j-1])"
|
|
if $Verbose;
|
|
# Append the pair ref[i]:hyp[j] to the front of the alignment
|
|
unshift @alignment, [$$ref_words[$i-1], $$hyp_words[$j-1]];
|
|
++$substpen;
|
|
--$j;
|
|
--$i;
|
|
print " - moving to $i $j\n" if $Verbose;
|
|
}
|
|
else {
|
|
last;
|
|
}
|
|
}
|
|
|
|
return (\@alignment, $inspen, $delpen, $substpen, $match);
|
|
}
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
word_align - Align reference and hypothesis files and calculate
|
|
recognition accuracy.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
word_align [options] reference_file hypothesis_file
|
|
|
|
=head1 OPTIONS
|
|
|
|
=over 8
|
|
|
|
=item B<--help>, B<-?>
|
|
|
|
Print a brief help message and exit.
|
|
|
|
=item B<--verbose>, B<-v>
|
|
|
|
Print out messages tracing the alignment algorithm.
|
|
|
|
=item B<--ignore-uttid>, B<-i>
|
|
|
|
Ignore utterance ID when aligning. This option requires that
|
|
reference and hypothesis files should strictly match line by line.
|
|
|
|
=item B<--cer>, B<-c>
|
|
|
|
Character based alignment; compute CER (Character Error Rate).
|
|
|
|
=item B<--comments>, B<--no-comments>
|
|
|
|
Specifies whenether reference file contains # commented lines which
|
|
should be ignored. B<Default>: --no-comments (read all). Useful during
|
|
sphinx_batch testing, when corresponing control and reference/transcription
|
|
file lines can be handily commented (and are ignored by the decoder, other
|
|
processing tools) and then they should also be ignored by this tool.
|
|
|
|
=item B<--ref-hyp-match>, B<--no-ref-hyp-match>
|
|
|
|
Specifies whenether reference and hypothesis files should match.
|
|
B<Default>: --ref-hyp-match (should match). Non-strict matching is useful
|
|
during sphinx_batch partial tests, when hypothesis file contains only part
|
|
of original transcription/reference utterances. The opposite still holds:
|
|
all utterances of hypothesis file should have a corresponding match in
|
|
reference file.
|
|
|
|
=cut
|