Skip to content

Commit cd15a47

Browse files
authored
Migrate editdistance metrics to rapidfuzz with empty-reference guard
Replace archived editdistance with rapidfuzz and return None when CER/WER references have zero total length. Supersedes #3105.
1 parent 6fd7bd0 commit cd15a47

3 files changed

Lines changed: 83 additions & 11 deletions

File tree

funasr/metrics/common.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import json
1010
import logging
1111
import sys
12-
1312
from itertools import groupby
13+
14+
from rapidfuzz.distance import Levenshtein
1415
import numpy as np
1516
import six
1617

@@ -155,7 +156,6 @@ def calculate_cer_ctc(self, ys_hat, ys_pad):
155156
:return: average sentence-level CER score
156157
:rtype float
157158
"""
158-
import editdistance
159159

160160
cers, char_ref_lens = [], []
161161
for i, y in enumerate(ys_hat):
@@ -175,7 +175,7 @@ def calculate_cer_ctc(self, ys_hat, ys_pad):
175175
hyp_chars = "".join(seq_hat)
176176
ref_chars = "".join(seq_true)
177177
if len(ref_chars) > 0:
178-
cers.append(editdistance.eval(hyp_chars, ref_chars))
178+
cers.append(Levenshtein.distance(hyp_chars, ref_chars))
179179
char_ref_lens.append(len(ref_chars))
180180

181181
cer_ctc = float(sum(cers)) / sum(char_ref_lens) if cers else None
@@ -214,16 +214,16 @@ def calculate_cer(self, seqs_hat, seqs_true):
214214
:return: average sentence-level CER score
215215
:rtype float
216216
"""
217-
import editdistance
218217

219218
char_eds, char_ref_lens = [], []
220219
for i, seq_hat_text in enumerate(seqs_hat):
221220
seq_true_text = seqs_true[i]
222221
hyp_chars = seq_hat_text.replace(" ", "")
223222
ref_chars = seq_true_text.replace(" ", "")
224-
char_eds.append(editdistance.eval(hyp_chars, ref_chars))
223+
char_eds.append(Levenshtein.distance(hyp_chars, ref_chars))
225224
char_ref_lens.append(len(ref_chars))
226-
return float(sum(char_eds)) / sum(char_ref_lens)
225+
ref_len = sum(char_ref_lens)
226+
return float(sum(char_eds)) / ref_len if ref_len > 0 else None
227227

228228
def calculate_wer(self, seqs_hat, seqs_true):
229229
"""Calculate sentence-level WER score.
@@ -233,13 +233,13 @@ def calculate_wer(self, seqs_hat, seqs_true):
233233
:return: average sentence-level WER score
234234
:rtype float
235235
"""
236-
import editdistance
237236

238237
word_eds, word_ref_lens = [], []
239238
for i, seq_hat_text in enumerate(seqs_hat):
240239
seq_true_text = seqs_true[i]
241240
hyp_words = seq_hat_text.split()
242241
ref_words = seq_true_text.split()
243-
word_eds.append(editdistance.eval(hyp_words, ref_words))
242+
word_eds.append(Levenshtein.distance(hyp_words, ref_words))
244243
word_ref_lens.append(len(ref_words))
245-
return float(sum(word_eds)) / sum(word_ref_lens)
244+
ref_len = sum(word_ref_lens)
245+
return float(sum(word_eds)) / ref_len if ref_len > 0 else None

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"jaconv",
3636
# Speaker & evaluation
3737
"umap_learn",
38-
"editdistance>=0.5.2",
38+
"rapidfuzz>=3.0.0",
3939
# Optional (training/enhancement)
4040
"torch_complex",
4141
"tensorboardX",
@@ -44,7 +44,7 @@
4444
],
4545
# train: The modules invoked when training only.
4646
"train": [
47-
"editdistance",
47+
"rapidfuzz>=3.0.0",
4848
],
4949
# all: The modules should be optionally installled due to some reason.
5050
# Please consider moving them to "install" occasionally
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import importlib.util
2+
import sys
3+
import types
4+
from pathlib import Path
5+
6+
7+
def _install_fake_rapidfuzz():
8+
rapidfuzz = types.ModuleType("rapidfuzz")
9+
distance = types.ModuleType("rapidfuzz.distance")
10+
11+
class Levenshtein:
12+
@staticmethod
13+
def distance(left, right):
14+
left = list(left)
15+
right = list(right)
16+
dp = list(range(len(right) + 1))
17+
for i, lval in enumerate(left, 1):
18+
prev = dp[0]
19+
dp[0] = i
20+
for j, rval in enumerate(right, 1):
21+
old = dp[j]
22+
dp[j] = min(
23+
dp[j] + 1,
24+
dp[j - 1] + 1,
25+
prev + (0 if lval == rval else 1),
26+
)
27+
prev = old
28+
return dp[-1]
29+
30+
distance.Levenshtein = Levenshtein
31+
rapidfuzz.distance = distance
32+
sys.modules.setdefault("rapidfuzz", rapidfuzz)
33+
sys.modules.setdefault("rapidfuzz.distance", distance)
34+
35+
36+
def _load_metrics_common():
37+
_install_fake_rapidfuzz()
38+
module_path = Path(__file__).resolve().parents[1] / "funasr" / "metrics" / "common.py"
39+
spec = importlib.util.spec_from_file_location("metrics_common_probe", module_path)
40+
module = importlib.util.module_from_spec(spec)
41+
sys.modules["metrics_common_probe"] = module
42+
assert spec.loader is not None
43+
spec.loader.exec_module(module)
44+
return module
45+
46+
47+
def test_cer_and_wer_return_none_when_reference_length_is_zero():
48+
common = _load_metrics_common()
49+
calc = common.ErrorCalculator(
50+
char_list=["<blank>", "a", "b", "c", "h", "e", "l", "o", "w", "r", "d", " "],
51+
sym_space=" ",
52+
sym_blank="<blank>",
53+
report_cer=True,
54+
report_wer=True,
55+
)
56+
57+
assert calc.calculate_cer(["abc"], [" "]) is None
58+
assert calc.calculate_wer(["hello world"], [" "]) is None
59+
60+
61+
def test_cer_and_wer_keep_normal_levenshtein_semantics():
62+
common = _load_metrics_common()
63+
calc = common.ErrorCalculator(
64+
char_list=["<blank>", "a", "b", "c", "x", "y", " "],
65+
sym_space=" ",
66+
sym_blank="<blank>",
67+
report_cer=True,
68+
report_wer=True,
69+
)
70+
71+
assert calc.calculate_cer(["abc"], ["axc"]) == 1 / 3
72+
assert calc.calculate_wer(["foo bar baz"], ["foo qux baz"]) == 1 / 3

0 commit comments

Comments
 (0)