Skip to content

Commit f2d133a

Browse files
authored
Merge pull request #95 from fgnt/greedy_ditcp
Add greedy DI-tcpWER
2 parents af222cd + e8beb1e commit f2d133a

10 files changed

Lines changed: 222 additions & 10 deletions

File tree

meeteval/wer/__main__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,34 @@ def greedy_dicpwer(
369369
_save_results(results, hypothesis, per_reco_out, average_out, wer_name='greedy-DI-cpWER')
370370

371371

372+
def greedy_ditcpwer(
373+
reference, hypothesis,
374+
average_out='{parent}/{stem}_greedy_ditcpwer.json',
375+
per_reco_out='{parent}/{stem}_greedy_ditcpwer_per_reco.json',
376+
regex=None,
377+
collar=0,
378+
hyp_pseudo_word_timing='character_based_points',
379+
ref_pseudo_word_timing='character_based',
380+
hypothesis_sort='segment',
381+
reference_sort='segment',
382+
uem=None,
383+
partial=False,
384+
normalizer=None,
385+
):
386+
"""Computes the time-constrained diarization-invariant cpWER (greedy DI-tcpWER)"""
387+
results = meeteval.wer.api.greedy_ditcpwer(
388+
reference, hypothesis, regex=regex,
389+
collar=collar,
390+
hyp_pseudo_word_timing=hyp_pseudo_word_timing,
391+
ref_pseudo_word_timing=ref_pseudo_word_timing,
392+
hypothesis_sort=hypothesis_sort,
393+
reference_sort=reference_sort,
394+
uem=uem, partial=partial,
395+
normalizer=normalizer,
396+
)
397+
_save_results(results, hypothesis, per_reco_out, average_out, wer_name='greedy-DI-tcpWER')
398+
399+
372400
def _merge(
373401
files: 'list[str]',
374402
out: str = '-',
@@ -695,6 +723,7 @@ def cli():
695723
cli.add_command(tcpwer)
696724
cli.add_command(tcorcwer)
697725
cli.add_command(greedy_dicpwer)
726+
cli.add_command(greedy_ditcpwer)
698727
cli.add_command(greedy_tcorcwer)
699728
cli.add_command(merge)
700729
cli.add_command(average)

meeteval/wer/api.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'tcorcwer',
1616
'greedy_tcorcwer',
1717
'greedy_dicpwer',
18+
'greedy_ditcpwer',
1819
]
1920

2021

@@ -361,6 +362,43 @@ def greedy_dicpwer(
361362
return results
362363

363364

365+
def greedy_ditcpwer(
366+
reference, hypothesis,
367+
regex=None,
368+
collar=0,
369+
hyp_pseudo_word_timing='character_based_points',
370+
ref_pseudo_word_timing='character_based',
371+
hypothesis_sort='segment',
372+
reference_sort='segment',
373+
uem=None,
374+
partial=False,
375+
normalizer=None,
376+
):
377+
"""Computes the Diarization Invariant cpWER (DI-cpWER) with a greedy
378+
algorithm."""
379+
from meeteval.wer.wer.di_cp import greedy_di_tcp_word_error_rate_multifile
380+
reference, hypothesis = _load_texts(
381+
reference, hypothesis, regex=regex,
382+
uem=uem, normalizer=normalizer,
383+
)
384+
results = greedy_di_tcp_word_error_rate_multifile(
385+
reference, hypothesis,
386+
reference_pseudo_word_level_timing=ref_pseudo_word_timing,
387+
hypothesis_pseudo_word_level_timing=hyp_pseudo_word_timing,
388+
collar=collar,
389+
reference_sort=reference_sort,
390+
hypothesis_sort=hypothesis_sort,
391+
partial=partial,
392+
)
393+
from meeteval.wer import combine_error_rates
394+
average: ErrorRate = combine_error_rates(results)
395+
if average.hypothesis_self_overlap is not None:
396+
average.hypothesis_self_overlap.warn('hypothesis')
397+
if average.reference_self_overlap is not None:
398+
average.reference_self_overlap.warn('reference')
399+
return results
400+
401+
364402
def greedy_tcorcwer(
365403
reference, hypothesis,
366404
regex=None,

meeteval/wer/matching/greedy_combination_matching.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def initialize_assignment(
180180
>>> initialize_assignment(segments, streams, initialization='cp')
181181
[0, 0]
182182
"""
183-
if initialization == 'cp':
183+
if initialization in ('cp', 'tcp'):
184184
# Special case when no streams are present
185185
if len(streams) == 0:
186186
return initialize_assignment(segments, streams, 'constant')
@@ -192,13 +192,14 @@ def initialize_assignment(
192192
# Compute cpWER to get a good starting point
193193
from meeteval.wer.wer.cp import _minimum_permutation_assignment
194194
from meeteval.wer.wer.siso import siso_levenshtein_distance
195+
from meeteval.wer.wer.time_constrained import time_constrained_siso_levenshtein_distance
195196
if isinstance(streams, SegLST):
196197
streams = streams.groupby('speaker')
197198
speaker_grouped_segments = segments.groupby('speaker')
198199
assignment, _, cost_matrix = _minimum_permutation_assignment(
199200
speaker_grouped_segments,
200201
streams,
201-
distance_fn=siso_levenshtein_distance,
202+
distance_fn=siso_levenshtein_distance if initialization == 'cp' else time_constrained_siso_levenshtein_distance,
202203
)
203204
# Use integers for the assignment labels.
204205
counter = iter(itertools.count())

meeteval/wer/wer/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
from .siso import siso_word_error_rate, siso_character_error_rate, siso_word_error_rate_multifile
55
from .error_rate import ErrorRate, combine_error_rates
66
from .time_constrained import time_constrained_minimum_permutation_word_error_rate, time_constrained_siso_word_error_rate, tcp_word_error_rate_multifile
7-
from .di_cp import greedy_di_cp_word_error_rate, DICPErrorRate, greedy_di_cp_word_error_rate_multifile
7+
from .di_cp import greedy_di_cp_word_error_rate, DICPErrorRate, greedy_di_cp_word_error_rate_multifile, greedy_di_tcp_word_error_rate, greedy_di_tcp_word_error_rate_multifile
8+
from .time_constrained_orc import time_constrained_orc_wer, time_constrained_orc_wer_multifile, greedy_time_constrained_orc_wer, greedy_time_constrained_orc_wer_multifile

meeteval/wer/wer/di_cp.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
'DICPErrorRate',
1212
'greedy_di_cp_word_error_rate',
1313
'greedy_di_cp_word_error_rate_multifile',
14+
'greedy_di_tcp_word_error_rate',
15+
'greedy_di_tcp_word_error_rate_multifile',
1416
'apply_dicp_assignment',
1517
]
1618

@@ -111,6 +113,86 @@ def greedy_di_cp_word_error_rate_multifile(
111113
)
112114

113115

116+
def greedy_di_tcp_word_error_rate(
117+
reference,
118+
hypothesis,
119+
reference_pseudo_word_level_timing='character_based',
120+
hypothesis_pseudo_word_level_timing='character_based_points',
121+
collar: int = 0,
122+
reference_sort='segment',
123+
hypothesis_sort='segment',
124+
):
125+
"""
126+
Computes the DI-tcpWER (time-constrained DI-cpWER) with a greedy algorithm
127+
128+
>>> reference = SegLST([
129+
... {'segment_index': 0, 'speaker': 'A', 'words': 'a', 'start_time': 0.0, 'end_time': 1.0},
130+
... {'segment_index': 1, 'speaker': 'A', 'words': 'b', 'start_time': 1.0, 'end_time': 2.0},
131+
... {'segment_index': 2, 'speaker': 'B', 'words': 'c', 'start_time': 2.0, 'end_time': 3.0},
132+
... {'segment_index': 3, 'speaker': 'B', 'words': 'd', 'start_time': 3.0, 'end_time': 4.0},
133+
... ])
134+
>>> greedy_di_tcp_word_error_rate(reference, reference)
135+
DICPErrorRate(error_rate=0.0, errors=0, length=4, insertions=0, deletions=0, substitutions=0, reference_self_overlap=SelfOverlap(overlap_rate=0.0, overlap_time=0, total_time=6.0), hypothesis_self_overlap=SelfOverlap(overlap_rate=0.0, overlap_time=0, total_time=6.0), assignment=('A', 'A', 'B', 'B'))
136+
>>> hypothesis = SegLST([
137+
... {'segment_index': 0, 'speaker': 'A', 'words': 'a b', 'start_time': 0.0, 'end_time': 2.0},
138+
... {'segment_index': 2, 'speaker': 'A', 'words': 'b c d', 'start_time': 1.0, 'end_time': 4.0},
139+
... ])
140+
>>> greedy_di_tcp_word_error_rate(reference, hypothesis)
141+
DICPErrorRate(error_rate=0.25, errors=1, length=4, insertions=1, deletions=0, substitutions=0, reference_self_overlap=SelfOverlap(overlap_rate=0.0, overlap_time=0, total_time=6.0), hypothesis_self_overlap=SelfOverlap(overlap_rate=0.25, overlap_time=1.0, total_time=4.0), assignment=('A', 'B'))
142+
"""
143+
144+
# The assignment of the DI-tcpWER is equal to the assignment of the tcORC-WER
145+
# with swapped arguments (reference <-> hypothesis)
146+
er = meeteval.wer.wer.time_constrained_orc.greedy_time_constrained_orc_wer(
147+
hypothesis, reference,
148+
hypothesis_pseudo_word_level_timing, reference_pseudo_word_level_timing,
149+
collar,
150+
hypothesis_sort, reference_sort
151+
)
152+
153+
# The error rate object can be constructed just from the ORC-WER error rate
154+
# object. Insertions and deletions are swapped, the length is different.
155+
return DICPErrorRate(
156+
er.errors, sum([len(s['words'].split()) for s in reference]),
157+
insertions=er.deletions,
158+
deletions=er.insertions,
159+
substitutions=er.substitutions,
160+
assignment=er.assignment,
161+
reference_self_overlap=er.hypothesis_self_overlap,
162+
hypothesis_self_overlap=er.reference_self_overlap,
163+
)
164+
165+
166+
def greedy_di_tcp_word_error_rate_multifile(
167+
reference,
168+
hypothesis,
169+
partial=False,
170+
reference_pseudo_word_level_timing='character_based',
171+
hypothesis_pseudo_word_level_timing='character_based_points',
172+
collar: int = 0,
173+
reference_sort='segment',
174+
hypothesis_sort='segment',
175+
) -> 'dict[str, DICPErrorRate]':
176+
"""
177+
Computes the (Greedy) DI-tcpWER for each example in the reference and hypothesis files.
178+
179+
To compute the overall WER, use
180+
`sum(greedy_di_tcp_word_error_rate_multifile(r, h).values())`.
181+
"""
182+
from meeteval.io.seglst import apply_multi_file
183+
return apply_multi_file(
184+
functools.partial(
185+
greedy_di_tcp_word_error_rate,
186+
reference_pseudo_word_level_timing=reference_pseudo_word_level_timing,
187+
hypothesis_pseudo_word_level_timing=hypothesis_pseudo_word_level_timing,
188+
collar=collar,
189+
reference_sort=reference_sort,
190+
hypothesis_sort=hypothesis_sort,
191+
), reference, hypothesis,
192+
partial=partial
193+
)
194+
195+
114196
def apply_dicp_assignment(
115197
assignment: 'list[int | str] | tuple[int | str]',
116198
reference: 'list[list[str]] | dict[str, list[str]] | SegLST',

meeteval/wer/wer/mimo.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class MimoErrorRate(ErrorRate):
2626
"""
2727
assignment: 'tuple[int, ...]'
2828

29+
def apply_assignment(self, reference, hypothesis):
30+
return apply_mimo_assignment(
31+
self.assignment,
32+
reference=reference,
33+
hypothesis=hypothesis,
34+
)
35+
2936

3037
def mimo_error_rate(
3138
reference: 'list[list[Iterable]] | dict[Any, list[Iterable]]',

meeteval/wer/wer/time_constrained.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,10 @@ def apply_collar(s: SegLST, collar: float):
350350
return s.map(
351351
lambda s: {
352352
**s,
353-
'start_time': [t - collar for t in s['start_time']] if isinstance(s['start_time'], list) else s['start_time'] - collar,
354-
'end_time': [t + collar for t in s['end_time']] if isinstance(s['end_time'], list) else s['end_time'] + collar,
353+
'start_time': [t - collar for t in s['start_time']] if isinstance(s['start_time'], list) else s[
354+
'start_time'] - collar,
355+
'end_time': [t + collar for t in s['end_time']] if isinstance(s['end_time'], list) else s[
356+
'end_time'] + collar,
355357
}
356358
)
357359

@@ -549,6 +551,18 @@ def time_constrained_siso_levenshtein_distance(
549551
) -> int:
550552
from meeteval.wer.matching.cy_levenshtein import time_constrained_levenshtein_distance
551553

554+
# Flatten words when segment_representation is 'segment'
555+
def flatten_words(s):
556+
if not isinstance(s['words'], list):
557+
return [s]
558+
return [
559+
{'words': w, 'start_time': st, 'end_time': et}
560+
for w, st, et in zip(s['words'], s['start_time'], s['end_time'])
561+
]
562+
563+
reference = reference.flatmap(flatten_words)
564+
hypothesis = hypothesis.flatmap(flatten_words)
565+
552566
# Ignore empty segments
553567
reference = reference.filter(lambda s: s['words'])
554568
hypothesis = hypothesis.filter(lambda s: s['words'])

meeteval/wer/wer/time_constrained_orc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def matching(reference, hypothesis):
142142
[list(zip(*r)) for r in reference.T['words', 'start_time', 'end_time']],
143143
[[w for words in stream.T['words', 'start_time', 'end_time'] for w in zip(*words)] for stream in
144144
hypothesis.values()],
145-
initial_assignment=initialize_assignment(reference, hypothesis, initialization='cp'),
145+
initial_assignment=initialize_assignment(reference, hypothesis, initialization='tcp'),
146146
)
147147
return distance, assignment
148148

tests/test_cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ def test_burn_greedy_dicp():
9696
run(f'meeteval-wer greedy_dicpwer -h hyp.stm -r ref.stm')
9797

9898

99+
def test_burn_greedy_ditcp():
100+
# Normal test with stm files
101+
run(f'python -m meeteval.wer greedy_ditcpwer -h hyp.stm -r ref.stm')
102+
run(f'meeteval-wer greedy_ditcpwer -h hyp.stm -r ref.stm')
103+
104+
99105
def test_burn_mimo():
100106
run(f'python -m meeteval.wer mimower -h hyp.stm -r ref.stm')
101107
run(f"python -m meeteval.wer mimower -h 'hyp?.stm' -r 'ref?.stm'")

tests/test_di_cp.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
from hypothesis import given, strategies as st, assume, settings
22
import meeteval
33

4-
54
seglst = st.builds(
65
meeteval.io.SegLST,
76
st.lists(
87
st.builds(
9-
dict,
8+
lambda **x: {
9+
'end_time': x['start_time'] + x.pop('duration'),
10+
**x,
11+
},
1012
speaker=st.sampled_from(['spkA', 'spkB', 'spkC', 'spkD', 'spkE', 'spkF', 'spkG', 'spkH']),
1113
words=st.text(),
14+
session_id=st.just('session1'),
15+
start_time=st.floats(min_value=0, max_value=100),
16+
duration=st.floats(min_value=0, max_value=10),
1217
),
1318
)
1419
)
1520

1621

1722
@given(seglst, seglst)
18-
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
23+
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
1924
def test_greedy_di_cp_bound_by_cp(ref, hyp):
2025
cp = meeteval.wer.wer.cp.cp_word_error_rate(ref, hyp)
2126
dicp = meeteval.wer.wer.di_cp.greedy_di_cp_word_error_rate(ref, hyp)
@@ -24,7 +29,7 @@ def test_greedy_di_cp_bound_by_cp(ref, hyp):
2429

2530

2631
@given(seglst, seglst)
27-
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
32+
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
2833
def test_greedy_di_cp_vs_greedy_orc(ref, hyp):
2934
"""
3035
Test that the total distance of the greedy di-cp algorithm is equal to the
@@ -37,3 +42,32 @@ def test_greedy_di_cp_vs_greedy_orc(ref, hyp):
3742
assert dicp.substitutions == orc.substitutions
3843
assert dicp.insertions == orc.deletions
3944
assert dicp.deletions == orc.insertions
45+
46+
47+
@given(seglst, seglst)
48+
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
49+
def test_greedy_di_tcp_bound_by_tcp(ref, hyp):
50+
cp = meeteval.wer.wer.time_constrained.tcp_word_error_rate(ref, hyp)
51+
dicp = meeteval.wer.wer.di_cp.greedy_di_tcp_word_error_rate(ref, hyp)
52+
53+
assert cp.error_rate is None and dicp.error_rate is None or cp.error_rate >= dicp.error_rate
54+
55+
56+
@given(seglst, seglst)
57+
@settings(deadline=None) # The tests take longer on the GitHub actions test servers
58+
def test_greedy_di_tcp_vs_greedy_torc(ref, hyp):
59+
"""
60+
Test that the total distance of the greedy di-tcp algorithm is equal to the
61+
distance computed by the greedy tcorc algorithm with swapped arguments
62+
"""
63+
dicp = meeteval.wer.wer.di_cp.greedy_di_tcp_word_error_rate(ref, hyp)
64+
orc = meeteval.wer.wer.time_constrained_orc.greedy_time_constrained_orc_wer(
65+
hyp, ref,
66+
reference_pseudo_word_level_timing='character_based_points',
67+
hypothesis_pseudo_word_level_timing='character_based',
68+
)
69+
70+
assert dicp.errors == orc.errors
71+
assert dicp.substitutions == orc.substitutions
72+
assert dicp.insertions == orc.deletions
73+
assert dicp.deletions == orc.insertions

0 commit comments

Comments
 (0)