From 912f216593441199ea1a8d995a959802455b05ec Mon Sep 17 00:00:00 2001 From: Michael McAuliffe Date: Thu, 16 Feb 2023 00:44:02 -0800 Subject: [PATCH] 2.2.1 (#568) --- docs/source/changelog/changelog_2.2.rst | 18 + docs/source/changelog/index.md | 1 + docs/source/conf.py | 2 +- docs/source/first_steps/index.rst | 45 +- montreal_forced_aligner/alignment/mixins.py | 2 +- .../command_line/train_tokenizer.py | 28 +- .../corpus/multiprocessing.py | 7 +- montreal_forced_aligner/g2p/mixins.py | 8 +- .../g2p/phonetisaurus_trainer.py | 511 +++++++++--------- montreal_forced_aligner/models.py | 27 +- .../tokenization/tokenizer.py | 146 ++++- .../tokenization/trainer.py | 449 ++++++++++++--- tests/conftest.py | 5 + .../test_tokenizer_model_phonetisaurus.zip | Bin 0 -> 72675 bytes tests/test_commandline_tokenize.py | 61 ++- 15 files changed, 948 insertions(+), 362 deletions(-) create mode 100644 docs/source/changelog/changelog_2.2.rst create mode 100644 tests/data/tokenizer/test_tokenizer_model_phonetisaurus.zip diff --git a/docs/source/changelog/changelog_2.2.rst b/docs/source/changelog/changelog_2.2.rst new file mode 100644 index 00000000..7a9e92f8 --- /dev/null +++ b/docs/source/changelog/changelog_2.2.rst @@ -0,0 +1,18 @@ + +.. _changelog_2.2: + +************* +2.2 Changelog +************* + +2.2.1 +===== + +- Fixed a couple of bugs in training Phonetisaurus models +- Added training of Phonetisaurus models for tokenizer + +2.2.0 +===== + +- Add support for training tokenizers and tokenization +- Migrate most os.path functionality to pathlib diff --git a/docs/source/changelog/index.md b/docs/source/changelog/index.md index 2d1f0fdf..705b527f 100644 --- a/docs/source/changelog/index.md +++ b/docs/source/changelog/index.md @@ -60,6 +60,7 @@ Not tied to 2.1, but in the near-ish term I would like to: :hidden: :maxdepth: 1 +changelog_2.2.rst news_2.1.rst changelog_2.1.rst news_2.0.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index f3eeb191..7285f2d7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -345,7 +345,7 @@ # "image_dark": "logo-dark.svg", }, "analytics": { - "google_analytics_id": "UA-73068199-4", + "google_analytics_id": "353930198", }, # "show_nav_level": 1, # "navigation_depth": 4, diff --git a/docs/source/first_steps/index.rst b/docs/source/first_steps/index.rst index 707b226c..3bac7a0c 100644 --- a/docs/source/first_steps/index.rst +++ b/docs/source/first_steps/index.rst @@ -34,6 +34,11 @@ There are several broad use cases that you might want to use MFA for. Take a lo #. Use the trained G2P model in :ref:`first_steps_g2p_pretrained` to generate a pronunciation dictionary #. Use the generated pronunciation dictionary in :ref:`first_steps_align_train_acoustic_model` to generate aligned TextGrids +#. **Use case 5:** You have a :ref:`speech corpus ` and the language involved is in the list of :xref:`pretrained_acoustic_models`, but the language does not mark word boundaries in its orthography. + + #. Follow :ref:`first_steps_tokenize` to tokenize the corpus + #. Use the tokenized transcripts and follow :ref:`first_steps_align_pretrained` + .. _first_steps_align_pretrained: Aligning a speech corpus with existing pronunciation dictionary and acoustic model @@ -90,8 +95,8 @@ Depending on your use case, you might have a list of words to run G2P over, or j .. code-block:: - mfa g2p english_us_arpa ~/mfa_data/my_corpus ~/mfa_data/new_dictionary.txt # If using a corpus - mfa g2p english_us_arpa ~/mfa_data/my_word_list.txt ~/mfa_data/new_dictionary.txt # If using a word list + mfa g2p ~/mfa_data/my_corpus english_us_arpa ~/mfa_data/new_dictionary.txt # If using a corpus + mfa g2p ~/mfa_data/my_word_list.txt english_us_arpa ~/mfa_data/new_dictionary.txt # If using a word list Running one of the above will output a text file pronunciation dictionary in the format that MFA uses (:ref:`dictionary_format`). I recommend looking over the pronunciations generated and make sure that they look sensible. For languages where the orthography is not transparent, it may be helpful to include :code:`--num_pronunciations 3` so that more pronunciations are generated than just the most likely one. For more details on running G2P, see :ref:`g2p_dictionary_generating`. @@ -170,11 +175,11 @@ Once the G2P model is trained, you should see the exported archive in the folder mfa model save g2p ~/mfa_data/my_g2p_model.zip - mfa g2p my_g2p_model ~/mfa_data/my_new_word_list.txt ~/mfa_data/my_new_dictionary.txt + mfa g2p ~/mfa_data/my_new_word_list.txt my_g2p_model ~/mfa_data/my_new_dictionary.txt # Or - mfa g2p ~/mfa_data/my_g2p_model.zip ~/mfa_data/my_new_word_list.txt ~/mfa_data/my_new_dictionary.txt + mfa g2p ~/mfa_data/my_new_word_list.txt ~/mfa_data/my_g2p_model.zip ~/mfa_data/my_new_dictionary.txt Take a look at :ref:`first_steps_g2p_pretrained` with this new model for a more detailed walk-through of generating a dictionary. @@ -182,6 +187,38 @@ Take a look at :ref:`first_steps_g2p_pretrained` with this new model for a more Please see :ref:`g2p_model_training_example` for an example using toy data. +.. _first_steps_tokenize: + +Tokenize a corpus to add word boundaries +---------------------------------------- + +For the purposes of this example, we'll also assume that you have done nothing else with MFA other than follow the :ref:`installation` instructions and you have the :code:`mfa` command working. Finally, we'll assume that your corpus is in Japanese and is stored in the folder :code:`~/mfa_data/my_corpus`, so when working with your data, this will be the main thing to update. + +To tokenize the Japanese text to add spaces, first download the Japanese tokenizer model via: + + +.. code-block:: + + mfa model download tokenizer japanese_mfa + +Once you have the model downloaded, you can tokenize your corpus via: + +.. code-block:: + + mfa tokenize ~/mfa_data/my_corpus japanese_mfa ~/mfa_data/tokenized_version + +You can check the tokenized text in :code:`~/mfa_data/tokenized_version`, verify that it looks good, and copy the files to replace the untokenized files in :code:`~/mfa_data/my_corpus` for use in alignment. + +.. warning:: + + MFA's tokenizer models are nowhere near state of the art, and I recommend using other tokenizers as they make sense: + + * Japanese: `nagisa `_ + * Chinese: `spacy-pkuseg `_ + * Thai: `sertiscorp/thai-word-segmentation `_ + + The above were used in the initial construction of the training corpora for MFA, though the training segmentations for Japanese have begun to diverge from :code:`nagisa`, as they break up phonological words into morphological parses where for the purposes of acoustic model training and alignment it makes more sense to not split (nagisa: :ipa_inline:`使っ て [ts ɨ k a Q t e]` vs mfa: :ipa_inline:`使って [ts ɨ k a tː e]`). The MFA tokenizer models are provided as an easy start up path as the ones listed above may have extra dependencies and platform restrictions. + .. toctree:: :maxdepth: 1 :hidden: diff --git a/montreal_forced_aligner/alignment/mixins.py b/montreal_forced_aligner/alignment/mixins.py index 56ee9c44..44a265a5 100644 --- a/montreal_forced_aligner/alignment/mixins.py +++ b/montreal_forced_aligner/alignment/mixins.py @@ -551,7 +551,7 @@ def compile_information(self) -> None: average_logdet_frames += data["logdet_frames"] average_logdet_sum += data["logdet"] * data["logdet_frames"] - if hasattr(self, "db_engine"): + if hasattr(self, "session"): csv_path = self.working_directory.joinpath("alignment_log_likelihood.csv") with mfa_open(csv_path, "w") as f, self.session() as session: writer = csv.writer(f) diff --git a/montreal_forced_aligner/command_line/train_tokenizer.py b/montreal_forced_aligner/command_line/train_tokenizer.py index 71325e36..2cf241ed 100644 --- a/montreal_forced_aligner/command_line/train_tokenizer.py +++ b/montreal_forced_aligner/command_line/train_tokenizer.py @@ -12,7 +12,10 @@ common_options, ) from montreal_forced_aligner.config import GLOBAL_CONFIG, MFA_PROFILE_VARIABLE -from montreal_forced_aligner.tokenization.trainer import TokenizerTrainer +from montreal_forced_aligner.tokenization.trainer import ( + PhonetisaurusTokenizerTrainer, + TokenizerTrainer, +) __all__ = ["train_tokenizer_cli"] @@ -48,6 +51,12 @@ "most of the data and validating on an unseen subset.", default=False, ) +@click.option( + "--phonetisaurus", + is_flag=True, + help="Flag for using Phonetisaurus-style models.", + default=False, +) @common_options @click.help_option("-h", "--help") @click.pass_context @@ -63,10 +72,19 @@ def train_tokenizer_cli(context, **kwargs) -> None: config_path = kwargs.get("config_path", None) corpus_directory = kwargs["corpus_directory"] output_model_path = kwargs["output_model_path"] - trainer = TokenizerTrainer( - corpus_directory=corpus_directory, - **TokenizerTrainer.parse_parameters(config_path, context.params, context.args), - ) + phonetisaurus = kwargs["phonetisaurus"] + if phonetisaurus: + trainer = PhonetisaurusTokenizerTrainer( + corpus_directory=corpus_directory, + **PhonetisaurusTokenizerTrainer.parse_parameters( + config_path, context.params, context.args + ), + ) + else: + trainer = TokenizerTrainer( + corpus_directory=corpus_directory, + **TokenizerTrainer.parse_parameters(config_path, context.params, context.args), + ) try: trainer.setup() diff --git a/montreal_forced_aligner/corpus/multiprocessing.py b/montreal_forced_aligner/corpus/multiprocessing.py index 22bf3a24..b30ce347 100644 --- a/montreal_forced_aligner/corpus/multiprocessing.py +++ b/montreal_forced_aligner/corpus/multiprocessing.py @@ -494,11 +494,8 @@ def _no_dictionary_sanitize(self, session): text.append(w) if character_text: character_text.append("") - if self.bracket_regex.match(w): - character_text.append(self.bracketed_word) - else: - for g in w: - character_text.append(g) + for g in w: + character_text.append(g) text = " ".join(text) character_text = " ".join(character_text) yield { diff --git a/montreal_forced_aligner/g2p/mixins.py b/montreal_forced_aligner/g2p/mixins.py index a5ec43ee..3d19ccb9 100644 --- a/montreal_forced_aligner/g2p/mixins.py +++ b/montreal_forced_aligner/g2p/mixins.py @@ -1,6 +1,6 @@ """Mixin module for G2P functionality""" import typing -from abc import ABCMeta, abstractmethod +from abc import ABCMeta from pathlib import Path from typing import Dict, List @@ -36,7 +36,6 @@ def __init__( self.g2p_threshold = g2p_threshold self.include_bracketed = include_bracketed - @abstractmethod def generate_pronunciations(self) -> Dict[str, List[str]]: """ Generate pronunciations @@ -46,13 +45,12 @@ def generate_pronunciations(self) -> Dict[str, List[str]]: dict[str, list[str]] Mappings of keys to their generated pronunciations """ - ... + raise NotImplementedError @property - @abstractmethod def words_to_g2p(self) -> List[str]: """Words to produce pronunciations""" - ... + raise NotImplementedError class G2PTopLevelMixin(MfaWorker, DictionaryMixin, G2PMixin): diff --git a/montreal_forced_aligner/g2p/phonetisaurus_trainer.py b/montreal_forced_aligner/g2p/phonetisaurus_trainer.py index c9d34954..94c79293 100644 --- a/montreal_forced_aligner/g2p/phonetisaurus_trainer.py +++ b/montreal_forced_aligner/g2p/phonetisaurus_trainer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import abc import collections import logging import multiprocessing as mp @@ -93,8 +92,8 @@ class AlignmentInitArguments: deletions: bool insertions: bool restrict: bool - phone_order: int - grapheme_order: int + output_order: int + input_order: int eps: str s1s2_sep: str seq_sep: str @@ -137,8 +136,8 @@ def __init__( self.deletions = args.deletions self.insertions = args.insertions self.restrict = args.restrict - self.phone_order = args.phone_order - self.grapheme_order = args.grapheme_order + self.output_order = args.output_order + self.input_order = args.input_order self.eps = args.eps self.s1s2_sep = args.s1s2_sep self.seq_sep = args.seq_sep @@ -149,6 +148,19 @@ def __init__( self.db_string = args.db_string self.batch_size = args.batch_size + def data_generator(self, session): + query = ( + session.query(Word.word, Pronunciation.pronunciation) + .join(Pronunciation.word) + .join(Word.job) + .filter(Word2Job.training == True) # noqa + .filter(Word2Job.job_id == self.job_name) + ) + for w, p in query: + w = list(w) + p = p.split() + yield w, p + def run(self) -> None: """Run the function""" engine = sqlalchemy.create_engine( @@ -161,59 +173,51 @@ def run(self) -> None: try: symbol_table = pynini.SymbolTable() symbol_table.add_symbol(self.eps) - Session = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False)) - valid_phone_ngrams = set() + valid_output_ngrams = set() base_dir = os.path.dirname(self.far_path) - with mfa_open(os.path.join(base_dir, "phone_ngram.ngrams"), "r") as f: + with mfa_open(os.path.join(base_dir, "output_ngram.ngrams"), "r") as f: for line in f: line = line.strip() - valid_phone_ngrams.add(line) - valid_grapheme_ngrams = set() - with mfa_open(os.path.join(base_dir, "grapheme_ngram.ngrams"), "r") as f: + valid_output_ngrams.add(line) + valid_input_ngrams = set() + with mfa_open(os.path.join(base_dir, "input_ngram.ngrams"), "r") as f: for line in f: line = line.strip() - valid_grapheme_ngrams.add(line) + valid_input_ngrams.add(line) count = 0 data = {} - with mfa_open(self.log_path, "w") as log_file, Session() as session: + with mfa_open(self.log_path, "w") as log_file, sqlalchemy.orm.Session( + engine + ) as session: far_writer = pywrapfst.FarWriter.create(self.far_path, arc_type="log") - query = ( - session.query(Pronunciation.pronunciation, Word.word) - .join(Pronunciation.word) - .join(Word.job) - .filter(Word2Job.training == True) # noqa - .filter(Word2Job.job_id == self.job_name) - ) - for current_index, (phones, graphemes) in enumerate(query): - graphemes = list(graphemes) - phones = phones.split() + for current_index, (input, output) in enumerate(self.data_generator(session)): if self.stopped.stop_check(): continue try: key = f"{current_index:08x}" fst = pynini.Fst(arc_type="log") - final_state = ((len(graphemes) + 1) * (len(phones) + 1)) - 1 + final_state = ((len(input) + 1) * (len(output) + 1)) - 1 for _ in range(final_state + 1): fst.add_state() - for i in range(len(graphemes) + 1): - for j in range(len(phones) + 1): - istate = i * (len(phones) + 1) + j + for i in range(len(input) + 1): + for j in range(len(output) + 1): + istate = i * (len(output) + 1) + j if self.deletions: - for phone_range in range(1, self.phone_order + 1): - if j + phone_range <= len(phones): - subseq_phones = phones[j : j + phone_range] - phone_string = self.seq_sep.join(subseq_phones) + for output_range in range(1, self.output_order + 1): + if j + output_range <= len(output): + subseq_output = output[j : j + output_range] + output_string = self.seq_sep.join(subseq_output) if ( - phone_range > 1 - and phone_string not in valid_phone_ngrams + output_range > 1 + and output_string not in valid_output_ngrams ): continue - symbol = self.s1s2_sep.join([self.skip, phone_string]) + symbol = self.s1s2_sep.join([self.skip, output_string]) ilabel = symbol_table.find(symbol) if ilabel == pynini.NO_LABEL: ilabel = symbol_table.add_symbol(symbol) - ostate = i * (len(phones) + 1) + (j + phone_range) + ostate = i * (len(output) + 1) + (j + output_range) fst.add_arc( istate, pywrapfst.Arc( @@ -224,22 +228,20 @@ def run(self) -> None: ), ) if self.insertions: - for grapheme_range in range(1, self.grapheme_order + 1): - if i + grapheme_range <= len(graphemes): - subseq_graphemes = graphemes[i : i + grapheme_range] - grapheme_string = self.seq_sep.join(subseq_graphemes) + for input_range in range(1, self.input_order + 1): + if i + input_range <= len(input): + subseq_input = input[i : i + input_range] + input_string = self.seq_sep.join(subseq_input) if ( - grapheme_range > 1 - and grapheme_string not in valid_grapheme_ngrams + input_range > 1 + and input_string not in valid_input_ngrams ): continue - symbol = self.s1s2_sep.join( - [grapheme_string, self.skip] - ) + symbol = self.s1s2_sep.join([input_string, self.skip]) ilabel = symbol_table.find(symbol) if ilabel == pynini.NO_LABEL: ilabel = symbol_table.add_symbol(symbol) - ostate = (i + grapheme_range) * (len(phones) + 1) + j + ostate = (i + input_range) * (len(output) + 1) + j fst.add_arc( istate, pywrapfst.Arc( @@ -250,39 +252,39 @@ def run(self) -> None: ), ) - for grapheme_range in range(1, self.grapheme_order + 1): - for phone_range in range(1, self.phone_order + 1): - if i + grapheme_range <= len( - graphemes - ) and j + phone_range <= len(phones): + for input_range in range(1, self.input_order + 1): + for output_range in range(1, self.output_order + 1): + if i + input_range <= len( + input + ) and j + output_range <= len(output): if ( self.restrict - and grapheme_range > 1 - and phone_range > 1 + and input_range > 1 + and output_range > 1 ): continue - subseq_phones = phones[j : j + phone_range] - phone_string = self.seq_sep.join(subseq_phones) + subseq_output = output[j : j + output_range] + output_string = self.seq_sep.join(subseq_output) if ( - phone_range > 1 - and phone_string not in valid_phone_ngrams + output_range > 1 + and output_string not in valid_output_ngrams ): continue - subseq_graphemes = graphemes[i : i + grapheme_range] - grapheme_string = self.seq_sep.join(subseq_graphemes) + subseq_input = input[i : i + input_range] + input_string = self.seq_sep.join(subseq_input) if ( - grapheme_range > 1 - and grapheme_string not in valid_grapheme_ngrams + input_range > 1 + and input_string not in valid_input_ngrams ): continue symbol = self.s1s2_sep.join( - [grapheme_string, phone_string] + [input_string, output_string] ) ilabel = symbol_table.find(symbol) if ilabel == pynini.NO_LABEL: ilabel = symbol_table.add_symbol(symbol) - ostate = (i + grapheme_range) * (len(phones) + 1) + ( - j + phone_range + ostate = (i + input_range) * (len(output) + 1) + ( + j + output_range ) fst.add_arc( istate, @@ -290,7 +292,7 @@ def run(self) -> None: ilabel, ilabel, pynini.Weight( - "log", float(grapheme_range * phone_range) + "log", float(input_range * output_range) ), ostate, ), @@ -706,6 +708,8 @@ class PhonetisaurusTrainerMixin: Threshold of minimum change for early stopping of EM training """ + alignment_init_function = AlignmentInitWorker + def __init__( self, order: int = 8, @@ -742,6 +746,8 @@ def __init__( self.deletions = deletions self.grapheme_order = grapheme_order self.phone_order = phone_order + self.input_order = self.grapheme_order + self.output_order = self.phone_order self.sequence_separator = sequence_separator self.alignment_separator = alignment_separator self.skip = skip @@ -775,7 +781,7 @@ def initialize_alignments(self) -> None: stopped = Stopped() finished_adding = Stopped() procs = [] - for i in range(GLOBAL_CONFIG.num_jobs): + for i in range(1, GLOBAL_CONFIG.num_jobs + 1): args = AlignmentInitArguments( self.db_string, self.working_log_directory.joinpath(f"alignment_init.{i}.log"), @@ -792,7 +798,7 @@ def initialize_alignments(self) -> None: self.batch_size, ) procs.append( - AlignmentInitWorker( + self.alignment_init_function( i, return_queue, stopped, @@ -800,7 +806,7 @@ def initialize_alignments(self) -> None: args, ) ) - procs[i].start() + procs[-1].start() finished_adding.stop() error_list = [] @@ -904,7 +910,7 @@ def maximization(self, last_iteration=False) -> float: return_queue = mp.Queue() stopped = Stopped() procs = [] - for i in range(GLOBAL_CONFIG.num_jobs): + for i in range(1, GLOBAL_CONFIG.num_jobs + 1): args = MaximizationArguments( self.db_string, self.working_directory.joinpath(f"{i}.far"), @@ -912,7 +918,7 @@ def maximization(self, last_iteration=False) -> float: self.batch_size, ) procs.append(MaximizationWorker(i, return_queue, stopped, args)) - procs[i].start() + procs[-1].start() error_list = [] with tqdm.tqdm( @@ -958,14 +964,14 @@ def expectation(self) -> None: stopped = Stopped() error_list = [] procs = [] - for i in range(GLOBAL_CONFIG.num_jobs): + for i in range(1, GLOBAL_CONFIG.num_jobs + 1): args = ExpectationArguments( self.db_string, self.working_directory.joinpath(f"{i}.far"), self.batch_size, ) procs.append(ExpectationWorker(i, return_queue, stopped, args)) - procs[i].start() + procs[-1].start() mappings = {} zero = pynini.Weight.zero("log") with tqdm.tqdm( @@ -1020,7 +1026,7 @@ def train_ngram_model(self) -> None: error_list = [] procs = [] count_paths = [] - for i in range(GLOBAL_CONFIG.num_jobs): + for i in range(1, GLOBAL_CONFIG.num_jobs + 1): args = NgramCountArguments( self.working_log_directory.joinpath(f"ngram_count.{i}.log"), self.working_directory.joinpath(f"{i}.far"), @@ -1029,7 +1035,7 @@ def train_ngram_model(self) -> None: ) procs.append(NgramCountWorker(return_queue, stopped, args)) count_paths.append(args.far_path.with_suffix(".cnts")) - procs[i].start() + procs[-1].start() with tqdm.tqdm( total=self.g2p_num_training_pronunciations, disable=GLOBAL_CONFIG.quiet @@ -1060,17 +1066,20 @@ def train_ngram_model(self) -> None: logger.info("Training ngram model...") with mfa_open(self.working_log_directory.joinpath("model.log"), "w") as logf: - ngrammerge_proc = subprocess.Popen( - [ - thirdparty_binary("ngrammerge"), - f'--ofile={self.ngram_path.with_suffix(".cnts")}', - *count_paths, - ], - stderr=logf, - # stdout=subprocess.PIPE, - env=os.environ, - ) - ngrammerge_proc.communicate() + if len(count_paths) > 1: + ngrammerge_proc = subprocess.Popen( + [ + thirdparty_binary("ngrammerge"), + f'--ofile={self.ngram_path.with_suffix(".cnts")}', + *count_paths, + ], + stderr=logf, + # stdout=subprocess.PIPE, + env=os.environ, + ) + ngrammerge_proc.communicate() + else: + os.rename(count_paths[0], self.ngram_path.with_suffix(".cnts")) ngrammake_proc = subprocess.Popen( [ thirdparty_binary("ngrammake"), @@ -1215,21 +1224,6 @@ def train_alignments(self) -> None: if change < self.em_threshold: break - @property - @abc.abstractmethod - def working_directory(self) -> Path: - ... - - @property - @abc.abstractmethod - def working_log_directory(self) -> Path: - ... - - @property - @abc.abstractmethod - def db_string(self) -> str: - ... - @property def data_directory(self) -> Path: """Data directory for trainer""" @@ -1312,7 +1306,7 @@ def export_alignments(self) -> None: error_list = [] procs = [] count_paths = [] - for i in range(GLOBAL_CONFIG.num_jobs): + for i in range(1, GLOBAL_CONFIG.num_jobs + 1): args = AlignmentExportArguments( self.db_string, self.working_log_directory.joinpath(f"ngram_count.{i}.log"), @@ -1321,7 +1315,7 @@ def export_alignments(self) -> None: ) procs.append(AlignmentExporter(return_queue, stopped, args)) count_paths.append(args.far_path.with_suffix(".cnts")) - procs[i].start() + procs[-1].start() with tqdm.tqdm( total=self.g2p_num_training_pronunciations, disable=GLOBAL_CONFIG.quiet @@ -1361,8 +1355,8 @@ def export_alignments(self) -> None: stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) - for j in range(GLOBAL_CONFIG.num_jobs): - text_path = self.working_directory.joinpath(f"{j}.far.strings") + for j in range(1, GLOBAL_CONFIG.num_jobs + 1): + text_path = self.working_directory.joinpath(f"{j}.strings") with mfa_open(text_path, "r") as f: for line in f: symbols_proc.stdin.write(line) @@ -1372,146 +1366,18 @@ def export_alignments(self) -> None: self.symbol_table = pynini.SymbolTable.read_text(self.alignment_symbols_path) logger.info("Done exporting alignments!") - -class PhonetisaurusTrainer( - MultispeakerDictionaryMixin, PhonetisaurusTrainerMixin, G2PTrainer, TopLevelMfaWorker -): - """ - Top level trainer class for Phonetisaurus-style models - """ - - def __init__( - self, - **kwargs, - ): - self._data_source = kwargs["dictionary_path"].stem - super().__init__(**kwargs) - self.ler = None - self.wer = None - - @property - def data_directory(self) -> Path: - """Data directory for trainer""" - return self.working_directory - - @property - def configuration(self) -> MetaDict: - """Configuration for G2P trainer""" - config = super().configuration - config.update({"dictionary_path": str(self.dictionary_model.path)}) - return config - - def setup(self) -> None: - """Setup for G2P training""" - super().setup() - self.create_new_current_workflow(WorkflowType.train_g2p) - wf = self.current_workflow - if wf.done: - logger.info("G2P training already done, skipping.") - return - self.dictionary_setup() - os.makedirs(self.phones_dir, exist_ok=True) - self.initialize_training() - self.initialized = True - - def train(self) -> None: - """ - Train a G2P model - """ - os.makedirs(self.working_log_directory, exist_ok=True) - begin = time.time() - self.train_alignments() - logger.debug( - f"Aligning {len(self.g2p_training_dictionary)} words took {time.time() - begin:.3f} seconds" - ) - self.export_alignments() - begin = time.time() - self.train_ngram_model() - logger.debug( - f"Generating model for {len(self.g2p_training_dictionary)} words took {time.time() - begin:.3f} seconds" - ) - self.finalize_training() - - def finalize_training(self) -> None: - """Finalize training and run evaluation if specified""" - if self.evaluation_mode: - self.evaluate_g2p_model() - - @property - def meta(self) -> MetaDict: - """Metadata for exported G2P model""" - from datetime import datetime - - from ..utils import get_mfa_version - - m = { - "version": get_mfa_version(), - "architecture": self.architecture, - "train_date": str(datetime.now()), - "phones": sorted(self.non_silence_phones), - "graphemes": self.g2p_training_graphemes, - "grapheme_order": self.grapheme_order, - "phone_order": self.phone_order, - "sequence_separator": self.sequence_separator, - "evaluation": {}, - "training": { - "num_words": self.g2p_num_training_words, - "num_graphemes": len(self.g2p_training_graphemes), - "num_phones": len(self.non_silence_phones), - }, - } - - if self.evaluation_mode: - m["evaluation"]["num_words"] = self.g2p_num_validation_words - m["evaluation"]["word_error_rate"] = self.wer - m["evaluation"]["phone_error_rate"] = self.ler - return m - - def evaluate_g2p_model(self) -> None: - """ - Validate the G2P model against held out data - """ - temp_model_path = self.working_log_directory.joinpath("g2p_model.zip") - self.export_model(temp_model_path) - temp_dir = self.working_directory.joinpath("validation") - os.makedirs(temp_dir, exist_ok=True) - with self.session() as session: - validation_set = collections.defaultdict(set) - query = ( - session.query(Word.word, Pronunciation.pronunciation) - .join(Pronunciation.word) - .join(Word.job) - .filter(Word2Job.training == False) # noqa - ) - for w, pron in query: - validation_set[w].add(pron) - gen = PyniniValidator( - g2p_model_path=temp_model_path, - word_list=list(validation_set.keys()), - num_pronunciations=self.num_pronunciations, - ) - output = gen.generate_pronunciations() - with mfa_open(temp_dir.joinpath("validation_output.txt"), "w") as f: - for (orthography, pronunciations) in output.items(): - if not pronunciations: - continue - for p in pronunciations: - if not p: - continue - f.write(f"{orthography}\t{p}\n") - gen.compute_validation_errors(validation_set, output) - def compute_initial_ngrams(self) -> None: - word_path = self.working_directory.joinpath("words.txt") - word_ngram_path = self.working_directory.joinpath("grapheme_ngram.fst") - word_symbols_path = self.working_directory.joinpath("grapheme_ngram.syms") + logger.info("Computing initial ngrams...") + input_path = self.working_directory.joinpath("input.txt") + input_ngram_path = self.working_directory.joinpath("input_ngram.fst") + input_symbols_path = self.working_directory.joinpath("input_ngram.syms") symbols_proc = subprocess.Popen( [ thirdparty_binary("ngramsymbols"), "--OOV_symbol=", "--epsilon_symbol=", - word_path, - word_symbols_path, + input_path, + input_symbols_path, ], encoding="utf8", ) @@ -1520,8 +1386,8 @@ def compute_initial_ngrams(self) -> None: [ thirdparty_binary("farcompilestrings"), "--token_type=symbol", - f"--symbols={word_symbols_path}", - word_path, + f"--symbols={input_symbols_path}", + input_path, ], stdout=subprocess.PIPE, env=os.environ, @@ -1531,7 +1397,7 @@ def compute_initial_ngrams(self) -> None: thirdparty_binary("ngramcount"), "--require_symbols=false", "--round_to_int", - f"--order={self.grapheme_order}", + f"--order={self.input_order}", ], stdin=farcompile_proc.stdout, stdout=subprocess.PIPE, @@ -1560,7 +1426,7 @@ def compute_initial_ngrams(self) -> None: print_proc = subprocess.Popen( [ thirdparty_binary("ngramprint"), - f"--symbols={word_symbols_path}", + f"--symbols={input_symbols_path}", ], env=os.environ, stdin=ngramshrink_proc.stdout, @@ -1576,20 +1442,20 @@ def compute_initial_ngrams(self) -> None: ngrams.add(ngram) print_proc.wait() - with mfa_open(word_ngram_path.with_suffix(".ngrams"), "w") as f: + with mfa_open(input_ngram_path.with_suffix(".ngrams"), "w") as f: for ngram in sorted(ngrams): f.write(f"{ngram}\n") - phone_path = self.working_directory.joinpath("pronunciations.txt") - phone_ngram_path = self.working_directory.joinpath("phone_ngram.fst") - phone_symbols_path = self.working_directory.joinpath("phone_ngram.syms") + output_path = self.working_directory.joinpath("output.txt") + output_ngram_path = self.working_directory.joinpath("output_ngram.fst") + output_symbols_path = self.working_directory.joinpath("output_ngram.syms") symbols_proc = subprocess.Popen( [ thirdparty_binary("ngramsymbols"), "--OOV_symbol=", "--epsilon_symbol=", - phone_path, - phone_symbols_path, + output_path, + output_symbols_path, ], encoding="utf8", ) @@ -1598,8 +1464,8 @@ def compute_initial_ngrams(self) -> None: [ thirdparty_binary("farcompilestrings"), "--token_type=symbol", - f"--symbols={phone_symbols_path}", - phone_path, + f"--symbols={output_symbols_path}", + output_path, ], stdout=subprocess.PIPE, env=os.environ, @@ -1609,7 +1475,7 @@ def compute_initial_ngrams(self) -> None: thirdparty_binary("ngramcount"), "--require_symbols=false", "--round_to_int", - f"--order={self.phone_order}", + f"--order={self.output_order}", ], stdin=farcompile_proc.stdout, stdout=subprocess.PIPE, @@ -1636,7 +1502,7 @@ def compute_initial_ngrams(self) -> None: env=os.environ, ) print_proc = subprocess.Popen( - [thirdparty_binary("ngramprint"), f"--symbols={phone_symbols_path}"], + [thirdparty_binary("ngramprint"), f"--symbols={output_symbols_path}"], env=os.environ, stdin=ngramshrink_proc.stdout, stdout=subprocess.PIPE, @@ -1651,10 +1517,139 @@ def compute_initial_ngrams(self) -> None: ngrams.add(ngram) print_proc.wait() - with mfa_open(phone_ngram_path.with_suffix(".ngrams"), "w") as f: + with mfa_open(output_ngram_path.with_suffix(".ngrams"), "w") as f: for ngram in sorted(ngrams): f.write(f"{ngram}\n") + def train(self) -> None: + """ + Train a G2P model + """ + os.makedirs(self.working_log_directory, exist_ok=True) + begin = time.time() + self.train_alignments() + logger.debug( + f"Aligning {len(self.g2p_training_dictionary)} words took {time.time() - begin:.3f} seconds" + ) + self.export_alignments() + begin = time.time() + self.train_ngram_model() + logger.debug( + f"Generating model for {len(self.g2p_training_dictionary)} words took {time.time() - begin:.3f} seconds" + ) + self.finalize_training() + + +class PhonetisaurusTrainer( + MultispeakerDictionaryMixin, PhonetisaurusTrainerMixin, G2PTrainer, TopLevelMfaWorker +): + """ + Top level trainer class for Phonetisaurus-style models + """ + + def __init__( + self, + **kwargs, + ): + self._data_source = kwargs["dictionary_path"].stem + super().__init__(**kwargs) + self.ler = None + self.wer = None + + @property + def data_directory(self) -> Path: + """Data directory for trainer""" + return self.working_directory + + @property + def configuration(self) -> MetaDict: + """Configuration for G2P trainer""" + config = super().configuration + config.update({"dictionary_path": str(self.dictionary_model.path)}) + return config + + def setup(self) -> None: + """Setup for G2P training""" + super().setup() + self.create_new_current_workflow(WorkflowType.train_g2p) + wf = self.current_workflow + if wf.done: + logger.info("G2P training already done, skipping.") + return + self.dictionary_setup() + os.makedirs(self.phones_dir, exist_ok=True) + self.initialize_training() + self.initialized = True + + def finalize_training(self) -> None: + """Finalize training and run evaluation if specified""" + if self.evaluation_mode: + self.evaluate_g2p_model() + + @property + def meta(self) -> MetaDict: + """Metadata for exported G2P model""" + from datetime import datetime + + from ..utils import get_mfa_version + + m = { + "version": get_mfa_version(), + "architecture": self.architecture, + "train_date": str(datetime.now()), + "phones": sorted(self.non_silence_phones), + "graphemes": self.g2p_training_graphemes, + "grapheme_order": self.grapheme_order, + "phone_order": self.phone_order, + "sequence_separator": self.sequence_separator, + "evaluation": {}, + "training": { + "num_words": self.g2p_num_training_words, + "num_graphemes": len(self.g2p_training_graphemes), + "num_phones": len(self.non_silence_phones), + }, + } + + if self.evaluation_mode: + m["evaluation"]["num_words"] = self.g2p_num_validation_words + m["evaluation"]["word_error_rate"] = self.wer + m["evaluation"]["phone_error_rate"] = self.ler + return m + + def evaluate_g2p_model(self) -> None: + """ + Validate the G2P model against held out data + """ + temp_model_path = self.working_log_directory.joinpath("g2p_model.zip") + self.export_model(temp_model_path) + temp_dir = self.working_directory.joinpath("validation") + os.makedirs(temp_dir, exist_ok=True) + with self.session() as session: + validation_set = collections.defaultdict(set) + query = ( + session.query(Word.word, Pronunciation.pronunciation) + .join(Pronunciation.word) + .join(Word.job) + .filter(Word2Job.training == False) # noqa + ) + for w, pron in query: + validation_set[w].add(pron) + gen = PyniniValidator( + g2p_model_path=temp_model_path, + word_list=list(validation_set.keys()), + num_pronunciations=self.num_pronunciations, + ) + output = gen.generate_pronunciations() + with mfa_open(temp_dir.joinpath("validation_output.txt"), "w") as f: + for (orthography, pronunciations) in output.items(): + if not pronunciations: + continue + for p in pronunciations: + if not p: + continue + f.write(f"{orthography}\t{p}\n") + gen.compute_validation_errors(validation_set, output) + def initialize_training(self) -> None: """Initialize training G2P model""" with self.session() as session: @@ -1664,7 +1659,7 @@ def initialize_training(self) -> None: session.query(Job).delete() session.commit() - job_objs = [{"id": j} for j in range(GLOBAL_CONFIG.num_jobs)] + job_objs = [{"id": j} for j in range(1, GLOBAL_CONFIG.num_jobs + 1)] self.g2p_num_training_pronunciations = 0 self.g2p_num_validation_pronunciations = 0 self.g2p_num_training_words = 0 @@ -1673,15 +1668,15 @@ def initialize_training(self) -> None: # so they're not completely overlapping and using more memory num_words = session.query(Word.id).count() words_per_job = int(num_words / GLOBAL_CONFIG.num_jobs) + 1 - current_job = 0 + current_job = 1 words = session.query(Word.id).filter( Word.word_type.in_([WordType.speech, WordType.clitic]) ) mappings = [] for i, (w,) in enumerate(words): if ( - i >= (current_job + 1) * words_per_job - and current_job != GLOBAL_CONFIG.num_jobs + i >= (current_job) * words_per_job + and current_job != GLOBAL_CONFIG.num_jobs + 1 ): current_job += 1 mappings.append({"word_id": w, "job_id": current_job, "training": 1}) @@ -1733,8 +1728,8 @@ def initialize_training(self) -> None: .join(Word.job) .filter(Word2Job.training == True) # noqa ) - with mfa_open(self.working_directory.joinpath("words.txt"), "w") as word_f, mfa_open( - self.working_directory.joinpath("pronunciations.txt"), "w" + with mfa_open(self.working_directory.joinpath("input.txt"), "w") as word_f, mfa_open( + self.working_directory.joinpath("output.txt"), "w" ) as phone_f: for pronunciation, word in query: word = list(word) diff --git a/montreal_forced_aligner/models.py b/montreal_forced_aligner/models.py index 59f3a09f..2d692ae1 100644 --- a/montreal_forced_aligner/models.py +++ b/montreal_forced_aligner/models.py @@ -931,7 +931,26 @@ def sym_path(self) -> Path: path = self.dirname.joinpath("graphemes.txt") if path.exists(): return path - return self.dirname.joinpath("graphemes.sym") + path = self.dirname.joinpath("graphemes.sym") + if path.exists(): + return path + return self.dirname.joinpath("graphemes.syms") + + @property + def input_sym_path(self) -> Path: + """Tokenizer model's input symbols path""" + path = self.dirname.joinpath("input.txt") + if path.exists(): + return path + return self.dirname.joinpath("input.syms") + + @property + def output_sym_path(self) -> Path: + """Tokenizer model's output symbols path""" + path = self.dirname.joinpath("output.txt") + if path.exists(): + return path + return self.dirname.joinpath("output.syms") def add_graphemes_path(self, source_directory: Path) -> None: """ @@ -942,8 +961,10 @@ def add_graphemes_path(self, source_directory: Path) -> None: source_directory: :class:`~pathlib.Path` Source directory path """ - if not self.sym_path.exists(): - copyfile(source_directory.joinpath("graphemes.txt"), self.sym_path) + for p in [self.sym_path, self.output_sym_path, self.input_sym_path]: + source_p = source_directory.joinpath(p.name) + if not p.exists() and source_p.exists(): + copyfile(source_p, p) def add_tokenizer_model(self, source_directory: Path) -> None: """ diff --git a/montreal_forced_aligner/tokenization/tokenizer.py b/montreal_forced_aligner/tokenization/tokenizer.py index 1ff162c3..91a83cf9 100644 --- a/montreal_forced_aligner/tokenization/tokenizer.py +++ b/montreal_forced_aligner/tokenization/tokenizer.py @@ -28,7 +28,7 @@ from montreal_forced_aligner.db import File, Utterance, bulk_update from montreal_forced_aligner.dictionary.mixins import DictionaryMixin from montreal_forced_aligner.exceptions import PyniniGenerationError -from montreal_forced_aligner.g2p.generator import Rewriter, RewriterWorker +from montreal_forced_aligner.g2p.generator import PhonetisaurusRewriter, Rewriter, RewriterWorker from montreal_forced_aligner.helper import edit_distance, mfa_open from montreal_forced_aligner.models import TokenizerModel from montreal_forced_aligner.utils import Stopped, run_kaldi_function @@ -97,6 +97,94 @@ def __call__(self, i: str) -> str: # pragma: no cover return "".join(hypothesis) +class TokenizerPhonetisaurusRewriter(PhonetisaurusRewriter): + """ + Helper function for rewriting + + Parameters + ---------- + fst: pynini.Fst + G2P FST model + input_token_type: pynini.SymbolTable + Grapheme symbol table + output_token_type: pynini.SymbolTable + num_pronunciations: int + Number of pronunciations, default to 0. If this is 0, thresholding is used + threshold: float + Threshold to use for pruning rewrite lattice, defaults to 1.5, only used if num_pronunciations is 0 + grapheme_order: int + Maximum number of graphemes to consider single segment + seq_sep: str + Separator to use between grapheme symbols + """ + + def __init__( + self, + fst: Fst, + input_token_type: SymbolTable, + output_token_type: SymbolTable, + input_order: int = 2, + seq_sep: str = "|", + ): + self.fst = fst + self.seq_sep = seq_sep + self.input_token_type = input_token_type + self.output_token_type = output_token_type + self.input_order = input_order + self.rewrite = functools.partial( + rewrite.top_rewrite, + rule=fst, + input_token_type=None, + output_token_type=output_token_type, + ) + + def __call__(self, graphemes: str) -> str: # pragma: no cover + """Call the rewrite function""" + graphemes = graphemes.replace(" ", "") + original = list(graphemes) + unks = [] + normalized = [] + for c in original: + if self.output_token_type.member(c): + normalized.append(c) + else: + unks.append(c) + normalized.append("") + fst = pynini.Fst() + one = pynini.Weight.one(fst.weight_type()) + max_state = 0 + for i in range(len(normalized)): + start_state = fst.add_state() + for j in range(1, self.input_order + 1): + if i + j <= len(normalized): + substring = self.seq_sep.join(normalized[i : i + j]) + ilabel = self.input_token_type.find(substring) + if ilabel != pynini.NO_LABEL: + fst.add_arc(start_state, pynini.Arc(ilabel, ilabel, one, i + j)) + if i + j >= max_state: + max_state = i + j + for _ in range(fst.num_states(), max_state + 1): + fst.add_state() + fst.set_start(0) + fst.set_final(len(normalized), one) + fst.set_input_symbols(self.input_token_type) + fst.set_output_symbols(self.input_token_type) + hypothesis = self.rewrite(fst).split() + unk_index = 0 + output = [] + for i, w in enumerate(hypothesis): + if w == "": + output.append(unks[unk_index]) + unk_index += 1 + elif w == "": + if i > 0 and hypothesis[i - 1] == " ": + continue + output.append(" ") + else: + output.append(w) + return "".join(output).strip() + + @dataclass class TokenizerArguments(MfaArguments): rewriter: Rewriter @@ -142,12 +230,27 @@ def setup(self) -> None: self._create_dummy_dictionary() self.normalize_text() self.fst = pynini.Fst.read(self.tokenizer_model.fst_path) - self.grapheme_symbols = pywrapfst.SymbolTable.read_text(self.tokenizer_model.sym_path) - self.rewriter = TokenizerRewriter( - self.fst, - self.grapheme_symbols, - ) + if self.tokenizer_model.meta["architecture"] == "phonetisaurus": + self.output_token_type = pywrapfst.SymbolTable.read_text( + self.tokenizer_model.output_sym_path + ) + self.input_token_type = pywrapfst.SymbolTable.read_text( + self.tokenizer_model.input_sym_path + ) + self.rewriter = TokenizerPhonetisaurusRewriter( + self.fst, + self.input_token_type, + self.output_token_type, + input_order=self.tokenizer_model.meta["input_order"], + ) + else: + self.grapheme_symbols = pywrapfst.SymbolTable.read_text(self.tokenizer_model.sym_path) + + self.rewriter = TokenizerRewriter( + self.fst, + self.grapheme_symbols, + ) self.initialized = True def export_files(self, output_directory: Path) -> None: @@ -236,6 +339,8 @@ def __init__(self, utterances_to_tokenize: typing.List[str] = None, **kwargs): if utterances_to_tokenize is None: utterances_to_tokenize = [] self.utterances_to_tokenize = utterances_to_tokenize + self.uer = None + self.cer = None def setup(self): TopLevelMfaWorker.setup(self) @@ -244,15 +349,28 @@ def setup(self): self._current_workflow = "validation" os.makedirs(self.working_log_directory, exist_ok=True) self.fst = pynini.Fst.read(self.tokenizer_model.fst_path) - self.grapheme_symbols = pywrapfst.SymbolTable.read_text(self.tokenizer_model.sym_path) - self.rewriter = TokenizerRewriter( - self.fst, - self.grapheme_symbols, - ) + if self.tokenizer_model.meta["architecture"] == "phonetisaurus": + self.output_token_type = pywrapfst.SymbolTable.read_text( + self.tokenizer_model.output_sym_path + ) + self.input_token_type = pywrapfst.SymbolTable.read_text( + self.tokenizer_model.input_sym_path + ) + self.rewriter = TokenizerPhonetisaurusRewriter( + self.fst, + self.input_token_type, + self.output_token_type, + input_order=self.tokenizer_model.meta["input_order"], + ) + else: + self.grapheme_symbols = pywrapfst.SymbolTable.read_text(self.tokenizer_model.sym_path) + + self.rewriter = TokenizerRewriter( + self.fst, + self.grapheme_symbols, + ) self.initialized = True - self.uer = None - self.cer = None def tokenize_utterances(self) -> typing.Dict[str, str]: """ @@ -269,7 +387,7 @@ def tokenize_utterances(self) -> typing.Dict[str, str]: self.setup() logger.info("Tokenizing utterances...") to_return = {} - if True or num_utterances < 30 or GLOBAL_CONFIG.num_jobs == 1: + if num_utterances < 30 or GLOBAL_CONFIG.num_jobs == 1: with tqdm.tqdm(total=num_utterances, disable=GLOBAL_CONFIG.quiet) as pbar: for utterance in self.utterances_to_tokenize: pbar.update(1) diff --git a/montreal_forced_aligner/tokenization/trainer.py b/montreal_forced_aligner/tokenization/trainer.py index 4fc64d3e..fbc3e116 100644 --- a/montreal_forced_aligner/tokenization/trainer.py +++ b/montreal_forced_aligner/tokenization/trainer.py @@ -7,6 +7,7 @@ import time from pathlib import Path +import pynini import pywrapfst import sqlalchemy @@ -14,9 +15,13 @@ from montreal_forced_aligner.config import GLOBAL_CONFIG from montreal_forced_aligner.corpus.text_corpus import TextCorpusMixin from montreal_forced_aligner.data import WorkflowType -from montreal_forced_aligner.db import Utterance +from montreal_forced_aligner.db import M2M2Job, M2MSymbol, Utterance from montreal_forced_aligner.dictionary.mixins import DictionaryMixin from montreal_forced_aligner.exceptions import KaldiProcessingError +from montreal_forced_aligner.g2p.phonetisaurus_trainer import ( + AlignmentInitWorker, + PhonetisaurusTrainerMixin, +) from montreal_forced_aligner.g2p.trainer import G2PTrainer, PyniniTrainerMixin from montreal_forced_aligner.helper import mfa_open from montreal_forced_aligner.models import TokenizerModel @@ -28,16 +33,177 @@ logger = logging.getLogger("mfa") -class TokenizerTrainer( - PyniniTrainerMixin, TextCorpusMixin, G2PTrainer, TopLevelMfaWorker, DictionaryMixin -): - def __init__(self, oov_count_threshold=5, **kwargs): - super().__init__(oov_count_threshold=oov_count_threshold, **kwargs) +class TokenizerAlignmentInitWorker(AlignmentInitWorker): + """ + Multiprocessing worker that initializes alignment FSTs for a subset of the data + + Parameters + ---------- + job_name: int + Integer ID for the job + return_queue: :class:`multiprocessing.Queue` + Queue to return data + stopped: :class:`~montreal_forced_aligner.utils.Stopped` + Stop check + finished_adding: :class:`~montreal_forced_aligner.utils.Stopped` + Check for whether the job queue is done + args: :class:`~montreal_forced_aligner.g2p.phonetisaurus_trainer.AlignmentInitArguments` + Arguments for initialization + """ + + def data_generator(self, session): + grapheme_table = pywrapfst.SymbolTable.read_text(self.far_path.with_name("graphemes.syms")) + query = session.query(Utterance.normalized_character_text).filter( + Utterance.ignored == False, Utterance.job_id == self.job_name # noqa + ) + for (text,) in query: + tokenized = [x if grapheme_table.member(x) else "" for x in text.split()] + untokenized = [x for x in tokenized if x != ""] + yield untokenized, tokenized + + def run(self) -> None: + """Run the function""" + engine = sqlalchemy.create_engine( + self.db_string, + poolclass=sqlalchemy.NullPool, + pool_reset_on_return=None, + isolation_level="AUTOCOMMIT", + logging_name=f"{type(self).__name__}_engine", + ).execution_options(logging_token=f"{type(self).__name__}_engine") + try: + symbol_table = pynini.SymbolTable() + symbol_table.add_symbol(self.eps) + valid_output_ngrams = set() + base_dir = os.path.dirname(self.far_path) + with mfa_open(os.path.join(base_dir, "output_ngram.ngrams"), "r") as f: + for line in f: + line = line.strip() + valid_output_ngrams.add(line) + valid_input_ngrams = set() + with mfa_open(os.path.join(base_dir, "input_ngram.ngrams"), "r") as f: + for line in f: + line = line.strip() + valid_input_ngrams.add(line) + count = 0 + data = {} + with mfa_open(self.log_path, "w") as log_file, sqlalchemy.orm.Session( + engine + ) as session: + far_writer = pywrapfst.FarWriter.create(self.far_path, arc_type="log") + for current_index, (input, output) in enumerate(self.data_generator(session)): + if self.stopped.stop_check(): + continue + try: + key = f"{current_index:08x}" + fst = pynini.Fst(arc_type="log") + final_state = ((len(input) + 1) * (len(output) + 1)) - 1 + + for _ in range(final_state + 1): + fst.add_state() + for i in range(len(input) + 1): + for j in range(len(output) + 1): + istate = i * (len(output) + 1) + j + + for input_range in range(1, self.input_order + 1): + for output_range in range(input_range, self.output_order + 1): + if i + input_range <= len( + input + ) and j + output_range <= len(output): + if ( + self.restrict + and input_range > 1 + and output_range > 1 + ): + continue + subseq_output = output[j : j + output_range] + output_string = self.seq_sep.join(subseq_output) + if ( + output_range > 1 + and output_string not in valid_output_ngrams + ): + continue + subseq_input = input[i : i + input_range] + input_string = self.seq_sep.join(subseq_input) + if output_range > 1: + if "" not in subseq_output: + continue + if input_string not in output_string: + continue + if ( + output_range == input_range + and input_string != output_string + ): + continue + if ( + input_range > 1 + and input_string not in valid_input_ngrams + ): + continue + symbol = self.s1s2_sep.join( + [input_string, output_string] + ) + ilabel = symbol_table.find(symbol) + if ilabel == pynini.NO_LABEL: + ilabel = symbol_table.add_symbol(symbol) + ostate = (i + input_range) * (len(output) + 1) + ( + j + output_range + ) + fst.add_arc( + istate, + pywrapfst.Arc( + ilabel, + ilabel, + pynini.Weight( + "log", float(input_range * output_range) + ), + ostate, + ), + ) + fst.set_start(0) + fst.set_final(final_state, pywrapfst.Weight.one(fst.weight_type())) + fst = pynini.connect(fst) + for state in fst.states(): + for arc in fst.arcs(state): + sym = symbol_table.find(arc.ilabel) + if sym not in data: + data[sym] = arc.weight + else: + data[sym] = pynini.plus(data[sym], arc.weight) + if count >= self.batch_size: + data = {k: float(v) for k, v in data.items()} + self.return_queue.put((self.job_name, data, count)) + data = {} + count = 0 + log_file.flush() + far_writer[key] = fst + del fst + count += 1 + except Exception as e: # noqa + self.stopped.stop() + self.return_queue.put(e) + if data: + data = {k: float(v) for k, v in data.items()} + self.return_queue.put((self.job_name, data, count)) + symbol_table.write_text(self.far_path.with_suffix(".syms")) + return + except Exception as e: + self.stopped.stop() + self.return_queue.put(e) + finally: + self.finished.stop() + del far_writer + + +class TokenizerMixin(TextCorpusMixin, G2PTrainer, DictionaryMixin, TopLevelMfaWorker): + def __init__(self, **kwargs): + super().__init__(**kwargs) self.training_graphemes = set() self.uer = None self.cer = None self.deletions = False self.insertions = True + self.num_training_utterances = 0 + self.num_validation_utterances = 0 def setup(self) -> None: super().setup() @@ -57,6 +223,202 @@ def setup(self) -> None: raise self.initialized = True + def evaluate_tokenizer(self) -> None: + """ + Validate the tokenizer model against held out data + """ + temp_model_path = self.working_log_directory.joinpath("tokenizer_model.zip") + self.export_model(temp_model_path) + temp_dir = self.working_directory.joinpath("validation") + temp_dir.mkdir(parents=True, exist_ok=True) + with self.session() as session: + validation_set = {} + query = session.query(Utterance.normalized_character_text).filter( + Utterance.ignored == True # noqa + ) + for (text,) in query: + tokenized = text.split() + untokenized = [x for x in tokenized if x != ""] + tokenized = [x if x != "" else " " for x in tokenized] + validation_set[" ".join(untokenized)] = "".join(tokenized) + gen = TokenizerValidator( + tokenizer_model_path=temp_model_path, + corpus_directory=self.corpus_directory, + utterances_to_tokenize=list(validation_set.keys()), + ) + output = gen.tokenize_utterances() + with mfa_open(temp_dir.joinpath("validation_output.txt"), "w") as f: + for (orthography, pronunciations) in output.items(): + if not pronunciations: + continue + for p in pronunciations: + if not p: + continue + f.write(f"{orthography}\t{p}\n") + gen.compute_validation_errors(validation_set, output) + self.uer = gen.uer + self.cer = gen.cer + + +class PhonetisaurusTokenizerTrainer(PhonetisaurusTrainerMixin, TokenizerMixin): + + alignment_init_function = TokenizerAlignmentInitWorker + + def __init__( + self, input_order: int = 2, output_order: int = 3, oov_count_threshold: int = 5, **kwargs + ): + super().__init__( + oov_count_threshold=oov_count_threshold, + grapheme_order=input_order, + phone_order=output_order, + **kwargs, + ) + + @property + def data_source_identifier(self) -> str: + """Corpus name""" + return self.corpus_directory.name + + @property + def meta(self) -> MetaDict: + """Metadata for exported tokenizer model""" + from datetime import datetime + + from ..utils import get_mfa_version + + m = { + "version": get_mfa_version(), + "architecture": self.architecture, + "train_date": str(datetime.now()), + "evaluation": {}, + "input_order": self.input_order, + "output_order": self.output_order, + "oov_count_threshold": self.oov_count_threshold, + "training": { + "num_utterances": self.num_training_utterances, + "num_graphemes": len(self.training_graphemes), + }, + } + + if self.evaluation_mode: + m["evaluation"]["num_utterances"] = self.num_validation_utterances + m["evaluation"]["utterance_error_rate"] = self.uer + m["evaluation"]["character_error_rate"] = self.cer + return m + + def train(self) -> None: + if os.path.exists(self.fst_path): + return + super().train() + + def initialize_training(self) -> None: + """Initialize training tokenizer model""" + logger.info("Initializing training...") + + self.create_new_current_workflow(WorkflowType.tokenizer_training) + with self.session() as session: + session.query(M2M2Job).delete() + session.query(M2MSymbol).delete() + session.commit() + self.num_validation_utterances = 0 + self.num_training_utterances = 0 + if self.evaluation_mode: + validation_items = int(self.num_utterances * self.validation_proportion) + validation_utterances = ( + sqlalchemy.select(Utterance.id) + .order_by(sqlalchemy.func.random()) + .limit(validation_items) + .scalar_subquery() + ) + query = ( + sqlalchemy.update(Utterance) + .execution_options(synchronize_session="fetch") + .values(ignored=True) + .where(Utterance.id.in_(validation_utterances)) + ) + with session.begin_nested(): + session.execute(query) + session.flush() + session.commit() + self.num_validation_utterances = ( + session.query(Utterance.id).filter(Utterance.ignored == True).count() # noqa + ) + + query = session.query(Utterance.normalized_character_text).filter( + Utterance.ignored == False # noqa + ) + unk_character = "" + self.training_graphemes.add(unk_character) + counts = collections.Counter() + for (text,) in query: + counts.update(text.split()) + with mfa_open( + self.working_directory.joinpath("input.txt"), "w" + ) as untokenized_f, mfa_open( + self.working_directory.joinpath("output.txt"), "w" + ) as tokenized_f: + for (text,) in query: + assert text + tokenized = [ + x if counts[x] >= self.oov_count_threshold else unk_character + for x in text.split() + ] + untokenized = [x for x in tokenized if x != ""] + self.num_training_utterances += 1 + self.training_graphemes.update(tokenized) + untokenized_f.write(" ".join(untokenized) + "\n") + tokenized_f.write(" ".join(tokenized) + "\n") + index = 1 + with mfa_open(self.working_directory.joinpath("graphemes.syms"), "w") as f: + f.write("\t0\n") + for g in sorted(self.training_graphemes): + f.write(f"{g}\t{index}\n") + index += 1 + self.compute_initial_ngrams() + self.g2p_num_training_pronunciations = self.num_training_utterances + + def finalize_training(self) -> None: + """Finalize training""" + shutil.copyfile(self.fst_path, self.working_directory.joinpath("tokenizer.fst")) + shutil.copyfile(self.grapheme_symbols_path, self.working_directory.joinpath("input.syms")) + shutil.copyfile(self.phone_symbols_path, self.working_directory.joinpath("output.syms")) + if self.evaluation_mode: + self.evaluate_tokenizer() + + def export_model(self, output_model_path: Path) -> None: + """ + Export tokenizer model to specified path + + Parameters + ---------- + output_model_path: :class:`~pathlib.Path` + Path to export model + """ + directory = output_model_path.parent + + models_temp_dir = self.working_directory.joinpath("model_archive_temp") + model = TokenizerModel.empty(output_model_path.stem, root_directory=models_temp_dir) + model.add_meta_file(self) + model.add_tokenizer_model(self.working_directory) + model.add_graphemes_path(self.working_directory) + if directory: + os.makedirs(directory, exist_ok=True) + model.dump(output_model_path) + if not GLOBAL_CONFIG.current_profile.debug: + model.clean_up() + # self.clean_up() + logger.info(f"Saved model to {output_model_path}") + + +class TokenizerTrainer(PyniniTrainerMixin, TokenizerMixin): + def __init__(self, oov_count_threshold=5, **kwargs): + super().__init__(oov_count_threshold=oov_count_threshold, **kwargs) + self.training_graphemes = set() + self.uer = None + self.cer = None + self.deletions = False + self.insertions = True + @property def meta(self) -> MetaDict: """Metadata for exported tokenizer model""" @@ -88,11 +450,7 @@ def data_source_identifier(self) -> str: @property def sym_path(self) -> Path: - return self.working_directory.joinpath("graphemes.txt") - - @property - def phone_symbol_table_path(self) -> Path: - return self.working_directory.joinpath("graphemes.txt") + return self.working_directory.joinpath("graphemes.syms") def initialize_training(self) -> None: """Initialize training tokenizer model""" @@ -100,8 +458,10 @@ def initialize_training(self) -> None: with self.session() as session: self.num_validation_utterances = 0 self.num_training_utterances = 0 - self.num_iterations = 2 - self.input_token_type = self.working_directory.joinpath("graphemes.txt") + self.num_iterations = 1 + self.random_starts = 1 + self.input_token_type = self.sym_path + self.output_token_type = self.sym_path if self.evaluation_mode: validation_items = int(self.num_utterances * self.validation_proportion) validation_utterances = ( @@ -167,14 +527,11 @@ def _lexicon_covering(self, input_path=None, output_path=None) -> None: thirdparty_binary("farcompilestrings"), "--fst_type=compact", ] - if self.input_token_type != "utf8": - com.append("--token_type=symbol") - com.append( - f"--symbols={self.input_token_type}", - ) - com.append("--unknown_symbol=") - else: - com.append("--token_type=utf8") + com.append("--token_type=symbol") + com.append( + f"--symbols={self.sym_path}", + ) + com.append("--unknown_symbol=") com.extend([input_path, self.input_far_path]) print(" ".join(map(str, com)), file=log_file) subprocess.check_call(com, env=os.environ, stderr=log_file, stdout=log_file) @@ -182,7 +539,7 @@ def _lexicon_covering(self, input_path=None, output_path=None) -> None: thirdparty_binary("farcompilestrings"), "--fst_type=compact", "--token_type=symbol", - f"--symbols={self.phone_symbol_table_path}", + f"--symbols={self.sym_path}", output_path, self.output_far_path, ] @@ -203,48 +560,6 @@ def _lexicon_covering(self, input_path=None, output_path=None) -> None: assert cg.verify(), "Label acceptor is ill-formed" cg.write(self.cg_path) - def evaluate_tokenizer(self) -> None: - """ - Validate the tokenizer model against held out data - """ - temp_model_path = self.working_log_directory.joinpath("tokenizer_model.zip") - self.export_model(temp_model_path) - temp_dir = self.working_directory.joinpath("validation") - temp_dir.mkdir(parents=True, exist_ok=True) - with self.session() as session: - validation_set = {} - query = session.query(Utterance.normalized_character_text).filter( - Utterance.ignored == True # noqa - ) - for (text,) in query: - tokenized = text.split() - untokenized = [x for x in tokenized if x != ""] - tokenized = [x if x != "" else " " for x in tokenized] - validation_set[" ".join(untokenized)] = "".join(tokenized) - gen = TokenizerValidator( - tokenizer_model_path=temp_model_path, - corpus_directory=self.corpus_directory, - utterances_to_tokenize=list(validation_set.keys()), - ) - output = gen.tokenize_utterances() - with mfa_open(temp_dir.joinpath("validation_output.txt"), "w") as f: - for (orthography, pronunciations) in output.items(): - if not pronunciations: - continue - for p in pronunciations: - if not p: - continue - f.write(f"{orthography}\t{p}\n") - gen.compute_validation_errors(validation_set, output) - self.uer = gen.uer - self.cer = gen.cer - - def finalize_training(self) -> None: - """Finalize training""" - shutil.copyfile(self.fst_path, self.working_directory.joinpath("tokenizer.fst")) - if self.evaluation_mode: - self.evaluate_tokenizer() - def train(self) -> None: """ Train a tokenizer model @@ -261,6 +576,12 @@ def train(self) -> None: logger.debug(f"Generating model took {time.time() - begin:.3f} seconds") self.finalize_training() + def finalize_training(self) -> None: + """Finalize training""" + shutil.copyfile(self.fst_path, self.working_directory.joinpath("tokenizer.fst")) + if self.evaluation_mode: + self.evaluate_tokenizer() + def export_model(self, output_model_path: Path) -> None: """ Export tokenizer model to specified path diff --git a/tests/conftest.py b/tests/conftest.py index 2a52e0b8..adbf347a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -265,6 +265,11 @@ def test_tokenizer_model(tokenizer_model_dir): return tokenizer_model_dir.joinpath("test_tokenizer_model.zip") +@pytest.fixture(scope="session") +def test_tokenizer_model_phonetisaurus(tokenizer_model_dir): + return tokenizer_model_dir.joinpath("test_tokenizer_model_phonetisaurus.zip") + + @pytest.fixture(scope="session") def transcription_language_model(language_model_dir, generated_dir): return language_model_dir.joinpath("test_lm.zip") diff --git a/tests/data/tokenizer/test_tokenizer_model_phonetisaurus.zip b/tests/data/tokenizer/test_tokenizer_model_phonetisaurus.zip new file mode 100644 index 0000000000000000000000000000000000000000..e2031a483033536bcc6039f6a0bba6cd15fac688 GIT binary patch literal 72675 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%0Dsn*Pd&f4pfwJj2wHuE&*+we0Ep!&O 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