CoolFace
Modelpublic

RASMUS/Finnish-ASR-Canary-v2

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes1.2kdownloads
eval_greedy_decoding_with_context_biasing.py512 linesDownload Raw Back to asr_context_biasing
1# Copyright (c) 2025, NVIDIA CORPORATION.  All rights reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14#15 16"""17# This script evaluates CTC and Transducer (RNNT) models (only Hybrid Transducer-CTC in case of Transducer) in context biasing mode18# by applying CTC-based Word Spotter (paper link) 19 20# Config Help21 22To discover all arguments of the script, please run :23python eval_greedy_decoding_with_context_biasing.py --help24python eval_greedy_decoding_with_context_biasing.py --cfg job25 26# USAGE27 28python eval_greedy_decoding_with_context_biasing.py \29            nemo_model_file=<path to the .nemo file of the model> \30            input_manifest=<path to the evaluation JSON manifest file \31            preds_output_folder=<folder to store the predictions> \32            decoder_type=<type of model decoder [ctc or rnnt]> \33            acoustic_batch_size=<batch size to calculate log probabilities> \34            apply_context_biasing=<True or False to apply context biasing> \35            context_file=<path to the context biasing file with key words/phrases> \36            beam_threshold=[<list of the beam thresholds, separated with commas>] \37            context_score=[<list of the context scores, separated with commas>] \38            ctc_ali_token_weight=[<list of the ctc alignment token weights, separated with commas>] \39            ...40 41# Description of context biasing graph:42Context biasing file contains words/phrases with their spellings43(one word/phrase per line, spellings are separated from word/phrase by underscore symbol):44WORD1_SPELLING145WORD2_SPELLING1_SPELLING246...47nvidia_nvidia48gpu_gpu_g p u49nvlink_nvlink_nv link50...51alternative spellings help to improve the recognition accuracy of abbreviations and complicated words,52which are often recognized as separate words (gpu -> g p u, nvlink -> nv link, tensorrt -> tensor rt, and so on).53 54 55# Grid Search for Hyper parameters56 57For grid search, you can provide a list of arguments as follows -58 59            beam_threshold=[4.0,5.0,6.0,....] \60            context_score=[1.0,1.5,...,4.0,4.5] \61            ctc_ali_token_weight=[0.1,0.2,...,0.7,0.8] \62 63"""64 65 66import contextlib67import json68import os69import tempfile70from dataclasses import dataclass, field, is_dataclass71from pathlib import Path72from typing import Dict, Optional73 74import editdistance75import numpy as np76import torch77from omegaconf import MISSING, OmegaConf78from sklearn.model_selection import ParameterGrid79from tqdm.auto import tqdm80 81import nemo.collections.asr as nemo_asr82from nemo.collections.asr.models import EncDecCTCModelBPE, EncDecHybridRNNTCTCModel83from nemo.collections.asr.parts import context_biasing84from nemo.core.config import hydra_runner85from nemo.utils import logging86 87 88@dataclass89class EvalContextBiasingConfig:90    """91    Evaluate CTC and Transducer (RNNT) ASR models in greedy decoding with context biasing.92    """93 94    # # The path of the '.nemo' file of the ASR model or the name of a pretrained model (ngc / huggingface)95    nemo_model_file: str = MISSING96 97    # File paths98    input_manifest: str = MISSING  # The manifest file of the evaluation set99    preds_output_folder: str = MISSING  # The folder where the predictions are stored100 101    # Parameters for inference102    acoustic_batch_size: int = 128  # The batch size to calculate log probabilities103    beam_batch_size: int = 128  # The batch size to be used for beam search decoding104    device: str = "cuda"  # The device to load the model onto to calculate log probabilities105    use_amp: bool = False  # Whether to use AMP if available to calculate log probabilities106    num_workers: int = 1  # Number of workers for DataLoader107    decoder_type: Optional[str] = None  # [ctc, rnnt] decoder type for asr model108 109    # Context-Biasing params110    apply_context_biasing: bool = False  # True in case of context biasing111    context_file: str = MISSING  # text file with context biasing words and their spellings112    spelling_separator: str = "_"  # separator between word and its spellings in context biasing file113    beam_threshold: list[float] = field(default_factory=lambda: [5.0])  # beam pruning threshold for ctc-ws decoding114    context_score: list[float] = field(default_factory=lambda: [3.0])  # per token weight for context biasing words115    ctc_ali_token_weight: list[float] = field(116        default_factory=lambda: [0.6]117    )  # weight of CTC tokens to prevent false accept errors118    print_cb_stats: bool = False  # print context biasing stats (mostly for debugging)119 120    # Auxiliary parameters121    sort_logits: bool = True  # do logits sorting before decoding - it reduces computation on puddings122    softmax_temperature: float = 1.00123    preserve_alignments: bool = False124 125 126def decoding_step(127    asr_model: nemo_asr.models.ASRModel,128    cfg: EvalContextBiasingConfig,129    encoder_outputs: list[torch.Tensor],130    ctc_logprobs: list[np.ndarray],131    target_transcripts: list[str],132    audio_file_paths: list[str],133    durations: list[str],134    preds_output_manifest: str,135    beam_batch_size: int = 128,136    progress_bar: bool = True,137    context_graph: context_biasing.ContextGraphCTC = None,138    blank_idx: int = 0,139    hp: Optional[Dict] = None,140) -> tuple[float, float]:141 142    # run CTC-based Word Spotter:143    if cfg.apply_context_biasing:144        ws_results = {}145        for idx, logits in tqdm(146            enumerate(ctc_logprobs), desc=f"Eval CTC-based Word Spotter...", ncols=120, total=len(ctc_logprobs)147        ):148            ws_results[audio_file_paths[idx]] = context_biasing.run_word_spotter(149                logits,150                context_graph,151                asr_model,152                blank_idx=blank_idx,153                beam_threshold=hp['beam_threshold'],154                cb_weight=hp['context_score'],155                ctc_ali_token_weight=hp['ctc_ali_token_weight'],156            )157 158    level = logging.getEffectiveLevel()159    logging.setLevel(logging.CRITICAL)160    # reset config161    asr_model.change_decoding_strategy(None)162 163    # preserve alignment:164    asr_model.cfg.decoding.preserve_alignments = cfg.preserve_alignments165 166    # update model's decoding strategy config167    if isinstance(asr_model, EncDecCTCModelBPE):168        # in case of ctc169        asr_model.cfg.decoding.strategy = "greedy"170    else:171        # in case of rnnt172        asr_model.cfg.decoding.strategy = "greedy_batch"173        # fast greedy batch decoding:174        asr_model.cfg.decoding.greedy.loop_labels = True175 176    # update model's decoding strategy177    asr_model.change_decoding_strategy(asr_model.cfg.decoding)178    logging.setLevel(level)179 180    wer_dist_first = cer_dist_first = 0181    words_count = chars_count = sample_idx = 0182 183    out_manifest = open(preds_output_manifest, 'w', encoding='utf_8', newline='\n')184 185    # ctc part for both EncDecCTCModelBPE and EncDecHybridRNNTCTCModel186    if cfg.decoder_type == "ctc":187        for batch_idx, probs in enumerate(ctc_logprobs):188            preds = np.argmax(probs, axis=1)189            if cfg.apply_context_biasing and ws_results[audio_file_paths[batch_idx]]:190                # make new text by mearging alignment with ctc-ws predictions:191                if cfg.print_cb_stats:192                    logging.info("\n" + "********" * 10)193                    logging.info(f"File name: {audio_file_paths[batch_idx]}")194                pred_text, raw_text = context_biasing.merge_alignment_with_ws_hyps(195                    preds,196                    asr_model,197                    ws_results[audio_file_paths[batch_idx]],198                    decoder_type="ctc",199                    blank_idx=blank_idx,200                    print_stats=cfg.print_cb_stats,201                )202                if cfg.print_cb_stats:203                    logging.info(f"raw text: {raw_text}")204                    logging.info(f"hyp text: {pred_text}")205                    logging.info(f"ref text: {target_transcripts[batch_idx]}")206            else:207                preds_tensor = torch.tensor(preds, device='cpu').unsqueeze(0)208                if isinstance(asr_model, EncDecHybridRNNTCTCModel):209                    hyp = asr_model.ctc_decoding.ctc_decoder_predictions_tensor(preds_tensor)[0]210                else:211                    hyp = asr_model.wer.decoding.ctc_decoder_predictions_tensor(preds_tensor)[0]212                pred_text = hyp.text213            pred_split_w = pred_text.split()214            target_split_w = target_transcripts[batch_idx].split()215            pred_split_c = list(pred_text)216            target_split_c = list(target_transcripts[batch_idx])217 218            wer_dist = editdistance.eval(target_split_w, pred_split_w)219            cer_dist = editdistance.eval(target_split_c, pred_split_c)220 221            wer_dist_first += wer_dist222            cer_dist_first += cer_dist223            words_count += len(target_split_w)224            chars_count += len(target_split_c)225 226            if preds_output_manifest:227                item = {228                    'audio_filepath': audio_file_paths[batch_idx],229                    'duration': durations[batch_idx],230                    'text': target_transcripts[batch_idx],231                    'pred_text': pred_text,232                    'wer': f"{wer_dist/len(target_split_w):.4f}",233                }234                print(json.dumps(item), file=out_manifest)235        out_manifest.close()236 237        return wer_dist_first / words_count, cer_dist_first / chars_count238 239    # rnnt part for EncDecHybridRNNTCTCModel240    else:241        if progress_bar:242            description = "Greedy_batch decoding.."243            it = tqdm(range(int(np.ceil(len(encoder_outputs) / beam_batch_size))), desc=description, ncols=120)244        else:245            it = range(int(np.ceil(len(encoder_outputs) / beam_batch_size)))246        for batch_idx in it:247            probs_batch = encoder_outputs[batch_idx * beam_batch_size : (batch_idx + 1) * beam_batch_size]248            probs_lens = torch.tensor([prob.shape[-1] for prob in probs_batch])249            with torch.no_grad():250                packed_batch = torch.zeros(len(probs_batch), probs_batch[0].shape[0], max(probs_lens), device='cpu')251 252                for prob_index in range(len(probs_batch)):253                    packed_batch[prob_index, :, : probs_lens[prob_index]] = torch.tensor(254                        probs_batch[prob_index].unsqueeze(0), device=packed_batch.device, dtype=packed_batch.dtype255                    )256                best_hyp_batch = asr_model.decoding.rnnt_decoder_predictions_tensor(257                    packed_batch,258                    probs_lens,259                    return_hypotheses=True,260                )261            beams_batch = [[x] for x in best_hyp_batch]262 263            for beams_idx, beams in enumerate(beams_batch):264                target = target_transcripts[sample_idx + beams_idx]265                target_split_w = target.split()266                target_split_c = list(target)267                words_count += len(target_split_w)268                chars_count += len(target_split_c)269                for candidate_idx, candidate in enumerate(beams):270                    if cfg.apply_context_biasing and ws_results[audio_file_paths[sample_idx + beams_idx]]:271                        # make new text by mearging alignment with ctc-ws predictions:272                        if cfg.print_cb_stats:273                            logging.info("\n" + "********" * 10)274                            logging.info(f"File name: {audio_file_paths[batch_idx]}")275                        pred_text, raw_text = context_biasing.merge_alignment_with_ws_hyps(276                            candidate,277                            asr_model,278                            ws_results[audio_file_paths[sample_idx + beams_idx]],279                            decoder_type="rnnt",280                            blank_idx=blank_idx,281                            print_stats=cfg.print_cb_stats,282                        )283                        if cfg.print_cb_stats:284                            logging.info(f"raw text: {raw_text}")285                            logging.info(f"hyp text: {pred_text}")286                            logging.info(f"ref text: {target_transcripts[sample_idx + beams_idx]}")287                    else:288                        pred_text = candidate.text289 290                    pred_split_w = pred_text.split()291                    wer_dist = editdistance.eval(target_split_w, pred_split_w)292                    pred_split_c = list(pred_text)293                    cer_dist = editdistance.eval(target_split_c, pred_split_c)294 295                    if candidate_idx == 0:296                        # first candidate297                        wer_dist_tosave = wer_dist298                        wer_dist_first += wer_dist299                        cer_dist_first += cer_dist300 301                # write manifest with prediction results302                alignment = []303 304                if preds_output_manifest:305                    item = {306                        'audio_filepath': audio_file_paths[sample_idx + beams_idx],307                        'duration': durations[sample_idx + beams_idx],308                        'text': target_transcripts[sample_idx + beams_idx],309                        'pred_text': pred_text,310                        'wer': f"{wer_dist_tosave/len(target_split_w):.3f}",311                        'alignment': f"{alignment}",312                    }313                    print(json.dumps(item), file=out_manifest)314 315            sample_idx += len(probs_batch)316        out_manifest.close()317 318        return wer_dist_first / words_count, cer_dist_first / chars_count319 320 321@hydra_runner(config_path=None, config_name='EvalContextBiasingConfig', schema=EvalContextBiasingConfig)322def main(cfg: EvalContextBiasingConfig):323    if is_dataclass(cfg):324        cfg = OmegaConf.structured(cfg)325 326    assert os.path.isfile(cfg.input_manifest), f"input_manifest {cfg.input_manifest} does not exist"327    assert cfg.context_file, "context_file must be provided for f-score computation"328    assert os.path.isfile(cfg.context_file), f"context_file {cfg.context_file} does not exist"329    assert cfg.decoder_type in ["ctc", "rnnt"], "decoder_type must be ctc or rnnt"330    assert cfg.preds_output_folder, "preds_output_folder must be provided"331    assert os.path.isdir(cfg.preds_output_folder), f"preds_output_folder {cfg.preds_output_folder} does not exist"332 333    # load nemo asr model334    if cfg.nemo_model_file.endswith('.nemo'):335        asr_model = nemo_asr.models.ASRModel.restore_from(cfg.nemo_model_file, map_location=torch.device(cfg.device))336    else:337        logging.warning(338            "nemo_model_file does not end with .nemo, therefore trying to load a pretrained model with this name."339        )340        asr_model = nemo_asr.models.ASRModel.from_pretrained(341            cfg.nemo_model_file, map_location=torch.device(cfg.device)342        )343    if not isinstance(asr_model, (EncDecCTCModelBPE, EncDecHybridRNNTCTCModel)):344        raise ValueError("ASR model must be CTC BPE or Hybrid Transducer-CTC")345 346    # load nemo manifest347    target_transcripts = []348    durations = []349    manifest_dir = Path(cfg.input_manifest).parent350    with open(cfg.input_manifest, 'r', encoding='utf_8') as manifest_file:351        audio_file_paths = []352        for line in tqdm(manifest_file, desc=f"Reading Manifest {cfg.input_manifest} ...", ncols=120):353            data = json.loads(line)354            audio_file = Path(data['audio_filepath'])355            if not audio_file.is_file() and not audio_file.is_absolute():356                audio_file = manifest_dir / audio_file357            target_transcripts.append(data['text'])358            durations.append(data['duration'])359            audio_file_paths.append(str(audio_file.absolute()))360 361    # manual calculation of encoder_embeddings362    with torch.amp.autocast(asr_model.device.type, enabled=cfg.use_amp):363        with torch.no_grad():364            asr_model.eval()365            asr_model.encoder.freeze()366            device = next(asr_model.parameters()).device367            encoder_outputs = []368            ctc_logprobs = []369            if isinstance(asr_model, EncDecCTCModelBPE):370                # in case of EncDecCTCModelBPE371                hyp_results = asr_model.transcribe(372                    audio_file_paths, batch_size=cfg.acoustic_batch_size, return_hypotheses=True373                )374                ctc_logprobs = [hyp.alignments.cpu().numpy() for hyp in hyp_results]375                blank_idx = asr_model.decoding.blank_id376            else:377                # in case of EncDecHybridRNNTCTCModel378                with tempfile.TemporaryDirectory() as tmpdir:379                    with open(os.path.join(tmpdir, 'manifest.json'), 'w', encoding='utf-8') as fp:380                        for audio_file in audio_file_paths:381                            entry = {'audio_filepath': audio_file, 'duration': 100000, 'text': ''}382                            fp.write(json.dumps(entry) + '\n')383                    config = {384                        'paths2audio_files': audio_file_paths,385                        'batch_size': cfg.acoustic_batch_size,386                        'temp_dir': tmpdir,387                        'num_workers': cfg.num_workers,388                        'channel_selector': None,389                        'augmentor': None,390                    }391                    temporary_datalayer = asr_model._setup_transcribe_dataloader(config)392 393                    for test_batch in tqdm(394                        temporary_datalayer, desc="Getting encoder and CTC decoder outputs...", disable=False395                    ):396                        encoded, encoded_len = asr_model.forward(397                            input_signal=test_batch[0].to(device), input_signal_length=test_batch[1].to(device)398                        )399                        ctc_dec_outputs = asr_model.ctc_decoder(encoder_output=encoded).cpu()400                        # dump encoder embeddings per file401                        for idx in range(encoded.shape[0]):402                            encoded_no_pad = encoded[idx, :, : encoded_len[idx]]403                            ctc_dec_outputs_no_pad = ctc_dec_outputs[idx, : encoded_len[idx]]404                            encoder_outputs.append(encoded_no_pad)405                            ctc_logprobs.append(ctc_dec_outputs_no_pad.cpu().numpy())406                    blank_idx = asr_model.decoder.blank_idx407 408    # load context biasing words409    context_transcripts = []410    for line in open(cfg.context_file).readlines():411        item = line.strip().lower().split(cfg.spelling_separator)412        word = item[0]413        word_tokenization = [asr_model.tokenizer.text_to_ids(x) for x in item[1:]]414        context_transcripts.append([word, word_tokenization])415    context_words = [item[0] for item in context_transcripts]416    # build context graph:417    if cfg.apply_context_biasing:418        context_graph = context_biasing.ContextGraphCTC(blank_id=blank_idx)419        context_graph.add_to_graph(context_transcripts)420    else:421        context_graph = None422 423    # sort encoder_outputs according to length:424    if cfg.decoder_type == "rnnt" and cfg.sort_logits:425        encoder_outputs_with_indeces = sorted(enumerate(encoder_outputs), key=lambda x: x[1].size()[1], reverse=True)426        encoder_outputs_sorted = []427        target_transcripts_sorted = []428        audio_file_paths_sorted = []429        durations_sorted = []430        ctc_logprobs_sorted = []431        for pair in encoder_outputs_with_indeces:432            encoder_outputs_sorted.append(pair[1])433            target_transcripts_sorted.append(target_transcripts[pair[0]])434            audio_file_paths_sorted.append(audio_file_paths[pair[0]])435            durations_sorted.append(durations[pair[0]])436            ctc_logprobs_sorted.append(ctc_logprobs[pair[0]])437        encoder_outputs = encoder_outputs_sorted438        target_transcripts = target_transcripts_sorted439        audio_file_paths = audio_file_paths_sorted440        durations = durations_sorted441        ctc_logprobs = ctc_logprobs_sorted442 443    # setup search parameters grid444    params = {445        'beam_threshold': cfg.beam_threshold,446        'context_score': cfg.context_score,447        'ctc_ali_token_weight': cfg.ctc_ali_token_weight,448    }449    hp_grid = ParameterGrid(params)450    hp_grid = list(hp_grid)451 452    logging.info(f"=========================Starting the decoding========================")453    logging.info(f"Grid search size: {len(hp_grid)}")454    logging.info(f"It may take some time...")455    logging.info(f"======================================================================")456 457    asr_model = asr_model.to('cpu')458    best_wer = 1e6459 460    # run decoding step for each hyper parameter set461    for hp in hp_grid:462        results_file = f"preds_out_manifest_bthr-{hp['beam_threshold']}_cs-{hp['context_score']}ctcw-{hp['ctc_ali_token_weight']}.json"463        preds_output_manifest = os.path.join(cfg.preds_output_folder, results_file)464        candidate_wer, candidate_cer = decoding_step(465            asr_model,466            cfg,467            encoder_outputs=encoder_outputs,468            target_transcripts=target_transcripts,469            audio_file_paths=audio_file_paths,470            durations=durations,471            beam_batch_size=cfg.beam_batch_size,472            progress_bar=True,473            preds_output_manifest=preds_output_manifest,474            context_graph=context_graph,475            ctc_logprobs=ctc_logprobs,476            blank_idx=blank_idx,477            hp=hp,478        )479 480        # compute fscore481        fscore_stats = context_biasing.compute_fscore(preds_output_manifest, context_words)482 483        # find the best wer value484        if candidate_wer < best_wer:485            best_beam_threshold = hp["beam_threshold"]486            best_context_score = hp["context_score"]487            best_ctc_ali_token_weight = hp["ctc_ali_token_weight"]488            best_wer = candidate_wer489            best_fscore_stats = fscore_stats490 491        logging.info(f"======================================================================")492        logging.info(f"Greedy WER/CER = {candidate_wer:.2%}/{candidate_cer:.2%}")493        logging.info(f"Precision/Recall/Fscore = {fscore_stats[0]:.4f}/{fscore_stats[1]:.4f}/{fscore_stats[2]:.4f}")494        logging.info(495            f"Params: b_thr = {hp['beam_threshold']}, cs = {hp['context_score']}, ctc_ali_weight = {hp['ctc_ali_token_weight']}"496        )497        logging.info(f"======================================================================")498 499    if len(hp_grid) > 1:500        logging.info(f"=========================Best Results=================================")501        logging.info(f"Best WER = {best_wer:.2%}")502        logging.info(503            f"Best Precision/Recall/Fscore = {best_fscore_stats[0]:.4f}/{best_fscore_stats[1]:.4f}/{best_fscore_stats[2]:.4f}"504        )505        logging.info(506            f"Best beam_threshold = {best_beam_threshold}, context_score = {best_context_score}, ctc_ali_token_weight = {best_ctc_ali_token_weight}"507        )508 509 510if __name__ == '__main__':511    main()512