Source code for nemo.collections.asr.models.hybrid_rnnt_ctc_bpe_models_prompt

# Copyright (c) 2025, NVIDIA CORPORATION.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from dataclasses import dataclass
from math import ceil
from typing import Dict, List, Optional, Union

import torch
from omegaconf import DictConfig, ListConfig, OmegaConf, open_dict
from pytorch_lightning import Trainer

from nemo.collections.asr.data import audio_to_text_dataset
from nemo.collections.asr.data.audio_to_text_dali import AudioToBPEDALIDataset, DALIOutputs
from nemo.collections.asr.data.audio_to_text_lhotse import LhotseSpeechToTextBpeDataset
from nemo.collections.asr.data.audio_to_text_lhotse_prompt_index import LhotseSpeechToTextBpeDatasetWithPromptIndex
from nemo.collections.asr.metrics.bleu import BLEU
from nemo.collections.asr.metrics.wer import WER
from nemo.collections.asr.models.hybrid_rnnt_ctc_bpe_models import EncDecHybridRNNTCTCBPEModel
from nemo.collections.asr.parts.mixins import ASRTranscriptionMixin, PromptStreamingMixin, TranscribeConfig
from nemo.collections.asr.parts.mixins.transcription import TranscriptionReturnType
from nemo.collections.asr.parts.preprocessing.segment import ChannelSelectorType
from nemo.collections.asr.parts.submodules.ctc_decoding import CTCBPEDecoding, CTCBPEDecodingConfig
from nemo.collections.asr.parts.submodules.rnnt_decoding import RNNTBPEDecoding
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis
from nemo.collections.common.data.lhotse import get_lhotse_dataloader_from_config
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.classes.mixins import AccessMixin
from nemo.core.neural_types import (
    AcousticEncodedRepresentation,
    AudioSignal,
    LabelsType,
    LengthsType,
    NeuralType,
    SpectrogramType,
)
from nemo.utils import logging, model_utils


@dataclass
class HybridRNNTCTCPromptTranscribeConfig(TranscribeConfig):
    """
    Configuration for Hybrid RNNT-CTC BPE Model with Prompt Transcription
    """

    target_lang: str = "auto"


