Skip to content

Commit 8b50d55

Browse files
authored
RASR FSA builder by orthography (#86)
Add an FSA builder class which creates an FSA given an orthography. Moreover, the classes have been refactored in order to achieve a better structure and extensibility. Finally, more documentation has been added.
1 parent 6441692 commit 8b50d55

1 file changed

Lines changed: 223 additions & 43 deletions

File tree

i6_models/parts/rasr_fsa.py

Lines changed: 223 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
from __future__ import annotations
22

3-
__all__ = ["RasrFsaBuilder", "WeightedFsa", "RasrFsaBuilderV2", "WeightedFsaV2"]
3+
__all__ = ["WeightedFsa", "WeightedFsaV2", "RasrFsaBuilder", "RasrFsaBuilderV2", "RasrFsaBuilderByOrthography"]
44

5+
from abc import ABC, abstractmethod
56
from functools import reduce
6-
from typing import Iterable, NamedTuple, Tuple, Union
7+
from typing import TYPE_CHECKING, Any, Iterable, List, NamedTuple, Tuple, Union
78

89
import numpy as np
910
import torch
1011

1112

13+
if TYPE_CHECKING:
14+
import librasr
15+
16+
17+
FsaTuple = Tuple[int, int, np.ndarray, np.ndarray]
18+
"""
19+
FSA as a tuple containing
20+
* number of states S
21+
* number of edges E
22+
* integer edge array of shape [E, 3] where each row is an edge
23+
consisting of from-state, to-state and the emission idx
24+
* float weight array of shape [E,]
25+
26+
This format is how RASR outputs FSAs when retrieving an FSA by orthography/sequence tag:
27+
https://github.com/rwth-i6/rasr/blob/2bf347fb70f1298950a4adbda39197242f78a619/src/Python/AllophoneStateFsaBuilder.cc#L60
28+
"""
29+
30+
1231
class WeightedFsa(NamedTuple):
1332
"""
1433
Convenience class that represents an FSA. It supports scaling the weights of the
@@ -87,44 +106,143 @@ def to(self, device: Union[str, torch.device]) -> WeightedFsaV2:
87106
)
88107

89108

90-
class RasrFsaBuilder:
109+
AppendedFsa = Tuple[List[int], List[int], torch.Tensor, torch.Tensor]
110+
"""Data structure used for FSA appending (see function below)."""
111+
112+
113+
def _append_fsa(original_fsa: AppendedFsa, fsa_to_append: FsaTuple) -> AppendedFsa:
91114
"""
92-
Builder class that wraps around the librasr.AllophoneStateFsaBuilder,
93-
bringing the FSAs into the correct format for the `i6_native_ops.fbw.fbw_loss`.
115+
Appends an FSA :paramref:`fsa_to_append` at the end of another FSA :paramref:`original_fsa`.
116+
117+
:param original_fsa: Original FSA.
118+
:param fsa_to_append: FSA to concatenate to :paramref:`original_fsa`.
119+
:return: FSA with the number of states/edges, the edges, and the weights of :paramref:`fsa_to_append`
120+
appended at the end of :paramref:`original_fsa`.
121+
"""
122+
edges = torch.from_numpy(np.int32(fsa_to_append[2])).reshape((3, fsa_to_append[1]))
123+
return (
124+
original_fsa[0] + [fsa_to_append[0]], # num states
125+
original_fsa[1] + [fsa_to_append[1]], # num edges
126+
torch.hstack([original_fsa[2], edges]), # edges
127+
torch.cat([original_fsa[3], torch.from_numpy(fsa_to_append[3])]), # weights
128+
)
129+
130+
131+
class _AbstractRasrFsaBuilder(ABC):
132+
"""
133+
Builder class that wraps around the `librasr.AllophoneStateFsaBuilder` class.
134+
Creates a single FSA, and joins a batch of FSAs with the correct format for the corresponding `i6_native_ops` loss.
135+
94136
Use of this class requires a working installation of the python package `librasr`.
95-
Hence, the package is locally imported in case other classes are accessed from
96-
this module.
97-
This class provides an explicit implementation of the `__getstate__` and `__setstate__`
98-
functions, necessary for pickling as the C++-class `librasr.AllophoneStateFsaBuilder`
99-
is not picklable.
137+
Hence, the package is locally imported in case other classes are accessed from this module.
100138
101-
:param config_path: path to the RASR fsa exporter config
102-
:param tdp_scale: multiply the weights by this scale
139+
This class provides an explicit implementation of the `__getstate__` and `__setstate__` functions.
140+
This is necessary for pickling as the C++ class `librasr.AllophoneStateFsaBuilder` is not picklable.
103141
"""
104142

105143
def __init__(self, config_path: str, tdp_scale: float = 1.0):
144+
"""
145+
:param config_path: Path to the RASR FSA exporter config. The FSA builder will be created from here.
146+
:param tdp_scale: Transition scale to be applied to the weights of the FSA.
147+
"""
148+
self.config_path = config_path
149+
self.builder = self.get_builder(config_path=self.config_path)
150+
self.tdp_scale = tdp_scale
151+
152+
def get_builder(self, config_path: str) -> librasr.AllophoneStateFsaBuilder:
106153
import librasr
107154

108-
self.config_path = config_path
109155
config = librasr.Configuration()
110-
config.set_from_file(self.config_path)
111-
self.builder = librasr.AllophoneStateFsaBuilder(config)
112-
self.tdp_scale = tdp_scale
156+
config.set_from_file(config_path)
157+
return librasr.AllophoneStateFsaBuilder(config)
113158

114159
def __getstate__(self):
115160
state = self.__dict__.copy()
116161
del state["builder"]
117162
return state
118163

119164
def __setstate__(self, state):
120-
import librasr
121-
122165
self.__dict__.update(state)
123-
config = librasr.Configuration()
124-
config.set_from_file(self.config_path)
125-
self.builder = librasr.AllophoneStateFsaBuilder(config)
166+
self.builder = self.get_builder(config_path=self.config_path)
167+
168+
def apply_tdp_scale_to_fsa_tuple(self, fsa: FsaTuple, tdp_scale: float) -> FsaTuple:
169+
"""
170+
Scales the weights of an FSA represented as a tuple by the factor (TDP scale) provided.
171+
172+
:param fsa: FSA as a tuple containing
173+
* number of states S
174+
* number of edges E
175+
* integer edge array of shape [E, 3] where each row is an edge
176+
consisting of from-state, to-state and the emission idx
177+
* float weight array of shape [E,]
178+
:param tdp_scale: TDP scale by which the weights must be multiplied.
179+
:return: FSA with scaled weights corresponding to :paramref:`tdp_scale`.
180+
"""
181+
if tdp_scale == 1.0:
182+
# No scaling.
183+
return fsa
184+
else:
185+
return (fsa[0], fsa[1], fsa[2], fsa[3] * tdp_scale)
186+
187+
@abstractmethod
188+
def build_single(self, single_identifier: Any) -> FsaTuple:
189+
"""
190+
Builds a single FSA by calling the respective builder function.
191+
The specific implementation depends on the type of FSA builder that is being created.
192+
193+
Note: it's recommended that the TDP scale is applied here.
194+
For that, :funcref:`apply_tdp_scale_to_fsa_tuple` can be called.
195+
196+
:param single_identifier: Identifier of the sequence for which an FSA must be built.
197+
:return: FSA as a tuple corresponding to the sequence identified by :paramref:`single_identifier`.
198+
The returned value contains the following fields in order:
199+
* number of states S
200+
* number of edges E
201+
* integer edge array of shape [E, 3] where each row is an edge
202+
consisting of from-state, to-state and the emission idx
203+
* float weight array of shape [E,]
204+
"""
205+
...
126206

127-
def build_single(self, seq_tag: str) -> Tuple[int, int, np.ndarray, np.ndarray]:
207+
@abstractmethod
208+
def build_batched_fsa(self, fsas: Iterable[FsaTuple]) -> Union[WeightedFsa, WeightedFsaV2]:
209+
"""
210+
Creates the final FSA to be used by the corresponding `fbw` op from `i6_native_ops`.
211+
212+
:param fsas: Sequence of FSAs to be batched together.
213+
:return: Single FSA which bundles together all FSAs provided as parameter.
214+
The final object is compatible with the corresponding `fbw` op from `i6_native_ops`.
215+
"""
216+
...
217+
218+
def build_batch(self, multiple_identifiers: Iterable[Any]) -> Union[WeightedFsa, WeightedFsaV2]:
219+
"""
220+
Build and concatenate the FSAs for a batch of data.
221+
222+
:funcref:`build_single` is called once for each item in :paramref:`multiple_identifiers`
223+
in order to obtain the individual FSAs.
224+
225+
:param multiple_identifiers: Multiple elements for which the builder should create the FSAs.
226+
In order to build each individual FSA,
227+
:funcref:`build_single` should be called once for each item in :paramref:`multiple_identifiers`.
228+
:return: Single FSA which joins all other FSAs retrieved.
229+
"""
230+
231+
fsas: Iterable[FsaTuple] = map(self.build_single, multiple_identifiers)
232+
233+
return self.build_batched_fsa(fsas)
234+
235+
236+
class RasrFsaBuilder(_AbstractRasrFsaBuilder):
237+
"""
238+
Builder class that wraps around the librasr.AllophoneStateFsaBuilder,
239+
bringing the FSAs into the correct format for the `i6_native_ops.fbw.fbw_loss`.
240+
241+
:param config_path: path to the RASR fsa exporter config
242+
:param tdp_scale: multiply the weights by this scale
243+
"""
244+
245+
def build_single(self, seq_tag: str) -> FsaTuple:
128246
"""
129247
Build the FSA for the given sequence tag in the corpus.
130248
@@ -139,7 +257,7 @@ def build_single(self, seq_tag: str) -> Tuple[int, int, np.ndarray, np.ndarray]:
139257
raw_fsa = self.builder.build_by_segment_name(seq_tag)
140258
return raw_fsa
141259

142-
def build_batch(self, seq_tags: Iterable[str]) -> WeightedFsa:
260+
def build_batched_fsa(self, fsas: Iterable[FsaTuple]) -> WeightedFsa:
143261
"""
144262
Build and concatenate the FSAs for a batch of sequence tags
145263
and reformat as an input to `i6_native_ops.fbw.fbw_loss`.
@@ -149,23 +267,17 @@ def build_batch(self, seq_tags: Iterable[str]) -> WeightedFsa:
149267
the batch.
150268
Additionally we apply an optional scale to the weights.
151269
152-
:param seq_tags: an iterable object of sequence tags
270+
:param fsas: Sequence of FSAs as a tuple containing:
271+
* number of states S
272+
* number of edges E
273+
* integer edge array of shape [E, 3] where each row is an edge
274+
consisting of from-state, to-state and the emission idx
275+
* float weight array of shape [E,]
153276
:return: a concatenated FSA
154277
"""
155278

156-
def append_fsa(a, b):
157-
edges = torch.from_numpy(np.int32(b[2])).reshape((3, b[1]))
158-
return (
159-
a[0] + [b[0]], # num states
160-
a[1] + [b[1]], # num edges
161-
torch.hstack([a[2], edges]), # edges
162-
torch.cat([a[3], torch.from_numpy(b[3])]), # weights
163-
)
164-
165-
# concatenate all FSAs in the batch into a single one where state ids are not yet unique
166-
fsas = map(self.build_single, seq_tags)
167279
empty_fsa = ([], [], torch.empty((3, 0), dtype=torch.int32), torch.empty((0,)))
168-
num_states, num_edges, all_edges, all_weights = reduce(append_fsa, fsas, empty_fsa)
280+
num_states, num_edges, all_edges, all_weights = reduce(_append_fsa, fsas, empty_fsa)
169281
num_edges = torch.tensor(num_edges, dtype=torch.int32)
170282
num_states = torch.tensor(num_states, dtype=torch.int32)
171283

@@ -194,13 +306,33 @@ def append_fsa(a, b):
194306
return out_fsa
195307

196308

197-
class RasrFsaBuilderV2(RasrFsaBuilder):
309+
class _RasrFsaBuilderFbw2(_AbstractRasrFsaBuilder):
198310
"""
199-
An update of the RasrFsaBuilder that is compatible with the fbw2 op from i6_native_ops.
311+
Abstract base class for building an FSA.
312+
Internally uses allophones to model the FSA by means of `librasr.AllophoneStateFsaBuilder`.
313+
314+
The implementation is compatible with the `fbw2` op from `i6_native_ops`.
315+
316+
The user must overwrite the :funcref:`build_single` method.
317+
318+
The TDP scale must be explicitly called when running :funcref:`build_single`.
319+
For that, :funcref:`apply_tdp_scale_to_fsa_tuple` can be used.
320+
321+
Using any subclass requires a working installation of the python package `librasr`.
200322
"""
201323

202-
def build_batch(self, seq_tags: Iterable[str]) -> WeightedFsaV2:
203-
fsas = list(map(self.build_single, seq_tags))
324+
def build_batched_fsa(self, fsas: Iterable[FsaTuple]) -> WeightedFsaV2:
325+
"""
326+
Joins a set of FSAs represented as tuples into a single :classref:`WeightedFsaV2` object.
327+
328+
:param fsas: FSAs to be concatenated, represented as tuples with the following fields:
329+
* number of states S
330+
* number of edges E
331+
* integer edge array of shape [E, 3] where each row is an edge
332+
consisting of from-state, to-state and the emission idx
333+
* float weight array of shape [E,]
334+
:return: Single FSA object corresponding to the joined FSAs passed as parameter.
335+
"""
204336

205337
num_states = [f[0] for f in fsas]
206338
num_edges = [f[1] for f in fsas]
@@ -222,7 +354,55 @@ def build_batch(self, seq_tags: Iterable[str]) -> WeightedFsaV2:
222354
torch.IntTensor(np.array([start_states, end_states])),
223355
)
224356

