Skip to content

Commit 2f4428f

Browse files
Copilotwannaphong
andcommitted
Add Word Error Rate (WER) metric to pythainlp.benchmarks
Co-authored-by: wannaphong <8536487+wannaphong@users.noreply.github.com>
1 parent 77921e1 commit 2f4428f

5 files changed

Lines changed: 176 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Added official support and CI testing for Python 3.14.
2323
Some features and fixes in this version are AI-assisted.
2424
See PR for prompt and details.
2525

26-
- Add BLEU and ROUGE metrics to pythainlp.benchmarks #<issue_number>
26+
- Add BLEU, ROUGE, and WER metrics to pythainlp.benchmarks #<issue_number>
2727
- Fix `royin` romanization #1172
2828
- Fix final consonant classification in `check_marttra()` #1173
2929
- Lazy load dictionaries to reduce memory usage #1186

docs/api/benchmarks.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,24 @@ ROUGE (Recall-Oriented Understudy for Gisting Evaluation) is a set of metrics fo
9191
for rouge_type, (precision, recall, fmeasure) in scores.items():
9292
print(f"{rouge_type}: P={precision:.4f}, R={recall:.4f}, F={fmeasure:.4f}")
9393
94+
Word Error Rate (WER)
95+
^^^^^^^^^^^^^^^^^^^^^
96+
97+
Word Error Rate is a common metric for evaluating speech recognition and machine translation systems. It measures the minimum number of word-level edits (insertions, deletions, substitutions) needed to transform the hypothesis into the reference.
98+
99+
.. autofunction:: pythainlp.benchmarks.word_error_rate
100+
101+
**Example:**
102+
103+
.. code-block:: python
104+
105+
from pythainlp.benchmarks import word_error_rate
106+
107+
reference = "สวัสดีครับ วันนี้อากาศดีมาก"
108+
hypothesis = "สวัสดีค่ะ วันนี้อากาศดี"
109+
wer = word_error_rate(reference, hypothesis)
110+
print(f"WER: {wer:.4f}")
111+
94112
Usage
95113
-----
96114

pythainlp/benchmarks/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# SPDX-License-Identifier: Apache-2.0
44
"""Performance benchmarking."""
55

6-
__all__: list[str] = ["benchmark", "bleu_score", "rouge_score"]
6+
__all__: list[str] = ["benchmark", "bleu_score", "rouge_score", "word_error_rate"]
77

8-
from pythainlp.benchmarks.metrics import bleu_score, rouge_score
8+
from pythainlp.benchmarks.metrics import bleu_score, rouge_score, word_error_rate
99
from pythainlp.benchmarks.word_tokenization import benchmark

pythainlp/benchmarks/metrics.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,80 @@ def rouge_score(
319319
)
320320

321321
return result
322+
323+
324+
def word_error_rate(
325+
reference: str,
326+
hypothesis: str,
327+
tokenize: str = "newmm",
328+
) -> float:
329+
"""
330+
Calculate Word Error Rate (WER) for Thai text with automatic tokenization.
331+
332+
Word Error Rate is a common metric for evaluating speech recognition
333+
and machine translation systems. It measures the minimum number of
334+
word-level edits (insertions, deletions, substitutions) needed to
335+
transform the hypothesis into the reference, normalized by the
336+
reference length.
337+
338+
WER = (S + D + I) / N
339+
340+
where:
341+
- S = number of substitutions
342+
- D = number of deletions
343+
- I = number of insertions
344+
- N = number of words in reference
345+
346+
:param str reference: reference text
347+
:param str hypothesis: hypothesis text to evaluate
348+
:param str tokenize: tokenization engine to use (default: "newmm").
349+
See :func:`pythainlp.tokenize.word_tokenize` for available engines.
350+
351+
:return: word error rate as a float (0.0 = perfect, >1.0 = very poor)
352+
:rtype: float
353+
354+
:Example:
355+
::
356+
357+
from pythainlp.benchmarks import word_error_rate
358+
359+
reference = "สวัสดีครับ วันนี้อากาศดีมาก"
360+
hypothesis = "สวัสดีค่ะ วันนี้อากาศดี"
361+
wer = word_error_rate(reference, hypothesis)
362+
print(f"WER: {wer:.4f}")
363+
"""
364+
from pythainlp.tokenize import word_tokenize
365+
366+
# Tokenize texts
367+
ref_tokens = word_tokenize(reference, engine=tokenize, keep_whitespace=False)
368+
hyp_tokens = word_tokenize(hypothesis, engine=tokenize, keep_whitespace=False)
369+
370+
# Calculate edit distance using dynamic programming
371+
r = len(ref_tokens)
372+
h = len(hyp_tokens)
373+
374+
# Create distance matrix
375+
d = [[0] * (h + 1) for _ in range(r + 1)]
376+
377+
# Initialize first row and column
378+
for i in range(r + 1):
379+
d[i][0] = i
380+
for j in range(h + 1):
381+
d[0][j] = j
382+
383+
# Fill in the rest of the matrix
384+
for i in range(1, r + 1):
385+
for j in range(1, h + 1):
386+
if ref_tokens[i - 1] == hyp_tokens[j - 1]:
387+
d[i][j] = d[i - 1][j - 1]
388+
else:
389+
substitution = d[i - 1][j - 1] + 1
390+
insertion = d[i][j - 1] + 1
391+
deletion = d[i - 1][j] + 1
392+
d[i][j] = min(substitution, insertion, deletion)
393+
394+
# Calculate WER
395+
if r == 0:
396+
return 0.0 if h == 0 else float('inf')
397+
398+
return d[r][h] / r

