Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled
Update API Documentation / build-api-docs (push) Has been cancelled
Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
|
||||
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
"""Common functions for ASR."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from itertools import groupby
|
||||
|
||||
from rapidfuzz.distance import Levenshtein
|
||||
import numpy as np
|
||||
import six
|
||||
|
||||
|
||||
def end_detect(ended_hyps, i, M=3, D_end=np.log(1 * np.exp(-10))):
|
||||
"""End detection.
|
||||
|
||||
described in Eq. (50) of S. Watanabe et al
|
||||
"Hybrid CTC/Attention Architecture for End-to-End Speech Recognition"
|
||||
|
||||
:param ended_hyps:
|
||||
:param i:
|
||||
:param M:
|
||||
:param D_end:
|
||||
:return:
|
||||
"""
|
||||
if len(ended_hyps) == 0:
|
||||
return False
|
||||
count = 0
|
||||
best_hyp = sorted(ended_hyps, key=lambda x: x["score"], reverse=True)[0]
|
||||
for m in six.moves.range(M):
|
||||
# get ended_hyps with their length is i - m
|
||||
hyp_length = i - m
|
||||
hyps_same_length = [x for x in ended_hyps if len(x["yseq"]) == hyp_length]
|
||||
if len(hyps_same_length) > 0:
|
||||
best_hyp_same_length = sorted(hyps_same_length, key=lambda x: x["score"], reverse=True)[
|
||||
0
|
||||
]
|
||||
if best_hyp_same_length["score"] - best_hyp["score"] < D_end:
|
||||
count += 1
|
||||
|
||||
if count == M:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# TODO(takaaki-hori): add different smoothing methods
|
||||
def label_smoothing_dist(odim, lsm_type, transcript=None, blank=0):
|
||||
"""Obtain label distribution for loss smoothing.
|
||||
|
||||
:param odim:
|
||||
:param lsm_type:
|
||||
:param blank:
|
||||
:param transcript:
|
||||
:return:
|
||||
"""
|
||||
if transcript is not None:
|
||||
with open(transcript, "rb") as f:
|
||||
trans_json = json.load(f)["utts"]
|
||||
|
||||
if lsm_type == "unigram":
|
||||
assert transcript is not None, "transcript is required for %s label smoothing" % lsm_type
|
||||
labelcount = np.zeros(odim)
|
||||
for k, v in trans_json.items():
|
||||
ids = np.array([int(n) for n in v["output"][0]["tokenid"].split()])
|
||||
# to avoid an error when there is no text in an uttrance
|
||||
if len(ids) > 0:
|
||||
labelcount[ids] += 1
|
||||
labelcount[odim - 1] = len(transcript) # count <eos>
|
||||
labelcount[labelcount == 0] = 1 # flooring
|
||||
labelcount[blank] = 0 # remove counts for blank
|
||||
labeldist = labelcount.astype(np.float32) / np.sum(labelcount)
|
||||
else:
|
||||
logging.error("Error: unexpected label smoothing type: %s" % lsm_type)
|
||||
sys.exit()
|
||||
|
||||
return labeldist
|
||||
|
||||
|
||||
def get_vgg2l_odim(idim, in_channel=3, out_channel=128):
|
||||
"""Return the output size of the VGG frontend.
|
||||
|
||||
:param in_channel: input channel size
|
||||
:param out_channel: output channel size
|
||||
:return: output size
|
||||
:rtype int
|
||||
"""
|
||||
idim = idim / in_channel
|
||||
idim = np.ceil(np.array(idim, dtype=np.float32) / 2) # 1st max pooling
|
||||
idim = np.ceil(np.array(idim, dtype=np.float32) / 2) # 2nd max pooling
|
||||
return int(idim) * out_channel # numer of channels
|
||||
|
||||
|
||||
class ErrorCalculator(object):
|
||||
"""Calculate CER and WER for E2E_ASR and CTC models during training.
|
||||
|
||||
:param y_hats: numpy array with predicted text
|
||||
:param y_pads: numpy array with true (target) text
|
||||
:param char_list:
|
||||
:param sym_space:
|
||||
:param sym_blank:
|
||||
:return:
|
||||
"""
|
||||
|
||||
def __init__(self, char_list, sym_space, sym_blank, report_cer=False, report_wer=False):
|
||||
"""Construct an ErrorCalculator object."""
|
||||
super(ErrorCalculator, self).__init__()
|
||||
|
||||
self.report_cer = report_cer
|
||||
self.report_wer = report_wer
|
||||
|
||||
self.char_list = char_list
|
||||
self.space = sym_space
|
||||
self.blank = sym_blank
|
||||
self.idx_blank = self.char_list.index(self.blank)
|
||||
if self.space in self.char_list:
|
||||
self.idx_space = self.char_list.index(self.space)
|
||||
else:
|
||||
self.idx_space = None
|
||||
|
||||
def __call__(self, ys_hat, ys_pad, is_ctc=False):
|
||||
"""Calculate sentence-level WER/CER score.
|
||||
|
||||
:param torch.Tensor ys_hat: prediction (batch, seqlen)
|
||||
:param torch.Tensor ys_pad: reference (batch, seqlen)
|
||||
:param bool is_ctc: calculate CER score for CTC
|
||||
:return: sentence-level WER score
|
||||
:rtype float
|
||||
:return: sentence-level CER score
|
||||
:rtype float
|
||||
"""
|
||||
cer, wer = None, None
|
||||
if is_ctc:
|
||||
return self.calculate_cer_ctc(ys_hat, ys_pad)
|
||||
elif not self.report_cer and not self.report_wer:
|
||||
return cer, wer
|
||||
|
||||
seqs_hat, seqs_true = self.convert_to_char(ys_hat, ys_pad)
|
||||
if self.report_cer:
|
||||
cer = self.calculate_cer(seqs_hat, seqs_true)
|
||||
|
||||
if self.report_wer:
|
||||
wer = self.calculate_wer(seqs_hat, seqs_true)
|
||||
return cer, wer
|
||||
|
||||
def calculate_cer_ctc(self, ys_hat, ys_pad):
|
||||
"""Calculate sentence-level CER score for CTC.
|
||||
|
||||
:param torch.Tensor ys_hat: prediction (batch, seqlen)
|
||||
:param torch.Tensor ys_pad: reference (batch, seqlen)
|
||||
:return: average sentence-level CER score
|
||||
:rtype float
|
||||
"""
|
||||
|
||||
cers, char_ref_lens = [], []
|
||||
for i, y in enumerate(ys_hat):
|
||||
y_hat = [x[0] for x in groupby(y)]
|
||||
y_true = ys_pad[i]
|
||||
seq_hat, seq_true = [], []
|
||||
for idx in y_hat:
|
||||
idx = int(idx)
|
||||
if idx != -1 and idx != self.idx_blank and idx != self.idx_space:
|
||||
seq_hat.append(self.char_list[int(idx)])
|
||||
|
||||
for idx in y_true:
|
||||
idx = int(idx)
|
||||
if idx != -1 and idx != self.idx_blank and idx != self.idx_space:
|
||||
seq_true.append(self.char_list[int(idx)])
|
||||
|
||||
hyp_chars = "".join(seq_hat)
|
||||
ref_chars = "".join(seq_true)
|
||||
if len(ref_chars) > 0:
|
||||
cers.append(Levenshtein.distance(hyp_chars, ref_chars))
|
||||
char_ref_lens.append(len(ref_chars))
|
||||
|
||||
cer_ctc = float(sum(cers)) / sum(char_ref_lens) if cers else None
|
||||
return cer_ctc
|
||||
|
||||
def convert_to_char(self, ys_hat, ys_pad):
|
||||
"""Convert index to character.
|
||||
|
||||
:param torch.Tensor seqs_hat: prediction (batch, seqlen)
|
||||
:param torch.Tensor seqs_true: reference (batch, seqlen)
|
||||
:return: token list of prediction
|
||||
:rtype list
|
||||
:return: token list of reference
|
||||
:rtype list
|
||||
"""
|
||||
seqs_hat, seqs_true = [], []
|
||||
for i, y_hat in enumerate(ys_hat):
|
||||
y_true = ys_pad[i]
|
||||
eos_true = np.where(y_true == -1)[0]
|
||||
ymax = eos_true[0] if len(eos_true) > 0 else len(y_true)
|
||||
# NOTE: padding index (-1) in y_true is used to pad y_hat
|
||||
seq_hat = [self.char_list[int(idx)] for idx in y_hat[:ymax]]
|
||||
seq_true = [self.char_list[int(idx)] for idx in y_true if int(idx) != -1]
|
||||
seq_hat_text = "".join(seq_hat).replace(self.space, " ")
|
||||
seq_hat_text = seq_hat_text.replace(self.blank, "")
|
||||
seq_true_text = "".join(seq_true).replace(self.space, " ")
|
||||
seqs_hat.append(seq_hat_text)
|
||||
seqs_true.append(seq_true_text)
|
||||
return seqs_hat, seqs_true
|
||||
|
||||
def calculate_cer(self, seqs_hat, seqs_true):
|
||||
"""Calculate sentence-level CER score.
|
||||
|
||||
:param list seqs_hat: prediction
|
||||
:param list seqs_true: reference
|
||||
:return: average sentence-level CER score
|
||||
:rtype float
|
||||
"""
|
||||
|
||||
char_eds, char_ref_lens = [], []
|
||||
for i, seq_hat_text in enumerate(seqs_hat):
|
||||
seq_true_text = seqs_true[i]
|
||||
hyp_chars = seq_hat_text.replace(" ", "")
|
||||
ref_chars = seq_true_text.replace(" ", "")
|
||||
char_eds.append(Levenshtein.distance(hyp_chars, ref_chars))
|
||||
char_ref_lens.append(len(ref_chars))
|
||||
ref_len = sum(char_ref_lens)
|
||||
return float(sum(char_eds)) / ref_len if ref_len > 0 else None
|
||||
|
||||
def calculate_wer(self, seqs_hat, seqs_true):
|
||||
"""Calculate sentence-level WER score.
|
||||
|
||||
:param list seqs_hat: prediction
|
||||
:param list seqs_true: reference
|
||||
:return: average sentence-level WER score
|
||||
:rtype float
|
||||
"""
|
||||
|
||||
word_eds, word_ref_lens = [], []
|
||||
for i, seq_hat_text in enumerate(seqs_hat):
|
||||
seq_true_text = seqs_true[i]
|
||||
hyp_words = seq_hat_text.split()
|
||||
ref_words = seq_true_text.split()
|
||||
word_eds.append(Levenshtein.distance(hyp_words, ref_words))
|
||||
word_ref_lens.append(len(ref_words))
|
||||
ref_len = sum(word_ref_lens)
|
||||
return float(sum(word_eds)) / ref_len if ref_len > 0 else None
|
||||
@@ -0,0 +1,40 @@
|
||||
import torch
|
||||
|
||||
|
||||
def th_accuracy(pad_outputs, pad_targets, ignore_label):
|
||||
"""Calculate accuracy.
|
||||
|
||||
Args:
|
||||
pad_outputs (Tensor): Prediction tensors (B * Lmax, D).
|
||||
pad_targets (LongTensor): Target label tensors (B, Lmax, D).
|
||||
ignore_label (int): Ignore label id.
|
||||
|
||||
Returns:
|
||||
float: Accuracy value (0.0 - 1.0).
|
||||
|
||||
"""
|
||||
pad_pred = pad_outputs.view(
|
||||
pad_targets.size(0), pad_targets.size(1), pad_outputs.size(1)
|
||||
).argmax(2)
|
||||
mask = pad_targets != ignore_label
|
||||
numerator = torch.sum(pad_pred.masked_select(mask) == pad_targets.masked_select(mask))
|
||||
denominator = torch.sum(mask)
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
def compute_accuracy(pad_outputs, pad_targets, ignore_label):
|
||||
"""Calculate accuracy.
|
||||
|
||||
Args:
|
||||
pad_outputs (LongTensor): Prediction tensors (B, Lmax).
|
||||
pad_targets (LongTensor): Target label tensors (B, Lmax).
|
||||
ignore_label (int): Ignore label id.
|
||||
|
||||
Returns:
|
||||
float: Accuracy value (0.0 - 1.0).
|
||||
|
||||
"""
|
||||
mask = pad_targets != ignore_label
|
||||
numerator = torch.sum(pad_outputs.masked_select(mask) == pad_targets.masked_select(mask))
|
||||
denominator = torch.sum(mask)
|
||||
return numerator.float() / denominator.float() # (FIX:MZY):return torch.Tensor type
|
||||
@@ -0,0 +1,66 @@
|
||||
import numpy as np
|
||||
from sklearn.metrics import roc_curve
|
||||
import argparse
|
||||
|
||||
|
||||
def _compute_eer(label, pred, positive_label=1):
|
||||
"""
|
||||
Python compute equal error rate (eer)
|
||||
ONLY tested on binary classification
|
||||
|
||||
:param label: ground-truth label, should be a 1-d list or np.array, each element represents the ground-truth label of one sample
|
||||
:param pred: model prediction, should be a 1-d list or np.array, each element represents the model prediction of one sample
|
||||
:param positive_label: the class that is viewed as positive class when computing EER
|
||||
:return: equal error rate (EER)
|
||||
"""
|
||||
|
||||
# all fpr, tpr, fnr, fnr, threshold are lists (in the format of np.array)
|
||||
fpr, tpr, threshold = roc_curve(label, pred, pos_label=positive_label)
|
||||
fnr = 1 - tpr
|
||||
|
||||
# the threshold of fnr == fpr
|
||||
eer_threshold = threshold[np.nanargmin(np.absolute((fnr - fpr)))]
|
||||
|
||||
# theoretically eer from fpr and eer from fnr should be identical but they can be slightly differ in reality
|
||||
eer_1 = fpr[np.nanargmin(np.absolute((fnr - fpr)))]
|
||||
eer_2 = fnr[np.nanargmin(np.absolute((fnr - fpr)))]
|
||||
|
||||
# return the mean of eer from fpr and from fnr
|
||||
eer = (eer_1 + eer_2) / 2
|
||||
return eer, eer_threshold
|
||||
|
||||
|
||||
def compute_eer(trials_path, scores_path):
|
||||
"""Compute eer.
|
||||
|
||||
Args:
|
||||
trials_path: TODO.
|
||||
scores_path: TODO.
|
||||
"""
|
||||
labels = []
|
||||
for one_line in open(trials_path, "r"):
|
||||
labels.append(one_line.strip().rsplit(" ", 1)[-1] == "target")
|
||||
labels = np.array(labels, dtype=int)
|
||||
|
||||
scores = []
|
||||
for one_line in open(scores_path, "r"):
|
||||
scores.append(float(one_line.strip().rsplit(" ", 1)[-1]))
|
||||
scores = np.array(scores, dtype=float)
|
||||
|
||||
eer, threshold = _compute_eer(labels, scores)
|
||||
return eer, threshold
|
||||
|
||||
|
||||
def main():
|
||||
"""Main."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("trials", help="trial list")
|
||||
parser.add_argument("scores", help="score file, normalized to [0, 1]")
|
||||
args = parser.parse_args()
|
||||
|
||||
eer, threshold = compute_eer(args.trials, args.scores)
|
||||
print("EER is {:.4f} at threshold {:.4f}".format(eer * 100.0, threshold))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2018 David Snyder
|
||||
# Apache 2.0
|
||||
|
||||
# This script computes the minimum detection cost function, which is a common
|
||||
# error metric used in speaker recognition. Compared to equal error-rate,
|
||||
# which assigns equal weight to false negatives and false positives, this
|
||||
# error-rate is usually used to assess performance in settings where achieving
|
||||
# a low false positive rate is more important than achieving a low false
|
||||
# negative rate. See the NIST 2016 Speaker Recognition Evaluation Plan at
|
||||
# https://www.nist.gov/sites/default/files/documents/2016/10/07/sre16_eval_plan_v1.3.pdf
|
||||
# for more details about the metric.
|
||||
from __future__ import print_function
|
||||
from operator import itemgetter
|
||||
import sys, argparse, os
|
||||
|
||||
|
||||
def GetArgs():
|
||||
"""Getargs."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Compute the minimum "
|
||||
"detection cost function along with the threshold at which it occurs. "
|
||||
"Usage: sid/compute_min_dcf.py [options...] <scores-file> "
|
||||
"<trials-file> "
|
||||
"E.g., sid/compute_min_dcf.py --p-target 0.01 --c-miss 1 --c-fa 1 "
|
||||
"exp/scores/trials data/test/trials",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--p-target",
|
||||
type=float,
|
||||
dest="p_target",
|
||||
default=0.01,
|
||||
help="The prior probability of the target speaker in a trial.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--c-miss",
|
||||
type=float,
|
||||
dest="c_miss",
|
||||
default=1,
|
||||
help="Cost of a missed detection. This is usually not changed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--c-fa",
|
||||
type=float,
|
||||
dest="c_fa",
|
||||
default=1,
|
||||
help="Cost of a spurious detection. This is usually not changed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"scores_filename",
|
||||
help="Input scores file, with columns of the form " "<utt1> <utt2> <score>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"trials_filename",
|
||||
help="Input trials file, with columns of the form " "<utt1> <utt2> <target/nontarget>",
|
||||
)
|
||||
sys.stderr.write(" ".join(sys.argv) + "\n")
|
||||
args = parser.parse_args()
|
||||
args = CheckArgs(args)
|
||||
return args
|
||||
|
||||
|
||||
def CheckArgs(args):
|
||||
"""Checkargs.
|
||||
|
||||
Args:
|
||||
args: TODO.
|
||||
"""
|
||||
if args.c_fa <= 0:
|
||||
raise Exception("--c-fa must be greater than 0")
|
||||
if args.c_miss <= 0:
|
||||
raise Exception("--c-miss must be greater than 0")
|
||||
if args.p_target <= 0 or args.p_target >= 1:
|
||||
raise Exception("--p-target must be greater than 0 and less than 1")
|
||||
return args
|
||||
|
||||
|
||||
# Creates a list of false-negative rates, a list of false-positive rates
|
||||
# and a list of decision thresholds that give those error-rates.
|
||||
def ComputeErrorRates(scores, labels):
|
||||
|
||||
# Sort the scores from smallest to largest, and also get the corresponding
|
||||
# indexes of the sorted scores. We will treat the sorted scores as the
|
||||
# thresholds at which the the error-rates are evaluated.
|
||||
"""Computeerrorrates.
|
||||
|
||||
Args:
|
||||
scores: TODO.
|
||||
labels: TODO.
|
||||
"""
|
||||
sorted_indexes, thresholds = zip(
|
||||
*sorted([(index, threshold) for index, threshold in enumerate(scores)], key=itemgetter(1))
|
||||
)
|
||||
labels = [labels[i] for i in sorted_indexes]
|
||||
fns = []
|
||||
tns = []
|
||||
|
||||
# At the end of this loop, fns[i] is the number of errors made by
|
||||
# incorrectly rejecting scores less than thresholds[i]. And, tns[i]
|
||||
# is the total number of times that we have correctly rejected scores
|
||||
# less than thresholds[i].
|
||||
for i in range(0, len(labels)):
|
||||
if i == 0:
|
||||
fns.append(labels[i])
|
||||
tns.append(1 - labels[i])
|
||||
else:
|
||||
fns.append(fns[i - 1] + labels[i])
|
||||
tns.append(tns[i - 1] + 1 - labels[i])
|
||||
positives = sum(labels)
|
||||
negatives = len(labels) - positives
|
||||
|
||||
# Now divide the false negatives by the total number of
|
||||
# positives to obtain the false negative rates across
|
||||
# all thresholds
|
||||
fnrs = [fn / float(positives) for fn in fns]
|
||||
|
||||
# Divide the true negatives by the total number of
|
||||
# negatives to get the true negative rate. Subtract these
|
||||
# quantities from 1 to get the false positive rates.
|
||||
fprs = [1 - tn / float(negatives) for tn in tns]
|
||||
return fnrs, fprs, thresholds
|
||||
|
||||
|
||||
# Computes the minimum of the detection cost function. The comments refer to
|
||||
# equations in Section 3 of the NIST 2016 Speaker Recognition Evaluation Plan.
|
||||
def ComputeMinDcf(fnrs, fprs, thresholds, p_target, c_miss, c_fa):
|
||||
"""Computemindcf.
|
||||
|
||||
Args:
|
||||
fnrs: TODO.
|
||||
fprs: TODO.
|
||||
thresholds: TODO.
|
||||
p_target: TODO.
|
||||
c_miss: TODO.
|
||||
c_fa: TODO.
|
||||
"""
|
||||
min_c_det = float("inf")
|
||||
min_c_det_threshold = thresholds[0]
|
||||
for i in range(0, len(fnrs)):
|
||||
# See Equation (2). it is a weighted sum of false negative
|
||||
# and false positive errors.
|
||||
c_det = c_miss * fnrs[i] * p_target + c_fa * fprs[i] * (1 - p_target)
|
||||
if c_det < min_c_det:
|
||||
min_c_det = c_det
|
||||
min_c_det_threshold = thresholds[i]
|
||||
# See Equations (3) and (4). Now we normalize the cost.
|
||||
c_def = min(c_miss * p_target, c_fa * (1 - p_target))
|
||||
min_dcf = min_c_det / c_def
|
||||
return min_dcf, min_c_det_threshold
|
||||
|
||||
|
||||
def compute_min_dcf(scores_filename, trials_filename, c_miss=1, c_fa=1, p_target=0.01):
|
||||
"""Compute min dcf.
|
||||
|
||||
Args:
|
||||
scores_filename: TODO.
|
||||
trials_filename: TODO.
|
||||
c_miss: TODO.
|
||||
c_fa: TODO.
|
||||
p_target: TODO.
|
||||
"""
|
||||
scores_file = open(scores_filename, "r").readlines()
|
||||
trials_file = open(trials_filename, "r").readlines()
|
||||
c_miss = c_miss
|
||||
c_fa = c_fa
|
||||
p_target = p_target
|
||||
|
||||
scores = []
|
||||
labels = []
|
||||
|
||||
trials = {}
|
||||
for line in trials_file:
|
||||
utt1, utt2, target = line.rstrip().split()
|
||||
trial = utt1 + " " + utt2
|
||||
trials[trial] = target
|
||||
|
||||
for line in scores_file:
|
||||
utt1, utt2, score = line.rstrip().split()
|
||||
trial = utt1 + " " + utt2
|
||||
if trial in trials:
|
||||
scores.append(float(score))
|
||||
if trials[trial] == "target":
|
||||
labels.append(1)
|
||||
else:
|
||||
labels.append(0)
|
||||
else:
|
||||
raise Exception("Missing entry for " + utt1 + " and " + utt2 + " " + scores_filename)
|
||||
|
||||
fnrs, fprs, thresholds = ComputeErrorRates(scores, labels)
|
||||
mindcf, threshold = ComputeMinDcf(fnrs, fprs, thresholds, p_target, c_miss, c_fa)
|
||||
return mindcf, threshold
|
||||
|
||||
|
||||
def main():
|
||||
"""Main."""
|
||||
args = GetArgs()
|
||||
mindcf, threshold = compute_min_dcf(
|
||||
args.scores_filename, args.trials_filename, args.c_miss, args.c_fa, args.p_target
|
||||
)
|
||||
sys.stdout.write(
|
||||
"minDCF is {0:.4f} at threshold {1:.4f} (p-target={2}, c-miss={3}, "
|
||||
"c-fa={4})\n".format(mindcf, threshold, args.p_target, args.c_miss, args.c_fa)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+249
@@ -0,0 +1,249 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import sys
|
||||
import hydra
|
||||
from omegaconf import DictConfig, OmegaConf, ListConfig
|
||||
|
||||
|
||||
def compute_wer(
|
||||
ref_file,
|
||||
hyp_file,
|
||||
cer_file,
|
||||
cn_postprocess=False,
|
||||
):
|
||||
"""Compute wer.
|
||||
|
||||
Args:
|
||||
ref_file: TODO.
|
||||
hyp_file: TODO.
|
||||
cer_file: TODO.
|
||||
cn_postprocess: TODO.
|
||||
"""
|
||||
rst = {
|
||||
"Wrd": 0,
|
||||
"Corr": 0,
|
||||
"Ins": 0,
|
||||
"Del": 0,
|
||||
"Sub": 0,
|
||||
"Snt": 0,
|
||||
"Err": 0.0,
|
||||
"S.Err": 0.0,
|
||||
"wrong_words": 0,
|
||||
"wrong_sentences": 0,
|
||||
}
|
||||
|
||||
hyp_dict = {}
|
||||
ref_dict = {}
|
||||
with open(hyp_file, "r") as hyp_reader:
|
||||
for line in hyp_reader:
|
||||
key = line.strip().split()[0]
|
||||
value = line.strip().split()[1:]
|
||||
if cn_postprocess:
|
||||
value = " ".join(value)
|
||||
value = value.replace(" ", "")
|
||||
# if value[0] == "请":
|
||||
# value = value[1:]
|
||||
value = [x for x in value]
|
||||
hyp_dict[key] = value
|
||||
with open(ref_file, "r") as ref_reader:
|
||||
for line in ref_reader:
|
||||
key = line.strip().split()[0]
|
||||
value = line.strip().split()[1:]
|
||||
if cn_postprocess:
|
||||
value = " ".join(value)
|
||||
value = value.replace(" ", "")
|
||||
value = [x for x in value]
|
||||
ref_dict[key] = value
|
||||
|
||||
cer_detail_writer = open(cer_file, "w")
|
||||
for hyp_key in hyp_dict:
|
||||
if hyp_key in ref_dict:
|
||||
out_item = compute_wer_by_line(hyp_dict[hyp_key], ref_dict[hyp_key])
|
||||
rst["Wrd"] += out_item["nwords"]
|
||||
rst["Corr"] += out_item["cor"]
|
||||
rst["wrong_words"] += out_item["wrong"]
|
||||
rst["Ins"] += out_item["ins"]
|
||||
rst["Del"] += out_item["del"]
|
||||
rst["Sub"] += out_item["sub"]
|
||||
rst["Snt"] += 1
|
||||
if out_item["wrong"] > 0:
|
||||
rst["wrong_sentences"] += 1
|
||||
cer_detail_writer.write(hyp_key + print_cer_detail(out_item) + "\n")
|
||||
cer_detail_writer.write(
|
||||
"ref:" + "\t" + " ".join(list(map(lambda x: x.lower(), ref_dict[hyp_key]))) + "\n"
|
||||
)
|
||||
cer_detail_writer.write(
|
||||
"hyp:" + "\t" + " ".join(list(map(lambda x: x.lower(), hyp_dict[hyp_key]))) + "\n"
|
||||
)
|
||||
cer_detail_writer.flush()
|
||||
|
||||
if rst["Wrd"] > 0:
|
||||
rst["Err"] = round(rst["wrong_words"] * 100 / rst["Wrd"], 2)
|
||||
if rst["Snt"] > 0:
|
||||
rst["S.Err"] = round(rst["wrong_sentences"] * 100 / rst["Snt"], 2)
|
||||
|
||||
cer_detail_writer.write("\n")
|
||||
cer_detail_writer.write(
|
||||
"%WER "
|
||||
+ str(rst["Err"])
|
||||
+ " [ "
|
||||
+ str(rst["wrong_words"])
|
||||
+ " / "
|
||||
+ str(rst["Wrd"])
|
||||
+ ", "
|
||||
+ str(rst["Ins"])
|
||||
+ " ins, "
|
||||
+ str(rst["Del"])
|
||||
+ " del, "
|
||||
+ str(rst["Sub"])
|
||||
+ " sub ]"
|
||||
+ "\n"
|
||||
)
|
||||
cer_detail_writer.write(
|
||||
"%SER "
|
||||
+ str(rst["S.Err"])
|
||||
+ " [ "
|
||||
+ str(rst["wrong_sentences"])
|
||||
+ " / "
|
||||
+ str(rst["Snt"])
|
||||
+ " ]"
|
||||
+ "\n"
|
||||
)
|
||||
cer_detail_writer.write(
|
||||
"Scored "
|
||||
+ str(len(hyp_dict))
|
||||
+ " sentences, "
|
||||
+ str(len(hyp_dict) - rst["Snt"])
|
||||
+ " not present in hyp."
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
cer_detail_writer.close()
|
||||
|
||||
|
||||
def compute_wer_by_line(hyp, ref):
|
||||
"""Compute wer by line.
|
||||
|
||||
Args:
|
||||
hyp: TODO.
|
||||
ref: TODO.
|
||||
"""
|
||||
hyp = list(map(lambda x: x.lower(), hyp))
|
||||
ref = list(map(lambda x: x.lower(), ref))
|
||||
|
||||
len_hyp = len(hyp)
|
||||
len_ref = len(ref)
|
||||
|
||||
cost_matrix = np.zeros((len_hyp + 1, len_ref + 1), dtype=np.int16)
|
||||
|
||||
ops_matrix = np.zeros((len_hyp + 1, len_ref + 1), dtype=np.int8)
|
||||
|
||||
for i in range(len_hyp + 1):
|
||||
cost_matrix[i][0] = i
|
||||
for j in range(len_ref + 1):
|
||||
cost_matrix[0][j] = j
|
||||
|
||||
for i in range(1, len_hyp + 1):
|
||||
for j in range(1, len_ref + 1):
|
||||
if hyp[i - 1] == ref[j - 1]:
|
||||
cost_matrix[i][j] = cost_matrix[i - 1][j - 1]
|
||||
else:
|
||||
substitution = cost_matrix[i - 1][j - 1] + 1
|
||||
insertion = cost_matrix[i - 1][j] + 1
|
||||
deletion = cost_matrix[i][j - 1] + 1
|
||||
|
||||
compare_val = [substitution, insertion, deletion]
|
||||
|
||||
min_val = min(compare_val)
|
||||
operation_idx = compare_val.index(min_val) + 1
|
||||
cost_matrix[i][j] = min_val
|
||||
ops_matrix[i][j] = operation_idx
|
||||
|
||||
match_idx = []
|
||||
i = len_hyp
|
||||
j = len_ref
|
||||
rst = {"nwords": len_ref, "cor": 0, "wrong": 0, "ins": 0, "del": 0, "sub": 0}
|
||||
while i >= 0 or j >= 0:
|
||||
i_idx = max(0, i)
|
||||
j_idx = max(0, j)
|
||||
|
||||
if ops_matrix[i_idx][j_idx] == 0: # correct
|
||||
if i - 1 >= 0 and j - 1 >= 0:
|
||||
match_idx.append((j - 1, i - 1))
|
||||
rst["cor"] += 1
|
||||
|
||||
i -= 1
|
||||
j -= 1
|
||||
|
||||
elif ops_matrix[i_idx][j_idx] == 2: # insert
|
||||
i -= 1
|
||||
rst["ins"] += 1
|
||||
|
||||
elif ops_matrix[i_idx][j_idx] == 3: # delete
|
||||
j -= 1
|
||||
rst["del"] += 1
|
||||
|
||||
elif ops_matrix[i_idx][j_idx] == 1: # substitute
|
||||
i -= 1
|
||||
j -= 1
|
||||
rst["sub"] += 1
|
||||
|
||||
if i < 0 and j >= 0:
|
||||
rst["del"] += 1
|
||||
elif j < 0 and i >= 0:
|
||||
rst["ins"] += 1
|
||||
|
||||
match_idx.reverse()
|
||||
wrong_cnt = cost_matrix[len_hyp][len_ref]
|
||||
rst["wrong"] = wrong_cnt
|
||||
|
||||
return rst
|
||||
|
||||
|
||||
def print_cer_detail(rst):
|
||||
"""Print cer detail.
|
||||
|
||||
Args:
|
||||
rst: TODO.
|
||||
"""
|
||||
return (
|
||||
"("
|
||||
+ "nwords="
|
||||
+ str(rst["nwords"])
|
||||
+ ",cor="
|
||||
+ str(rst["cor"])
|
||||
+ ",ins="
|
||||
+ str(rst["ins"])
|
||||
+ ",del="
|
||||
+ str(rst["del"])
|
||||
+ ",sub="
|
||||
+ str(rst["sub"])
|
||||
+ ") corr:"
|
||||
+ "{:.2%}".format(rst["cor"] / rst["nwords"])
|
||||
+ ",cer:"
|
||||
+ "{:.2%}".format(rst["wrong"] / rst["nwords"])
|
||||
)
|
||||
|
||||
|
||||
@hydra.main(config_name=None, version_base=None)
|
||||
def main_hydra(cfg: DictConfig):
|
||||
"""Main hydra.
|
||||
|
||||
Args:
|
||||
cfg: Configuration overrides.
|
||||
"""
|
||||
ref_file = cfg.get("ref_file", None)
|
||||
hyp_file = cfg.get("hyp_file", None)
|
||||
cer_file = cfg.get("cer_file", None)
|
||||
cn_postprocess = cfg.get("cn_postprocess", False)
|
||||
if ref_file is None or hyp_file is None or cer_file is None:
|
||||
print(
|
||||
"usage : python -m funasr.metrics.wer ++ref_file=test.ref ++hyp_file=test.hyp ++cer_file=test.wer ++cn_postprocess=false"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
compute_wer(ref_file, hyp_file, cer_file, cn_postprocess)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_hydra()
|
||||
Reference in New Issue
Block a user