225-
if self.tdp_scale != 1.0:
226-
out_fsa *= self.tdp_scale
227-
228357
return out_fsa
358+
359+
360+
class RasrFsaBuilderV2(_RasrFsaBuilderFbw2):
361+
"""
362+
Builds an FSA given a sequence tag.
363+
The orthography will be pulled from the corpus provided in the configuration file.
364+
Internally uses allophones to model the FSA by means of `librasr.AllophoneStateFsaBuilder`.
365+
366+
The implementation is compatible with the `fbw2` op from `i6_native_ops`.
367+
"""
368+
369+
def build_single(self, single_identifier: str) -> FsaTuple:
370+
"""
371+
Build the FSA for the given sequence tag in the corpus.
372+
373+
:param single_identifier: sequence tag
374+
:return: FSA as a tuple containing
375+
* number of states S
376+
* number of edges E
377+
* integer edge array of shape [E, 3] where each row is an edge
378+
consisting of from-state, to-state and the emission idx
379+
* float weight array of shape [E,]
380+
"""
381+
raw_fsa = self.builder.build_by_segment_name(single_identifier)
382+
383+
return self.apply_tdp_scale_to_fsa_tuple(raw_fsa, self.tdp_scale)
384+
385+
386+
class RasrFsaBuilderByOrthography(_RasrFsaBuilderFbw2):
387+
"""
388+
Builds an FSA given an orthography.
389+
Internally uses allophones to model the FSA by means of `librasr.AllophoneStateFsaBuilder`.
390+
391+
The implementation is compatible with the `fbw2` op from `i6_native_ops`.
392+
"""
393+
394+
def build_single(self, single_identifier: str) -> FsaTuple:
395+
"""
396+
Build the FSA for the given orthography in the corpus.
397+
398+
:param single_identifier: Segment orthography.
399+
:return: FSA as a tuple containing
400+
* number of states S
401+
* number of edges E
402+
* integer edge array of shape [E, 3] where each row is an edge
403+
consisting of from-state, to-state and the emission idx
404+
* float weight array of shape [E,]
405+
"""
406+
raw_fsa = self.builder.build_by_orthography(single_identifier)
407+
408+
return self.apply_tdp_scale_to_fsa_tuple(raw_fsa, self.tdp_scale)

0 commit comments

Comments
 (0)