tests/extra/testx_benchmarks.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,81 @@ def test_rouge_score_no_overlap(self):
241241
self.assertGreaterEqual(precision, 0.0)
242242
self.assertGreaterEqual(recall, 0.0)
243243
self.assertGreaterEqual(fmeasure, 0.0)
244+
245+
def test_word_error_rate_basic(self):
246+
"""Test WER with basic Thai text."""
247+
from pythainlp.benchmarks import word_error_rate
248+
249+
reference = "สวัสดีครับ วันนี้อากาศดีมาก"
250+
hypothesis = "สวัสดีค่ะ วันนี้อากาศดี"
251+
252+
wer = word_error_rate(reference, hypothesis)
253+
254+
self.assertIsNotNone(wer)
255+
self.assertGreaterEqual(wer, 0.0)
256+
# WER can be > 1.0 if hypothesis has many insertions
257+
self.assertIsInstance(wer, float)
258+
259+
def test_word_error_rate_perfect_match(self):
260+
"""Test WER when reference and hypothesis are identical."""
261+
from pythainlp.benchmarks import word_error_rate
262+
263+
text = "สวัสดีครับ วันนี้อากาศดีมาก"
264+
265+
wer = word_error_rate(text, text)
266+
267+
# Perfect match should have WER = 0
268+
self.assertEqual(wer, 0.0)
269+
270+
def test_word_error_rate_completely_different(self):
271+
"""Test WER with completely different text."""
272+
from pythainlp.benchmarks import word_error_rate
273+
274+
reference = "สวัสดีครับ"
275+
hypothesis = "ลาก่อนค่ะ"
276+
277+
wer = word_error_rate(reference, hypothesis)
278+
279+
# Should be > 0 since texts are different
280+
self.assertGreater(wer, 0.0)
281+
282+
def test_word_error_rate_different_engines(self):
283+
"""Test WER with different tokenization engines."""
284+
from pythainlp.benchmarks import word_error_rate
285+
286+
reference = "สวัสดีครับ"
287+
hypothesis = "สวัสดีค่ะ"
288+
289+
# Test with newmm (default)
290+
wer_newmm = word_error_rate(reference, hypothesis, tokenize="newmm")
291+
self.assertIsNotNone(wer_newmm)
292+
self.assertGreaterEqual(wer_newmm, 0.0)
293+
294+
# Test with longest
295+
wer_longest = word_error_rate(reference, hypothesis, tokenize="longest")
296+
self.assertIsNotNone(wer_longest)
297+
self.assertGreaterEqual(wer_longest, 0.0)
298+
299+
def test_word_error_rate_empty_reference(self):
300+
"""Test WER with empty reference."""
301+
from pythainlp.benchmarks import word_error_rate
302+
303+
reference = ""
304+
hypothesis = "สวัสดีครับ"
305+
306+
wer = word_error_rate(reference, hypothesis)
307+
308+
# Empty reference with non-empty hypothesis should return inf or 0
309+
self.assertTrue(wer == 0.0 or wer == float('inf'))
310+
311+
def test_word_error_rate_insertions(self):
312+
"""Test WER with insertions (hypothesis longer than reference)."""
313+
from pythainlp.benchmarks import word_error_rate
314+
315+
reference = "สวัสดี"
316+
hypothesis = "สวัสดี ครับ วันนี้"
317+
318+
wer = word_error_rate(reference, hypothesis)
319+
320+
# WER can be > 1.0 due to insertions
321+
self.assertGreater(wer, 0.0)

0 commit comments

Comments
 (0)