[docs] class EncDecHybridRNNTCTCBPEModelWithPrompt(PromptStreamingMixin, EncDecHybridRNNTCTCBPEModel, ASRTranscriptionMixin): """Base class for encoder decoder RNNT-based models with auxiliary CTC decoder/loss, subword tokenization, and prompt conditioning.""" def __init__(self, cfg: DictConfig, trainer: Trainer = None): # Convert to Hydra 1.0 compatible DictConfig cfg = model_utils.convert_model_config_to_dict_config(cfg) cfg = model_utils.maybe_update_config_version(cfg) # Tokenizer is necessary for this model if 'tokenizer' not in cfg: raise ValueError("`cfg` must have `tokenizer` config to create a tokenizer !") if not isinstance(cfg, DictConfig): cfg = OmegaConf.create(cfg) # Setup the tokenizer self._setup_tokenizer(cfg.tokenizer) # Initialize a dummy vocabulary vocabulary = self.tokenizer.tokenizer.get_vocab() # Set the new vocabulary with open_dict(cfg): cfg.labels = ListConfig(list(vocabulary)) with open_dict(cfg.decoder): cfg.decoder.vocab_size = len(vocabulary) with open_dict(cfg.joint): cfg.joint.num_classes = len(vocabulary) cfg.joint.vocabulary = ListConfig(list(vocabulary)) cfg.joint.jointnet.encoder_hidden = cfg.model_defaults.enc_hidden cfg.joint.jointnet.pred_hidden = cfg.model_defaults.pred_hidden # setup auxiliary CTC decoder if 'aux_ctc' not in cfg: raise ValueError( "The config need to have a section for the CTC decoder named as aux_ctc for Hybrid models." ) with open_dict(cfg): if self.tokenizer_type == "agg": cfg.aux_ctc.decoder.vocabulary = ListConfig(vocabulary) else: cfg.aux_ctc.decoder.vocabulary = ListConfig(list(vocabulary.keys())) # Setup prompt settings - default to 128 prompts if not specified cfg.num_prompts = cfg.model_defaults.get('num_prompts', 128) if 'prompt_dictionary' not in cfg.model_defaults: logging.warning( "No prompt_dictionary in config; using empty dict " "(expected during checkpoint restoration)." ) cfg.model_defaults.prompt_dictionary = {} # Set subsampling_factor in a place accessible to the class self.subsampling_factor = cfg.get('subsampling_factor', 8) if cfg.aux_ctc.decoder["num_classes"] < 1: logging.info( "\nReplacing placholder number of classes ({}) with actual number of classes - {}".format( cfg.aux_ctc.decoder["num_classes"], len(vocabulary) ) ) cfg.aux_ctc.decoder["num_classes"] = len(vocabulary) super().__init__(cfg=cfg, trainer=trainer) # Initialize concat flag self.concat = False if self.cfg.model_defaults.get('initialize_prompt_feature', False): self.initialize_prompt_feature()
[docs] def initialize_prompt_feature(self): """Initialize model components for prompt feature via concatenation.""" super().initialize_prompt_feature() logging.info("Model with prompt feature has been initialized") # Setup decoding object self.decoding = RNNTBPEDecoding( decoding_cfg=self.cfg.decoding, decoder=self.decoder, joint=self.joint, tokenizer=self.tokenizer, ) # Setup wer object self.wer = WER( decoding=self.decoding, batch_dim_index=0, use_cer=self.cfg.get('use_cer', False), log_prediction=self.cfg.get('log_prediction', True), dist_sync_on_step=True, ) # Setup bleu object self._bleu = BLEU(decoding=self.decoding, tokenize=self.cfg.get('bleu_tokenizer', "13a"), log_prediction=True) # Setup fused Joint step if flag is set if self.joint.fuse_loss_wer: self.joint.set_loss(self.loss) self.joint.set_wer(self.wer) # Setup CTC decoding ctc_decoding_cfg = self.cfg.aux_ctc.get('decoding', None) if ctc_decoding_cfg is None: ctc_decoding_cfg = OmegaConf.structured(CTCBPEDecodingConfig) with open_dict(self.cfg.aux_ctc): self.cfg.aux_ctc.decoding = ctc_decoding_cfg self.ctc_decoding = CTCBPEDecoding(self.cfg.aux_ctc.decoding, tokenizer=self.tokenizer) # Setup CTC WER self.ctc_wer = WER( decoding=self.ctc_decoding, use_cer=self.cfg.aux_ctc.get('use_cer', False), dist_sync_on_step=True, log_prediction=self.cfg.get("log_prediction", False), ) # setting the RNNT decoder as the default one self.cur_decoder = "rnnt"
def _setup_dataloader_from_config(self, config: Optional[Dict]): if config.get("use_lhotse"): if config.get('initialize_prompt_feature', True): # Use index-based dataset - returns prompt indices instead of full tensors # The model creates prompt tensors after encoding, guaranteeing no size mismatch dataset_config = ( OmegaConf.to_container(config, resolve=True) if isinstance(config, DictConfig) else dict(config) ) if hasattr(self, 'cfg') and 'encoder' in self.cfg: dataset_config['encoder'] = ( OmegaConf.to_container(self.cfg.encoder, resolve=True) if isinstance(self.cfg.encoder, DictConfig) else dict(self.cfg.encoder) ) dataset = LhotseSpeechToTextBpeDatasetWithPromptIndex(tokenizer=self.tokenizer, cfg=dataset_config) logging.info("Setting up Lhotse dataset with prompt index support (model creates prompt tensors)") else: dataset = LhotseSpeechToTextBpeDataset(tokenizer=self.tokenizer) return get_lhotse_dataloader_from_config( config, global_rank=self.global_rank, world_size=self.world_size, dataset=dataset, tokenizer=self.tokenizer, ) dataset = audio_to_text_dataset.get_audio_to_text_bpe_dataset_from_config( config=config, local_rank=self.local_rank, global_rank=self.global_rank, world_size=self.world_size, tokenizer=self.tokenizer, preprocessor_cfg=self.cfg.get("preprocessor", None), ) if dataset is None: return None if isinstance(dataset, AudioToBPEDALIDataset): # DALI Dataset implements dataloader interface return dataset shuffle = config['shuffle'] if isinstance(dataset, torch.utils.data.IterableDataset): shuffle = False if hasattr(dataset, 'collate_fn'): collate_fn = dataset.collate_fn elif hasattr(dataset.datasets[0], 'collate_fn'): # support datasets that are lists of entries collate_fn = dataset.datasets[0].collate_fn else: # support datasets that are lists of lists collate_fn = dataset.datasets[0].datasets[0].collate_fn return torch.utils.data.DataLoader( dataset=dataset, batch_size=config['batch_size'], collate_fn=collate_fn, drop_last=config.get('drop_last', False), shuffle=shuffle, num_workers=config.get('num_workers', 0), pin_memory=config.get('pin_memory', False), ) def _setup_transcribe_dataloader(self, config: Dict) -> 'torch.utils.data.DataLoader': """ Setup function for a temporary data loader which wraps the provided audio file. Args: config: A python dictionary which contains the following keys: paths2audio_files: (a list) of paths to audio files. The files should be relatively short fragments. \ Recommended length per file is between 5 and 25 seconds. batch_size: (int) batch size to use during inference. \ Bigger will result in better throughput performance but would use more memory. temp_dir: (str) A temporary directory where the audio manifest is temporarily stored. target_lang: (str) target language ID for transcription Returns: A pytorch DataLoader for the given audio file(s). """ if 'manifest_filepath' in config: manifest_filepath = config['manifest_filepath'] batch_size = config['batch_size'] else: manifest_filepath = os.path.join(config['temp_dir'], 'manifest.json') batch_size = min(config['batch_size'], len(config['paths2audio_files'])) # Get target language from config target_lang = config.get('target_lang', 'en-US') dl_config = { 'manifest_filepath': manifest_filepath, 'sample_rate': self.preprocessor._sample_rate, 'labels': self.joint.vocabulary, 'batch_size': batch_size, 'trim_silence': False, 'shuffle': False, 'num_workers': config.get('num_workers', min(batch_size, os.cpu_count() - 1)), 'pin_memory': True, 'use_lhotse': config.get('use_lhotse', True), 'use_bucketing': False, 'drop_last': False, 'initialize_prompt_feature': True, 'prompt_dictionary': self.cfg.model_defaults.get('prompt_dictionary'), 'num_prompts': self.cfg.model_defaults.get('num_prompts', 128), 'subsampling_factor': self.cfg.get('subsampling_factor', 8), 'default_lang': target_lang, 'window_stride': self.cfg.preprocessor.get('window_stride', 0.01), } if config.get("augmentor"): dl_config['augmentor'] = config.get("augmentor") temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config)) return temporary_datalayer def _transcribe_forward(self, batch: tuple[torch.Tensor, ...], trcfg: HybridRNNTCTCPromptTranscribeConfig) -> dict: """ Internal function to perform the model's custom forward pass to return outputs that are processed by `_transcribe_output_processing()`. This function is called by `transcribe()` and `transcribe_generator()` to perform the model's forward pass. Args: batch: A batch of input data from the data loader that is used to perform the model's forward pass. Expected structure: (audio, audio_lens, tokens, token_lens, prompt_targets) For transcription, we may only have (audio, audio_lens) or (audio, audio_lens, ..., prompt_targets) trcfg: The transcription config dataclass. Subclasses can change this to a different dataclass if needed. Returns: The model's outputs that are processed by `_transcribe_output_processing()`. """ audio, audio_lens = batch[0], batch[1] prompt, prompt_indices = None, None if len(batch) >= 5: prompt_or_indices = batch[4] if prompt_or_indices.dim() == 1: prompt_indices = prompt_or_indices else: prompt = prompt_or_indices batch_size = audio.shape[0] if prompt is None and prompt_indices is None: target_lang = trcfg.target_lang prompt_dict = self.cfg.model_defaults.get('prompt_dictionary') if not prompt_dict: raise ValueError("Prompt dictionary is empty. Cannot create dynamic prompts.") if target_lang not in prompt_dict: available_keys = list(prompt_dict.keys()) raise ValueError( f"Unknown target language: '{target_lang}'. Available languages: {available_keys[:10]}{'...' if len(available_keys) > 10 else ''}" ) prompt_id = prompt_dict[target_lang] prompt_indices = torch.full((batch_size,), prompt_id, dtype=torch.long, device=audio.device) encoded, encoded_len = self.forward( input_signal=audio, input_signal_length=audio_lens, prompt=prompt, prompt_indices=prompt_indices ) # Prepare output dictionary based on decoder type if self.cur_decoder == "rnnt": # RNNT Path - just use encoded outputs directly output = dict(encoded=encoded, encoded_len=encoded_len) else: # CTC Path - compute logits from encoder output logits = self.ctc_decoder(encoder_output=encoded) output = dict(logits=logits, encoded_len=encoded_len) del encoded return output
[docs] @torch.no_grad() def transcribe( self, audio: List[str], batch_size: int = 4, return_hypotheses: bool = False, partial_hypothesis: Optional[List['Hypothesis']] = None, num_workers: int = 0, channel_selector: Optional[ChannelSelectorType] = None, augmentor: DictConfig = None, verbose: bool = True, timestamps: Optional[bool] = None, override_config: Optional[HybridRNNTCTCPromptTranscribeConfig] = None, **prompt, ) -> TranscriptionReturnType: """ Uses greedy decoding to transcribe audio files. Use this method for debugging and prototyping. Args: audio: (a single or list) of paths to audio files or a np.ndarray audio array. Can also be a dataloader object that provides values that can be consumed by the model. Recommended length per file is between 5 and 25 seconds. \ But it is possible to pass a few hours long file if enough GPU memory is available. batch_size: (int) batch size to use during inference. \ Bigger will result in better throughput performance but would use more memory. return_hypotheses: (bool) Either return hypotheses or text With hypotheses can do some postprocessing like getting timestamp or rescoring partial_hypothesis: (Optional[List['Hypothesis']]) partial hypotheses for streaming num_workers: (int) number of workers for DataLoader channel_selector: (Optional[ChannelSelectorType]) select a single channel or a subset of channels from multi-channel audio. If set to `'average'`, it performs averaging across channels. Disabled if set to `None`. Defaults to `None`. Uses zero-based indexing. augmentor: (DictConfig): Augment audio samples during transcription if augmentor is applied. verbose: (bool) whether to display tqdm progress bar timestamps: (Optional[bool]) timestamps will be returned if set to True as part of hypothesis object override_config: (Optional[HybridRNNTCTCPromptTranscribeConfig]) override transcription config pre-defined by the user. **prompt: Optional input to construct the prompts for the model. Accepted formats include: target_lang: (str) target language ID for transcription (e.g., "en-US", "de-DE") Additional prompt parameters can be passed and will be forwarded to the transcription config. Returns: Returns a tuple of 2 items - * A list of greedy transcript texts / Hypothesis * An optional list of beam search transcript texts / Hypothesis / NBestHypothesis. """ if self.cur_decoder not in ["ctc", "rnnt"]: raise ValueError( f"{self.cur_decoder} is not supported for cur_decoder. Supported values are ['ctc', 'rnnt']" ) if timestamps is not None: if self.cur_decoder not in ["ctc", "rnnt"]: raise ValueError( f"{self.cur_decoder} is not supported for cur_decoder. Supported values are ['ctc', 'rnnt']" ) decoding_cfg = self.cfg.aux_ctc.decoding if self.cur_decoder == "ctc" else self.cfg.decoding if timestamps or (override_config is not None and override_config.timestamps): logging.info( "Timestamps requested, setting decoding timestamps to True. Capture them in Hypothesis object, \ with output[idx].timestep['word'/'segment'/'char']" ) return_hypotheses = True with open_dict(decoding_cfg): decoding_cfg.compute_timestamps = True decoding_cfg.preserve_alignments = True else: with open_dict(decoding_cfg): decoding_cfg.compute_timestamps = False decoding_cfg.preserve_alignments = False self.change_decoding_strategy(decoding_cfg, decoder_type=self.cur_decoder, verbose=False) # Create transcription config if not provided if override_config is None: # Extract target_lang from prompt or use default target_lang = prompt.get('target_lang', 'auto') trcfg = HybridRNNTCTCPromptTranscribeConfig( batch_size=batch_size, return_hypotheses=return_hypotheses, num_workers=num_workers, channel_selector=channel_selector, augmentor=augmentor, verbose=verbose, timestamps=timestamps, target_lang=target_lang, ) else: if not isinstance(override_config, HybridRNNTCTCPromptTranscribeConfig): raise ValueError( f"override_config must be of type {HybridRNNTCTCPromptTranscribeConfig}, " f"but got {type(override_config)}" ) trcfg = override_config # Call parent class transcribe method with proper parameters return super().transcribe( audio=audio, batch_size=batch_size, return_hypotheses=return_hypotheses, partial_hypothesis=partial_hypothesis, num_workers=num_workers, channel_selector=channel_selector, augmentor=augmentor, verbose=verbose, timestamps=timestamps, override_config=trcfg, )
@property def input_types(self) -> Optional[Dict[str, NeuralType]]: if hasattr(self.preprocessor, '_sample_rate'): input_signal_eltype = AudioSignal(freq=self.preprocessor._sample_rate) else: input_signal_eltype = AudioSignal() return { "input_signal": NeuralType(('B', 'T'), input_signal_eltype, optional=True), "input_signal_length": NeuralType(tuple('B'), LengthsType(), optional=True), "processed_signal": NeuralType(('B', 'D', 'T'), SpectrogramType(), optional=True), "processed_signal_length": NeuralType(tuple('B'), LengthsType(), optional=True), "prompt": NeuralType(('B', 'T', 'D'), LabelsType(), optional=True), "prompt_indices": NeuralType(tuple('B'), LabelsType(), optional=True), } @property def output_types(self) -> Optional[Dict[str, NeuralType]]: return { "outputs": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()), "encoded_lengths": NeuralType(tuple('B'), LengthsType()), }
[docs] @typecheck() def forward( self, input_signal=None, input_signal_length=None, processed_signal=None, processed_signal_length=None, prompt=None, prompt_indices=None, ): """ Forward pass of the model. Note that for RNNT Models, the forward pass of the model is a 3 step process, and this method only performs the first step - forward of the acoustic model. Args: input_signal: Tensor that represents a batch of raw audio signals, of shape [B, T]. T here represents timesteps, with 1 second of audio represented as `self.sample_rate` number of floating point values. input_signal_length: Vector of length B, that contains the individual lengths of the audio sequences. processed_signal: Tensor that represents a batch of processed audio signals, of shape (B, D, T) that has undergone processing via some DALI preprocessor. processed_signal_length: Vector of length B, that contains the individual lengths of the processed audio sequences. prompt: (backward-compatible) Pre-built one-hot prompt tensor of shape [B, T, D]. If provided, used directly (trimmed to encoder length if needed). prompt_indices: Tensor of shape [B] containing language ID indices per sample. The model creates the prompt tensor after encoding using the actual encoder output length, guaranteeing no size mismatch. Ignored if ``prompt`` is also provided. Returns: A tuple of 2 elements - 1) The encoded tensor of shape [B, D, T]. 2) The lengths of the acoustic sequence after propagation through the encoder, of shape [B]. """ has_input_signal = input_signal is not None and input_signal_length is not None has_processed_signal = processed_signal is not None and processed_signal_length is not None if (has_input_signal ^ has_processed_signal) is False: raise ValueError( f"{self} Arguments ``input_signal`` and ``input_signal_length`` are mutually exclusive " " with ``processed_signal`` and ``processed_signal_len`` arguments." ) if not has_processed_signal: processed_signal, processed_signal_length = self.preprocessor( input_signal=input_signal, length=input_signal_length, ) # Spec augment is not applied during evaluation/testing if self.spec_augmentation is not None and self.training: processed_signal = self.spec_augmentation(input_spec=processed_signal, length=processed_signal_length) encoded, encoded_len = self.encoder(audio_signal=processed_signal, length=processed_signal_length) encoded = torch.transpose(encoded, 1, 2) # B * D * T -> B * T * D if self.concat: if prompt is not None: # Backward-compatible path: caller provided a pre-built [B, T, D] one-hot tensor if prompt.shape[1] > encoded.shape[1]: prompt = prompt[:, : encoded.shape[1], :] elif prompt_indices is not None: # New path: build one-hot from per-sample language ID indices batch_size = encoded.shape[0] time_steps = encoded.shape[1] num_prompts = self.num_prompts prompt = torch.zeros(batch_size, time_steps, num_prompts, dtype=encoded.dtype, device=encoded.device) prompt.scatter_(2, prompt_indices.view(batch_size, 1, 1).expand(-1, time_steps, -1), 1.0) else: raise ValueError("Either prompt or prompt_indices must be provided when concat mode is enabled.") out_dtype = encoded.dtype concat_enc_states = torch.cat([encoded, prompt], dim=-1) encoded = self.prompt_kernel(concat_enc_states).to(out_dtype) encoded = torch.transpose(encoded, 1, 2) # B * T * D -> B * D * T return encoded, encoded_len
[docs] def training_step(self, batch, batch_nb): # Reset access registry if AccessMixin.is_access_enabled(): AccessMixin.reset_registry(self) if self.is_interctc_enabled(): AccessMixin.set_access_enabled(access_enabled=True) signal, signal_len, transcript, transcript_len, prompt_or_indices = batch # Detect whether batch[4] is old-style prompt [B, T, D] or new-style indices [B] prompt, prompt_indices = None, None if prompt_or_indices.dim() == 1: prompt_indices = prompt_or_indices else: prompt = prompt_or_indices if isinstance(batch, DALIOutputs) and batch.has_processed_signal: encoded, encoded_len = self.forward(processed_signal=signal, processed_signal_length=signal_len) else: encoded, encoded_len = self.forward( input_signal=signal, input_signal_length=signal_len, prompt=prompt, prompt_indices=prompt_indices ) del signal # During training, loss must be computed, so decoder forward is necessary decoder, target_length, states = self.decoder(targets=transcript, target_length=transcript_len) if hasattr(self, '_trainer') and self._trainer is not None: log_every_n_steps = self._trainer.log_every_n_steps sample_id = self._trainer.global_step else: log_every_n_steps = 1 sample_id = batch_nb if (sample_id + 1) % log_every_n_steps == 0: compute_wer = True else: compute_wer = False # If fused Joint-Loss-WER is not used if not self.joint.fuse_loss_wer: # Compute full joint and loss joint = self.joint(encoder_outputs=encoded, decoder_outputs=decoder) loss_value = self.loss( log_probs=joint, targets=transcript, input_lengths=encoded_len, target_lengths=target_length ) # Add auxiliary losses, if registered loss_value = self.add_auxiliary_losses(loss_value) tensorboard_logs = { 'learning_rate': self._optimizer.param_groups[0]['lr'], 'global_step': torch.tensor(self.trainer.global_step, dtype=torch.float32), } if compute_wer: self.wer.update( predictions=encoded, predictions_lengths=encoded_len, targets=transcript, targets_lengths=transcript_len, ) _, scores, words = self.wer.compute() self.wer.reset() tensorboard_logs.update({'training_batch_wer': scores.float() / words}) else: # If fused Joint-Loss-WER is used # Fused joint step loss_value, wer, _, _ = self.joint( encoder_outputs=encoded, decoder_outputs=decoder, encoder_lengths=encoded_len, transcripts=transcript, transcript_lengths=transcript_len, compute_wer=compute_wer, ) # Add auxiliary losses, if registered loss_value = self.add_auxiliary_losses(loss_value) tensorboard_logs = { 'learning_rate': self._optimizer.param_groups[0]['lr'], 'global_step': torch.tensor(self.trainer.global_step, dtype=torch.float32), } if compute_wer: tensorboard_logs.update({'training_batch_wer': wer}) if self.ctc_loss_weight > 0: log_probs = self.ctc_decoder(encoder_output=encoded) ctc_loss = self.ctc_loss( log_probs=log_probs, targets=transcript, input_lengths=encoded_len, target_lengths=transcript_len ) tensorboard_logs['train_rnnt_loss'] = loss_value tensorboard_logs['train_ctc_loss'] = ctc_loss loss_value = (1 - self.ctc_loss_weight) * loss_value + self.ctc_loss_weight * ctc_loss if compute_wer: self.ctc_wer.update( predictions=log_probs, targets=transcript, targets_lengths=transcript_len, predictions_lengths=encoded_len, ) ctc_wer, _, _ = self.ctc_wer.compute() self.ctc_wer.reset() tensorboard_logs.update({'training_batch_wer_ctc': ctc_wer}) # note that we want to apply interctc independent of whether main ctc # loss is used or not (to allow rnnt + interctc training). # assuming ``ctc_loss_weight=0.3`` and interctc is applied to a single # layer with weight of ``0.1``, the total loss will be # ``loss = 0.9 * (0.3 * ctc_loss + 0.7 * rnnt_loss) + 0.1 * interctc_loss`` loss_value, additional_logs = self.add_interctc_losses( loss_value, transcript, transcript_len, compute_wer=compute_wer ) tensorboard_logs.update(additional_logs) tensorboard_logs['train_loss'] = loss_value # Reset access registry if AccessMixin.is_access_enabled(): AccessMixin.reset_registry(self) # Log items self.log_dict(tensorboard_logs) return {'loss': loss_value}
[docs] def predict_step(self, batch, batch_idx, dataloader_idx=0): signal, signal_len, transcript, transcript_len, prompt_or_indices = batch prompt, prompt_indices = None, None if prompt_or_indices.dim() == 1: prompt_indices = prompt_or_indices else: prompt = prompt_or_indices if isinstance(batch, DALIOutputs) and batch.has_processed_signal: encoded, encoded_len = self.forward(processed_signal=signal, processed_signal_length=signal_len) else: encoded, encoded_len = self.forward( input_signal=signal, input_signal_length=signal_len, prompt=prompt, prompt_indices=prompt_indices ) del signal if self.cur_decoder == 'rnnt': best_hyp = self.decoding.rnnt_decoder_predictions_tensor( encoder_output=encoded, encoded_lengths=encoded_len, return_hypotheses=False ) else: logits = self.ctc_decoder(encoder_output=encoded) best_hyp = self.ctc_decoding.ctc_decoder_predictions_tensor( decoder_outputs=logits, decoder_lengths=encoded_len, return_hypotheses=False, ) batch_size = signal_len.shape[0] sample_id = torch.arange(batch_idx * batch_size, (batch_idx + 1) * batch_size).cpu().detach().numpy() return list(zip(sample_id, best_hyp))
[docs] def validation_pass(self, batch, batch_idx, dataloader_idx): if self.is_interctc_enabled(): AccessMixin.set_access_enabled(access_enabled=True) signal, signal_len, transcript, transcript_len, prompt_or_indices = batch prompt, prompt_indices = None, None if prompt_or_indices.dim() == 1: prompt_indices = prompt_or_indices else: prompt = prompt_or_indices if isinstance(batch, DALIOutputs) and batch.has_processed_signal: encoded, encoded_len = self.forward(processed_signal=signal, processed_signal_length=signal_len) else: encoded, encoded_len = self.forward( input_signal=signal, input_signal_length=signal_len, prompt=prompt, prompt_indices=prompt_indices ) del signal tensorboard_logs = {} loss_value = None # If experimental fused Joint-Loss-WER is not used if not self.joint.fuse_loss_wer: if self.compute_eval_loss: decoder, target_length, states = self.decoder(targets=transcript, target_length=transcript_len) joint = self.joint(encoder_outputs=encoded, decoder_outputs=decoder) loss_value = self.loss( log_probs=joint, targets=transcript, input_lengths=encoded_len, target_lengths=target_length ) tensorboard_logs['val_loss'] = loss_value self.wer.update( predictions=encoded, predictions_lengths=encoded_len, targets=transcript, targets_lengths=transcript_len, ) wer, wer_num, wer_denom = self.wer.compute() self.wer.reset() tensorboard_logs['val_wer_num'] = wer_num tensorboard_logs['val_wer_denom'] = wer_denom tensorboard_logs['val_wer'] = wer else: # If experimental fused Joint-Loss-WER is used compute_wer = True if self.compute_eval_loss: decoded, target_len, states = self.decoder(targets=transcript, target_length=transcript_len) else: decoded = None target_len = transcript_len # Fused joint step loss_value, wer, wer_num, wer_denom = self.joint( encoder_outputs=encoded, decoder_outputs=decoded, encoder_lengths=encoded_len, transcripts=transcript, transcript_lengths=target_len, compute_wer=compute_wer, ) if loss_value is not None: tensorboard_logs['val_loss'] = loss_value tensorboard_logs['val_wer_num'] = wer_num tensorboard_logs['val_wer_denom'] = wer_denom tensorboard_logs['val_wer'] = wer log_probs = self.ctc_decoder(encoder_output=encoded) if self.compute_eval_loss: ctc_loss = self.ctc_loss( log_probs=log_probs, targets=transcript, input_lengths=encoded_len, target_lengths=transcript_len ) tensorboard_logs['val_ctc_loss'] = ctc_loss tensorboard_logs['val_rnnt_loss'] = loss_value loss_value = (1 - self.ctc_loss_weight) * loss_value + self.ctc_loss_weight * ctc_loss tensorboard_logs['val_loss'] = loss_value # CTC WER calculation self.ctc_wer.update( predictions=log_probs, targets=transcript, targets_lengths=transcript_len, predictions_lengths=encoded_len, ) ctc_wer, ctc_wer_num, ctc_wer_denom = self.ctc_wer.compute() self.ctc_wer.reset() tensorboard_logs['val_wer_num_ctc'] = ctc_wer_num tensorboard_logs['val_wer_denom_ctc'] = ctc_wer_denom tensorboard_logs['val_wer_ctc'] = ctc_wer # BLEU score calculation self.bleu.update( predictions=encoded, predictions_lengths=encoded_len, targets=transcript, targets_lengths=transcript_len ) bleu_metrics = self.bleu.compute(return_all_metrics=True, prefix="val_") tensorboard_logs.update( { 'val_bleu_num': bleu_metrics['val_bleu_num'], 'val_bleu_denom': bleu_metrics['val_bleu_denom'], 'val_bleu_pred_len': bleu_metrics['val_bleu_pred_len'], 'val_bleu_target_len': bleu_metrics['val_bleu_target_len'], 'val_bleu': bleu_metrics['val_bleu'], } ) self.bleu.reset() self.log('global_step', torch.tensor(self.trainer.global_step, dtype=torch.float32)) # Inter-CTC losses and additional logging loss_value, additional_logs = self.add_interctc_losses( loss_value, transcript, transcript_len, compute_wer=True, compute_loss=self.compute_eval_loss, log_wer_num_denom=True, log_prefix="val_", ) if self.compute_eval_loss: # overriding total loss value. Note that the previous # rnnt + ctc loss is available in metrics as "val_final_loss" now tensorboard_logs['val_loss'] = loss_value tensorboard_logs.update(additional_logs) # Reset access registry if AccessMixin.is_access_enabled(): AccessMixin.reset_registry(self) return tensorboard_logs
[docs] def validation_step(self, batch, batch_idx, dataloader_idx=0): tensorboard_logs = self.validation_pass(batch, batch_idx, dataloader_idx) if type(self.trainer.val_dataloaders) == list and len(self.trainer.val_dataloaders) > 1: self.validation_step_outputs[dataloader_idx].append(tensorboard_logs) else: self.validation_step_outputs.append(tensorboard_logs) return tensorboard_logs
[docs] def test_step(self, batch, batch_idx, dataloader_idx=0): logs = self.validation_pass(batch, batch_idx, dataloader_idx=dataloader_idx) test_logs = {name.replace("val_", "test_"): value for name, value in logs.items()} if type(self.trainer.test_dataloaders) == list and len(self.trainer.test_dataloaders) > 1: self.test_step_outputs[dataloader_idx].append(test_logs) else: self.test_step_outputs.append(test_logs) return test_logs
[docs] def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0): # Calculate validation loss if required if self.compute_eval_loss: val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean() val_loss_log = {'val_loss': val_loss_mean} else: val_loss_log = {} # Calculate WER wer_num = torch.stack([x['val_wer_num'] for x in outputs]).sum() wer_denom = torch.stack([x['val_wer_denom'] for x in outputs]).sum() tensorboard_logs = {**val_loss_log, 'val_wer': wer_num.float() / wer_denom} # Calculate CTC WER if applicable if self.ctc_loss_weight > 0: ctc_wer_num = torch.stack([x['val_wer_num_ctc'] for x in outputs]).sum() ctc_wer_denom = torch.stack([x['val_wer_denom_ctc'] for x in outputs]).sum() tensorboard_logs['val_wer_ctc'] = ctc_wer_num.float() / ctc_wer_denom # Calculate BLEU score bleu_num = torch.stack([x['val_bleu_num'] for x in outputs]).sum(dim=0) bleu_denom = torch.stack([x['val_bleu_denom'] for x in outputs]).sum(dim=0) bleu_pred_len = torch.stack([x['val_bleu_pred_len'] for x in outputs]).sum(dim=0) bleu_target_len = torch.stack([x['val_bleu_target_len'] for x in outputs]).sum(dim=0) val_bleu = self.bleu._compute_bleu(bleu_pred_len, bleu_target_len, bleu_num, bleu_denom) tensorboard_logs['val_bleu'] = val_bleu # Finalize and log metrics metrics = {**val_loss_log, 'log': tensorboard_logs} self.finalize_interctc_metrics(metrics, outputs, prefix="val_") return metrics
@property def bleu(self): return self._bleu @bleu.setter def bleu(self, bleu): self._bleu = bleu
[docs] @classmethod def get_transcribe_config(cls) -> HybridRNNTCTCPromptTranscribeConfig: """ Get the default transcribe config for this model. """ return HybridRNNTCTCPromptTranscribeConfig()
[docs] def setup_training_data(self, train_data_config: Optional[DictConfig]): """ Sets up the training data loader via a Dict-like object. Args: train_data_config: A config that contains the information regarding construction of an ASR Training dataset. Supported Datasets: - :class:`~nemo.collections.asr.data.audio_to_text_lhotse_prompt.LhotseSpeechToTextBpeDatasetWithPrompt` """ # create audio-only data loader self._update_dataset_config(dataset_name='train', config=train_data_config) self._train_dl = self._setup_dataloader_from_config(config=train_data_config) # Need to set this because if using an IterableDataset, the length of the # dataloader is the total number of samples rather than the number of batches, # and this messes up the tqdm progress bar. So we set the number of steps manually # (to the correct number) to fix this. if 'is_tarred' in train_data_config and train_data_config['is_tarred']: # We also need to check if limit_train_batches is already set. # If it's an int, we assume that the user has set it to something sane, # i.e. <= # training batches, and don't change it. Otherwise, adjust # batches accordingly if it's a float (including 1.0). if self._trainer is not None and isinstance(self._trainer.limit_train_batches, float): self._trainer.limit_train_batches = int( self._trainer.limit_train_batches * ceil((len(self._train_dl.dataset) / self.world_size) / train_data_config['batch_size']) ) elif self._trainer is None: logging.warning( "Model Trainer was not set before constructing the dataset, incorrect number of " "training batches will be used. Please set the trainer and rebuild the dataset." )
[docs] def setup_validation_data(self, val_data_config: Optional[Union[DictConfig, Dict]]): """ Sets up the validation data loader via a Dict-like object. Args: val_data_config: A config that contains the information regarding construction of an ASR Training dataset. Supported Datasets: - :class:`~nemo.collections.asr.data.audio_to_text_lhotse_prompt.LhotseSpeechToTextBpeDatasetWithPrompt` """ if 'shuffle' not in val_data_config: val_data_config['shuffle'] = False # preserve config self._update_dataset_config(dataset_name='validation', config=val_data_config) self._validation_dl = self._setup_dataloader_from_config(config=val_data_config)
[docs] def setup_test_data(self, test_data_config: Optional[Union[DictConfig, Dict]]): """ Sets up the test data loader via a Dict-like object. Args: test_data_config: A config that contains the information regarding construction of an ASR Training dataset. Supported Datasets: - :class:`~nemo.collections.asr.data.audio_to_text_lhotse_prompt.LhotseSpeechToTextBpeDatasetWithPrompt` """ if 'shuffle' not in test_data_config: test_data_config['shuffle'] = False # preserve config self._update_dataset_config(dataset_name='test', config=test_data_config) self._test_dl = self._setup_dataloader_from_config(config=test_data_config)
[docs] @classmethod def list_available_models(cls) -> List[PretrainedModelInfo]: """ This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud. Returns: List of available pre-trained models. """ return None